update nostr connect

This commit is contained in:
2026-08-05 10:37:33 +07:00
parent ed93d81a26
commit 8de6018e28
+45 -42
View File
@@ -9,10 +9,9 @@ use nostr_sdk::prelude::*;
use signed_core::filters; use signed_core::filters;
use signed_nostr::{NostrBackend, SignedAuthUrlHandler, UniversalSigner, Update}; use signed_nostr::{NostrBackend, SignedAuthUrlHandler, UniversalSigner, Update};
/// Keyring entry holding the user credential (`nsec1...` or `bunker://...`). /// Keyring entry holding the user credential (`nsec1...` or `bunker://...`
pub const USER_KEYRING: &str = "su.reya.signed#user"; /// with an embedded `?master=<nsec>` NIP-46 session key).
/// Keyring entry holding the locally generated key for NIP-46 sessions. pub const USER_KEYRING: &str = "Signed Safe Storage";
pub const MASTER_KEYRING: &str = "su.reya.signed#master";
/// Timeout for NIP-46 signer responses. /// Timeout for NIP-46 signer responses.
pub const NOSTR_CONNECT_TIMEOUT: u64 = 60; pub const NOSTR_CONNECT_TIMEOUT: u64 = 60;
@@ -175,7 +174,6 @@ impl Backend {
} }
let user = cx.read_credentials(USER_KEYRING); let user = cx.read_credentials(USER_KEYRING);
let master = self.master_key(cx);
self.tasks.push(cx.spawn(async move |this, cx| { self.tasks.push(cx.spawn(async move |this, cx| {
let content = match user.await { let content = match user.await {
@@ -191,10 +189,11 @@ impl Backend {
let keys = Keys::new(SecretKey::parse(&content)?); let keys = Keys::new(SecretKey::parse(&content)?);
this.update(cx, |this, cx| this.set_signer(keys, cx))?; this.update(cx, |this, cx| this.set_signer(keys, cx))?;
} else if content.starts_with("bunker://") { } 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( let mut signer = NostrConnect::new(
uri, uri,
master.await, keys,
Duration::from_secs(NOSTR_CONNECT_TIMEOUT), Duration::from_secs(NOSTR_CONNECT_TIMEOUT),
None, None,
)?; )?;
@@ -246,9 +245,11 @@ impl Backend {
})); }));
} }
/// Login with a `bunker://...` URI (NIP-46). The auth URL, if any, is /// Login with a `bunker://...` URI (NIP-46). A fresh session key is
/// opened in the default browser. The credential is persisted in the /// generated and embedded into the stored URI as `?master=<nsec>`, so
/// keyring after the signer proves reachable. /// 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<Self>) { pub fn login_with_bunker(&mut self, uri: &str, cx: &mut Context<Self>) {
let uri_string = uri.trim().to_owned(); let uri_string = uri.trim().to_owned();
@@ -260,14 +261,15 @@ impl Backend {
} }
}; };
let master = self.master_key(cx); let keys = Keys::generate();
let write = cx.write_credentials(USER_KEYRING, "bunker", uri_string.as_bytes()); 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| { self.tasks.push(cx.spawn(async move |this, cx| {
let result = async { let result = async {
let mut signer = NostrConnect::new( let mut signer = NostrConnect::new(
connect_uri, connect_uri,
master.await, keys,
Duration::from_secs(NOSTR_CONNECT_TIMEOUT), Duration::from_secs(NOSTR_CONNECT_TIMEOUT),
None, 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<Keys> {
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 /// Fetch the user's grasp list (kind `10317`) and add the listed grasp
/// servers as relays. /// servers as relays.
fn bootstrap_user(&mut self, public_key: PublicKey, cx: &mut Context<Self>) { fn bootstrap_user(&mut self, public_key: PublicKey, cx: &mut Context<Self>) {
@@ -631,7 +606,10 @@ impl Backend {
/// Subscribe only on the bootstrap relays, auto-closing after EOSE or a /// Subscribe only on the bootstrap relays, auto-closing after EOSE or a
/// short timeout. Use for one-shot data fetches (repo events, profiles) /// short timeout. Use for one-shot data fetches (repo events, profiles)
/// instead of persistent gossip-routed subscriptions. /// instead of persistent gossip-routed subscriptions.
pub(crate) async fn subscribe_bootstrap_only(client: &Client, filters: Vec<Filter>) -> Result<(), Error> { pub(crate) async fn subscribe_bootstrap_only(
client: &Client,
filters: Vec<Filter>,
) -> Result<(), Error> {
let opts = SubscribeAutoCloseOptions::default() let opts = SubscribeAutoCloseOptions::default()
.exit_policy(ReqExitPolicy::ExitOnEOSE) .exit_policy(ReqExitPolicy::ExitOnEOSE)
.timeout(Some(Duration::from_secs(10))); .timeout(Some(Duration::from_secs(10)));
@@ -652,6 +630,31 @@ pub(crate) async fn sync_bootstrap_only(
filter: Filter, filter: Filter,
opts: SyncOptions, opts: SyncOptions,
) -> Result<SyncSummary, Error> { ) -> Result<SyncSummary, Error> {
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) Ok(output.value)
} }
/// Embed a NIP-46 session key into a bunker URI as `?master=<nsec>`.
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()),
}
}