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