1use crate::canonical_json;
107use crate::crypto::account::OlmAccountState;
108use crate::crypto::device_tracker::{DeviceTracker, StoredDevice};
109use crate::error::MessengerError;
110use crate::ids::{DeviceId, RequestId, UserId};
111use crate::store::CryptoStore;
112use crate::wire::{OlmCiphertextInfo, OlmEncryptedContent, OutgoingRequest};
113use base64::Engine;
114use mail4agent_vodozemac::olm::{OlmMessage, Session, SessionConfig, SessionPickle};
115use mail4agent_vodozemac::{Curve25519PublicKey, Ed25519PublicKey};
116use serde::{Deserialize, Serialize};
117use std::collections::BTreeMap;
118
119const SIGNED_CURVE25519: &str = "signed_curve25519";
122
123fn wire_base64_engine() -> base64::engine::GeneralPurpose {
131 base64::engine::GeneralPurpose::new(
132 &base64::alphabet::STANDARD,
133 base64::engine::general_purpose::NO_PAD
134 .with_decode_padding_mode(base64::engine::DecodePaddingMode::Indifferent),
135 )
136}
137
138fn pickle_session(session: &Session) -> Result<Vec<u8>, MessengerError> {
139 serde_json::to_vec(&session.pickle())
140 .map_err(|source| MessengerError::Crypto(format!("encode pickled Olm session: {source}")))
141}
142
143fn unpickle_session(bytes: &[u8]) -> Result<Session, MessengerError> {
144 let pickle: SessionPickle = serde_json::from_slice(bytes)
145 .map_err(|source| MessengerError::Crypto(format!("decode pickled Olm session: {source}")))?;
146 Ok(Session::from_pickle(pickle))
147}
148
149#[derive(Serialize, Deserialize)]
154struct OlmPlaintextPayload {
155 #[serde(rename = "type")]
156 event_type: String,
157 content: serde_json::Value,
158 sender: UserId,
159 sender_device: DeviceId,
160 keys: OlmPlaintextKeys,
161 recipient: UserId,
162 recipient_keys: OlmPlaintextKeys,
163}
164
165#[derive(Serialize, Deserialize)]
166struct OlmPlaintextKeys {
167 ed25519: String,
168}
169
170#[derive(Deserialize)]
174struct KeysClaimResponseBody {
175 #[serde(default)]
176 one_time_keys: BTreeMap<UserId, BTreeMap<DeviceId, BTreeMap<String, serde_json::Value>>>,
177}
178
179#[derive(Debug, Clone, Default, PartialEq, Eq)]
181pub struct KeysClaimOutcome {
182 pub established: Vec<(UserId, DeviceId)>,
184 pub refused: Vec<KeysClaimRefusal>,
187}
188
189#[derive(Debug, Clone, PartialEq, Eq)]
192pub struct KeysClaimRefusal {
193 pub user_id: UserId,
195 pub device_id: DeviceId,
197 pub reason: String,
199}
200
201#[derive(Debug, Clone, PartialEq)]
204pub struct DecryptedToDevice {
205 pub sender: UserId,
208 pub sender_device_curve25519: Curve25519PublicKey,
210 pub sender_ed25519: Ed25519PublicKey,
212 pub event_type: String,
215 pub content: serde_json::Value,
217}
218
219#[derive(Debug, thiserror::Error)]
225pub enum OlmDecryptError {
226 #[error("the Olm envelope has no ciphertext entry for this device's own curve25519 key")]
230 NotAddressedToUs,
231 #[error("malformed Olm ciphertext: {0}")]
234 MalformedCiphertext(String),
235 #[error("no known Olm session for sender key {0} decrypted this message, and it is not a pre-key message")]
239 NoMatchingSession(String),
240 #[error("failed to establish an inbound Olm session: {0}")]
246 SessionEstablishment(String),
247 #[error("sender device for curve25519 key {curve25519} is not yet known to this account")]
251 UnknownSenderDevice {
252 user_id: UserId,
254 curve25519: String,
256 },
257 #[error("payload binding check failed: {0}")]
260 PayloadMismatch(String),
261 #[error("decrypted payload was not valid JSON: {0}")]
263 InvalidPayload(#[from] serde_json::Error),
264 #[error("store: {0}")]
266 Store(#[from] crate::store::StoreError),
267 #[error("{0}")]
270 Messenger(#[from] MessengerError),
271}
272
273pub struct OlmSessionManager;
278
279impl OlmSessionManager {
280 pub fn encode_olm_message(message: &OlmMessage) -> OlmCiphertextInfo {
282 let (message_type, ciphertext) = message.to_parts();
283 OlmCiphertextInfo { message_type: message_type as u8, body: wire_base64_engine().encode(ciphertext) }
284 }
285
286 pub fn decode_olm_message(info: &OlmCiphertextInfo) -> Result<OlmMessage, MessengerError> {
288 let bytes = wire_base64_engine()
289 .decode(&info.body)
290 .map_err(|source| MessengerError::Crypto(format!("malformed Olm ciphertext body: {source}")))?;
291 OlmMessage::from_parts(info.message_type as usize, &bytes)
292 .map_err(|source| MessengerError::Crypto(format!("malformed Olm message: {source}")))
293 }
294
295 pub fn sessions_missing_for<'a, S: CryptoStore>(
298 store: &S,
299 devices: &'a [StoredDevice],
300 ) -> Result<Vec<&'a StoredDevice>, MessengerError> {
301 let mut missing = Vec::new();
302 for device in devices {
303 let curve_b64 = device.curve25519.to_base64();
304 if store.olm_sessions_for_device(&curve_b64)?.is_empty() {
305 missing.push(device);
306 }
307 }
308 Ok(missing)
309 }
310
311 pub fn keys_claim_request(id: RequestId, devices: &[&StoredDevice]) -> Option<OutgoingRequest> {
314 if devices.is_empty() {
315 return None;
316 }
317 let mut by_user: BTreeMap<UserId, serde_json::Map<String, serde_json::Value>> = BTreeMap::new();
318 for device in devices {
319 by_user.entry(device.user_id.clone()).or_default().insert(
320 device.device_id.as_str().to_string(),
321 serde_json::Value::String(SIGNED_CURVE25519.to_string()),
322 );
323 }
324 let mut top = serde_json::Map::new();
325 for (user_id, device_map) in by_user {
326 top.insert(user_id.as_str().to_string(), serde_json::Value::Object(device_map));
327 }
328 let body = serde_json::json!({ "one_time_keys": serde_json::Value::Object(top) });
329 Some(OutgoingRequest::keys_claim(id, body))
330 }
331
332 pub fn on_keys_claim_response<S: CryptoStore>(
341 store: &mut S,
342 account: &OlmAccountState,
343 requested: &[&StoredDevice],
344 body: &[u8],
345 ) -> Result<KeysClaimOutcome, MessengerError> {
346 let parsed: KeysClaimResponseBody = serde_json::from_slice(body)?;
347 let mut outcome = KeysClaimOutcome::default();
348
349 for device in requested {
350 let Some(by_key_id) =
351 parsed.one_time_keys.get(&device.user_id).and_then(|by_device| by_device.get(&device.device_id))
352 else {
353 continue;
354 };
355 let Some((key_id, raw)) = by_key_id.iter().next() else { continue };
356
357 match Self::verify_and_claim(account, device, key_id, raw) {
358 Ok(session) => {
359 let curve_b64 = device.curve25519.to_base64();
360 let session_id = session.session_id();
361 store.save_olm_session(&curve_b64, &session_id, pickle_session(&session)?)?;
362 outcome.established.push((device.user_id.clone(), device.device_id.clone()));
363 }
364 Err(reason) => outcome.refused.push(KeysClaimRefusal {
365 user_id: device.user_id.clone(),
366 device_id: device.device_id.clone(),
367 reason,
368 }),
369 }
370 }
371
372 Ok(outcome)
373 }
374
375 fn verify_and_claim(
376 account: &OlmAccountState,
377 device: &StoredDevice,
378 key_id: &str,
379 raw: &serde_json::Value,
380 ) -> Result<Session, String> {
381 if !key_id.starts_with(SIGNED_CURVE25519) {
382 return Err(format!("unexpected one-time-key algorithm in key id {key_id:?}"));
383 }
384 let sig_key_id = format!("ed25519:{}", device.device_id.as_str());
385 canonical_json::verify_json_signature(raw, device.user_id.as_str(), &sig_key_id, &device.ed25519)
386 .map_err(|source| format!("claimed one-time key signature failed to verify: {source}"))?;
387
388 let key_b64 = raw
389 .get("key")
390 .and_then(serde_json::Value::as_str)
391 .ok_or_else(|| "claimed one-time key object has no \"key\" field".to_string())?;
392 let one_time_key = Curve25519PublicKey::from_base64(key_b64)
393 .map_err(|source| format!("malformed claimed one-time key: {source}"))?;
394
395 account
396 .create_outbound_session(SessionConfig::version_1(), device.curve25519, one_time_key)
397 .map_err(|source| format!("failed to create outbound Olm session: {source}"))
398 }
399
400 pub fn encrypt_to_device<S: CryptoStore>(
405 store: &mut S,
406 account: &OlmAccountState,
407 our_user_id: &UserId,
408 our_device_id: &DeviceId,
409 target: &StoredDevice,
410 event_type: &str,
411 content: serde_json::Value,
412 ) -> Result<OlmEncryptedContent, MessengerError> {
413 let their_curve_b64 = target.curve25519.to_base64();
414 let sessions = store.olm_sessions_for_device(&their_curve_b64)?;
415 let latest = sessions.into_iter().next().ok_or_else(|| {
419 MessengerError::Crypto(format!(
420 "no Olm session established with device {} of {} yet",
421 target.device_id.as_str(),
422 target.user_id.as_str()
423 ))
424 })?;
425 let mut session = unpickle_session(&latest)?;
426
427 let payload = OlmPlaintextPayload {
428 event_type: event_type.to_string(),
429 content,
430 sender: our_user_id.clone(),
431 sender_device: our_device_id.clone(),
432 keys: OlmPlaintextKeys { ed25519: account.identity_keys().ed25519.to_base64() },
433 recipient: target.user_id.clone(),
434 recipient_keys: OlmPlaintextKeys { ed25519: target.ed25519.to_base64() },
435 };
436 let plaintext = serde_json::to_vec(&payload)?;
437
438 let olm_message = session.encrypt(plaintext).map_err(|source| {
439 MessengerError::Crypto(format!("Olm encrypt to {}: {source}", target.device_id.as_str()))
440 })?;
441
442 let session_id = session.session_id();
446 store.save_olm_session(&their_curve_b64, &session_id, pickle_session(&session)?)?;
447
448 let mut ciphertext = BTreeMap::new();
449 ciphertext.insert(their_curve_b64, Self::encode_olm_message(&olm_message));
450
451 Ok(OlmEncryptedContent { sender_key: account.identity_keys().curve25519.to_base64(), ciphertext })
452 }
453
454 pub fn decrypt_to_device<S: CryptoStore>(
458 store: &mut S,
459 account: &mut OlmAccountState,
460 our_user_id: &UserId,
461 event_sender: &UserId,
462 content: &OlmEncryptedContent,
463 ) -> Result<DecryptedToDevice, OlmDecryptError> {
464 let our_curve_b64 = account.identity_keys().curve25519.to_base64();
465 let cipher_info = content.ciphertext.get(&our_curve_b64).ok_or(OlmDecryptError::NotAddressedToUs)?;
466 let olm_message = Self::decode_olm_message(cipher_info)
467 .map_err(|source| OlmDecryptError::MalformedCiphertext(source.to_string()))?;
468
469 let sender_curve_b64 = content.sender_key.clone();
470
471 let sender_devices = DeviceTracker::devices_for_user(store, event_sender)?;
474 let Some(sender_device) = sender_devices.iter().find(|device| device.curve25519.to_base64() == sender_curve_b64) else {
475 store.mark_user_tracked(event_sender, true)?;
476 return Err(OlmDecryptError::UnknownSenderDevice { user_id: event_sender.clone(), curve25519: sender_curve_b64 });
477 };
478
479 let known_sessions: Vec<Vec<u8>> = store.olm_sessions_for_device(&sender_curve_b64)?;
483
484 let mut established: Option<(Session, Vec<u8>)> = None;
485 for pickle_bytes in &known_sessions {
486 let mut session = unpickle_session(pickle_bytes)
487 .map_err(|source| OlmDecryptError::SessionEstablishment(format!("corrupt stored session: {source}")))?;
488 if let Ok(plaintext) = session.decrypt(&olm_message) {
489 established = Some((session, plaintext));
490 break;
491 }
492 }
493
494 let plaintext = match established {
495 Some((session, plaintext)) => {
496 let session_id = session.session_id();
497 store.save_olm_session(&sender_curve_b64, &session_id, pickle_session(&session)?)?;
498 plaintext
499 }
500 None => match &olm_message {
501 OlmMessage::PreKey(pre_key_message) => {
502 let sender_curve = Curve25519PublicKey::from_base64(&sender_curve_b64).map_err(|source| {
503 OlmDecryptError::MalformedCiphertext(format!("malformed sender_key: {source}"))
504 })?;
505 let result = account
506 .create_inbound_session(store, SessionConfig::version_1(), sender_curve, pre_key_message)
507 .map_err(|source| OlmDecryptError::SessionEstablishment(source.to_string()))?;
508 let session_id = result.session.session_id();
509 store.save_olm_session(&sender_curve_b64, &session_id, pickle_session(&result.session)?)?;
510 result.plaintext
511 }
512 OlmMessage::Normal(_) => return Err(OlmDecryptError::NoMatchingSession(sender_curve_b64)),
513 },
514 };
515
516 let payload: OlmPlaintextPayload = serde_json::from_slice(&plaintext)?;
517
518 if payload.recipient != *our_user_id {
519 return Err(OlmDecryptError::PayloadMismatch(format!(
520 "payload recipient {:?} does not match this account's own user id {:?}",
521 payload.recipient.as_str(),
522 our_user_id.as_str()
523 )));
524 }
525 let our_ed25519_b64 = account.identity_keys().ed25519.to_base64();
526 if payload.recipient_keys.ed25519 != our_ed25519_b64 {
527 return Err(OlmDecryptError::PayloadMismatch(
528 "payload recipient_keys.ed25519 does not match this account's own ed25519 key".to_string(),
529 ));
530 }
531 if payload.sender != *event_sender {
532 return Err(OlmDecryptError::PayloadMismatch(format!(
533 "payload sender {:?} does not match the to-device event's own sender {:?}",
534 payload.sender.as_str(),
535 event_sender.as_str()
536 )));
537 }
538
539 if payload.keys.ed25519 != sender_device.ed25519.to_base64() {
540 return Err(OlmDecryptError::PayloadMismatch(
541 "payload keys.ed25519 does not match the stored ed25519 key of the sender device".to_string(),
542 ));
543 }
544
545 Ok(DecryptedToDevice {
546 sender: payload.sender,
547 sender_device_curve25519: sender_device.curve25519,
548 sender_ed25519: sender_device.ed25519,
549 event_type: payload.event_type,
550 content: payload.content,
551 })
552 }
553}
554
555#[cfg(test)]
556mod tests {
557 use super::*;
558 use crate::store::{InsecurePlainCodecForTests, Store};
559
560 fn new_store(device_id: &str) -> Store<InsecurePlainCodecForTests> {
561 Store::new(DeviceId::parse(device_id).expect("valid device id"), InsecurePlainCodecForTests)
562 }
563
564 fn user(name: &str) -> UserId {
565 UserId::parse(format!("@{name}:example.org")).expect("valid user id")
566 }
567
568 fn device(name: &str) -> DeviceId {
569 DeviceId::parse(name).expect("valid device id")
570 }
571
572 fn fallback_present() -> Vec<String> {
576 vec![SIGNED_CURVE25519.to_string()]
577 }
578
579 fn stored_device(user_id: &UserId, device_id: &DeviceId, account: &OlmAccountState) -> StoredDevice {
580 let identity = account.identity_keys();
581 StoredDevice {
582 user_id: user_id.clone(),
583 device_id: device_id.clone(),
584 curve25519: identity.curve25519,
585 ed25519: identity.ed25519,
586 algorithms: vec!["m.olm.v1.curve25519-aes-sha2".to_string(), "m.megolm.v1.aes-sha2".to_string()],
587 display_name: None,
588 verified: false,
589 blocked: false,
590 }
591 }
592
593 fn claim_response_body_from_upload(upload_body: &serde_json::Value, user_id: &UserId, device_id: &DeviceId) -> Vec<u8> {
599 let one_time_keys =
600 upload_body.get("one_time_keys").and_then(serde_json::Value::as_object).expect("upload carries OTKs");
601 let (key_id, key_value) = one_time_keys.iter().next().expect("at least one OTK");
602 serde_json::to_vec(&serde_json::json!({
603 "one_time_keys": { user_id.as_str(): { device_id.as_str(): { key_id: key_value } } }
604 }))
605 .expect("valid JSON")
606 }
607
608 #[test]
609 fn olm_session_established_via_claimed_otk() {
610 let mut alice_store = new_store("ALICEDEV");
611 let alice_account = OlmAccountState::load_or_create(&mut alice_store).expect("create alice's account");
612
613 let bob_user = user("bob");
614 let bob_device_id = device("BOBDEV");
615 let mut bob_store = new_store("BOBDEV");
616 let mut bob_account = OlmAccountState::load_or_create(&mut bob_store).expect("create bob's account");
617 bob_account.on_sync_counts(&mut bob_store, 0, &fallback_present()).expect("top up bob's OTKs");
618
619 let upload = bob_account
620 .keys_upload_request(RequestId::next(0), &bob_user, &bob_device_id)
621 .expect("builds a request")
622 .expect("something to upload");
623 let claim_body =
624 claim_response_body_from_upload(&upload.body.expect("upload has a body"), &bob_user, &bob_device_id);
625 let bob_device = stored_device(&bob_user, &bob_device_id, &bob_account);
626
627 let outcome = OlmSessionManager::on_keys_claim_response(&mut alice_store, &alice_account, &[&bob_device], &claim_body)
628 .expect("applies the claim response");
629
630 assert_eq!(outcome.established, vec![(bob_user, bob_device_id)]);
631 assert!(outcome.refused.is_empty());
632 let sessions = alice_store
633 .olm_sessions_for_device(&bob_account.identity_keys().curve25519.to_base64())
634 .expect("no error");
635 assert_eq!(sessions.len(), 1, "exactly one session established");
636 }
637
638 #[test]
639 fn olm_session_rejects_out_of_order_ratchet_message() {
640 let mut alice_store = new_store("ALICEDEV");
641 let alice_account = OlmAccountState::load_or_create(&mut alice_store).expect("create alice's account");
642 let alice_user = user("alice");
643 let alice_device_id = device("ALICEDEV");
644
645 let bob_user = user("bob");
646 let bob_device_id = device("BOBDEV");
647 let mut bob_store = new_store("BOBDEV");
648 let mut bob_account = OlmAccountState::load_or_create(&mut bob_store).expect("create bob's account");
649 bob_account.on_sync_counts(&mut bob_store, 0, &fallback_present()).expect("top up bob's OTKs");
650
651 let upload = bob_account
652 .keys_upload_request(RequestId::next(0), &bob_user, &bob_device_id)
653 .expect("builds a request")
654 .expect("something to upload");
655 let claim_body =
656 claim_response_body_from_upload(&upload.body.expect("upload has a body"), &bob_user, &bob_device_id);
657 let bob_device = stored_device(&bob_user, &bob_device_id, &bob_account);
658 OlmSessionManager::on_keys_claim_response(&mut alice_store, &alice_account, &[&bob_device], &claim_body)
659 .expect("alice establishes a session with bob");
660
661 let alice_device_keys =
664 alice_account.device_keys_json(&alice_user, &alice_device_id).expect("sign alice's device_keys");
665 let query_body = serde_json::to_vec(&serde_json::json!({
666 "device_keys": { alice_user.as_str(): { alice_device_id.as_str(): alice_device_keys } }
667 }))
668 .expect("valid JSON");
669 DeviceTracker::on_keys_query_response(&mut bob_store, &query_body).expect("bob learns alice's device");
670
671 let message_0 = OlmSessionManager::encrypt_to_device(
675 &mut alice_store,
676 &alice_account,
677 &alice_user,
678 &alice_device_id,
679 &bob_device,
680 "m.test",
681 serde_json::json!({ "i": 0 }),
682 )
683 .expect("encrypt message 0");
684 let message_1 = OlmSessionManager::encrypt_to_device(
685 &mut alice_store,
686 &alice_account,
687 &alice_user,
688 &alice_device_id,
689 &bob_device,
690 "m.test",
691 serde_json::json!({ "i": 1 }),
692 )
693 .expect("encrypt message 1");
694
695 let decrypted_1 = OlmSessionManager::decrypt_to_device(&mut bob_store, &mut bob_account, &bob_user, &alice_user, &message_1)
698 .expect("message 1 decrypts, establishing the session");
699 assert_eq!(decrypted_1.content, serde_json::json!({ "i": 1 }));
700
701 let decrypted_0 = OlmSessionManager::decrypt_to_device(&mut bob_store, &mut bob_account, &bob_user, &alice_user, &message_0)
702 .expect("message 0 decrypts through the same, already-established session");
703 assert_eq!(decrypted_0.content, serde_json::json!({ "i": 0 }));
704
705 let replay = OlmSessionManager::decrypt_to_device(&mut bob_store, &mut bob_account, &bob_user, &alice_user, &message_0);
709 assert!(replay.is_err(), "replaying an already-decrypted message must be rejected");
710 assert!(
711 matches!(replay, Err(OlmDecryptError::SessionEstablishment(_))),
712 "got {replay:?}"
713 );
714 }
715
716 #[test]
717 fn replay_is_rejected_after_reload_from_records() {
718 let mut alice_store = new_store("ALICEDEV");
719 let alice_account = OlmAccountState::load_or_create(&mut alice_store).expect("create alice's account");
720 let alice_user = user("alice");
721 let alice_device_id = device("ALICEDEV");
722
723 let bob_user = user("bob");
724 let bob_device_id = device("BOBDEV");
725 let mut bob_store = new_store("BOBDEV");
726 let mut bob_account = OlmAccountState::load_or_create(&mut bob_store).expect("create bob's account");
727 bob_account.on_sync_counts(&mut bob_store, 0, &fallback_present()).expect("top up bob's OTKs");
728
729 let upload = bob_account
730 .keys_upload_request(RequestId::next(0), &bob_user, &bob_device_id)
731 .expect("builds a request")
732 .expect("something to upload");
733 let claim_body =
734 claim_response_body_from_upload(&upload.body.expect("upload has a body"), &bob_user, &bob_device_id);
735 let bob_device = stored_device(&bob_user, &bob_device_id, &bob_account);
736 OlmSessionManager::on_keys_claim_response(&mut alice_store, &alice_account, &[&bob_device], &claim_body)
737 .expect("alice establishes a session with bob");
738
739 let alice_device_keys =
740 alice_account.device_keys_json(&alice_user, &alice_device_id).expect("sign alice's device_keys");
741 let query_body = serde_json::to_vec(&serde_json::json!({
742 "device_keys": { alice_user.as_str(): { alice_device_id.as_str(): alice_device_keys } }
743 }))
744 .expect("valid JSON");
745 DeviceTracker::on_keys_query_response(&mut bob_store, &query_body).expect("bob learns alice's device");
746
747 let message_0 = OlmSessionManager::encrypt_to_device(
748 &mut alice_store,
749 &alice_account,
750 &alice_user,
751 &alice_device_id,
752 &bob_device,
753 "m.test",
754 serde_json::json!({ "i": 0 }),
755 )
756 .expect("encrypt message 0");
757
758 OlmSessionManager::decrypt_to_device(&mut bob_store, &mut bob_account, &bob_user, &alice_user, &message_0)
759 .expect("message 0 decrypts and establishes bob's session");
760
761 let batch = bob_store.take_flush_batch().expect("decrypting dirtied the store");
767 bob_store.ack_flush(batch.id);
768 let mut reloaded_store =
769 Store::load(batch.records, InsecurePlainCodecForTests, bob_device_id.clone()).expect("reload succeeds");
770 let mut reloaded_account =
771 OlmAccountState::load_or_create(&mut reloaded_store).expect("reload reuses the persisted account");
772
773 let replay = OlmSessionManager::decrypt_to_device(
774 &mut reloaded_store,
775 &mut reloaded_account,
776 &bob_user,
777 &alice_user,
778 &message_0,
779 );
780 assert!(replay.is_err(), "a replay must still be rejected after a full store reload");
781 assert!(
782 matches!(replay, Err(OlmDecryptError::SessionEstablishment(_))),
783 "got {replay:?}"
784 );
785 }
786
787 #[test]
788 fn unknown_sender_prekey_message_stays_decryptable_after_the_device_is_learned() {
789 let mut alice_store = new_store("ALICEDEV");
790 let alice_account = OlmAccountState::load_or_create(&mut alice_store).expect("create alice's account");
791 let alice_user = user("alice");
792 let alice_device_id = device("ALICEDEV");
793
794 let bob_user = user("bob");
795 let bob_device_id = device("BOBDEV");
796 let mut bob_store = new_store("BOBDEV");
797 let mut bob_account = OlmAccountState::load_or_create(&mut bob_store).expect("create bob's account");
798 bob_account.on_sync_counts(&mut bob_store, 0, &fallback_present()).expect("top up bob's OTKs");
799
800 let upload = bob_account
801 .keys_upload_request(RequestId::next(0), &bob_user, &bob_device_id)
802 .expect("builds a request")
803 .expect("something to upload");
804 let claim_body =
805 claim_response_body_from_upload(&upload.body.expect("upload has a body"), &bob_user, &bob_device_id);
806 let bob_device = stored_device(&bob_user, &bob_device_id, &bob_account);
807 OlmSessionManager::on_keys_claim_response(&mut alice_store, &alice_account, &[&bob_device], &claim_body)
808 .expect("alice establishes a session with bob");
809 let message_0 = OlmSessionManager::encrypt_to_device(
810 &mut alice_store,
811 &alice_account,
812 &alice_user,
813 &alice_device_id,
814 &bob_device,
815 "m.test",
816 serde_json::json!({ "i": 0 }),
817 )
818 .expect("encrypt message 0");
819
820 let first = OlmSessionManager::decrypt_to_device(&mut bob_store, &mut bob_account, &bob_user, &alice_user, &message_0);
823 assert!(matches!(first, Err(OlmDecryptError::UnknownSenderDevice { .. })), "got {first:?}");
824 let alice_curve = alice_account.identity_keys().curve25519.to_base64();
825 assert!(
826 bob_store.olm_sessions_for_device(&alice_curve).expect("no error").is_empty(),
827 "the refused attempt must not create an inbound session (it would consume the one-time key)"
828 );
829
830 let alice_device_keys =
832 alice_account.device_keys_json(&alice_user, &alice_device_id).expect("sign alice's device_keys");
833 let query_body = serde_json::to_vec(&serde_json::json!({
834 "device_keys": { alice_user.as_str(): { alice_device_id.as_str(): alice_device_keys } }
835 }))
836 .expect("valid JSON");
837 DeviceTracker::on_keys_query_response(&mut bob_store, &query_body).expect("bob learns alice's device");
838 let retry = OlmSessionManager::decrypt_to_device(&mut bob_store, &mut bob_account, &bob_user, &alice_user, &message_0)
839 .expect("the retry decrypts: nothing was consumed by the refused attempt");
840 assert_eq!(retry.content, serde_json::json!({ "i": 0 }));
841 }
842}