Skip to main content

nostr_double_ratchet/
lib.rs

1#[cfg(feature = "full")]
2pub mod app_keys;
3#[cfg(feature = "full")]
4pub mod device_link;
5#[cfg(feature = "full")]
6pub mod direct_message_subscriptions;
7pub mod error;
8#[cfg(feature = "full")]
9pub mod group;
10#[cfg(feature = "full")]
11pub mod group_manager;
12#[cfg(feature = "full")]
13pub mod group_wire;
14pub mod ids;
15pub mod invite;
16#[cfg(feature = "full")]
17pub mod message_author_subscriptions;
18#[cfg(feature = "full")]
19pub mod message_builders;
20#[cfg(feature = "full")]
21pub mod message_origin;
22mod metadata_privacy;
23#[cfg(feature = "full")]
24pub mod multi_device;
25#[cfg(feature = "full")]
26pub mod one_to_many;
27pub mod protocol_types;
28#[cfg(feature = "full")]
29pub mod roster;
30#[cfg(feature = "full")]
31pub mod roster_editor;
32#[cfg(feature = "full")]
33pub mod sender_key;
34mod serde_hex;
35pub mod session;
36pub use metadata_privacy::{has_metadata_privacy_version, METADATA_PRIVACY_VERSION};
37#[cfg(feature = "full")]
38pub mod session_manager;
39#[cfg(feature = "full")]
40pub mod shared_channel;
41pub mod utils;
42pub mod wire;
43
44#[cfg(feature = "full")]
45pub use app_keys::{
46    build_app_keys_device_authorization_filter,
47    encrypted_device_label_payloads_from_app_keys_event, is_app_keys_event,
48    resolve_app_keys_owner_for_device, AppKeys, DeviceEntry, DeviceLabels, DeviceMembership,
49    VerifiedAppKeysIndex, APP_KEYS_ENCRYPTED_DEVICE_LABELS_FACT,
50    APP_KEYS_ENCRYPTED_DEVICE_LABELS_SCHEMA, APP_KEYS_FACT_TYPE, APP_KEYS_OWNER_PUBKEY_FACT,
51    APP_KEYS_SCHEMA, APP_KEYS_SNAPSHOT_KIND,
52};
53#[cfg(feature = "full")]
54pub use device_link::{
55    deterministic_link_invite_for_device, deterministic_link_invite_for_device_link_request,
56    encode_compact_device_link_request, parse_compact_device_link_request, DeviceLinkRequest,
57};
58#[cfg(feature = "full")]
59pub use direct_message_subscriptions::{
60    app_keys_subscription_authors, build_app_keys_backfill_filter,
61    build_direct_message_backfill_filter, build_invite_backfill_filter,
62    build_invite_response_backfill_filter, build_protocol_discovery_filters,
63    build_runtime_backfill_filters, direct_message_subscription_authors,
64    invite_response_subscription_recipients, DirectMessageSubscriptionTracker,
65    RuntimeSubscriptionRegistration, RuntimeSubscriptionTracker,
66};
67pub use error::{DomainError, Error, Result};
68#[cfg(feature = "full")]
69pub use group::*;
70#[cfg(feature = "full")]
71pub use group_manager::*;
72#[cfg(feature = "full")]
73pub use group_wire::{
74    build_group_roster_fact_filter, group_roster_unsigned_event, is_group_roster_fact_event,
75    parse_group_roster_fact_event, project_group_roster_fact_events, GroupEventManager,
76    GroupRosterFact, JsonGroupPayloadCodecV1, GROUP_ROSTER_FACT_KIND, GROUP_ROSTER_FACT_SCHEMA,
77    GROUP_ROSTER_FACT_TYPE,
78};
79pub use ids::{DevicePubkey, OwnerPubkey, UnixSeconds};
80pub use invite::{Invite, InviteResponse, InviteResponseEnvelope, OwnerClaimVerifier};
81#[cfg(feature = "full")]
82pub use message_author_subscriptions::{
83    build_message_author_subscription_filters, prioritized_message_authors,
84    MessageAuthorSubscriptionPlan, MESSAGE_AUTHOR_FILTER_CHUNK_SIZE,
85    MESSAGE_AUTHOR_SUBSCRIPTION_LIMIT,
86};
87#[cfg(feature = "full")]
88pub use message_builders::*;
89#[cfg(feature = "full")]
90pub use message_origin::{classify_message_origin, MessageOrigin};
91#[cfg(feature = "full")]
92pub use multi_device::{
93    apply_app_keys_snapshot, apply_app_keys_snapshot_with_required_device,
94    evaluate_device_registration_state, resolve_conversation_candidate_pubkeys,
95    resolve_invite_owner_routing, resolve_rumor_peer_pubkey, select_latest_app_keys_from_events,
96    should_require_relay_registration_confirmation, AppKeysSnapshot, AppKeysSnapshotDecision,
97    DeviceRegistrationState, InviteOwnerRoutingResolution,
98};
99#[cfg(feature = "full")]
100pub use one_to_many::*;
101pub use protocol_types::{ProtocolContext, MAX_SKIP};
102#[cfg(feature = "full")]
103pub use roster::{AuthorizedDevice, DeviceRoster, RosterSnapshotDecision};
104#[cfg(feature = "full")]
105pub use roster_editor::RosterEditor;
106#[cfg(feature = "full")]
107pub use sender_key::*;
108pub use session::{
109    Header, MessageEnvelope, ReceiveOutcome, ReceivePlan, SendOutcome, SendPlan,
110    SerializableKeyPair, Session, SessionState, SkippedKeysEntry,
111};
112#[cfg(feature = "full")]
113pub use session_manager::{
114    Delivery, DeviceRecordSnapshot, MessageSenderRecord, PreparedSend, ProcessedInviteResponse,
115    PruneReport, ReceiveInputFingerprint, ReceivedMessage, RelayGap, SessionManager,
116    SessionManagerSnapshot, UserRecordSnapshot,
117};
118#[cfg(feature = "full")]
119pub use shared_channel::SharedChannel;
120#[cfg(feature = "full")]
121pub use wire::{
122    group_sender_key_message_event, parse_group_sender_key_message_event,
123    parse_group_sender_key_message_event_unchecked, parse_roster_event, roster_unsigned_event,
124    DecodedRosterEvent, GROUP_SENDER_KEY_MESSAGE_KIND, ROSTER_EVENT_KIND,
125};
126pub use wire::{
127    invite_response_event, invite_unsigned_event, invite_url, message_event, parse_invite_event,
128    parse_invite_response_event, parse_invite_url, parse_message_event, INVITE_EVENT_KIND,
129    INVITE_LIST_LABEL, INVITE_RESPONSE_KIND, MESSAGE_EVENT_KIND,
130};
131
132pub(crate) use ids::owner_pubkey_from_device_pubkey;
133pub(crate) use utils::{
134    device_pubkey_from_secret_bytes, kdf2, random_secret_key_bytes, secret_key_from_bytes,
135};
136
137#[cfg(feature = "full")]
138impl OwnerClaimVerifier for AppKeys {
139    fn has_device(&self, _device_pubkey: DevicePubkey, device_identity: nostr::PublicKey) -> bool {
140        self.get_device(&device_identity).is_some()
141    }
142}
143
144#[cfg(feature = "full")]
145pub const APP_KEYS_EVENT_KIND: u32 = APP_KEYS_SNAPSHOT_KIND;
146pub const CHAT_MESSAGE_KIND: u32 = 14;
147pub const CHAT_SETTINGS_KIND: u32 = 10448;
148pub const REACTION_KIND: u32 = 7;
149pub const RECEIPT_KIND: u32 = 15;
150pub const TYPING_KIND: u32 = 25;
151pub const SHARED_CHANNEL_KIND: u32 = 4;
152pub const EXPIRATION_TAG: &str = "expiration";
153
154#[derive(Debug, Clone, Default, serde::Serialize, serde::Deserialize)]
155#[serde(rename_all = "camelCase")]
156pub struct SendOptions {
157    pub expires_at: Option<u64>,
158    pub ttl_seconds: Option<u64>,
159}
160
161#[derive(Debug, Clone, serde::Serialize, serde::Deserialize, PartialEq)]
162#[serde(rename_all = "camelCase")]
163pub struct ChatSettingsPayloadV1 {
164    #[serde(rename = "type")]
165    pub typ: String,
166    pub v: u32,
167    #[serde(skip_serializing_if = "Option::is_none")]
168    pub message_ttl_seconds: Option<u64>,
169}
170
171pub fn process_invite_response_event(
172    invite: &Invite,
173    event: &nostr::Event,
174    inviter_private_key: [u8; 32],
175) -> Result<Option<InviteResponse>> {
176    let envelope = parse_invite_response_event(event)
177        .map_err(|error| Error::InvalidEvent(error.to_string()))?;
178    let mut rng = rand::rngs::OsRng;
179    let mut ctx = ProtocolContext::new(UnixSeconds(event.created_at.as_secs()), &mut rng);
180    let mut invite = invite.clone();
181    invite
182        .process_response(&mut ctx, &envelope, inviter_private_key)
183        .map(Some)
184}
185
186pub trait SessionNostrExt {
187    fn send_event(&mut self, event: nostr::UnsignedEvent) -> Result<nostr::Event>;
188    fn receive(&mut self, event: &nostr::Event) -> Result<Option<String>>;
189}
190
191#[derive(serde::Deserialize)]
192#[serde(deny_unknown_fields)]
193struct StrictUnsignedRumor {
194    id: nostr::EventId,
195    pubkey: nostr::PublicKey,
196    created_at: nostr::Timestamp,
197    kind: nostr::Kind,
198    tags: nostr::Tags,
199    content: String,
200}
201
202fn validate_unsigned_rumor(payload: &[u8]) -> Result<()> {
203    let rumor: StrictUnsignedRumor = serde_json::from_slice(payload)?;
204    nostr::UnsignedEvent {
205        id: Some(rumor.id),
206        pubkey: rumor.pubkey,
207        created_at: rumor.created_at,
208        kind: rumor.kind,
209        tags: rumor.tags,
210        content: rumor.content,
211    }
212    .verify_id()?;
213    Ok(())
214}
215
216impl SessionNostrExt for Session {
217    fn send_event(&mut self, mut event: nostr::UnsignedEvent) -> Result<nostr::Event> {
218        event.ensure_id();
219        let payload = serde_json::to_vec(&event)?;
220        let now = UnixSeconds(
221            std::time::SystemTime::now()
222                .duration_since(std::time::UNIX_EPOCH)
223                .unwrap()
224                .as_secs(),
225        );
226        let plan = self.plan_send(&payload, now)?;
227        let envelope = self.apply_send(plan).envelope;
228        message_event(&envelope).map_err(|error| Error::InvalidEvent(error.to_string()))
229    }
230
231    fn receive(&mut self, event: &nostr::Event) -> Result<Option<String>> {
232        let envelope =
233            parse_message_event(event).map_err(|error| Error::InvalidEvent(error.to_string()))?;
234        if !self.matches_sender(envelope.sender) {
235            return Ok(None);
236        }
237        let mut rng = rand::rngs::OsRng;
238        let mut ctx = ProtocolContext::new(UnixSeconds(event.created_at.as_secs()), &mut rng);
239        let plan = self.plan_receive(&mut ctx, &envelope)?;
240        validate_unsigned_rumor(&plan.payload)?;
241        let outcome = self.apply_receive(plan);
242        let plaintext = String::from_utf8(outcome.payload)
243            .map_err(|error| Error::Decryption(error.to_string()))?;
244        Ok(Some(plaintext))
245    }
246}
247
248pub trait InviteNostrExt {
249    fn get_url(&self, root: &str) -> Result<String>;
250    fn get_event(&self) -> Result<nostr::UnsignedEvent>;
251    fn from_url(url: &str) -> Result<Invite>;
252    fn from_event(event: &nostr::Event) -> Result<Invite>;
253    fn process_invite_response(
254        &self,
255        event: &nostr::Event,
256        inviter_private_key: [u8; 32],
257    ) -> Result<Option<InviteResponse>>;
258}
259
260impl InviteNostrExt for Invite {
261    fn get_url(&self, root: &str) -> Result<String> {
262        invite_url(self, root).map_err(|error| Error::InvalidEvent(error.to_string()))
263    }
264
265    fn get_event(&self) -> Result<nostr::UnsignedEvent> {
266        if self.device_id.is_none() {
267            return Err(Error::DeviceIdRequired);
268        }
269        invite_unsigned_event(self).map_err(|error| Error::InvalidEvent(error.to_string()))
270    }
271
272    fn from_url(url: &str) -> Result<Invite> {
273        parse_invite_url(url).map_err(|error| Error::InvalidEvent(error.to_string()))
274    }
275
276    fn from_event(event: &nostr::Event) -> Result<Invite> {
277        parse_invite_event(event).map_err(|error| Error::InvalidEvent(error.to_string()))
278    }
279
280    fn process_invite_response(
281        &self,
282        event: &nostr::Event,
283        inviter_private_key: [u8; 32],
284    ) -> Result<Option<InviteResponse>> {
285        process_invite_response_event(self, event, inviter_private_key)
286    }
287}
288
289#[cfg(test)]
290mod architecture_tests {
291    #[test]
292    fn core_modules_do_not_manage_runtime_or_wire() {
293        let sources = [
294            include_str!("group.rs"),
295            include_str!("group_manager.rs"),
296            include_str!("invite.rs"),
297            include_str!("roster.rs"),
298            include_str!("roster_editor.rs"),
299            include_str!("sender_key.rs"),
300            include_str!("session.rs"),
301            include_str!("session_manager.rs"),
302        ];
303        for banned in [
304            "NdrRuntime",
305            "SessionManagerEvent",
306            "StorageAdapter",
307            "crossbeam",
308            "tokio",
309            "EventBuilder",
310            "Filter",
311            "crate::wire",
312            "wire::",
313            "pairwise_codec",
314            "GroupWireEnvelopeV1",
315            "GroupPairwisePayloadV1",
316            "GroupSenderKeyPlaintextV1",
317            "wire_format_version",
318        ] {
319            for source in sources {
320                assert!(
321                    !source.contains(banned),
322                    "core should stay deterministic and wire/runtime-free; found `{banned}`"
323                );
324            }
325        }
326    }
327}