lib

Core libraries for Radroots
git clone https://radroots.dev/git/lib.git
Log | Files | Refs | README | LICENSE

commit d9a3493a4bc65b8187b9d52de420e4a5e44e8da0
parent 9cfbe20e86656898cc9fb124c662e29f3d1d32dc
Author: triesap <tyson@radroots.org>
Date:   Sun, 12 Apr 2026 05:45:53 +0000

nostr_signer: extract nip46 session codec

Diffstat:
Mcrates/nostr_signer/Cargo.toml | 1+
Mcrates/nostr_signer/src/lib.rs | 6++++++
Acrates/nostr_signer/src/nip46.rs | 292+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
3 files changed, 299 insertions(+), 0 deletions(-)

diff --git a/crates/nostr_signer/Cargo.toml b/crates/nostr_signer/Cargo.toml @@ -23,6 +23,7 @@ radroots_identity = { workspace = true, default-features = false, features = [ "std", "profile", ] } +radroots_nostr = { workspace = true, features = ["std"] } radroots_nostr_connect = { workspace = true } radroots_runtime = { workspace = true } radroots_sql_core = { workspace = true, optional = true } diff --git a/crates/nostr_signer/src/lib.rs b/crates/nostr_signer/src/lib.rs @@ -9,6 +9,7 @@ pub mod manager; #[cfg(feature = "native")] pub mod migrations; pub mod model; +pub mod nip46; #[cfg(feature = "native")] pub mod sqlite; pub mod store; @@ -48,6 +49,11 @@ pub mod prelude { RadrootsNostrSignerRequestId, RadrootsNostrSignerSecretDigestAlgorithm, RadrootsNostrSignerStoreState, RadrootsNostrSignerWorkflowId, }; + pub use crate::nip46::{ + RadrootsNostrSignerHandledRequest, RadrootsNostrSignerNip46Codec, + RadrootsNostrSignerNip46Signer, connect_response_outcome, handled_request_for_action, + response_from_hint, + }; #[cfg(feature = "native")] pub use crate::sqlite::RadrootsNostrSignerSqliteDb; #[cfg(feature = "native")] diff --git a/crates/nostr_signer/src/nip46.rs b/crates/nostr_signer/src/nip46.rs @@ -0,0 +1,292 @@ +use nostr::UnsignedEvent; +use radroots_nostr::prelude::{ + RadrootsNostrEvent, RadrootsNostrEventBuilder, RadrootsNostrFilter, RadrootsNostrKind, + RadrootsNostrPublicKey, RadrootsNostrTag, RadrootsNostrTimestamp, radroots_nostr_filter_tag, +}; +use radroots_nostr_connect::prelude::{ + RADROOTS_NOSTR_CONNECT_RPC_KIND, RadrootsNostrConnectRequest, + RadrootsNostrConnectRequestMessage, RadrootsNostrConnectResponse, +}; + +use crate::error::RadrootsNostrSignerError; +use crate::evaluation::{RadrootsNostrSignerRequestAction, RadrootsNostrSignerRequestResponseHint}; +use crate::model::{RadrootsNostrSignerConnectionId, RadrootsNostrSignerConnectionRecord}; + +pub trait RadrootsNostrSignerNip46Signer: Clone + Send + Sync { + fn signer_public_key_hex(&self) -> String; + fn decrypt_request( + &self, + client_public_key: &RadrootsNostrPublicKey, + ciphertext: &str, + ) -> Result<String, RadrootsNostrSignerError>; + fn encrypt_response( + &self, + client_public_key: &RadrootsNostrPublicKey, + payload: &str, + ) -> Result<String, RadrootsNostrSignerError>; + fn user_public_key(&self) -> RadrootsNostrPublicKey; + fn sign_user_event( + &self, + unsigned_event: UnsignedEvent, + ) -> Result<RadrootsNostrEvent, RadrootsNostrSignerError>; + fn nip04_encrypt( + &self, + public_key: &RadrootsNostrPublicKey, + plaintext: &str, + ) -> Result<String, RadrootsNostrSignerError>; + fn nip04_decrypt( + &self, + public_key: &RadrootsNostrPublicKey, + ciphertext: &str, + ) -> Result<String, RadrootsNostrSignerError>; + fn nip44_encrypt( + &self, + public_key: &RadrootsNostrPublicKey, + plaintext: &str, + ) -> Result<String, RadrootsNostrSignerError>; + fn nip44_decrypt( + &self, + public_key: &RadrootsNostrPublicKey, + ciphertext: &str, + ) -> Result<String, RadrootsNostrSignerError>; +} + +#[derive(Clone)] +pub struct RadrootsNostrSignerNip46Codec<S> { + signer: S, +} + +#[derive(Debug, Clone, PartialEq, Eq)] +pub enum RadrootsNostrSignerHandledRequest { + Respond { + response: RadrootsNostrConnectResponse, + connection_id: Option<RadrootsNostrSignerConnectionId>, + consume_connect_secret_for: Option<RadrootsNostrSignerConnectionId>, + }, + Ignore, +} + +impl<S: RadrootsNostrSignerNip46Signer> RadrootsNostrSignerNip46Codec<S> { + pub fn new(signer: S) -> Self { + Self { signer } + } + + pub fn filter(&self) -> Result<RadrootsNostrFilter, RadrootsNostrSignerError> { + let filter = RadrootsNostrFilter::new() + .kind(RadrootsNostrKind::Custom(RADROOTS_NOSTR_CONNECT_RPC_KIND)) + .since(RadrootsNostrTimestamp::now()); + radroots_nostr_filter_tag(filter, "p", vec![self.signer.signer_public_key_hex()]) + .map_err(|error| RadrootsNostrSignerError::InvalidState(error.to_string())) + } + + pub fn parse_request_event( + &self, + event: &RadrootsNostrEvent, + ) -> Result<RadrootsNostrConnectRequestMessage, RadrootsNostrSignerError> { + let decrypted = self.signer.decrypt_request(&event.pubkey, &event.content)?; + serde_json::from_str(&decrypted) + .map_err(radroots_nostr_connect::prelude::RadrootsNostrConnectError::from) + .map_err(|error| RadrootsNostrSignerError::InvalidState(error.to_string())) + } + + pub fn build_response_event( + &self, + client_public_key: RadrootsNostrPublicKey, + request_id: impl Into<String>, + response: RadrootsNostrConnectResponse, + ) -> Result<RadrootsNostrEventBuilder, RadrootsNostrSignerError> { + let envelope = response + .into_envelope(request_id.into()) + .map_err(|error| RadrootsNostrSignerError::InvalidState(error.to_string()))?; + let payload = serde_json::to_string(&envelope) + .map_err(|error| RadrootsNostrSignerError::InvalidState(error.to_string()))?; + let ciphertext = self.signer.encrypt_response(&client_public_key, &payload)?; + + Ok(RadrootsNostrEventBuilder::new( + RadrootsNostrKind::Custom(RADROOTS_NOSTR_CONNECT_RPC_KIND), + ciphertext, + ) + .tags(vec![RadrootsNostrTag::public_key(client_public_key)])) + } + + pub fn sign_event_response( + &self, + unsigned_event: UnsignedEvent, + ) -> Result<RadrootsNostrConnectResponse, RadrootsNostrSignerError> { + let user_public_key = self.signer.user_public_key(); + if unsigned_event.pubkey != user_public_key { + return Ok(RadrootsNostrConnectResponse::Error { + result: None, + error: "sign_event pubkey does not match the managed user identity".to_owned(), + }); + } + + match self.signer.sign_user_event(unsigned_event) { + Ok(event) => Ok(RadrootsNostrConnectResponse::SignedEvent(event)), + Err(error) => Ok(RadrootsNostrConnectResponse::Error { + result: None, + error: format!("failed to sign event: {error}"), + }), + } + } + + pub fn crypto_response( + &self, + request: RadrootsNostrConnectRequest, + ) -> Result<RadrootsNostrConnectResponse, RadrootsNostrSignerError> { + Ok(match request { + RadrootsNostrConnectRequest::Nip04Encrypt { + public_key, + plaintext, + } => match self.signer.nip04_encrypt(&public_key, &plaintext) { + Ok(ciphertext) => RadrootsNostrConnectResponse::Nip04Encrypt(ciphertext), + Err(error) => RadrootsNostrConnectResponse::Error { + result: None, + error: format!("nip04 encrypt failed: {error}"), + }, + }, + RadrootsNostrConnectRequest::Nip04Decrypt { + public_key, + ciphertext, + } => match self.signer.nip04_decrypt(&public_key, &ciphertext) { + Ok(plaintext) => RadrootsNostrConnectResponse::Nip04Decrypt(plaintext), + Err(error) => RadrootsNostrConnectResponse::Error { + result: None, + error: format!("nip04 decrypt failed: {error}"), + }, + }, + RadrootsNostrConnectRequest::Nip44Encrypt { + public_key, + plaintext, + } => match self.signer.nip44_encrypt(&public_key, &plaintext) { + Ok(ciphertext) => RadrootsNostrConnectResponse::Nip44Encrypt(ciphertext), + Err(error) => RadrootsNostrConnectResponse::Error { + result: None, + error: format!("nip44 encrypt failed: {error}"), + }, + }, + RadrootsNostrConnectRequest::Nip44Decrypt { + public_key, + ciphertext, + } => match self.signer.nip44_decrypt(&public_key, &ciphertext) { + Ok(plaintext) => RadrootsNostrConnectResponse::Nip44Decrypt(plaintext), + Err(error) => RadrootsNostrConnectResponse::Error { + result: None, + error: format!("nip44 decrypt failed: {error}"), + }, + }, + other => RadrootsNostrConnectResponse::Error { + result: None, + error: format!("request `{}` is not a crypto method", other.method()), + }, + }) + } +} + +impl RadrootsNostrSignerHandledRequest { + pub fn respond(response: RadrootsNostrConnectResponse) -> Self { + Self::respond_for_connection(None, response) + } + + pub fn respond_for_connection( + connection_id: Option<RadrootsNostrSignerConnectionId>, + response: RadrootsNostrConnectResponse, + ) -> Self { + Self::Respond { + response, + connection_id, + consume_connect_secret_for: None, + } + } + + pub fn into_publish_parts( + self, + ) -> Option<( + RadrootsNostrConnectResponse, + Option<RadrootsNostrSignerConnectionId>, + Option<RadrootsNostrSignerConnectionId>, + )> { + match self { + Self::Respond { + response, + connection_id, + consume_connect_secret_for, + } => Some((response, connection_id, consume_connect_secret_for)), + Self::Ignore => None, + } + } +} + +pub fn connect_response_outcome( + connection: &RadrootsNostrSignerConnectionRecord, + secret: Option<String>, +) -> RadrootsNostrSignerHandledRequest { + let consume_connect_secret_for = secret.as_ref().map(|_| connection.connection_id.clone()); + RadrootsNostrSignerHandledRequest::Respond { + response: match secret { + Some(secret) => RadrootsNostrConnectResponse::ConnectSecretEcho(secret), + None => RadrootsNostrConnectResponse::ConnectAcknowledged, + }, + connection_id: Some(connection.connection_id.clone()), + consume_connect_secret_for, + } +} + +pub fn response_from_hint( + connection: &RadrootsNostrSignerConnectionRecord, + hint: RadrootsNostrSignerRequestResponseHint, +) -> RadrootsNostrConnectResponse { + match hint { + RadrootsNostrSignerRequestResponseHint::Pong => RadrootsNostrConnectResponse::Pong, + RadrootsNostrSignerRequestResponseHint::UserPublicKey(public_key) => { + RadrootsNostrConnectResponse::UserPublicKey(public_key) + } + RadrootsNostrSignerRequestResponseHint::RemoteSessionCapability(capability) => { + RadrootsNostrConnectResponse::RemoteSessionCapability(capability) + } + RadrootsNostrSignerRequestResponseHint::RelayList(relays) => { + if relays == connection.relays { + RadrootsNostrConnectResponse::RelayList(relays) + } else { + RadrootsNostrConnectResponse::RelayList(connection.relays.clone()) + } + } + RadrootsNostrSignerRequestResponseHint::None => RadrootsNostrConnectResponse::Error { + result: None, + error: "request evaluation did not provide a response hint".to_owned(), + }, + } +} + +pub fn handled_request_for_action<F>( + connection: &RadrootsNostrSignerConnectionRecord, + action: RadrootsNostrSignerRequestAction, + on_allowed: F, +) -> Result<RadrootsNostrSignerHandledRequest, RadrootsNostrSignerError> +where + F: FnOnce() -> Result<RadrootsNostrConnectResponse, RadrootsNostrSignerError>, +{ + Ok(match action { + RadrootsNostrSignerRequestAction::Denied { reason } => { + RadrootsNostrSignerHandledRequest::respond_for_connection( + Some(connection.connection_id.clone()), + RadrootsNostrConnectResponse::Error { + result: None, + error: reason, + }, + ) + } + RadrootsNostrSignerRequestAction::Challenged { auth_challenge, .. } => { + RadrootsNostrSignerHandledRequest::respond_for_connection( + Some(connection.connection_id.clone()), + RadrootsNostrConnectResponse::AuthUrl(auth_challenge.auth_url), + ) + } + RadrootsNostrSignerRequestAction::Allowed { .. } => { + RadrootsNostrSignerHandledRequest::respond_for_connection( + Some(connection.connection_id.clone()), + on_allowed()?, + ) + } + }) +}