nostr-connect 0.45.1

Nostr Remote Signing (NIP46)
Documentation
// Copyright (c) 2022-2023 Yuki Kishimoto
// Copyright (c) 2023-2025 Rust Nostr Developers
// Distributed under the MIT software license

//! Nostr Connect signer

use std::sync::Arc;
use std::sync::atomic::{AtomicBool, Ordering};

use nostr::nips::nip46::{
    NostrConnectEventBuilder, NostrConnectMessage, NostrConnectRequest, NostrConnectResponse,
    NostrConnectUri, ResponseResult,
};
use nostr::nips::{nip04, nip44};
use nostr_sdk::prelude::*;

use crate::error::Error;

/// Nostr Connect Keys
#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
pub struct NostrConnectKeys {
    /// The keys used for communication with the client.
    ///
    /// This may be the same as the `user` one, but not necessarily.
    pub signer: Keys,
    /// The keys used to sign events and so on.
    pub user: Keys,
}

impl NostrConnectKeys {
    /// Construct a new [`NostrConnectKeys`]
    pub fn new(signer: Keys, user: Keys) -> Self {
        Self { signer, user }
    }
}

/// Nostr Connect Signer
///
/// Signer that listen for requests from a client, handle them and send the response.
///
/// <https://github.com/nostr-protocol/nips/blob/master/46.md>
#[derive(Debug, Clone)]
pub struct NostrConnectRemoteSigner {
    keys: NostrConnectKeys,
    relays: Vec<RelayUrl>,
    client: Client,
    opts: RelayOptions,
    secret: Option<String>,
    nostr_connect_client_public_key: Option<PublicKey>,
    bootstrapped: Arc<AtomicBool>,
}

