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