Skip to main content

nostr_double_ratchet/
lib.rs

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