impl NostrConnectRemoteSigner {
    /// Construct new remote signer
    pub fn new<'a, I, U>(
        keys: NostrConnectKeys,
        relays: I,
        secret: Option<String>,
        opts: Option<RelayOptions>,
    ) -> Result<Self, Error>
    where
        I: IntoIterator<Item = U>,
        U: Into<RelayUrlArg<'a>>,
    {
        let mut _relays = Vec::new();
        for relay in relays.into_iter() {
            _relays.push(relay.into().try_into_relay_url()?.into_owned());
        }

        Ok(Self {
            keys,
            relays: _relays,
            client: Client::default(),
            opts: opts.unwrap_or_default(),
            secret,
            nostr_connect_client_public_key: None,
            bootstrapped: Arc::new(AtomicBool::new(false)),
        })
    }

    /// Construct remote signer from client URI (`nostrconnect://..`)
    ///
    /// The `secret` embedded in the URI is used automatically. Per NIP-46, the signer must
    /// echo this secret back in the `connect` response so the client can validate it.
    pub fn from_uri(
        uri: NostrConnectUri,
        keys: NostrConnectKeys,
        opts: Option<RelayOptions>,
    ) -> Result<Self, Error> {
        match uri {
            NostrConnectUri::Client {
                public_key,
                relays,
                secret,
                ..
            } => {
                let mut signer = Self::new(keys, relays, Some(secret), opts)?;
                signer.nostr_connect_client_public_key = Some(public_key);
                Ok(signer)
            }
            NostrConnectUri::Bunker { .. } => Err(Error::unexpected_uri()),
        }
    }

    /// Get signer relays
    pub fn relays(&self) -> &[RelayUrl] {
        &self.relays
    }

    /// Get `bunker` URI
    pub fn bunker_uri(&self) -> NostrConnectUri {
        NostrConnectUri::Bunker {
            remote_signer_public_key: self.keys.signer.public_key(),
            relays: self.relays().to_vec(),
            secret: self.secret.clone(),
        }
    }

    async fn send_connect_response(&self, public_key: PublicKey) -> Result<(), Error> {
        let Some(secret) = self.secret.clone() else {
            return Err(Error::no_client_secret());
        };

        // TODO: Fix the request id, should we?
        let res = NostrConnectResponse::with_result(ResponseResult::ConnectSecret(secret));
        let msg: NostrConnectMessage = NostrConnectMessage::response("urmom", res);
        let event: Event =
            NostrConnectEventBuilder::new(public_key, msg).finalize(&self.keys.signer)?;
        self.client.send_event(&event).await?;
        Ok(())
    }

    async fn bootstrap(&self) -> Result<(), Error> {
        // Check if already bootstrapped
        if self.bootstrapped.load(Ordering::SeqCst) {
            return Ok(());
        }

        // Add relays to client
        for url in self.relays.iter().cloned() {
            self.client.add_relay(url).opts(self.opts.clone()).await?;
        }

        // Connect
        self.client.connect().await;

        let filter = Filter::new()
            .pubkey(self.keys.signer.public_key())
            .kind(Kind::NostrConnect)
            .since(Timestamp::now());

        // Subscribe
        self.client.subscribe(filter).await?;

        // Mark as bootstrapped
        self.bootstrapped.store(true, Ordering::SeqCst);

        Ok(())
    }

    fn match_secret(&self, secret: Option<String>) -> bool {
        match (&self.secret, secret) {
            // Both secrets are set, check if values are equal.
            (Some(s1), Some(s2)) => s1 == &s2,
            // Only the secret on our side is set, must return `false`.
            (Some(..), None) => false,
            // Only the secret on their side is set, can continue, return `true`.
            (None, Some(..)) => true,
            // No secret is set
            (None, None) => true,
        }
    }

    /// Serve signer
    pub async fn serve<T>(&self, actions: T) -> Result<(), Error>
    where
        T: NostrConnectSignerActions,
    {
        self.bootstrap().await?;

        // Subscribe to notifications before sending the connect response
        // to avoid missing any client messages that arrive immediately after.
        let mut notifications = self.client.notifications();

        if let Some(public_key) = self.nostr_connect_client_public_key {
            self.send_connect_response(public_key).await?;
        }

        while let Some(notification) = notifications.next().await {
            if let ClientNotification::Event { event, .. } = notification {
                if event.kind == Kind::NostrConnect {
                    match nip44::decrypt(
                        self.keys.signer.secret_key(),
                        &event.pubkey,
                        &event.content,
                    ) {
                        Ok(msg) => {
                            tracing::debug!("New Nostr Connect message received: {msg}");

                            let msg: NostrConnectMessage = NostrConnectMessage::from_json(msg)?;
                            let id: String = msg.id().to_string();

                            if let Ok(req) = msg.to_request() {
                                // Generate response
                                let response: NostrConnectResponse = if actions
                                    .approve(&event.pubkey, &req)
                                {
                                    match req {
                                        NostrConnectRequest::Connect {
                                            remote_signer_public_key,
                                            secret,
                                        } => {
                                            if remote_signer_public_key
                                                == self.keys.signer.public_key()
                                            {
                                                if self.match_secret(secret) {
                                                    NostrConnectResponse::with_result(
                                                        ResponseResult::Ack,
                                                    )
                                                } else {
                                                    NostrConnectResponse::with_error(
                                                        "Secret not match",
                                                    )
                                                }
                                            } else {
                                                NostrConnectResponse::with_error(
                                                    "Remote signer public key not match",
                                                )
                                            }
                                        }
                                        NostrConnectRequest::GetPublicKey => {
                                            NostrConnectResponse::with_result(
                                                ResponseResult::GetPublicKey(
                                                    self.keys.user.public_key(),
                                                ),
                                            )
                                        }
                                        NostrConnectRequest::Nip04Encrypt { public_key, text } => {
                                            match nip04::encrypt(
                                                self.keys.user.secret_key(),
                                                &public_key,
                                                text,
                                            ) {
                                                Ok(ciphertext) => {
                                                    NostrConnectResponse::with_result(
                                                        ResponseResult::Nip04Encrypt { ciphertext },
                                                    )
                                                }
                                                Err(e) => {
                                                    NostrConnectResponse::with_error(e.to_string())
                                                }
                                            }
                                        }
                                        NostrConnectRequest::Nip04Decrypt {
                                            public_key,
                                            ciphertext,
                                        } => {
                                            match nip04::decrypt(
                                                self.keys.user.secret_key(),
                                                &public_key,
                                                ciphertext,
                                            ) {
                                                Ok(plaintext) => NostrConnectResponse::with_result(
                                                    ResponseResult::Nip04Decrypt { plaintext },
                                                ),
                                                Err(e) => {
                                                    NostrConnectResponse::with_error(e.to_string())
                                                }
                                            }
                                        }
                                        NostrConnectRequest::Nip44Encrypt { public_key, text } => {
                                            match nip44::encrypt(
                                                self.keys.user.secret_key(),
                                                &public_key,
                                                text,
                                                nip44::Version::default(),
                                            ) {
                                                Ok(ciphertext) => {
                                                    NostrConnectResponse::with_result(
                                                        ResponseResult::Nip44Encrypt { ciphertext },
                                                    )
                                                }
                                                Err(e) => {
                                                    NostrConnectResponse::with_error(e.to_string())
                                                }
                                            }
                                        }
                                        NostrConnectRequest::Nip44Decrypt {
                                            public_key,
                                            ciphertext,
                                        } => {
                                            match nip44::decrypt(
                                                self.keys.user.secret_key(),
                                                &public_key,
                                                ciphertext,
                                            ) {
                                                Ok(plaintext) => NostrConnectResponse::with_result(
                                                    ResponseResult::Nip44Decrypt { plaintext },
                                                ),
                                                Err(e) => {
                                                    NostrConnectResponse::with_error(e.to_string())
                                                }
                                            }
                                        }
                                        NostrConnectRequest::SignEvent(unsigned) => {
                                            match unsigned.finalize(&self.keys.user) {
                                                Ok(event) => NostrConnectResponse::with_result(
                                                    ResponseResult::SignEvent(Box::new(event)),
                                                ),
                                                Err(e) => {
                                                    NostrConnectResponse::with_error(e.to_string())
                                                }
                                            }
                                        }
                                        NostrConnectRequest::Ping => {
                                            NostrConnectResponse::with_result(ResponseResult::Pong)
                                        }
                                    }
                                } else {
                                    NostrConnectResponse::with_error("Rejected")
                                };

                                // Compose message
                                let msg: NostrConnectMessage =
                                    NostrConnectMessage::response(id, response);

                                // Compose and publish event
                                let event = NostrConnectEventBuilder::new(event.pubkey, msg)
                                    .finalize(&self.keys.signer)?;
                                self.client.send_event(&event).await?;
                            }
                        }
                        Err(e) => {
                            tracing::error!(error = %e, "Impossible to decrypt message.")
                        }
                    }
                }
            }
        }

        Ok(())
    }
}

/// Nostr Connect signer actions
pub trait NostrConnectSignerActions {
    /// Approve
    fn approve(&self, public_key: &PublicKey, req: &NostrConnectRequest) -> bool;
}