Skip to main content

iris_chat_protocol/protocol_engine/
engine_invite_owner.rs

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