1use crate::canonical_json;
52use crate::error::MessengerError;
53use crate::ids::{DeviceId, RequestId, UserId};
54use crate::store::CryptoStore;
55use crate::wire::OutgoingRequest;
56use mail4agent_vodozemac::{Curve25519PublicKey, Ed25519PublicKey};
57use serde::{Deserialize, Serialize};
58use serde_json::Value;
59use std::collections::{BTreeMap, BTreeSet};
60
61#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
65pub struct StoredDevice {
66 pub user_id: UserId,
68 pub device_id: DeviceId,
70 pub curve25519: Curve25519PublicKey,
72 pub ed25519: Ed25519PublicKey,
74 pub algorithms: Vec<String>,
76 pub display_name: Option<String>,
78 pub verified: bool,
82 pub blocked: bool,
85}
86
87#[derive(Clone, Debug, PartialEq, Eq)]
91pub struct DroppedDevice {
92 pub user_id: UserId,
94 pub device_id: DeviceId,
96 pub reason: String,
98}
99
100#[derive(Clone, Debug, PartialEq, Eq)]
105pub struct DeviceKeyChanged {
106 pub user_id: UserId,
108 pub device_id: DeviceId,
110}
111
112#[derive(Clone, Debug, Default, PartialEq, Eq)]
114pub struct KeysQueryOutcome {
115 pub resolved_users: Vec<UserId>,
118 pub accepted_devices: Vec<(UserId, DeviceId)>,
121 pub dropped_devices: Vec<DroppedDevice>,
124 pub key_changes: Vec<DeviceKeyChanged>,
127}
128
129#[derive(Serialize, Deserialize)]
134struct DeviceListRecord {
135 devices: Vec<StoredDevice>,
136}
137
138#[derive(Deserialize)]
143struct DeviceKeysObject {
144 user_id: UserId,
145 device_id: DeviceId,
146 #[serde(default)]
147 algorithms: Vec<String>,
148 #[serde(default)]
149 keys: BTreeMap<String, String>,
150 #[serde(default)]
151 unsigned: DeviceKeysUnsigned,
152}
153
154#[derive(Default, Deserialize)]
155struct DeviceKeysUnsigned {
156 #[serde(default)]
157 device_display_name: Option<String>,
158}
159
160#[derive(Deserialize)]
165struct KeysQueryResponseBody {
166 #[serde(default)]
167 device_keys: BTreeMap<UserId, BTreeMap<DeviceId, Value>>,
168}
169
170pub struct DeviceTracker;
175
176impl DeviceTracker {
177 pub fn on_device_lists<S: CryptoStore>(
181 store: &mut S,
182 changed: &[UserId],
183 left: &[UserId],
184 ) -> Result<(), MessengerError> {
185 for user_id in changed {
186 store.mark_user_tracked(user_id, true)?;
187 }
188
189 if !left.is_empty() {
190 let currently_tracked: BTreeSet<UserId> =
191 store.tracked_users()?.into_iter().map(|(user_id, _)| user_id).collect();
192 for user_id in left {
193 if currently_tracked.contains(user_id) {
194 store.mark_user_tracked(user_id, false)?;
195 }
196 }
197 }
198 Ok(())
199 }
200
201 pub fn keys_query_request<S: CryptoStore>(
204 store: &S,
205 id: RequestId,
206 ) -> Result<Option<OutgoingRequest>, MessengerError> {
207 let outdated: Vec<UserId> = store
208 .tracked_users()?
209 .into_iter()
210 .filter_map(|(user_id, outdated)| outdated.then_some(user_id))
211 .collect();
212 if outdated.is_empty() {
213 return Ok(None);
214 }
215
216 let mut device_keys = serde_json::Map::new();
217 for user_id in &outdated {
218 device_keys.insert(user_id.as_str().to_string(), Value::Array(Vec::new()));
219 }
220 let body = serde_json::json!({ "device_keys": Value::Object(device_keys) });
221 Ok(Some(OutgoingRequest::keys_query(id, body)))
222 }
223
224 pub fn devices_for_user<S: CryptoStore>(
227 store: &S,
228 user_id: &UserId,
229 ) -> Result<Vec<StoredDevice>, MessengerError> {
230 match store.devices_for_user(user_id)? {
231 Some(bytes) => {
232 let record: DeviceListRecord = serde_json::from_slice(bytes).map_err(|source| {
233 MessengerError::Crypto(format!("decode stored device list for {user_id}: {source}"))
234 })?;
235 Ok(record.devices)
236 }
237 None => Ok(Vec::new()),
238 }
239 }
240
241 pub fn on_keys_query_response<S: CryptoStore>(
249 store: &mut S,
250 body: &[u8],
251 ) -> Result<KeysQueryOutcome, MessengerError> {
252 let parsed: KeysQueryResponseBody = serde_json::from_slice(body)?;
253 let mut outcome = KeysQueryOutcome::default();
254
255 for (user_id, devices_by_id) in parsed.device_keys {
256 let existing = Self::devices_for_user(store, &user_id)?;
257 let mut existing_by_id: BTreeMap<DeviceId, StoredDevice> =
258 existing.into_iter().map(|device| (device.device_id.clone(), device)).collect();
259 let mut merged: Vec<StoredDevice> = Vec::with_capacity(devices_by_id.len());
260
261 for (device_id, raw) in &devices_by_id {
262 match Self::verify_device(&user_id, device_id, raw) {
263 Ok(candidate) => match existing_by_id.remove(device_id) {
264 Some(known)
265 if known.curve25519 != candidate.curve25519 || known.ed25519 != candidate.ed25519 =>
266 {
267 outcome.key_changes.push(DeviceKeyChanged {
268 user_id: user_id.clone(),
269 device_id: device_id.clone(),
270 });
271 merged.push(StoredDevice {
275 verified: false,
276 blocked: known.blocked,
277 ..candidate
278 });
279 }
280 Some(known) => {
281 outcome.accepted_devices.push((user_id.clone(), device_id.clone()));
282 merged.push(StoredDevice {
283 verified: known.verified,
284 blocked: known.blocked,
285 ..candidate
286 });
287 }
288 None => {
289 outcome.accepted_devices.push((user_id.clone(), device_id.clone()));
291 merged.push(StoredDevice {
292 verified: true,
293 ..candidate
294 });
295 }
296 },
297 Err(reason) => {
298 outcome.dropped_devices.push(DroppedDevice {
299 user_id: user_id.clone(),
300 device_id: device_id.clone(),
301 reason,
302 });
303 if let Some(known) = existing_by_id.remove(device_id) {
304 merged.push(known);
305 }
306 }
307 }
308 }
309
310 let record = DeviceListRecord { devices: merged };
311 let bytes = serde_json::to_vec(&record).map_err(|source| {
312 MessengerError::Crypto(format!("encode stored device list for {user_id}: {source}"))
313 })?;
314 store.save_devices(&user_id, bytes)?;
315 store.mark_user_tracked(&user_id, false)?;
316 outcome.resolved_users.push(user_id);
317 }
318
319 Ok(outcome)
320 }
321
322 fn verify_device(user_id: &UserId, device_id: &DeviceId, raw: &Value) -> Result<StoredDevice, String> {
330 let parsed: DeviceKeysObject = serde_json::from_value(raw.clone())
331 .map_err(|source| format!("malformed device_keys object: {source}"))?;
332
333 if parsed.user_id != *user_id {
334 return Err(format!(
335 "user_id {:?} in the object does not match the map key {:?}",
336 parsed.user_id.as_str(),
337 user_id.as_str()
338 ));
339 }
340 if parsed.device_id != *device_id {
341 return Err(format!(
342 "device_id {:?} in the object does not match the map key {:?}",
343 parsed.device_id.as_str(),
344 device_id.as_str()
345 ));
346 }
347
348 let ed25519_b64 = parsed
349 .keys
350 .get(&format!("ed25519:{}", device_id.as_str()))
351 .ok_or_else(|| "missing the device's own ed25519 key".to_string())?;
352 let ed25519 =
353 Ed25519PublicKey::from_base64(ed25519_b64).map_err(|source| format!("malformed ed25519 key: {source}"))?;
354
355 let curve25519_b64 = parsed
356 .keys
357 .get(&format!("curve25519:{}", device_id.as_str()))
358 .ok_or_else(|| "missing the device's own curve25519 key".to_string())?;
359 let curve25519 = Curve25519PublicKey::from_base64(curve25519_b64)
360 .map_err(|source| format!("malformed curve25519 key: {source}"))?;
361
362 let key_id = format!("ed25519:{}", device_id.as_str());
363 canonical_json::verify_json_signature(raw, user_id.as_str(), &key_id, &ed25519)
364 .map_err(|source| format!("self-signature failed to verify: {source}"))?;
365
366 Ok(StoredDevice {
367 user_id: user_id.clone(),
368 device_id: device_id.clone(),
369 curve25519,
370 ed25519,
371 algorithms: parsed.algorithms,
372 display_name: parsed.unsigned.device_display_name,
373 verified: false,
374 blocked: false,
375 })
376 }
377}
378
379#[cfg(test)]
380mod tests {
381 use super::*;
382 use crate::store::{InsecurePlainCodecForTests, Store};
383 use mail4agent_vodozemac::olm::Account;
384
385 fn new_store() -> Store<InsecurePlainCodecForTests> {
386 Store::new(DeviceId::parse("OURDEVICE").expect("valid device id"), InsecurePlainCodecForTests)
387 }
388
389 fn user(name: &str) -> UserId {
390 UserId::parse(format!("@{name}:example.org")).expect("valid user id")
391 }
392
393 fn device(name: &str) -> DeviceId {
394 DeviceId::parse(name).expect("valid device id")
395 }
396
397 fn signed_device_keys(account: &Account, user_id: &UserId, device_id: &DeviceId) -> Value {
403 let identity = account.identity_keys();
404 let mut value = serde_json::json!({
405 "algorithms": ["m.olm.v1.curve25519-aes-sha2", "m.megolm.v1.aes-sha2"],
406 "device_id": device_id.as_str(),
407 "user_id": user_id.as_str(),
408 "keys": {
409 format!("curve25519:{}", device_id.as_str()): identity.curve25519.to_base64(),
410 format!("ed25519:{}", device_id.as_str()): identity.ed25519.to_base64(),
411 },
412 });
413 let key_id = format!("ed25519:{}", device_id.as_str());
414 canonical_json::sign_json(&mut value, user_id.as_str(), &key_id, |bytes| account.sign(bytes))
415 .expect("signing a freshly built device_keys object succeeds");
416 value
417 }
418
419 fn keys_query_body(entries: &[(&UserId, &DeviceId, Value)]) -> Vec<u8> {
420 let mut device_keys = serde_json::Map::new();
421 for (user_id, device_id, value) in entries {
422 let by_device = device_keys
423 .entry(user_id.as_str().to_string())
424 .or_insert_with(|| Value::Object(serde_json::Map::new()));
425 by_device
426 .as_object_mut()
427 .expect("just inserted as an object")
428 .insert(device_id.as_str().to_string(), value.clone());
429 }
430 serde_json::to_vec(&serde_json::json!({ "device_keys": device_keys })).expect("valid JSON")
431 }
432
433 #[test]
434 fn device_tracker_marks_outdated_on_sync_device_lists_changed() {
435 let mut store = new_store();
436 let bob = user("bob");
437 assert!(store.tracked_users().expect("no error").is_empty(), "nothing tracked yet");
438
439 DeviceTracker::on_device_lists(&mut store, std::slice::from_ref(&bob), &[]).expect("applies device_lists.changed");
440 assert_eq!(store.tracked_users().expect("no error"), vec![(bob.clone(), true)]);
441
442 let request = DeviceTracker::keys_query_request(&store, RequestId::next(0))
443 .expect("builds a request")
444 .expect("bob is outdated, something to query");
445 assert_eq!(request.body.expect("keys_query has a body")["device_keys"][bob.as_str()], serde_json::json!([]));
446
447 let carol = user("carol");
451 DeviceTracker::on_device_lists(&mut store, &[], std::slice::from_ref(&carol))
452 .expect("left, but never tracked: no-op");
453 assert!(store.tracked_users().expect("no error").iter().all(|(u, _)| *u != carol));
454
455 DeviceTracker::on_device_lists(&mut store, &[], std::slice::from_ref(&bob)).expect("bob left");
456 assert_eq!(store.tracked_users().expect("no error"), vec![(bob, false)]);
457 }
458
459 #[test]
460 fn unsigned_device_keys_are_dropped() {
461 let mut store = new_store();
462 let alice = user("alice");
463 let dev1 = device("DEV1");
464
465 let account = Account::new();
466 let mut tampered = signed_device_keys(&account, &alice, &dev1);
467 let sig_path = tampered["signatures"][alice.as_str()]["ed25519:DEV1"]
470 .as_str()
471 .expect("signature present")
472 .to_string();
473 let sig_len = sig_path.len();
474 let mut corrupted = sig_path.into_bytes();
475 corrupted[0] ^= 0xFF;
476 tampered["signatures"][alice.as_str()]["ed25519:DEV1"] =
477 Value::String(mail4agent_vodozemac::base64_encode(&corrupted[..sig_len]));
478
479 let body = keys_query_body(&[(&alice, &dev1, tampered)]);
480 let outcome = DeviceTracker::on_keys_query_response(&mut store, &body).expect("applies response");
481
482 assert!(outcome.accepted_devices.is_empty());
483 assert_eq!(outcome.dropped_devices.len(), 1);
484 assert_eq!(outcome.dropped_devices[0].user_id, alice);
485 assert_eq!(outcome.dropped_devices[0].device_id, dev1);
486 assert!(
487 outcome.dropped_devices[0].reason.contains("signature"),
488 "reason names the problem: {}",
489 outcome.dropped_devices[0].reason
490 );
491
492 assert!(
493 DeviceTracker::devices_for_user(&store, &alice).expect("no error").is_empty(),
494 "an unsigned device is never stored"
495 );
496 assert_eq!(store.tracked_users().expect("no error"), vec![(alice, false)], "the user is still resolved");
497 }
498
499 #[test]
500 fn key_change_on_a_known_device_id_is_applied_and_flagged() {
501 let mut store = new_store();
502 let bob = user("bob");
503 let dev1 = device("DEV1");
504
505 let original_account = Account::new();
506 let first_body =
507 keys_query_body(&[(&bob, &dev1, signed_device_keys(&original_account, &bob, &dev1))]);
508 DeviceTracker::on_keys_query_response(&mut store, &first_body).expect("first query applies");
509 let original =
510 DeviceTracker::devices_for_user(&store, &bob).expect("no error").into_iter().next().expect("stored");
511 assert!(original.verified, "TOFU trusts the first-seen device");
512
513 let reset_account = Account::new();
514 let second_body =
515 keys_query_body(&[(&bob, &dev1, signed_device_keys(&reset_account, &bob, &dev1))]);
516 let outcome = DeviceTracker::on_keys_query_response(&mut store, &second_body).expect("second query applies");
517
518 assert_eq!(outcome.key_changes, vec![DeviceKeyChanged { user_id: bob.clone(), device_id: dev1.clone() }]);
519 assert!(outcome.accepted_devices.is_empty(), "a key change is reported via key_changes, not accepted_devices");
520
521 let stored = DeviceTracker::devices_for_user(&store, &bob).expect("no error").into_iter().next().expect("kept");
522 assert_ne!(stored.curve25519, original.curve25519, "new identity keys are applied");
523 assert_eq!(stored.curve25519, reset_account.identity_keys().curve25519);
524 assert!(!stored.verified, "verification is cleared on a key reset");
525 }
526
527 #[test]
528 fn first_seen_device_is_trusted_once() {
529 let mut store = new_store();
530 let bob = user("bob");
531 let dev1 = device("DEV1");
532 let account = Account::new();
533 let body = keys_query_body(&[(&bob, &dev1, signed_device_keys(&account, &bob, &dev1))]);
534 let outcome = DeviceTracker::on_keys_query_response(&mut store, &body).expect("query");
535 assert_eq!(outcome.accepted_devices, vec![(bob.clone(), dev1.clone())]);
536 let stored = DeviceTracker::devices_for_user(&store, &bob).expect("no error").into_iter().next().expect("stored");
537 assert!(stored.verified, "TOFU marks first sight verified");
538 let again = DeviceTracker::on_keys_query_response(&mut store, &body).expect("again");
540 assert!(again.key_changes.is_empty());
541 let stored = DeviceTracker::devices_for_user(&store, &bob).expect("no error").into_iter().next().expect("stored");
542 assert!(stored.verified, "unchanged re-publish keeps TOFU trust");
543 }
544}