Skip to main content

iris_chat_protocol/protocol_engine/
engine_invite_owner.rs

1use super::*;
2
3const INVITE_OWNER_APP_KEYS_MAX_EVENT_BYTES: usize = 64 * 1024;
4const INVITE_OWNER_APP_KEYS_MAX_DEVICES: usize = 64;
5const INVITE_OWNER_APP_KEYS_MAX_FUTURE_SKEW_SECS: u64 = 5 * 60;
6
7impl ProtocolEngine {
8    pub(super) fn update_invite_owner_app_keys_evidence(
9        &mut self,
10        owner: NdrOwnerPubkey,
11        event: &Event,
12        app_keys: &AppKeys,
13    ) -> bool {
14        if !app_keys_event_is_acceptable_for_owner(event.pubkey, event, unix_now().get()) {
15            return false;
16        }
17        let previous = self.invite_owner_app_keys_evidence.get(&owner).cloned();
18        let created_at_secs = event.created_at.as_secs();
19        let next = match previous.clone() {
20            None => ProtocolAppKeysEvidence::Verified(Box::new(event.clone())),
21            Some(ProtocolAppKeysEvidence::Ambiguous {
22                created_at_secs: ambiguous_at,
23            }) if created_at_secs > ambiguous_at => {
24                ProtocolAppKeysEvidence::Verified(Box::new(event.clone()))
25            }
26            Some(ProtocolAppKeysEvidence::Ambiguous {
27                created_at_secs: ambiguous_at,
28            }) => ProtocolAppKeysEvidence::Ambiguous {
29                created_at_secs: ambiguous_at,
30            },
31            Some(ProtocolAppKeysEvidence::Verified(current)) => {
32                let current_created_at = current.created_at.as_secs();
33                if created_at_secs > current_created_at {
34                    ProtocolAppKeysEvidence::Verified(Box::new(event.clone()))
35                } else if created_at_secs < current_created_at || current.id == event.id {
36                    ProtocolAppKeysEvidence::Verified(current)
37                } else {
38                    let same_roster =
39                        AppKeys::from_event(current.as_ref()).is_ok_and(|current_app_keys| {
40                            app_keys_device_pubkeys(&current_app_keys)
41                                == app_keys_device_pubkeys(app_keys)
42                        });
43                    if same_roster {
44                        ProtocolAppKeysEvidence::Verified(if event.id < current.id {
45                            Box::new(event.clone())
46                        } else {
47                            current
48                        })
49                    } else {
50                        ProtocolAppKeysEvidence::Ambiguous { created_at_secs }
51                    }
52                }
53            }
54        };
55        let changed = previous.as_ref() != Some(&next);
56        self.invite_owner_app_keys_evidence.insert(owner, next);
57        changed
58    }
59
60    pub fn import_private_invite_session_once(
61        &mut self,
62        response_event_id: &str,
63        owner_pubkey: PublicKey,
64        authenticated_device: PublicKey,
65        state: SessionState,
66        now: UnixSeconds,
67    ) -> anyhow::Result<ProtocolInviteSessionImportOutcome> {
68        if self
69            .processed_private_invite_response_ids
70            .iter()
71            .any(|processed| processed == response_event_id)
72        {
73            return Ok(ProtocolInviteSessionImportOutcome::AlreadyImported);
74        }
75        if owner_pubkey != authenticated_device {
76            match self
77                .invite_owner_app_keys_membership(ndr_owner(owner_pubkey), authenticated_device)
78            {
79                Some(true) => {}
80                Some(false) => {
81                    return Ok(ProtocolInviteSessionImportOutcome::Blocked(
82                        ProtocolAcceptInviteBlock::UnauthorizedDevice {
83                            owner_pubkey,
84                            device_pubkey: authenticated_device,
85                        },
86                    ))
87                }
88                None => {
89                    return Ok(ProtocolInviteSessionImportOutcome::Blocked(
90                        ProtocolAcceptInviteBlock::MissingOwnerRoster {
91                            owner_pubkey,
92                            device_pubkey: authenticated_device,
93                        },
94                    ))
95                }
96            }
97        }
98
99        let checkpoint = self.state_checkpoint();
100        self.session_manager.import_session_state(
101            ndr_owner(owner_pubkey),
102            ndr_device(authenticated_device),
103            state,
104            NdrUnixSeconds(now.get()),
105        );
106        self.processed_private_invite_response_ids
107            .push(response_event_id.to_string());
108        // Reusable links remain valid after many joins. Never evict a processed
109        // response: replaying it would reinstall an old ratchet session.
110        self.invalidate_known_message_author_cache();
111        // The proof can arrive before the private session is imported. Retry
112        // messages now that both are present, even if the proof-only attempt
113        // already moved them into backoff.
114        self.wake_pending_protocol_for_owner(ndr_owner(owner_pubkey));
115        if let Err(error) = self.persist_now() {
116            self.restore_checkpoint(checkpoint);
117            return Err(error);
118        }
119        let retry_batch = self.retry_pending_protocol(NdrUnixSeconds(now.get()))?;
120        Ok(ProtocolInviteSessionImportOutcome::Imported(retry_batch))
121    }
122
123    pub(super) fn invite_owner_app_keys_membership(
124        &self,
125        owner: NdrOwnerPubkey,
126        device: PublicKey,
127    ) -> Option<bool> {
128        self.invite_owner_exact_app_keys_membership(owner, device)
129    }
130
131    pub(super) fn invite_owner_exact_app_keys_membership(
132        &self,
133        owner: NdrOwnerPubkey,
134        device: PublicKey,
135    ) -> Option<bool> {
136        match self.invite_owner_app_keys_evidence.get(&owner)? {
137            ProtocolAppKeysEvidence::Verified(event) => AppKeys::from_event(event)
138                .ok()
139                .map(|app_keys| app_keys.get_device(&device).is_some()),
140            ProtocolAppKeysEvidence::Ambiguous { .. } => None,
141        }
142    }
143
144    /// Reads only existing protocol state and checks exact signed AppKeys
145    /// evidence. It never creates protocol state or treats a generic
146    /// cached roster projection as authorization.
147    pub fn persisted_invite_owner_device_is_authorized(
148        storage: Arc<dyn StorageAdapter>,
149        local_owner_pubkey: PublicKey,
150        local_device_keys: &Keys,
151        invite_owner_pubkey: PublicKey,
152        invite_device_pubkey: PublicKey,
153    ) -> anyhow::Result<bool> {
154        if invite_owner_pubkey == invite_device_pubkey {
155            return Ok(true);
156        }
157        let local_owner = ndr_owner(local_owner_pubkey);
158        let local_device = ndr_device(local_device_keys.public_key());
159        let Some(engine) = Self::load_persisted_state(
160            storage,
161            local_owner_pubkey,
162            local_owner,
163            local_device,
164            local_device_keys.secret_key().to_secret_bytes(),
165            false,
166        )?
167        else {
168            return Ok(false);
169        };
170        Ok(engine.invite_owner_device_is_authorized(invite_owner_pubkey, invite_device_pubkey))
171    }
172
173    /// Checks exact signed evidence, including an encrypted proof ingested by
174    /// a read-only notification preview. Generic roster projections do not count.
175    pub fn invite_owner_device_is_authorized(
176        &self,
177        invite_owner_pubkey: PublicKey,
178        invite_device_pubkey: PublicKey,
179    ) -> bool {
180        invite_owner_pubkey == invite_device_pubkey
181            || matches!(
182                self.invite_owner_exact_app_keys_membership(
183                    ndr_owner(invite_owner_pubkey),
184                    invite_device_pubkey,
185                ),
186                Some(true)
187            )
188    }
189}
190
191pub(super) fn sanitize_invite_owner_persisted_state(state: &mut ProtocolEnginePersistedState) {
192    state
193        .invite_owner_app_keys_evidence
194        .retain(|owner, evidence| match evidence {
195            ProtocolAppKeysEvidence::Verified(event) => public_owner(*owner).is_ok_and(|owner| {
196                app_keys_event_is_acceptable_for_owner(owner, event, unix_now().get())
197            }),
198            ProtocolAppKeysEvidence::Ambiguous { .. } => true,
199        });
200    let exact_verified_owners = state
201        .invite_owner_app_keys_evidence
202        .iter()
203        .filter_map(|(owner, evidence)| {
204            matches!(evidence, ProtocolAppKeysEvidence::Verified(_)).then_some(*owner)
205        })
206        .collect::<BTreeSet<_>>();
207    if state.app_keys_provenance_version != PROTOCOL_APP_KEYS_PROVENANCE_VERSION {
208        state.verified_app_keys_owners = exact_verified_owners.clone();
209    } else {
210        state
211            .verified_app_keys_owners
212            .extend(exact_verified_owners.iter().copied());
213    }
214}
215
216/// Verifies the exact signed AppKeys evidence accepted for direct-chat setup.
217/// Profile indexes and reconstructed device lists must never substitute for it.
218pub fn app_keys_event_is_acceptable_for_owner(
219    owner: PublicKey,
220    event: &Event,
221    now_secs: u64,
222) -> bool {
223    let device_tags = event
224        .tags
225        .iter()
226        .filter(|tag| tag.as_slice().first().map(String::as_str) == Some("device"))
227        .collect::<Vec<_>>();
228    event.pubkey == owner
229        && event.created_at.as_secs()
230            <= now_secs.saturating_add(INVITE_OWNER_APP_KEYS_MAX_FUTURE_SKEW_SECS)
231        && serde_json::to_vec(event)
232            .is_ok_and(|bytes| bytes.len() <= INVITE_OWNER_APP_KEYS_MAX_EVENT_BYTES)
233        && device_tags.len() <= INVITE_OWNER_APP_KEYS_MAX_DEVICES
234        && device_tags.iter().all(|tag| {
235            let values = tag.as_slice();
236            values.len() >= 3 && values[2].parse::<u64>().is_ok()
237        })
238        && AppKeys::from_event(event).is_ok()
239}