iris_chat_protocol/protocol_engine/
engine_invite_owner.rs1const 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(¤t_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 let excess = self
108 .processed_private_invite_response_ids
109 .len()
110 .saturating_sub(PROCESSED_PRIVATE_INVITE_RESPONSE_LIMIT);
111 if excess > 0 {
112 self.processed_private_invite_response_ids.drain(0..excess);
113 }
114 self.invalidate_known_message_author_cache();
115 self.wake_pending_protocol_for_owner(ndr_owner(owner_pubkey));
119 if let Err(error) = self.persist() {
120 self.restore_checkpoint(checkpoint);
121 return Err(error);
122 }
123 let retry_batch = self.retry_pending_protocol(NdrUnixSeconds(now.get()))?;
124 Ok(ProtocolInviteSessionImportOutcome::Imported(retry_batch))
125 }
126
127 fn invite_owner_app_keys_membership(
128 &self,
129 owner: NdrOwnerPubkey,
130 device: PublicKey,
131 ) -> Option<bool> {
132 self.invite_owner_exact_app_keys_membership(owner, device)
133 }
134
135 fn invite_owner_exact_app_keys_membership(
136 &self,
137 owner: NdrOwnerPubkey,
138 device: PublicKey,
139 ) -> Option<bool> {
140 match self.invite_owner_app_keys_evidence.get(&owner)? {
141 ProtocolAppKeysEvidence::Verified(event) => AppKeys::from_event(event)
142 .ok()
143 .map(|app_keys| app_keys.get_device(&device).is_some()),
144 ProtocolAppKeysEvidence::Ambiguous { .. } => None,
145 }
146 }
147
148 pub fn persisted_invite_owner_device_is_authorized(
152 storage: Arc<dyn StorageAdapter>,
153 local_owner_pubkey: PublicKey,
154 local_device_keys: &Keys,
155 invite_owner_pubkey: PublicKey,
156 invite_device_pubkey: PublicKey,
157 ) -> anyhow::Result<bool> {
158 if invite_owner_pubkey == invite_device_pubkey {
159 return Ok(true);
160 }
161 let local_owner = ndr_owner(local_owner_pubkey);
162 let local_device = ndr_device(local_device_keys.public_key());
163 let Some(engine) = Self::load_persisted_state(
164 storage,
165 local_owner_pubkey,
166 local_owner,
167 local_device,
168 local_device_keys.secret_key().to_secret_bytes(),
169 )?
170 else {
171 return Ok(false);
172 };
173 Ok(matches!(
174 engine.invite_owner_exact_app_keys_membership(
175 ndr_owner(invite_owner_pubkey),
176 invite_device_pubkey,
177 ),
178 Some(true)
179 ))
180 }
181}
182
183fn sanitize_invite_owner_persisted_state(state: &mut ProtocolEnginePersistedState) {
184 state.invite_owner_app_keys_evidence.retain(|owner, evidence| {
185 match evidence {
186 ProtocolAppKeysEvidence::Verified(event) => {
187 public_owner(*owner).is_ok_and(|owner| {
188 app_keys_event_is_acceptable_for_owner(owner, event, unix_now().get())
189 })
190 }
191 ProtocolAppKeysEvidence::Ambiguous { .. } => true,
192 }
193 });
194 let exact_verified_owners = state
195 .invite_owner_app_keys_evidence
196 .iter()
197 .filter_map(|(owner, evidence)| {
198 matches!(evidence, ProtocolAppKeysEvidence::Verified(_)).then_some(*owner)
199 })
200 .collect::<BTreeSet<_>>();
201 if state.app_keys_provenance_version != PROTOCOL_APP_KEYS_PROVENANCE_VERSION {
202 state.verified_app_keys_owners = exact_verified_owners.clone();
203 } else {
204 state
205 .verified_app_keys_owners
206 .extend(exact_verified_owners.iter().copied());
207 }
208 let excess = state
209 .processed_private_invite_response_ids
210 .len()
211 .saturating_sub(PROCESSED_PRIVATE_INVITE_RESPONSE_LIMIT);
212 if excess > 0 {
213 state.processed_private_invite_response_ids.drain(0..excess);
214 }
215}
216
217pub fn app_keys_event_is_acceptable_for_owner(
220 owner: PublicKey,
221 event: &Event,
222 now_secs: u64,
223) -> bool {
224 let device_tags = event
225 .tags
226 .iter()
227 .filter(|tag| tag.as_slice().first().map(String::as_str) == Some("device"))
228 .collect::<Vec<_>>();
229 event.pubkey == owner
230 && event.created_at.as_secs()
231 <= now_secs.saturating_add(INVITE_OWNER_APP_KEYS_MAX_FUTURE_SKEW_SECS)
232 && serde_json::to_vec(event)
233 .is_ok_and(|bytes| bytes.len() <= INVITE_OWNER_APP_KEYS_MAX_EVENT_BYTES)
234 && device_tags.len() <= INVITE_OWNER_APP_KEYS_MAX_DEVICES
235 && device_tags.iter().all(|tag| {
236 let values = tag.as_slice();
237 values.len() >= 3 && values[2].parse::<u64>().is_ok()
238 })
239 && AppKeys::from_event(event).is_ok()
240}