From 8de6018e28febd006d8f0447749796d58a8e42d4 Mon Sep 17 00:00:00 2001 From: Ren Amamiya Date: Wed, 5 Aug 2026 10:37:33 +0700 Subject: [PATCH] update nostr connect --- crates/signed_state/src/backend.rs | 87 +++++++++++++++--------------- 1 file changed, 45 insertions(+), 42 deletions(-) diff --git a/crates/signed_state/src/backend.rs b/crates/signed_state/src/backend.rs index 8b1f1f9..fa3e145 100644 --- a/crates/signed_state/src/backend.rs +++ b/crates/signed_state/src/backend.rs @@ -9,10 +9,9 @@ use nostr_sdk::prelude::*; use signed_core::filters; use signed_nostr::{NostrBackend, SignedAuthUrlHandler, UniversalSigner, Update}; -/// Keyring entry holding the user credential (`nsec1...` or `bunker://...`). -pub const USER_KEYRING: &str = "su.reya.signed#user"; -/// Keyring entry holding the locally generated key for NIP-46 sessions. -pub const MASTER_KEYRING: &str = "su.reya.signed#master"; +/// Keyring entry holding the user credential (`nsec1...` or `bunker://...` +/// with an embedded `?master=` NIP-46 session key). +pub const USER_KEYRING: &str = "Signed Safe Storage"; /// Timeout for NIP-46 signer responses. pub const NOSTR_CONNECT_TIMEOUT: u64 = 60; @@ -175,7 +174,6 @@ impl Backend { } let user = cx.read_credentials(USER_KEYRING); - let master = self.master_key(cx); self.tasks.push(cx.spawn(async move |this, cx| { let content = match user.await { @@ -191,10 +189,11 @@ impl Backend { let keys = Keys::new(SecretKey::parse(&content)?); this.update(cx, |this, cx| this.set_signer(keys, cx))?; } else if content.starts_with("bunker://") { - let uri = NostrConnectUri::parse(&content)?; + let (base, keys) = extract_master_key(&content); + let uri = NostrConnectUri::parse(base)?; let mut signer = NostrConnect::new( uri, - master.await, + keys, Duration::from_secs(NOSTR_CONNECT_TIMEOUT), None, )?; @@ -246,9 +245,11 @@ impl Backend { })); } - /// Login with a `bunker://...` URI (NIP-46). The auth URL, if any, is - /// opened in the default browser. The credential is persisted in the - /// keyring after the signer proves reachable. + /// Login with a `bunker://...` URI (NIP-46). A fresh session key is + /// generated and embedded into the stored URI as `?master=`, so + /// no separate keyring entry is needed. The auth URL, if any, is opened + /// in the default browser. The credential is persisted in the keyring + /// after the signer proves reachable. pub fn login_with_bunker(&mut self, uri: &str, cx: &mut Context) { let uri_string = uri.trim().to_owned(); @@ -260,14 +261,15 @@ impl Backend { } }; - let master = self.master_key(cx); - let write = cx.write_credentials(USER_KEYRING, "bunker", uri_string.as_bytes()); + let keys = Keys::generate(); + let credential = with_master_key(&uri_string, &keys); + let write = cx.write_credentials(USER_KEYRING, "bunker", credential.as_bytes()); self.tasks.push(cx.spawn(async move |this, cx| { let result = async { let mut signer = NostrConnect::new( connect_uri, - master.await, + keys, Duration::from_secs(NOSTR_CONNECT_TIMEOUT), None, )?; @@ -310,33 +312,6 @@ impl Backend { })); } - /// Get (or generate and persist) the key used for NIP-46 sessions. - fn master_key(&self, cx: &App) -> Task { - let task = cx.read_credentials(MASTER_KEYRING); - - cx.spawn(async move |cx| { - let (keys, new_key) = match task.await { - Ok(Some((_user, secret))) => match SecretKey::from_slice(&secret) { - Ok(secret_key) => (Keys::new(secret_key), false), - _ => (Keys::generate(), true), - }, - _ => (Keys::generate(), true), - }; - - if new_key { - let username = keys.public_key().to_hex(); - let password = keys.secret_key().to_secret_bytes(); - - cx.update(|cx| { - let task = cx.write_credentials(MASTER_KEYRING, &username, &password); - cx.background_spawn(async move { task.await.ok() }).detach(); - }); - } - - keys - }) - } - /// Fetch the user's grasp list (kind `10317`) and add the listed grasp /// servers as relays. fn bootstrap_user(&mut self, public_key: PublicKey, cx: &mut Context) { @@ -631,7 +606,10 @@ impl Backend { /// Subscribe only on the bootstrap relays, auto-closing after EOSE or a /// short timeout. Use for one-shot data fetches (repo events, profiles) /// instead of persistent gossip-routed subscriptions. -pub(crate) async fn subscribe_bootstrap_only(client: &Client, filters: Vec) -> Result<(), Error> { +pub(crate) async fn subscribe_bootstrap_only( + client: &Client, + filters: Vec, +) -> Result<(), Error> { let opts = SubscribeAutoCloseOptions::default() .exit_policy(ReqExitPolicy::ExitOnEOSE) .timeout(Some(Duration::from_secs(10))); @@ -652,6 +630,31 @@ pub(crate) async fn sync_bootstrap_only( filter: Filter, opts: SyncOptions, ) -> Result { - let output = client.sync(filter).with(BOOTSTRAP_RELAYS).opts(opts).await?; + let output = client + .sync(filter) + .with(BOOTSTRAP_RELAYS) + .opts(opts) + .await?; Ok(output.value) } + +/// Embed a NIP-46 session key into a bunker URI as `?master=`. +fn with_master_key(uri: &str, keys: &Keys) -> String { + let separator = if uri.contains('?') { '&' } else { '?' }; + let nsec = keys.secret_key().to_bech32().expect("infallible"); + format!("{uri}{separator}master={nsec}") +} + +/// Split a stored bunker credential into the plain URI and the session key. +/// Credentials without an embedded key (legacy) get a fresh one. +fn extract_master_key(credential: &str) -> (&str, Keys) { + match credential.split_once("master=") { + Some((base, nsec)) => { + let keys = SecretKey::parse(nsec) + .map(Keys::new) + .unwrap_or_else(|_| Keys::generate()); + (base.trim_end_matches(['?', '&']), keys) + } + None => (credential, Keys::generate()), + } +}