Skip to main content

iris_chat_protocol/protocol_engine/
engine_state_helpers.rs

1use super::*;
2
3/// Earlier protocol states already retained exact signed owner evidence, but
4/// predated the ratchet library's separate evidence index. Feed those original
5/// events into restoration so valid sessions do not become unauthorized after
6/// an upgrade. Keep existing heads: the library resolves newer revocations and
7/// conflicting signed events instead of trusting cached authorization flags.
8pub(super) fn restore_signed_peer_evidence_for_session_manager(
9    state: &mut ProtocolEnginePersistedState,
10) {
11    for (owner, evidence) in &state.invite_owner_app_keys_evidence {
12        if *owner == state.session_manager.local_owner_pubkey {
13            continue;
14        }
15        let ProtocolAppKeysEvidence::Verified(event) = evidence else {
16            continue;
17        };
18        if !state
19            .session_manager
20            .verified_peer_app_keys_events
21            .iter()
22            .any(|existing| existing.id == event.id)
23        {
24            state
25                .session_manager
26                .verified_peer_app_keys_events
27                .push(event.as_ref().clone());
28        }
29    }
30}
31
32pub(super) fn quarantine_unverified_owner_rosters(
33    snapshot: &mut SessionManagerSnapshot,
34    verified_app_keys_owners: &BTreeSet<NdrOwnerPubkey>,
35) {
36    for user in &mut snapshot.users {
37        if verified_app_keys_owners.contains(&user.owner_pubkey) {
38            continue;
39        }
40
41        // A device may always speak for the identity represented by its own
42        // key. Preserve only that exact legacy singleton; every O != D binding
43        // needs persisted AppKeys provenance.
44        let exact_self_owned_singleton = user.roster.as_ref().is_some_and(|roster| {
45            roster.devices().len() == 1
46                && provisional_owner_from_sender_pubkey(roster.devices()[0].device_pubkey)
47                    == user.owner_pubkey
48        });
49        if !exact_self_owned_singleton {
50            user.roster = None;
51        }
52
53        for device in &mut user.devices {
54            // O = D is authenticated by possession of D's key even when an
55            // older persisted session has no synthetic singleton roster.
56            device.authorized =
57                provisional_owner_from_sender_pubkey(device.device_pubkey) == user.owner_pubkey;
58            if !device.authorized {
59                device.is_stale = false;
60                device.stale_since = None;
61            }
62        }
63    }
64}
65
66pub(super) fn load_or_create_local_invite(
67    storage: &dyn StorageAdapter,
68    device_pubkey: PublicKey,
69    device_id: &str,
70    owner_pubkey: PublicKey,
71) -> anyhow::Result<Invite> {
72    let storage_key = format!("device-invite/{device_id}");
73    if let Some(serialized) = storage.get(&storage_key)? {
74        if let Ok(invite) = Invite::deserialize(&serialized) {
75            return Ok(normalize_local_invite_owner(invite, owner_pubkey));
76        }
77    }
78
79    let mut invite = Invite::create_new(device_pubkey, Some(device_id.to_string()), None)?;
80    invite = normalize_local_invite_owner(invite, owner_pubkey);
81    storage.put(&storage_key, invite.serialize()?)?;
82    Ok(invite)
83}
84
85pub(super) fn normalize_local_invite_owner(mut invite: Invite, owner_pubkey: PublicKey) -> Invite {
86    if invite.metadata_privacy_version != nostr_double_ratchet::METADATA_PRIVACY_VERSION {
87        invite.metadata_privacy_version = nostr_double_ratchet::METADATA_PRIVACY_VERSION;
88        invite.created_at = NdrUnixSeconds(
89            unix_now()
90                .get()
91                .max(invite.created_at.get().saturating_add(1)),
92        );
93    }
94    invite.inviter_owner_pubkey = Some(ndr_owner(owner_pubkey));
95    invite.owner_public_key = Some(owner_pubkey);
96    invite
97}
98
99pub(super) fn user_record_snapshot(
100    snapshot: &SessionManagerSnapshot,
101    owner: NdrOwnerPubkey,
102) -> Option<&UserRecordSnapshot> {
103    snapshot
104        .users
105        .iter()
106        .find(|user| user.owner_pubkey == owner)
107}
108
109pub(super) fn roster_device_pubkeys(user: &UserRecordSnapshot) -> Option<Vec<NdrDevicePubkey>> {
110    user.roster.as_ref().map(|roster| {
111        roster
112            .devices()
113            .iter()
114            .map(|entry| entry.device_pubkey)
115            .collect()
116    })
117}
118
119impl ProtocolEngine {
120    /// Original signed evidence for authenticated own-device reconciliation.
121    pub fn authorization_events(&self) -> Vec<Event> {
122        let mut events: BTreeMap<nostr::EventId, Event> = BTreeMap::new();
123        for evidence in self.invite_owner_app_keys_evidence.values() {
124            if let ProtocolAppKeysEvidence::Verified(event) = evidence {
125                events.insert(event.id, event.as_ref().clone());
126            }
127        }
128        for history in self.group_roster_fact_histories.values() {
129            for event in &history.events {
130                events.insert(event.id, event.clone());
131            }
132        }
133        events.into_values().collect()
134    }
135}
136
137impl ProtocolEngine {
138    pub(super) fn with_group_fanout_checkpoint<T>(
139        &mut self,
140        operation: impl FnOnce(&mut Self, &mut GroupFanoutRetryUndo) -> anyhow::Result<T>,
141    ) -> anyhow::Result<T> {
142        // Fanout retries mutate only sessions, selected targets, and ready
143        // publications. Keep unrelated queues and deferred fanouts installed.
144        // Persistence is the final fallible step: after a successful save its
145        // delivery-journal cleanup cannot need an in-memory rollback.
146        let checkpoint = (
147            self.session_manager.clone(),
148            self.ready_group_publications.clone(),
149            self.batch_persist_dirty.get(),
150        );
151        let mut undo = GroupFanoutRetryUndo::default();
152        let result = operation(self, &mut undo);
153        if result.is_err() {
154            self.invalidate_pending_group_fanout_retry_deadline();
155            undo.restore(&mut self.pending_group_fanouts);
156            self.session_manager = checkpoint.0;
157            self.ready_group_publications = checkpoint.1;
158            self.batch_persist_dirty.set(checkpoint.2);
159        }
160        self.invalidate_known_message_author_cache();
161        result
162    }
163
164    pub(super) fn state_checkpoint(&self) -> ProtocolEngineCheckpoint {
165        ProtocolEngineCheckpoint {
166            session_manager: self.session_manager.clone(),
167            group_manager: self.group_manager.clone(),
168            pending_inbound: self.pending_inbound.clone(),
169            pending_group_fanouts: self.pending_group_fanouts.clone(),
170            ready_group_publications: self.ready_group_publications.clone(),
171            pending_local_sibling_sends: self.pending_local_sibling_sends.clone(),
172            pending_remote_sends: self.pending_remote_sends.clone(),
173            pending_group_pairwise_payloads: self.pending_group_pairwise_payloads.clone(),
174            pending_group_sender_key_messages: self.pending_group_sender_key_messages.clone(),
175            pending_group_sender_key_repairs: self.pending_group_sender_key_repairs.clone(),
176            processed_group_sender_key_messages: self.processed_group_sender_key_messages.clone(),
177            group_sender_key_retry: self.group_sender_key_retry.borrow().clone(),
178            answered_group_sender_key_repairs: self.answered_group_sender_key_repairs.clone(),
179            pending_decrypted_deliveries: self.pending_decrypted_deliveries.clone(),
180            group_roster_fact_histories: self.group_roster_fact_histories.clone(),
181            verified_app_keys_owners: self.verified_app_keys_owners.clone(),
182            invite_owner_app_keys_evidence: self.invite_owner_app_keys_evidence.clone(),
183            processed_private_invite_response_ids: self
184                .processed_private_invite_response_ids
185                .clone(),
186            group_invite_uses: self.group_invite_uses.clone(),
187            local_app_keys_observed: self.local_app_keys_observed,
188            subscription_generation: self.subscription_generation,
189        }
190    }
191
192    pub(super) fn restore_checkpoint(&mut self, checkpoint: ProtocolEngineCheckpoint) {
193        self.group_invite_uses = checkpoint.group_invite_uses;
194        self.session_manager = checkpoint.session_manager;
195        self.group_manager = checkpoint.group_manager;
196        self.invalidate_pending_inbound_retry_deadline();
197        self.pending_inbound = checkpoint.pending_inbound;
198        self.invalidate_pending_group_fanout_retry_deadline();
199        self.pending_group_fanouts = checkpoint.pending_group_fanouts;
200        self.ready_group_publications = checkpoint.ready_group_publications;
201        self.pending_local_sibling_sends = checkpoint.pending_local_sibling_sends;
202        self.pending_remote_sends = checkpoint.pending_remote_sends;
203        self.pending_group_pairwise_payloads = checkpoint.pending_group_pairwise_payloads;
204        self.pending_group_sender_key_messages = checkpoint.pending_group_sender_key_messages;
205        self.pending_group_sender_key_repairs = checkpoint.pending_group_sender_key_repairs;
206        self.processed_group_sender_key_messages = checkpoint.processed_group_sender_key_messages;
207        self.group_sender_key_retry
208            .replace(checkpoint.group_sender_key_retry);
209        self.answered_group_sender_key_repairs = checkpoint.answered_group_sender_key_repairs;
210        self.pending_decrypted_deliveries = checkpoint.pending_decrypted_deliveries;
211        self.group_roster_fact_histories = checkpoint.group_roster_fact_histories;
212        self.verified_app_keys_owners = checkpoint.verified_app_keys_owners;
213        self.invite_owner_app_keys_evidence = checkpoint.invite_owner_app_keys_evidence;
214        self.processed_private_invite_response_ids =
215            checkpoint.processed_private_invite_response_ids;
216        self.local_app_keys_observed = checkpoint.local_app_keys_observed;
217        self.subscription_generation = checkpoint.subscription_generation;
218        self.invalidate_known_message_author_cache();
219    }
220
221    pub(super) fn with_state_checkpoint<T>(
222        &mut self,
223        operation: impl FnOnce(&mut Self) -> anyhow::Result<T>,
224    ) -> anyhow::Result<T> {
225        let checkpoint = self.state_checkpoint();
226        match operation(self) {
227            Ok(value) => {
228                self.invalidate_known_message_author_cache();
229                Ok(value)
230            }
231            Err(error) => {
232                self.restore_checkpoint(checkpoint);
233                Err(error)
234            }
235        }
236    }
237}