add passphrase dialog
This commit is contained in:
@@ -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();
|
||||
|
||||
Reference in New Issue
Block a user