add passphrase dialog

This commit is contained in:
2026-08-07 07:24:33 +07:00
parent e2ec35a673
commit b137f54a66
6 changed files with 241 additions and 7 deletions
+74 -4
View File
@@ -34,6 +34,9 @@ pub const INDEXER_RELAYS: [&str; 3] = [
pub enum BackendEvent {
/// User has no signer configured.
SignerRequired,
/// The stored identity is NIP-49 encrypted (`ncryptsec1...`); a
/// passphrase is required to decrypt it before the session can resume.
PassphraseRequired,
/// The signer has changed (login/logout/account switch).
SignerChanged,
/// Relay bootstrap finished.
@@ -76,6 +79,9 @@ pub struct Backend {
current_user: Option<PublicKey>,
connected: bool,
sync_progress: Option<(u64, u64)>,
/// Whether the stored credential is NIP-49 encrypted and a passphrase
/// is still needed to resume the session.
passphrase_required: bool,
tasks: Vec<Task<Result<(), Error>>>,
}
@@ -125,6 +131,7 @@ impl Backend {
current_user: None,
connected: false,
sync_progress: None,
passphrase_required: false,
tasks: vec![pump],
};
@@ -171,7 +178,9 @@ impl Backend {
}
/// Restore the saved session from the keyring. Emits
/// [`BackendEvent::SignerRequired`] if no credential is stored.
/// [`BackendEvent::SignerRequired`] if no credential is stored, or
/// [`BackendEvent::PassphraseRequired`] if the stored identity is
/// NIP-49 encrypted.
pub fn restore_session(&mut self, cx: &mut Context<Self>) {
if cfg!(target_arch = "wasm32") {
cx.emit(BackendEvent::SignerRequired);
@@ -206,9 +215,12 @@ impl Backend {
this.update(cx, |this, cx| this.set_signer(signer, cx))?;
} else if content.starts_with("ncryptsec1") {
// Encrypted identity: a passphrase is required to
// decrypt it, which is not implemented yet.
log::warn!("stored identity is ncryptsec-encrypted; passphrase restore is not implemented");
this.update(cx, |_, cx| cx.emit(BackendEvent::SignerRequired))?;
// decrypt it before the session can resume.
log::warn!("stored identity is ncryptsec-encrypted; waiting for passphrase");
this.update(cx, |this, cx| {
this.passphrase_required = true;
cx.emit(BackendEvent::PassphraseRequired);
})?;
} else {
this.update(cx, |_, cx| cx.emit(BackendEvent::SignerRequired))?;
}
@@ -228,6 +240,56 @@ impl Backend {
}));
}
/// Decrypt the NIP-49 encrypted credential stored in the keyring with
/// the given passphrase and resume the session.
///
/// The scrypt decryption runs off the UI thread. The returned receiver
/// yields the public key on success, or the failure reason (e.g. wrong
/// passphrase), so callers can render inline errors.
pub fn restore_with_passphrase(
&mut self,
password: &str,
cx: &mut Context<Self>,
) -> flume::Receiver<Result<PublicKey, Error>> {
let (tx, rx) = flume::bounded(1);
let password = password.to_owned();
let user = cx.read_credentials(USER_KEYRING);
self.tasks.push(cx.spawn(async move |this, cx| {
let result = async {
let content = user
.await?
.map(|(_username, secret)| String::from_utf8(secret))
.transpose()?
.ok_or_else(|| anyhow!("no stored credential; nothing to unlock"))?;
if !content.starts_with("ncryptsec1") {
Err(anyhow!("stored credential is not passphrase-encrypted"))?;
}
let decrypt_task = cx.background_spawn(async move {
let encrypted = EncryptedSecretKey::from_bech32(&content)?;
let secret = encrypted.decrypt(&password)?;
Ok::<_, Error>(Keys::new(secret))
});
let keys = decrypt_task.await?;
let public_key = keys.public_key();
this.update(cx, |this, cx| this.set_signer(keys, cx))?;
Ok::<_, Error>(public_key)
}
.await;
tx.send_async(result).await.ok();
Ok(())
}));
rx
}
/// Create a new identity: generate keys, encrypt the secret key with the
/// passphrase (NIP-49) and persist it in the keyring, then publish the
/// user's NIP-65 relay list, metadata and grasp list.
@@ -448,6 +510,7 @@ impl Backend {
this.update(cx, |this, cx| {
this.signer.swap_inner(Keys::generate());
this.current_user = None;
this.passphrase_required = false;
cx.emit(BackendEvent::SignerChanged);
cx.emit(BackendEvent::SignerRequired);
cx.notify();
@@ -510,6 +573,12 @@ impl Backend {
self.current_user
}
/// Whether the stored credential is NIP-49 encrypted and a passphrase
/// is still needed to resume the session.
pub fn passphrase_required(&self) -> bool {
self.passphrase_required
}
/// Surface an error message through [`BackendEvent::Error`].
pub fn emit_error(&mut self, message: impl Into<String>, cx: &mut Context<Self>) {
cx.emit(BackendEvent::error(message));
@@ -540,6 +609,7 @@ impl Backend {
this.update(cx, |this, cx| {
this.signer.swap_inner(new_signer);
this.current_user = Some(public_key);
this.passphrase_required = false;
this.bootstrap_user(public_key, cx);
cx.emit(BackendEvent::SignerChanged);
cx.notify();