From ff49164f47a6d2a5c64d85df74ef973328f2ecf3 Mon Sep 17 00:00:00 2001 From: Yuki Kishimoto Date: Wed, 30 Oct 2024 15:00:45 +0100 Subject: [PATCH] ffi(nostr): expose `NostrSigner` Signed-off-by: Yuki Kishimoto --- Cargo.lock | 5 +- bindings/nostr-ffi/Cargo.toml | 1 + .../bindings-python/examples/event_builder.py | 53 ++++--- bindings/nostr-ffi/src/error.rs | 6 + bindings/nostr-ffi/src/event/builder.rs | 30 ++-- bindings/nostr-ffi/src/key/mod.rs | 54 ++++++- bindings/nostr-ffi/src/lib.rs | 1 + bindings/nostr-ffi/src/nips/nip59.rs | 27 ++-- bindings/nostr-ffi/src/signer.rs | 145 ++++++++++++++++++ 9 files changed, 277 insertions(+), 45 deletions(-) create mode 100644 bindings/nostr-ffi/src/signer.rs diff --git a/Cargo.lock b/Cargo.lock index ffbe5c838..226fe4968 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2832,6 +2832,7 @@ dependencies = [ name = "nostr-ffi" version = "0.1.0" dependencies = [ + "async-trait", "nostr", "uniffi", ] @@ -5905,9 +5906,9 @@ dependencies = [ [[package]] name = "uniffi_checksum_derive" -version = "0.28.1" +version = "0.28.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a22dbe67c1c957ac6e7611bdf605a6218aa86b0eebeb8be58b70ae85ad7d73dc" +checksum = "d2c801f0f05b06df456a2da4c41b9c2c4fdccc6b9916643c6c67275c4c9e4d07" dependencies = [ "quote", "syn 2.0.77", diff --git a/bindings/nostr-ffi/Cargo.toml b/bindings/nostr-ffi/Cargo.toml index 72b119cd3..029d19c18 100644 --- a/bindings/nostr-ffi/Cargo.toml +++ b/bindings/nostr-ffi/Cargo.toml @@ -9,6 +9,7 @@ name = "nostr_ffi" crate-type = ["lib", "cdylib", "staticlib"] [dependencies] +async-trait.workspace = true nostr = { workspace = true, features = ["std", "all-nips"] } uniffi = { workspace = true, features = ["tokio"] } diff --git a/bindings/nostr-ffi/bindings-python/examples/event_builder.py b/bindings/nostr-ffi/bindings-python/examples/event_builder.py index b2398d93e..58232a09f 100644 --- a/bindings/nostr-ffi/bindings-python/examples/event_builder.py +++ b/bindings/nostr-ffi/bindings-python/examples/event_builder.py @@ -1,30 +1,37 @@ -from nostr_protocol import Keys, PublicKey, EventBuilder, Event, Tag, Kind +import asyncio +from nostr_protocol import Keys, EventBuilder, Kind -keys = Keys.generate() -# Build a text note -event = EventBuilder.text_note("New note from Rust Nostr python bindings", []).to_event(keys) -print(event.as_json()) +async def main(): + keys = Keys.generate() -# Build a DM -receiver_pk = PublicKey.from_bech32("npub14f8usejl26twx0dhuxjh9cas7keav9vr0v8nvtwtrjqx3vycc76qqh9nsy") -event = EventBuilder.encrypted_direct_msg(keys, receiver_pk, "New note from Rust Nostr python bindings", None).to_event(keys) -print(event.as_json()) + # Build a text note + builder = EventBuilder.text_note("Note from rust-nostr python bindings", []) + event = await builder.sign(keys) + print(event.as_json()) -# Build a custom event -kind = Kind(1234) -content = "My custom content" -tags = [] -builder = EventBuilder(kind, content, tags) + # Build a custom event + kind = Kind(1234) + content = "My custom content" + tags = [] + builder = EventBuilder(kind, content, tags) -# Normal -event = builder.to_event(keys) -print(f"Event: {event.as_json()}") + # Sign with generic signer + event = await builder.sign(keys) + print(f"Event: {event.as_json()}") -# POW -event = builder.to_pow_event(keys, 20) -print(f"POW event: {event.as_json()}") + # Sign specifically with keys + event = builder.sign_with_keys(keys) + print(f"Event: {event.as_json()}") -# Unsigned -event = builder.to_unsigned_event(keys.public_key()) -print(f"Event: {event.as_json()}") \ No newline at end of file + # POW + event = await builder.pow(24).sign(keys) + print(f"POW event: {event.as_json()}") + + # Build unsigned event + event = builder.build(keys.public_key()) + print(f"Event: {event.as_json()}") + + +if __name__ == '__main__': + asyncio.run(main()) diff --git a/bindings/nostr-ffi/src/error.rs b/bindings/nostr-ffi/src/error.rs index abf8a3793..046174ed6 100644 --- a/bindings/nostr-ffi/src/error.rs +++ b/bindings/nostr-ffi/src/error.rs @@ -38,6 +38,12 @@ impl From for NostrError { } } +impl From for NostrError { + fn from(e: nostr::signer::SignerError) -> NostrError { + Self::Generic(e.to_string()) + } +} + impl From for NostrError { fn from(e: nostr::key::Error) -> NostrError { Self::Generic(e.to_string()) diff --git a/bindings/nostr-ffi/src/event/builder.rs b/bindings/nostr-ffi/src/event/builder.rs index 8f2a46cf3..ae3d59c73 100644 --- a/bindings/nostr-ffi/src/event/builder.rs +++ b/bindings/nostr-ffi/src/event/builder.rs @@ -22,6 +22,7 @@ use crate::nips::nip53::LiveEvent; use crate::nips::nip57::ZapRequestData; use crate::nips::nip90::JobFeedbackData; use crate::nips::nip98::HttpData; +use crate::signer::{IntermediateNostrSigner, NostrSigner}; use crate::types::{Contact, Metadata}; use crate::{ FileMetadata, Image, ImageDimensions, NostrConnectMessage, PublicKey, RelayMetadata, Tag, @@ -48,8 +49,11 @@ impl Deref for EventBuilder { } } -#[uniffi::export] +#[uniffi::export(async_runtime = "tokio")] impl EventBuilder { + // `#[uniffi::export(async_runtime = "tokio")]` require an async method + async fn _none(&self) {} + #[uniffi::constructor] pub fn new(kind: &Kind, content: &str, tags: &[Arc]) -> Self { let tags = tags.iter().map(|t| t.as_ref().deref().clone()); @@ -82,13 +86,19 @@ impl EventBuilder { builder } - pub fn to_event(&self, keys: &Keys) -> Result { - let event = self.inner.clone().to_event(keys.deref())?; + pub async fn sign(&self, signer: Arc) -> Result { + let signer = IntermediateNostrSigner::new(signer); + let event = self.inner.clone().sign(&signer).await?; + Ok(event.into()) + } + + pub fn sign_with_keys(&self, keys: &Keys) -> Result { + let event = self.inner.clone().sign_with_keys(keys.deref())?; Ok(event.into()) } - pub fn to_unsigned_event(&self, public_key: &PublicKey) -> UnsignedEvent { - self.inner.clone().to_unsigned_event(**public_key).into() + pub fn build(&self, public_key: &PublicKey) -> UnsignedEvent { + self.inner.clone().build(**public_key).into() } /// Profile metadata @@ -549,17 +559,19 @@ impl EventBuilder { /// #[inline] #[uniffi::constructor] - pub fn seal( - sender_keys: &Keys, + pub async fn seal( + signer: Arc, receiver_public_key: &PublicKey, rumor: &UnsignedEvent, ) -> Result { + let signer = IntermediateNostrSigner::new(signer); Ok(Self { inner: nostr::EventBuilder::seal( - sender_keys.deref(), + &signer, receiver_public_key.deref(), rumor.deref().clone(), - )?, + ) + .await?, }) } diff --git a/bindings/nostr-ffi/src/key/mod.rs b/bindings/nostr-ffi/src/key/mod.rs index 2b5f45990..e08d9c7e2 100644 --- a/bindings/nostr-ffi/src/key/mod.rs +++ b/bindings/nostr-ffi/src/key/mod.rs @@ -3,10 +3,11 @@ // Distributed under the MIT software license use std::ops::Deref; +use std::sync::Arc; -use nostr::key; use nostr::nips::nip06::FromMnemonic; use nostr::secp256k1::Message; +use nostr::{key, NostrSigner as _}; use uniffi::Object; mod public_key; @@ -15,6 +16,8 @@ mod secret_key; pub use self::public_key::PublicKey; pub use self::secret_key::SecretKey; use crate::error::Result; +use crate::signer::NostrSigner; +use crate::{Event, UnsignedEvent}; /// Nostr keys #[derive(Debug, PartialEq, Eq, Object)] @@ -110,3 +113,52 @@ impl Keys { Ok(self.inner.sign_schnorr(&message).to_string()) } } + +#[uniffi::export] +#[async_trait::async_trait] +impl NostrSigner for Keys { + async fn get_public_key(&self) -> Result>> { + Ok(Some(Arc::new(self.inner.get_public_key().await?.into()))) + } + + async fn sign_event(&self, unsigned: Arc) -> Result>> { + Ok(Some(Arc::new( + self.inner + .sign_event(unsigned.as_ref().deref().clone()) + .await? + .into(), + ))) + } + + async fn nip04_encrypt(&self, public_key: Arc, content: String) -> Result { + Ok(self + .inner + .nip04_encrypt(public_key.as_ref().deref(), &content) + .await?) + } + + async fn nip04_decrypt( + &self, + public_key: Arc, + encrypted_content: String, + ) -> Result { + Ok(self + .inner + .nip04_decrypt(public_key.as_ref().deref(), &encrypted_content) + .await?) + } + + async fn nip44_encrypt(&self, public_key: Arc, content: String) -> Result { + Ok(self + .inner + .nip44_encrypt(public_key.as_ref().deref(), &content) + .await?) + } + + async fn nip44_decrypt(&self, public_key: Arc, payload: String) -> Result { + Ok(self + .inner + .nip44_decrypt(public_key.as_ref().deref(), &payload) + .await?) + } +} diff --git a/bindings/nostr-ffi/src/lib.rs b/bindings/nostr-ffi/src/lib.rs index e9316757a..0db018c58 100644 --- a/bindings/nostr-ffi/src/lib.rs +++ b/bindings/nostr-ffi/src/lib.rs @@ -14,6 +14,7 @@ pub mod helper; pub mod key; pub mod message; pub mod nips; +pub mod signer; pub mod types; pub mod util; diff --git a/bindings/nostr-ffi/src/nips/nip59.rs b/bindings/nostr-ffi/src/nips/nip59.rs index ffa6bb6af..fda303b01 100644 --- a/bindings/nostr-ffi/src/nips/nip59.rs +++ b/bindings/nostr-ffi/src/nips/nip59.rs @@ -9,24 +9,27 @@ use nostr::EventBuilder; use uniffi::Object; use crate::error::Result; -use crate::{Event, Keys, PublicKey, Timestamp, UnsignedEvent}; +use crate::signer::{IntermediateNostrSigner, NostrSigner}; +use crate::{Event, PublicKey, Timestamp, UnsignedEvent}; /// Build Gift Wrap /// /// -#[uniffi::export(default(expiration = None))] -pub fn gift_wrap( - sender_keys: &Keys, +#[uniffi::export(async_runtime = "tokio", default(expiration = None))] +pub async fn gift_wrap( + signer: Arc, receiver_pubkey: &PublicKey, rumor: &UnsignedEvent, expiration: Option>, ) -> Result { + let signer = IntermediateNostrSigner::new(signer); Ok(EventBuilder::gift_wrap( - sender_keys.deref(), + &signer, receiver_pubkey.deref(), rumor.deref().clone(), expiration.map(|t| **t), - )? + ) + .await? .into()) } @@ -55,20 +58,24 @@ pub struct UnwrappedGift { } impl From for UnwrappedGift { - fn from(inner: nostr::prelude::UnwrappedGift) -> Self { + fn from(inner: nip59::UnwrappedGift) -> Self { Self { inner } } } -#[uniffi::export] +#[uniffi::export(async_runtime = "tokio")] impl UnwrappedGift { + // `#[uniffi::export(async_runtime = "tokio")]` require an async method + async fn _none(&self) {} + /// Unwrap Gift Wrap event /// /// Internally verify the `seal` event #[uniffi::constructor] - pub fn from_gift_wrap(receiver_keys: &Keys, gift_wrap: &Event) -> Result { + pub async fn from_gift_wrap(signer: Arc, gift_wrap: &Event) -> Result { + let signer = IntermediateNostrSigner::new(signer); Ok(Self { - inner: nip59::UnwrappedGift::from_gift_wrap(receiver_keys.deref(), gift_wrap.deref())?, + inner: nip59::UnwrappedGift::from_gift_wrap(&signer, gift_wrap.deref()).await?, }) } diff --git a/bindings/nostr-ffi/src/signer.rs b/bindings/nostr-ffi/src/signer.rs new file mode 100644 index 000000000..8913dd795 --- /dev/null +++ b/bindings/nostr-ffi/src/signer.rs @@ -0,0 +1,145 @@ +// Copyright (c) 2022-2023 Yuki Kishimoto +// Copyright (c) 2023-2024 Rust Nostr Developers +// Distributed under the MIT software license + +use std::fmt; +use std::sync::Arc; + +use crate::error::Result; +use crate::event::{Event, UnsignedEvent}; +use crate::key::PublicKey; + +// NOTE: for some weird reason the `Arc` as output must be wrapped inside a `Vec` or an `Option` +// otherwise compilation will fail. +#[uniffi::export(with_foreign)] +#[async_trait::async_trait] +pub trait NostrSigner: Send + Sync { + /// Get signer public key + async fn get_public_key(&self) -> Result>>; + + /// Sign an unsigned event + async fn sign_event(&self, unsigned: Arc) -> Result>>; + + /// NIP04 encrypt (deprecate and unsecure) + async fn nip04_encrypt(&self, public_key: Arc, content: String) -> Result; + + /// NIP04 decrypt + async fn nip04_decrypt( + &self, + public_key: Arc, + encrypted_content: String, + ) -> Result; + + /// NIP44 encrypt + async fn nip44_encrypt(&self, public_key: Arc, content: String) -> Result; + + /// NIP44 decrypt + async fn nip44_decrypt(&self, public_key: Arc, payload: String) -> Result; +} + +pub struct IntermediateNostrSigner { + pub(super) inner: Arc, +} + +impl fmt::Debug for IntermediateNostrSigner { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + f.debug_struct("IntermediateNostrSigner").finish() + } +} + +impl IntermediateNostrSigner { + pub fn new(inner: Arc) -> Self { + Self { inner } + } +} + +mod inner { + use std::ops::Deref; + use std::sync::Arc; + + use async_trait::async_trait; + use nostr::prelude::*; + + use super::IntermediateNostrSigner; + use crate::NostrError; + + #[async_trait] + impl NostrSigner for IntermediateNostrSigner { + async fn get_public_key(&self) -> Result { + let public_key = self + .inner + .get_public_key() + .await + .map_err(SignerError::backend)? + .ok_or_else(|| { + SignerError::backend(NostrError::Generic(String::from( + "Received None instead of public key", + ))) + })?; + Ok(**public_key) + } + + async fn sign_event(&self, unsigned: UnsignedEvent) -> Result { + let unsigned = Arc::new(unsigned.into()); + let event = self + .inner + .sign_event(unsigned) + .await + .map_err(SignerError::backend)? + .ok_or_else(|| { + SignerError::backend(NostrError::Generic(String::from( + "Received None instead of event", + ))) + })?; + Ok(event.as_ref().deref().clone()) + } + + async fn nip04_encrypt( + &self, + public_key: &PublicKey, + content: &str, + ) -> Result { + let public_key = Arc::new((*public_key).into()); + self.inner + .nip04_encrypt(public_key, content.to_string()) + .await + .map_err(SignerError::backend) + } + + async fn nip04_decrypt( + &self, + public_key: &PublicKey, + encrypted_content: &str, + ) -> Result { + let public_key = Arc::new((*public_key).into()); + self.inner + .nip04_decrypt(public_key, encrypted_content.to_string()) + .await + .map_err(SignerError::backend) + } + + async fn nip44_encrypt( + &self, + public_key: &PublicKey, + content: &str, + ) -> Result { + let public_key = Arc::new((*public_key).into()); + self.inner + .nip44_encrypt(public_key, content.to_string()) + .await + .map_err(SignerError::backend) + } + + async fn nip44_decrypt( + &self, + public_key: &PublicKey, + payload: &str, + ) -> Result { + let public_key = Arc::new((*public_key).into()); + self.inner + .nip44_decrypt(public_key, payload.to_string()) + .await + .map_err(SignerError::backend) + } + } +}