Skip to main content

nostr_double_ratchet/
session_manager.rs

1use crate::{
2    AuthorizedDevice, DeviceMembership, DevicePubkey, DeviceRoster, DomainError, Error, Invite,
3    InviteResponse, InviteResponseEnvelope, MessageEnvelope, OwnerPubkey, ProtocolContext, Result,
4    RosterSnapshotDecision, Session, SessionState, UnixSeconds, VerifiedAppKeysIndex,
5};
6use rand::{CryptoRng, RngCore};
7use serde::{Deserialize, Serialize};
8use std::collections::{BTreeMap, BTreeSet};
9
10const MAX_INACTIVE_SESSIONS: usize = 10;
11#[derive(Debug, Clone)]
12pub struct SessionManager {
13    local_owner_pubkey: OwnerPubkey,
14    local_device_pubkey: DevicePubkey,
15    local_device_secret_key: [u8; 32],
16    local_invite: Option<Invite>,
17    verified_peer_app_keys: VerifiedAppKeysIndex,
18    users: BTreeMap<OwnerPubkey, UserRecord>,
19}
20
21#[derive(Debug, Clone)]
22struct UserRecord {
23    owner_pubkey: OwnerPubkey,
24    roster: Option<DeviceRoster>,
25    devices: BTreeMap<DevicePubkey, DeviceRecord>,
26}
27
28#[derive(Debug, Clone)]
29struct DeviceRecord {
30    device_pubkey: DevicePubkey,
31    authorized: bool,
32    is_stale: bool,
33    stale_since: Option<UnixSeconds>,
34    claimed_owner_pubkey: Option<OwnerPubkey>,
35    public_invite: Option<Invite>,
36    invite_response_generated: bool,
37    active_session: Option<Session>,
38    inactive_sessions: Vec<Session>,
39    last_activity: Option<UnixSeconds>,
40    created_at: UnixSeconds,
41}
42
43#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
44pub struct SessionManagerSnapshot {
45    pub local_owner_pubkey: OwnerPubkey,
46    pub local_device_pubkey: DevicePubkey,
47    pub local_invite: Option<Invite>,
48    #[serde(default)]
49    pub verified_peer_app_keys_events: Vec<nostr::Event>,
50    pub users: Vec<UserRecordSnapshot>,
51}
52
53#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
54pub struct UserRecordSnapshot {
55    pub owner_pubkey: OwnerPubkey,
56    pub roster: Option<DeviceRoster>,
57    pub devices: Vec<DeviceRecordSnapshot>,
58}
59
60#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
61pub struct DeviceRecordSnapshot {
62    pub device_pubkey: DevicePubkey,
63    pub authorized: bool,
64    pub is_stale: bool,
65    pub stale_since: Option<UnixSeconds>,
66    #[serde(default, skip_serializing_if = "Option::is_none")]
67    pub claimed_owner_pubkey: Option<OwnerPubkey>,
68    pub public_invite: Option<Invite>,
69    #[serde(default)]
70    pub invite_response_generated: bool,
71    pub active_session: Option<SessionState>,
72    pub inactive_sessions: Vec<SessionState>,
73    pub last_activity: Option<UnixSeconds>,
74    pub created_at: UnixSeconds,
75}
76
77#[derive(Debug, Clone, PartialEq, Eq)]
78pub struct PreparedSend {
79    pub recipient_owner: OwnerPubkey,
80    pub payload: Vec<u8>,
81    pub deliveries: Vec<Delivery>,
82    pub invite_responses: Vec<InviteResponseEnvelope>,
83    pub relay_gaps: Vec<RelayGap>,
84}
85
86#[derive(Debug, Clone, PartialEq, Eq)]
87pub struct Delivery {
88    pub owner_pubkey: OwnerPubkey,
89    pub device_pubkey: DevicePubkey,
90    pub envelope: MessageEnvelope,
91}
92
93#[derive(Debug, Clone, PartialEq, Eq)]
94pub struct ProcessedInviteResponse {
95    pub owner_pubkey: OwnerPubkey,
96    pub device_pubkey: DevicePubkey,
97    pub claimed_owner_pubkey: Option<OwnerPubkey>,
98}
99
100#[derive(Debug, Clone, PartialEq, Eq)]
101pub struct ReceivedMessage {
102    pub owner_pubkey: OwnerPubkey,
103    pub device_pubkey: DevicePubkey,
104    pub payload: Vec<u8>,
105}
106
107/// A session routing hint, not proof of the claimed owner's authorization.
108#[derive(Debug, Clone, Copy, PartialEq, Eq)]
109pub struct MessageSenderRecord {
110    pub owner_pubkey: OwnerPubkey,
111    pub device_pubkey: DevicePubkey,
112    pub claimed_owner_pubkey: Option<OwnerPubkey>,
113}
114
115#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq, PartialOrd, Ord)]
116pub enum RelayGap {
117    MissingRoster {
118        owner_pubkey: OwnerPubkey,
119    },
120    MissingDeviceInvite {
121        owner_pubkey: OwnerPubkey,
122        device_pubkey: DevicePubkey,
123    },
124}
125
126#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
127pub struct PruneReport {
128    pub removed_devices: Vec<(OwnerPubkey, DevicePubkey)>,
129    pub removed_users: Vec<OwnerPubkey>,
130}
131
132#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord)]
133struct TargetDevice {
134    owner_pubkey: OwnerPubkey,
135    device_pubkey: DevicePubkey,
136}
137
138#[derive(Debug, Clone, Copy, PartialEq, Eq)]
139enum SendSessionSource {
140    Active,
141    Inactive(usize),
142}
143
144mod lifecycle;
145mod receive_fingerprint;
146#[cfg(test)]
147mod receive_fingerprint_tests;
148mod records;
149mod rosters;
150mod sending;
151
152pub use receive_fingerprint::ReceiveInputFingerprint;
153
154fn apply_roster_snapshot(
155    current_roster: Option<&DeviceRoster>,
156    incoming_roster: &DeviceRoster,
157) -> (RosterSnapshotDecision, DeviceRoster) {
158    let Some(current_roster) = current_roster else {
159        return (RosterSnapshotDecision::Advanced, incoming_roster.clone());
160    };
161
162    if incoming_roster.created_at > current_roster.created_at {
163        return (RosterSnapshotDecision::Advanced, incoming_roster.clone());
164    }
165
166    if incoming_roster.created_at < current_roster.created_at {
167        return (RosterSnapshotDecision::Stale, current_roster.clone());
168    }
169
170    (
171        RosterSnapshotDecision::MergedEqualTimestamp,
172        current_roster.merge(incoming_roster),
173    )
174}
175
176fn session_priority(session: &Session) -> (u8, u32, u32) {
177    let can_send = session.can_send();
178    let can_receive = session.state.receiving_chain_key.is_some()
179        || session.state.their_current_nostr_public_key.is_some()
180        || session.state.receiving_chain_message_number > 0;
181
182    let directionality = match (can_send, can_receive) {
183        (true, true) => 3,
184        (true, false) => 2,
185        (false, true) => 1,
186        (false, false) => 0,
187    };
188
189    (
190        directionality,
191        session.state.receiving_chain_message_number,
192        session.state.sending_chain_message_number,
193    )
194}
195
196fn is_one_way_bootstrap_session(session: &Session) -> bool {
197    session.state.receiving_chain_key.is_none()
198        && session.state.their_current_nostr_public_key.is_none()
199}
200
201fn merge_created_at(current: UnixSeconds, observed: UnixSeconds) -> UnixSeconds {
202    match (current.get(), observed.get()) {
203        (0, _) => observed,
204        (_, 0) => current,
205        _ => current.min(observed),
206    }
207}