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