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 records;
146mod rosters;
147mod sending;
148
149fn apply_roster_snapshot(
150    current_roster: Option<&DeviceRoster>,
151    incoming_roster: &DeviceRoster,
152) -> (RosterSnapshotDecision, DeviceRoster) {
153    let Some(current_roster) = current_roster else {
154        return (RosterSnapshotDecision::Advanced, incoming_roster.clone());
155    };
156
157    if incoming_roster.created_at > current_roster.created_at {
158        return (RosterSnapshotDecision::Advanced, incoming_roster.clone());
159    }
160
161    if incoming_roster.created_at < current_roster.created_at {
162        return (RosterSnapshotDecision::Stale, current_roster.clone());
163    }
164
165    (
166        RosterSnapshotDecision::MergedEqualTimestamp,
167        current_roster.merge(incoming_roster),
168    )
169}
170
171fn session_priority(session: &Session) -> (u8, u32, u32) {
172    let can_send = session.can_send();
173    let can_receive = session.state.receiving_chain_key.is_some()
174        || session.state.their_current_nostr_public_key.is_some()
175        || session.state.receiving_chain_message_number > 0;
176
177    let directionality = match (can_send, can_receive) {
178        (true, true) => 3,
179        (true, false) => 2,
180        (false, true) => 1,
181        (false, false) => 0,
182    };
183
184    (
185        directionality,
186        session.state.receiving_chain_message_number,
187        session.state.sending_chain_message_number,
188    )
189}
190
191fn is_one_way_bootstrap_session(session: &Session) -> bool {
192    session.state.receiving_chain_key.is_none()
193        && session.state.their_current_nostr_public_key.is_none()
194}
195
196fn merge_created_at(current: UnixSeconds, observed: UnixSeconds) -> UnixSeconds {
197    match (current.get(), observed.get()) {
198        (0, _) => observed,
199        (_, 0) => current,
200        _ => current.min(observed),
201    }
202}