Skip to main content

obby_client/
e2ee.rs

1//! X3DH key agreement and the Double Ratchet, implemented against the wire the running client and
2//! server actually speak rather than against the published
3//! <https://github.com/obbyworld/extensions/blob/main/e2ee.md>. The two disagree,
4//! and the clearest case is the spec's own example,
5//! which inlines a top-level `ik` field on the `init` frame that the real wire never sends, only
6//! the nested [`PreKeyBundle`] does.
7//!
8//! This module is `no_std` plus `alloc`, has no entropy source of its own, and never touches
9//! `std::`, `getrandom`, or any system RNG: every function that needs randomness takes one
10//! through [`RandomSource`], which the host fills from whatever CSPRNG it already holds. That
11//! keeps this buildable for `wasm32-unknown-unknown`, which has no ambient entropy to reach for.
12//!
13//! Wire encoding (base64, JSON, the `?obe2ee:` body marker, tag fragmentation over the wire) is
14//! deliberately out of scope: the types here model the frame set's fields with the research doc's
15//! exact names, but turning them into bytes on a `TAGMSG`/`PRIVMSG` is the transport's job, done
16//! elsewhere. For the same reason, the AEAD associated data authenticates a canonical binary
17//! encoding of a message header (`dh || pn || n`) rather than literal wire JSON bytes; a future
18//! wire codec must reproduce the same JSON bytes the reference client authenticates to interop
19//! with it, but that does not change anything this module does with the fields once decoded.
20//!
21//! Not wired into `client.rs` or `session.rs` yet, so nothing outside this module's own tests
22//! calls into it. The `expect` below is scoped to non-test builds specifically so that wiring
23//! this in later without removing the annotation fails loudly (an unfulfilled expectation)
24//! instead of silently doing nothing, which is what `#[allow(dead_code)]` would do.
25
26use alloc::collections::{BTreeMap, VecDeque};
27use alloc::string::String;
28use alloc::vec::Vec;
29use core::fmt::Write as _;
30use core::mem;
31
32use chacha20poly1305::aead::{Aead, Payload};
33use chacha20poly1305::{Key, KeyInit, XChaCha20Poly1305, XNonce};
34use ed25519_dalek::{Signature, Signer, SigningKey, VerifyingKey};
35use hkdf::Hkdf;
36use hkdf::hmac::{Hmac, Mac};
37use sha2::{Digest, Sha256};
38use x25519_dalek::{PublicKey, StaticSecret};
39use zeroize::Zeroizing;
40
41/// The wire's protocol version, carried as `v` on every frame. Never varies today; kept as a
42/// named constant for whoever writes the wire codec rather than a magic `1` in two places.
43pub const PROTOCOL_VERSION: u32 = 1;
44
45/// One skip-ahead jump's bound: a single message whose counter implies more than this many
46/// unseen messages in one chain is refused outright, rather than buffered.
47pub const MAX_SKIP: u32 = 1000;
48
49/// The total number of out-of-order message keys this ratchet will hold onto at once, across
50/// every chain it has ever had. Beyond this, the oldest key is evicted to make room, so a peer
51/// cannot exhaust memory by never sending the messages a lower counter promised.
52pub const MAX_SKIPPED_KEYS: usize = 2000;
53
54/// Plaintext is padded to a multiple of this many bytes before encryption, so ciphertext length
55/// reveals only a size bucket rather than the exact message length.
56const PAD_BLOCK: usize = 64;
57
58const X3DH_INFO: &[u8] = b"obby.world/e2ee x3dh";
59const ROOT_INFO: &[u8] = b"obby.world/e2ee root";
60const MESSAGE_KEY_INFO: &[u8] = b"obby.world/e2ee message";
61const NONCE_INFO: &[u8] = b"obby.world/e2ee nonce";
62
63/// Everything that can go wrong here: a doomed handshake, a ratchet that refuses to advance, or
64/// a caller asking the state machine for a transition it does not allow.
65#[derive(Debug, Clone, Copy, PartialEq, Eq)]
66#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
67pub enum Error {
68    /// A signature the protocol requires did not verify.
69    InvalidSignature,
70    /// A Diffie-Hellman output was non-contributory: a small-order or otherwise degenerate
71    /// public key, which HKDF would otherwise turn into a predictable key.
72    NonContributoryDh,
73    /// An AEAD operation failed: on decrypt, a tampered, misrouted, or out-of-session
74    /// ciphertext; on encrypt, only ever a coding error in this module.
75    Aead,
76    /// A decrypted plaintext's padding was not well-formed ISO/IEC 7816-4 padding.
77    Padding,
78    /// A message implied a skip-ahead of more than [`MAX_SKIP`] messages in one jump.
79    TooManySkipped,
80    /// A message counter would overflow `u32`, which cannot happen in a real conversation and
81    /// therefore signals a malicious or corrupted counter.
82    CounterOverflow,
83    /// There is no established sending or receiving chain to use yet.
84    NoChain,
85    /// The peer's fingerprint changed since it was first pinned. The caller must call
86    /// [`Session::confirm_fingerprint_change`] before the same operation can succeed.
87    FingerprintChanged {
88        /// The fingerprint pinned from an earlier conversation.
89        previous: Fingerprint,
90        /// The fingerprint just observed.
91        current: Fingerprint,
92    },
93    /// The session is not in a state that allows this operation, such as decrypting content
94    /// before the handshake's `ack` has been received and decrypted.
95    WrongState,
96    /// A `frag` set could not be reassembled: a mismatched `id`/`n`, a duplicate or
97    /// out-of-range index, or a missing piece.
98    Fragmentation,
99    /// An underlying primitive rejected an input this module always constructs to be valid
100    /// (an HMAC key length, an HKDF output length). Never expected to occur in practice.
101    Internal,
102}
103
104/// A source of random bytes, supplied by the caller.
105///
106/// This module has no entropy source of its own: no OS RNG, no `getrandom`, nothing that would
107/// need a host bridge on a target like `wasm32-unknown-unknown` where none exists. A caller
108/// fills this from a CSPRNG it already holds, or from a fixed seed for reproducible tests.
109pub trait RandomSource {
110    /// Fill `dest` with bytes suitable for key material.
111    fn fill_bytes(&mut self, dest: &mut [u8]);
112}
113
114fn random_array<const N: usize>(rng: &mut impl RandomSource) -> [u8; N] {
115    let mut bytes = [0u8; N];
116    rng.fill_bytes(&mut bytes);
117    bytes
118}
119
120fn generate_x25519_keypair(rng: &mut impl RandomSource) -> ([u8; 32], [u8; 32]) {
121    let secret = random_array::<32>(rng);
122    let public = *PublicKey::from(&StaticSecret::from(secret)).as_bytes();
123    (secret, public)
124}
125
126fn concat(parts: &[&[u8]]) -> Vec<u8> {
127    let mut out = Vec::new();
128    for part in parts {
129        out.extend_from_slice(part);
130    }
131    out
132}
133
134fn hkdf_sha256(salt: &[u8], ikm: &[u8], info: &[u8], out: &mut [u8]) -> Result<(), Error> {
135    let hk = Hkdf::<Sha256>::new(Some(salt), ikm);
136    hk.expand(info, out).map_err(|_| Error::Internal)
137}
138
139fn hmac_sha256(key: &[u8; 32], data: &[u8]) -> Result<[u8; 32], Error> {
140    let mut mac = <Hmac<Sha256> as Mac>::new_from_slice(key).map_err(|_| Error::Internal)?;
141    mac.update(data);
142    Ok(mac.finalize().into_bytes().into())
143}
144
145fn diffie_hellman_raw(secret: &[u8; 32], public: &[u8; 32]) -> Result<[u8; 32], Error> {
146    let secret = StaticSecret::from(*secret);
147    let public = PublicKey::from(*public);
148    let shared = secret.diffie_hellman(&public);
149    if !shared.was_contributory() {
150        return Err(Error::NonContributoryDh);
151    }
152    Ok(*shared.as_bytes())
153}
154
155// ---------------------------------------------------------------------------------------------
156// Identity, fingerprints and trust-on-first-use
157// ---------------------------------------------------------------------------------------------
158
159/// The public half of an [`Identity`]: an X25519 agreement key (`ik`) and an Ed25519 signing
160/// key (`sik`), exactly as they travel inside [`PreKeyBundle`] and [`HandshakeResponse`].
161#[derive(Debug, Clone, Copy, PartialEq, Eq)]
162#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
163pub struct IdentityPublic {
164    /// The X25519 identity agreement key, `ik`.
165    pub agreement: [u8; 32],
166    /// The Ed25519 signing key, `sik`. Every signature in this protocol verifies against this
167    /// key, and [`Fingerprint`] is derived from it, never from `agreement`.
168    pub signing: [u8; 32],
169}
170
171struct IdentitySecret {
172    agreement: Zeroizing<[u8; 32]>,
173    signing: Zeroizing<[u8; 32]>,
174}
175
176/// A long-term identity: an X25519 agreement key and an Ed25519 signing key, generated once and
177/// kept for the lifetime of an account.
178pub struct Identity {
179    secret: IdentitySecret,
180    public: IdentityPublic,
181}
182
183impl Identity {
184    /// Generate a fresh identity from caller-supplied randomness.
185    pub fn generate(rng: &mut impl RandomSource) -> Self {
186        let agreement_secret = random_array::<32>(rng);
187        let signing_secret = random_array::<32>(rng);
188        let agreement_public = *PublicKey::from(&StaticSecret::from(agreement_secret)).as_bytes();
189        let signing_public = *SigningKey::from_bytes(&signing_secret)
190            .verifying_key()
191            .as_bytes();
192        Self {
193            secret: IdentitySecret {
194                agreement: Zeroizing::new(agreement_secret),
195                signing: Zeroizing::new(signing_secret),
196            },
197            public: IdentityPublic {
198                agreement: agreement_public,
199                signing: signing_public,
200            },
201        }
202    }
203
204    /// The public half of this identity, safe to publish.
205    pub const fn public(&self) -> IdentityPublic {
206        self.public
207    }
208
209    /// This identity's own fingerprint, derived from its signing key.
210    pub fn fingerprint(&self) -> Fingerprint {
211        Fingerprint::of_signing_key(&self.public.signing)
212    }
213
214    fn sign(&self, message: &[u8]) -> Result<Vec<u8>, Error> {
215        let signing_key = SigningKey::from_bytes(&self.secret.signing);
216        let signature: Signature = signing_key
217            .try_sign(message)
218            .map_err(|_| Error::InvalidSignature)?;
219        Ok(signature.to_bytes().to_vec())
220    }
221}
222
223/// A peer's identity fingerprint: the first 16 bytes of `SHA-256(signing_public_key)`.
224///
225/// Derived from the signing key, never the agreement key, so the key whose signature is
226/// verified on every handshake is provably the same key a safety number displays.
227#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
228#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
229pub struct Fingerprint([u8; 16]);
230
231impl Fingerprint {
232    /// Derive a fingerprint from a raw Ed25519 signing public key.
233    pub fn of_signing_key(signing_public: &[u8; 32]) -> Self {
234        let digest = Sha256::digest(signing_public);
235        let (head, _tail) = digest.split_at(16);
236        let mut bytes = [0u8; 16];
237        bytes.copy_from_slice(head);
238        Self(bytes)
239    }
240
241    /// Render as 8 groups of 4 uppercase hex characters separated by spaces: the safety number
242    /// two people compare out of band to confirm they share the same peer.
243    pub fn safety_number(&self) -> String {
244        let mut out = String::with_capacity(39);
245        for (index, pair) in self.0.chunks(2).enumerate() {
246            if index > 0 {
247                out.push(' ');
248            }
249            for byte in pair {
250                let _ = write!(out, "{byte:02X}");
251            }
252        }
253        out
254    }
255}
256
257/// The result of observing a peer's fingerprint against what was previously pinned for them.
258#[derive(Debug, Clone, Copy, PartialEq, Eq)]
259pub enum PinOutcome {
260    /// No fingerprint was pinned yet; this one now is.
261    New,
262    /// The observed fingerprint matches what was already pinned.
263    Same,
264    /// The observed fingerprint does not match the pinned one. The pin is left unchanged;
265    /// only [`PeerTrust::repin`] moves it.
266    Changed {
267        /// The fingerprint that stays pinned until the caller explicitly repins.
268        previous: Fingerprint,
269    },
270}
271
272/// Trust-on-first-use bookkeeping for one peer.
273///
274/// Pins a fingerprint the first time a peer is seen and never moves the pin silently: a later
275/// mismatch is reported, not applied, and only [`PeerTrust::repin`] accepts a new key.
276#[derive(Debug, Clone, Default)]
277pub struct PeerTrust {
278    pinned: Option<Fingerprint>,
279    verified: bool,
280}
281
282impl PeerTrust {
283    /// A trust record for a peer that has never been seen.
284    pub fn new() -> Self {
285        Self::default()
286    }
287
288    /// Compare `fingerprint` against the pin, recording it on first contact.
289    pub fn observe(&mut self, fingerprint: Fingerprint) -> PinOutcome {
290        match self.pinned {
291            None => {
292                self.pinned = Some(fingerprint);
293                PinOutcome::New
294            }
295            Some(pinned) if pinned == fingerprint => PinOutcome::Same,
296            Some(previous) => PinOutcome::Changed { previous },
297        }
298    }
299
300    /// Explicitly accept `fingerprint` as the pin, after the caller has decided a key change is
301    /// legitimate. Clears verification, since that was asserted for the old key.
302    pub fn repin(&mut self, fingerprint: Fingerprint) {
303        self.pinned = Some(fingerprint);
304        self.verified = false;
305    }
306
307    /// The currently pinned fingerprint, if any.
308    pub const fn pinned(&self) -> Option<Fingerprint> {
309        self.pinned
310    }
311
312    /// Whether the pinned fingerprint has been confirmed out of band.
313    pub const fn is_verified(&self) -> bool {
314        self.verified
315    }
316
317    /// Record that the pinned fingerprint has (or has not) been confirmed out of band.
318    pub fn set_verified(&mut self, verified: bool) {
319        self.verified = verified;
320    }
321}
322
323/// Decide who keeps their offer when both sides send `init` at the same moment.
324///
325/// The side with the lower fingerprint keeps its offer and stays the initiator; the other side
326/// answers. A side that cannot read the peer's fingerprint (`peer` is `None`, an offer that
327/// failed to parse) always answers, since holding in that case could deadlock both sides
328/// forever.
329pub fn keeps_own_offer(own: Fingerprint, peer: Option<Fingerprint>) -> bool {
330    match peer {
331        Some(peer) => own < peer,
332        None => false,
333    }
334}
335
336// ---------------------------------------------------------------------------------------------
337// The wire's frame set
338// ---------------------------------------------------------------------------------------------
339
340/// A responder-published prekey bundle, decoded from the wire's base64 JSON blob that `init`
341/// carries as `bundle`. Field names match the wire exactly.
342#[derive(Debug, Clone, PartialEq, Eq)]
343#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
344pub struct PreKeyBundle {
345    /// The sender's identity agreement key.
346    pub ik: [u8; 32],
347    /// The sender's identity signing key.
348    pub sik: [u8; 32],
349    /// A freshly generated signed-prekey.
350    pub spk: [u8; 32],
351    /// `Ed25519(ik ‖ spk ‖ opk)`, signed with `sik`.
352    pub sig: Vec<u8>,
353    /// A freshly generated one-time prekey.
354    pub opk: [u8; 32],
355}
356
357/// The responder's answer, decoded from the wire's base64 JSON blob that `accept` carries as
358/// `response`.
359#[derive(Debug, Clone, PartialEq, Eq)]
360#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
361pub struct HandshakeResponse {
362    /// The responder's identity agreement key.
363    pub ik: [u8; 32],
364    /// The responder's identity signing key.
365    pub sik: [u8; 32],
366    /// A freshly generated ephemeral key, doubling as the responder's first ratchet keypair.
367    pub ek: [u8; 32],
368    /// `Ed25519(ik ‖ ek)`, signed with `sik`.
369    pub sig: Vec<u8>,
370    /// The first ratchet message, carrying empty plaintext, proving the responder derived the
371    /// same X3DH secret the initiator will.
372    pub boot: RatchetMessage,
373}
374
375/// One Double Ratchet message: a header carried as authenticated associated data, and an AEAD
376/// ciphertext.
377#[derive(Debug, Clone, PartialEq, Eq)]
378#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
379pub struct RatchetMessage {
380    /// The sender's current ratchet public key.
381    pub dh: [u8; 32],
382    /// The length of the sender's previous sending chain.
383    pub pn: u32,
384    /// This message's counter within the sender's current chain.
385    pub n: u32,
386    /// The AEAD ciphertext.
387    pub ct: Vec<u8>,
388}
389
390/// One `t`/`v` protocol frame, exactly as the wire's client-only tag carries it (`init`,
391/// `accept`, `reject`, `ack`, `close`) or, for `msg` and `media`, the `?obe2ee:`-prefixed body.
392///
393/// `frag` is not a variant here: it wraps another frame's encoded bytes across several wire
394/// lines and belongs to the transport that reassembles it, not to session logic. See [`Frag`].
395#[derive(Debug, Clone, PartialEq, Eq)]
396#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
397pub enum Frame {
398    /// An offer to start an encrypted session.
399    Init {
400        /// The offering side's prekey bundle.
401        bundle: PreKeyBundle,
402        /// The sender's SASL account, when it has one.
403        account: Option<String>,
404    },
405    /// An answer to an offer.
406    Accept {
407        /// The answering side's handshake response.
408        response: HandshakeResponse,
409        /// The sender's SASL account, when it has one.
410        account: Option<String>,
411    },
412    /// A refusal of an offer.
413    Reject {
414        /// An optional human-readable reason.
415        reason: Option<String>,
416    },
417    /// The initiator's first encrypted payload, proving the session works.
418    Ack {
419        /// The ratchet-encrypted empty payload.
420        ct: RatchetMessage,
421    },
422    /// The end of a session.
423    Close,
424    /// An encrypted text message.
425    Msg {
426        /// The ratchet-encrypted payload.
427        ct: RatchetMessage,
428    },
429    /// An encrypted file descriptor.
430    Media {
431        /// The ratchet-encrypted payload, whose plaintext is a media descriptor.
432        ct: RatchetMessage,
433    },
434}
435
436/// The fragmentation envelope the real client uses to split a frame too large for one wire
437/// line, on either carrier. The working client sends this, and no spec text describes it.
438#[derive(Debug, Clone, PartialEq, Eq)]
439#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
440pub struct Frag {
441    /// The id every fragment of one split frame shares.
442    pub id: String,
443    /// This fragment's 0-based index.
444    pub i: u32,
445    /// The total number of fragments in the set.
446    pub n: u32,
447    /// This fragment's slice of the encoded payload.
448    pub ct: Vec<u8>,
449}
450
451/// Reassemble a complete set of fragments back into the payload they were split from.
452///
453/// Rejects a set with a mismatched `id` or `n` across fragments, a duplicate or out-of-range
454/// index, or a missing piece. Buffering fragments as they trickle in, and sweeping a stream
455/// that never completes, needs a clock this module is never given; that bookkeeping belongs to
456/// the transport that owns `tick`, not here.
457pub fn reassemble(fragments: &[Frag]) -> Result<Vec<u8>, Error> {
458    let first = fragments.first().ok_or(Error::Fragmentation)?;
459    let id = &first.id;
460    let total = first.n;
461    let expected = u32::try_from(fragments.len()).map_err(|_| Error::Fragmentation)?;
462    if expected != total {
463        return Err(Error::Fragmentation);
464    }
465
466    let mut slots: Vec<Option<&[u8]>> = alloc::vec![None; fragments.len()];
467    for fragment in fragments {
468        if &fragment.id != id || fragment.n != total {
469            return Err(Error::Fragmentation);
470        }
471        let index = usize::try_from(fragment.i).map_err(|_| Error::Fragmentation)?;
472        let slot = slots.get_mut(index).ok_or(Error::Fragmentation)?;
473        if slot.is_some() {
474            return Err(Error::Fragmentation);
475        }
476        *slot = Some(&fragment.ct);
477    }
478
479    let mut out = Vec::new();
480    for slot in slots {
481        out.extend_from_slice(slot.ok_or(Error::Fragmentation)?);
482    }
483    Ok(out)
484}
485
486// ---------------------------------------------------------------------------------------------
487// X3DH
488// ---------------------------------------------------------------------------------------------
489
490/// The offering side's freshly generated prekeys, retained locally until the peer's `accept`
491/// arrives. Never sent as-is; [`PreKeyBundle`] is the public half that is.
492pub struct PendingOffer {
493    bundle: PreKeyBundle,
494    spk_secret: Zeroizing<[u8; 32]>,
495    opk_secret: Zeroizing<[u8; 32]>,
496}
497
498/// Build the prekey bundle an `init` frame carries: a fresh signed-prekey and one-time-prekey,
499/// signed together with the identity key by the long-term signing key.
500pub fn create_offer(
501    identity: &Identity,
502    rng: &mut impl RandomSource,
503) -> Result<PendingOffer, Error> {
504    let spk_secret = random_array::<32>(rng);
505    let opk_secret = random_array::<32>(rng);
506    let spk_public = *PublicKey::from(&StaticSecret::from(spk_secret)).as_bytes();
507    let opk_public = *PublicKey::from(&StaticSecret::from(opk_secret)).as_bytes();
508
509    let public = identity.public();
510    let signed = concat(&[&public.agreement, &spk_public, &opk_public]);
511    let sig = identity.sign(&signed)?;
512
513    Ok(PendingOffer {
514        bundle: PreKeyBundle {
515            ik: public.agreement,
516            sik: public.signing,
517            spk: spk_public,
518            sig,
519            opk: opk_public,
520        },
521        spk_secret: Zeroizing::new(spk_secret),
522        opk_secret: Zeroizing::new(opk_secret),
523    })
524}
525
526fn verify_signature(
527    signing_public: &[u8; 32],
528    message: &[u8],
529    signature: &[u8],
530) -> Result<(), Error> {
531    let verifying_key =
532        VerifyingKey::from_bytes(signing_public).map_err(|_| Error::InvalidSignature)?;
533    let signature = Signature::try_from(signature).map_err(|_| Error::InvalidSignature)?;
534    verifying_key
535        .verify_strict(message, &signature)
536        .map_err(|_| Error::InvalidSignature)
537}
538
539fn verify_bundle_signature(bundle: &PreKeyBundle) -> Result<(), Error> {
540    let signed = concat(&[&bundle.ik, &bundle.spk, &bundle.opk]);
541    verify_signature(&bundle.sik, &signed, &bundle.sig)
542}
543
544fn x3dh_kdf(
545    dh1: &[u8; 32],
546    dh2: &[u8; 32],
547    dh3: &[u8; 32],
548    dh4: &[u8; 32],
549) -> Result<Zeroizing<[u8; 32]>, Error> {
550    let ikm = concat(&[dh1, dh2, dh3, dh4]);
551    let mut sk = [0u8; 32];
552    hkdf_sha256(&[0u8; 32], &ikm, X3DH_INFO, &mut sk)?;
553    Ok(Zeroizing::new(sk))
554}
555
556/// The responder's (wire sense: the side that received `init`) X3DH shared secret.
557fn x3dh_secret_responder(
558    own_identity: &[u8; 32],
559    own_ephemeral: &[u8; 32],
560    peer_identity: &[u8; 32],
561    peer_signed_prekey: &[u8; 32],
562    peer_one_time_prekey: &[u8; 32],
563) -> Result<Zeroizing<[u8; 32]>, Error> {
564    let dh1 = diffie_hellman_raw(own_identity, peer_signed_prekey)?;
565    let dh2 = diffie_hellman_raw(own_ephemeral, peer_identity)?;
566    let dh3 = diffie_hellman_raw(own_ephemeral, peer_signed_prekey)?;
567    let dh4 = diffie_hellman_raw(own_ephemeral, peer_one_time_prekey)?;
568    x3dh_kdf(&dh1, &dh2, &dh3, &dh4)
569}
570
571/// The initiator's (wire sense: the side that sent `init`) X3DH shared secret, from the
572/// opposite pairing; equal to the responder's by X25519's DH symmetry.
573fn x3dh_secret_initiator(
574    own_signed_prekey: &[u8; 32],
575    own_identity: &[u8; 32],
576    own_one_time_prekey: &[u8; 32],
577    peer_identity: &[u8; 32],
578    peer_ephemeral: &[u8; 32],
579) -> Result<Zeroizing<[u8; 32]>, Error> {
580    let dh1 = diffie_hellman_raw(own_signed_prekey, peer_identity)?;
581    let dh2 = diffie_hellman_raw(own_identity, peer_ephemeral)?;
582    let dh3 = diffie_hellman_raw(own_signed_prekey, peer_ephemeral)?;
583    let dh4 = diffie_hellman_raw(own_one_time_prekey, peer_ephemeral)?;
584    x3dh_kdf(&dh1, &dh2, &dh3, &dh4)
585}
586
587/// The responder's reaction to an inbound `init`: verify the bundle's self-signature, derive
588/// the X3DH secret, and open a sending-only ratchet whose first message (`boot`) proves it
589/// derived the same secret the initiator will.
590///
591/// The signature check happens before anything else in this function touches the bundle's keys,
592/// so there is no path from an unverified bundle to a live ratchet or a pinned fingerprint.
593pub fn accept_offer(
594    identity: &Identity,
595    bundle: &PreKeyBundle,
596    rng: &mut impl RandomSource,
597) -> Result<(HandshakeResponse, Ratchet), Error> {
598    verify_bundle_signature(bundle)?;
599
600    let (ek_secret, ek_public) = generate_x25519_keypair(rng);
601    let sk = x3dh_secret_responder(
602        &identity.secret.agreement,
603        &ek_secret,
604        &bundle.ik,
605        &bundle.spk,
606        &bundle.opk,
607    )?;
608
609    let mut ratchet = Ratchet::init_as_responder(*sk, ek_secret, ek_public, bundle.spk)?;
610    let boot = ratchet.encrypt(&[])?;
611
612    let public = identity.public();
613    let signed = concat(&[&public.agreement, &ek_public]);
614    let sig = identity.sign(&signed)?;
615
616    let response = HandshakeResponse {
617        ik: public.agreement,
618        sik: public.signing,
619        ek: ek_public,
620        sig,
621        boot,
622    };
623    Ok((response, ratchet))
624}
625
626/// The initiator's reaction to an inbound `accept`: verify the responder's signature over its
627/// own ephemeral key, derive the X3DH secret, and decrypt `boot` to complete the receiving side
628/// of the ratchet.
629///
630/// The signature check happens before this function touches `response.sik` for anything other
631/// than that check, which is what makes it impossible to reach a fingerprint for an unverified
632/// peer through this function: [`Session::receive_accept`] only ever computes one from a
633/// [`HandshakeResponse`] this call already accepted.
634pub fn complete_handshake(
635    identity: &Identity,
636    pending: &PendingOffer,
637    response: &HandshakeResponse,
638    rng: &mut impl RandomSource,
639) -> Result<Ratchet, Error> {
640    let signed = concat(&[&response.ik, &response.ek]);
641    verify_signature(&response.sik, &signed, &response.sig)?;
642
643    let sk = x3dh_secret_initiator(
644        &pending.spk_secret,
645        &identity.secret.agreement,
646        &pending.opk_secret,
647        &response.ik,
648        &response.ek,
649    )?;
650
651    let mut ratchet = Ratchet::init_as_initiator(*sk, *pending.spk_secret, pending.bundle.spk);
652    ratchet.decrypt(&response.boot, rng)?;
653    Ok(ratchet)
654}
655
656// ---------------------------------------------------------------------------------------------
657// The Double Ratchet
658// ---------------------------------------------------------------------------------------------
659
660#[derive(Clone)]
661struct SkippedKeys {
662    by_id: BTreeMap<([u8; 32], u32), Zeroizing<[u8; 32]>>,
663    order: VecDeque<([u8; 32], u32)>,
664}
665
666impl SkippedKeys {
667    fn new() -> Self {
668        Self {
669            by_id: BTreeMap::new(),
670            order: VecDeque::new(),
671        }
672    }
673
674    fn insert(&mut self, dh: [u8; 32], n: u32, key: Zeroizing<[u8; 32]>) {
675        let id = (dh, n);
676        if self.by_id.insert(id, key).is_none() {
677            self.order.push_back(id);
678        }
679        while self.order.len() > MAX_SKIPPED_KEYS {
680            if let Some(oldest) = self.order.pop_front() {
681                self.by_id.remove(&oldest);
682            }
683        }
684    }
685
686    fn take(&mut self, dh: [u8; 32], n: u32) -> Option<Zeroizing<[u8; 32]>> {
687        let id = (dh, n);
688        let key = self.by_id.remove(&id)?;
689        self.order.retain(|entry| *entry != id);
690        Some(key)
691    }
692}
693
694/// One party's half of a Double Ratchet session: a sending chain, a receiving chain, and the
695/// skipped-key store that lets messages arrive out of order.
696#[derive(Clone)]
697pub struct Ratchet {
698    root_key: Zeroizing<[u8; 32]>,
699    dhs_secret: Zeroizing<[u8; 32]>,
700    dhs_public: [u8; 32],
701    dhr: Option<[u8; 32]>,
702    send_chain: Option<Zeroizing<[u8; 32]>>,
703    recv_chain: Option<Zeroizing<[u8; 32]>>,
704    n_send: u32,
705    n_recv: u32,
706    prev_chain_len: u32,
707    skipped: SkippedKeys,
708}
709
710impl Ratchet {
711    /// Open a ratchet as the responder (wire sense): derive a sending chain immediately against
712    /// the initiator's retained signed-prekey, with no receiving chain yet.
713    fn init_as_responder(
714        root_key: [u8; 32],
715        own_ek_secret: [u8; 32],
716        own_ek_public: [u8; 32],
717        their_spk_public: [u8; 32],
718    ) -> Result<Self, Error> {
719        let dh_out = diffie_hellman_raw(&own_ek_secret, &their_spk_public)?;
720        let (new_root, send_chain) = kdf_root(&root_key, &dh_out)?;
721        Ok(Self {
722            root_key: Zeroizing::new(new_root),
723            dhs_secret: Zeroizing::new(own_ek_secret),
724            dhs_public: own_ek_public,
725            dhr: Some(their_spk_public),
726            send_chain: Some(Zeroizing::new(send_chain)),
727            recv_chain: None,
728            n_send: 0,
729            n_recv: 0,
730            prev_chain_len: 0,
731            skipped: SkippedKeys::new(),
732        })
733    }
734
735    /// Open a ratchet as the initiator (wire sense): reuse the retained signed-prekey as the
736    /// first ratchet keypair, with no peer ratchet key and no chain yet. The first inbound
737    /// message (`boot`) supplies the peer's key and completes the receiving chain.
738    fn init_as_initiator(
739        root_key: [u8; 32],
740        own_dhs_secret: [u8; 32],
741        own_dhs_public: [u8; 32],
742    ) -> Self {
743        Self {
744            root_key: Zeroizing::new(root_key),
745            dhs_secret: Zeroizing::new(own_dhs_secret),
746            dhs_public: own_dhs_public,
747            dhr: None,
748            send_chain: None,
749            recv_chain: None,
750            n_send: 0,
751            n_recv: 0,
752            prev_chain_len: 0,
753            skipped: SkippedKeys::new(),
754        }
755    }
756
757    /// Encrypt `plaintext`, advancing the sending chain by one step.
758    pub fn encrypt(&mut self, plaintext: &[u8]) -> Result<RatchetMessage, Error> {
759        let Some(chain) = self.send_chain.clone() else {
760            return Err(Error::NoChain);
761        };
762        let (message_key, next_chain) = kdf_chain(&chain)?;
763        self.send_chain = Some(Zeroizing::new(next_chain));
764
765        let dh = self.dhs_public;
766        let pn = self.prev_chain_len;
767        let n = self.n_send;
768        self.n_send = self.n_send.checked_add(1).ok_or(Error::CounterOverflow)?;
769
770        let padded = pad(plaintext);
771        let aad = header_aad(&dh, pn, n);
772        let ct = aead_encrypt(&message_key, &aad, &padded)?;
773        Ok(RatchetMessage { dh, pn, n, ct })
774    }
775
776    /// Decrypt `msg`, advancing the receiving chain (and performing a DH ratchet step, when the
777    /// header names a new peer key) only once the AEAD tag verifies.
778    ///
779    /// The whole state is cloned, mutated on the clone, and only committed back on success, so a
780    /// forged frame with a plausible header but garbage ciphertext cannot burn a skipped-key slot
781    /// or desync the receiving chain.
782    pub fn decrypt(
783        &mut self,
784        msg: &RatchetMessage,
785        rng: &mut impl RandomSource,
786    ) -> Result<Vec<u8>, Error> {
787        let mut trial = self.clone();
788
789        if let Some(message_key) = trial.skipped.take(msg.dh, msg.n) {
790            let plaintext = decrypt_with_key(&message_key, msg)?;
791            *self = trial;
792            return Ok(plaintext);
793        }
794
795        if trial.dhr != Some(msg.dh) {
796            trial.skip_current_receiving_chain(msg.pn)?;
797            trial.dh_ratchet_receive(msg.dh, rng)?;
798        }
799        trial.skip_current_receiving_chain(msg.n)?;
800
801        let Some(chain) = trial.recv_chain.clone() else {
802            return Err(Error::NoChain);
803        };
804        let (message_key, next_chain) = kdf_chain(&chain)?;
805        trial.recv_chain = Some(Zeroizing::new(next_chain));
806        trial.n_recv = trial.n_recv.checked_add(1).ok_or(Error::CounterOverflow)?;
807
808        let plaintext = decrypt_with_key(&Zeroizing::new(message_key), msg)?;
809        *self = trial;
810        Ok(plaintext)
811    }
812
813    /// Advance the current receiving chain up to (but not including) counter `until`, storing
814    /// each skipped message key. A no-op when there is no receiving chain yet, or `until` is
815    /// already behind the current position.
816    fn skip_current_receiving_chain(&mut self, until: u32) -> Result<(), Error> {
817        let Some(dhr) = self.dhr else { return Ok(()) };
818        let Some(mut chain) = self.recv_chain.take() else {
819            return Ok(());
820        };
821        if until <= self.n_recv {
822            self.recv_chain = Some(chain);
823            return Ok(());
824        }
825        let span = until - self.n_recv;
826        if span > MAX_SKIP {
827            self.recv_chain = Some(chain);
828            return Err(Error::TooManySkipped);
829        }
830        for _ in 0..span {
831            let (message_key, next_chain) = kdf_chain(&chain)?;
832            self.skipped
833                .insert(dhr, self.n_recv, Zeroizing::new(message_key));
834            chain = Zeroizing::new(next_chain);
835            self.n_recv = self.n_recv.checked_add(1).ok_or(Error::CounterOverflow)?;
836        }
837        self.recv_chain = Some(chain);
838        Ok(())
839    }
840
841    /// A DH ratchet step: adopt the peer's new ratchet key, derive a fresh receiving chain
842    /// against it with the current keypair, then generate a fresh own keypair and derive a
843    /// fresh sending chain against the same peer key.
844    fn dh_ratchet_receive(
845        &mut self,
846        new_dhr: [u8; 32],
847        rng: &mut impl RandomSource,
848    ) -> Result<(), Error> {
849        self.prev_chain_len = self.n_send;
850        self.n_send = 0;
851        self.n_recv = 0;
852        self.dhr = Some(new_dhr);
853
854        let dh_out = diffie_hellman_raw(&self.dhs_secret, &new_dhr)?;
855        let (root_after_recv, recv_chain) = kdf_root(&self.root_key, &dh_out)?;
856        self.root_key = Zeroizing::new(root_after_recv);
857        self.recv_chain = Some(Zeroizing::new(recv_chain));
858
859        let (dhs_secret, dhs_public) = generate_x25519_keypair(rng);
860        self.dhs_secret = Zeroizing::new(dhs_secret);
861        self.dhs_public = dhs_public;
862
863        let dh_out2 = diffie_hellman_raw(&self.dhs_secret, &new_dhr)?;
864        let (root_after_send, send_chain) = kdf_root(&self.root_key, &dh_out2)?;
865        self.root_key = Zeroizing::new(root_after_send);
866        self.send_chain = Some(Zeroizing::new(send_chain));
867        Ok(())
868    }
869}
870
871fn decrypt_with_key(message_key: &[u8; 32], msg: &RatchetMessage) -> Result<Vec<u8>, Error> {
872    let aad = header_aad(&msg.dh, msg.pn, msg.n);
873    let padded = aead_decrypt(message_key, &aad, &msg.ct)?;
874    unpad(&padded)
875}
876
877fn kdf_root(root_key: &[u8; 32], dh_out: &[u8; 32]) -> Result<([u8; 32], [u8; 32]), Error> {
878    let mut okm = [0u8; 64];
879    hkdf_sha256(root_key, dh_out, ROOT_INFO, &mut okm)?;
880    let (root_half, chain_half) = okm.split_at(32);
881    let new_root: [u8; 32] = root_half.try_into().map_err(|_| Error::Internal)?;
882    let new_chain: [u8; 32] = chain_half.try_into().map_err(|_| Error::Internal)?;
883    Ok((new_root, new_chain))
884}
885
886fn kdf_chain(chain_key: &[u8; 32]) -> Result<([u8; 32], [u8; 32]), Error> {
887    let message_key = hmac_sha256(chain_key, &[0x01])?;
888    let next_chain = hmac_sha256(chain_key, &[0x02])?;
889    Ok((message_key, next_chain))
890}
891
892fn message_aead_params(message_key: &[u8; 32]) -> Result<(Key, XNonce), Error> {
893    let mut key_bytes = [0u8; 32];
894    hkdf_sha256(&[0u8; 32], message_key, MESSAGE_KEY_INFO, &mut key_bytes)?;
895    let mut nonce_bytes = [0u8; 24];
896    hkdf_sha256(&[0u8; 32], message_key, NONCE_INFO, &mut nonce_bytes)?;
897    Ok((Key::from(key_bytes), XNonce::from(nonce_bytes)))
898}
899
900fn aead_encrypt(message_key: &[u8; 32], aad: &[u8], plaintext: &[u8]) -> Result<Vec<u8>, Error> {
901    let (key, nonce) = message_aead_params(message_key)?;
902    XChaCha20Poly1305::new(&key)
903        .encrypt(
904            &nonce,
905            Payload {
906                msg: plaintext,
907                aad,
908            },
909        )
910        .map_err(|_| Error::Aead)
911}
912
913fn aead_decrypt(message_key: &[u8; 32], aad: &[u8], ciphertext: &[u8]) -> Result<Vec<u8>, Error> {
914    let (key, nonce) = message_aead_params(message_key)?;
915    XChaCha20Poly1305::new(&key)
916        .decrypt(
917            &nonce,
918            Payload {
919                msg: ciphertext,
920                aad,
921            },
922        )
923        .map_err(|_| Error::Aead)
924}
925
926fn header_aad(dh: &[u8; 32], pn: u32, n: u32) -> Vec<u8> {
927    let mut aad = Vec::with_capacity(40);
928    aad.extend_from_slice(dh);
929    aad.extend_from_slice(&pn.to_be_bytes());
930    aad.extend_from_slice(&n.to_be_bytes());
931    aad
932}
933
934fn pad(plaintext: &[u8]) -> Vec<u8> {
935    let mut out = Vec::with_capacity(plaintext.len() + PAD_BLOCK);
936    out.extend_from_slice(plaintext);
937    out.push(0x80);
938    let remainder = out.len() % PAD_BLOCK;
939    if remainder != 0 {
940        out.resize(out.len() + (PAD_BLOCK - remainder), 0);
941    }
942    out
943}
944
945fn unpad(padded: &[u8]) -> Result<Vec<u8>, Error> {
946    let marker = padded
947        .iter()
948        .rposition(|&byte| byte != 0)
949        .ok_or(Error::Padding)?;
950    if padded.get(marker) != Some(&0x80) {
951        return Err(Error::Padding);
952    }
953    padded
954        .get(..marker)
955        .map(<[u8]>::to_vec)
956        .ok_or(Error::Padding)
957}
958
959// ---------------------------------------------------------------------------------------------
960// Session state machine
961// ---------------------------------------------------------------------------------------------
962
963/// Which side of the handshake a session played, once negotiation has started.
964#[derive(Debug, Clone, Copy, PartialEq, Eq)]
965pub enum Role {
966    /// This side sent `init`.
967    Initiator,
968    /// This side sent `accept`.
969    Responder,
970}
971
972/// How far a conversation has got towards being encrypted.
973///
974/// Only [`SessionState::Established`] carries a ratchet that will decrypt content, which is what
975/// makes "an accept does not mean established" a property of the type.
976///
977/// Deliberately not `Debug`: two of these variants hold key material, and a derived formatter would
978/// put it in whatever log the caller happens to write.
979pub enum SessionState {
980    /// Nothing has been offered yet.
981    Idle,
982    /// We sent `init` and are waiting for an answer.
983    Offered {
984        /// The keys we offered, kept until the answer arrives.
985        pending: PendingOffer,
986    },
987    /// They sent `init` and we have not answered yet.
988    OfferReceived {
989        /// What they offered.
990        bundle: PreKeyBundle,
991    },
992    /// We answered and are waiting for the `ack` that proves they can decrypt.
993    ///
994    /// An `accept` can be lost, so showing a lock here would tell the user something untrue. This
995    /// state deliberately exposes no way to decrypt content.
996    AwaitingAck {
997        /// The ratchet, held but not yet trusted to carry content.
998        ratchet: Ratchet,
999    },
1000    /// The session works in both directions.
1001    Established {
1002        /// The live ratchet.
1003        ratchet: Ratchet,
1004    },
1005    /// They refused.
1006    Rejected,
1007    /// The session ended.
1008    Closed,
1009}
1010
1011/// One pairwise end-to-end encrypted conversation.
1012///
1013/// An `accept` alone never reaches [`SessionState::Established`]: the responder side lands in
1014/// [`SessionState::AwaitingAck`], which exposes no way to decrypt content, and only
1015/// [`Session::receive_ack`] can move it onward. There is no method on this type that decrypts a
1016/// `msg`/`media` payload from any other state, which is what makes the "accept is not
1017/// established" rule a property of the API rather than a rule callers must remember to enforce.
1018pub struct Session {
1019    state: SessionState,
1020    trust: PeerTrust,
1021    role: Option<Role>,
1022}
1023
1024impl Default for Session {
1025    fn default() -> Self {
1026        Self {
1027            state: SessionState::Idle,
1028            trust: PeerTrust::new(),
1029            role: None,
1030        }
1031    }
1032}
1033
1034impl Session {
1035    /// A session that has not started negotiating with anyone.
1036    pub fn new() -> Self {
1037        Self::default()
1038    }
1039
1040    /// Start as the wire's initiator: publish a fresh prekey bundle.
1041    pub fn start(
1042        &mut self,
1043        identity: &Identity,
1044        rng: &mut impl RandomSource,
1045    ) -> Result<PreKeyBundle, Error> {
1046        let pending = create_offer(identity, rng)?;
1047        let bundle = pending.bundle.clone();
1048        self.state = SessionState::Offered { pending };
1049        Ok(bundle)
1050    }
1051
1052    /// Record an inbound `init`, returning the offered (not yet verified or pinned) fingerprint
1053    /// for a pre-acceptance prompt.
1054    ///
1055    /// When this side already has its own offer outstanding, the caller must resolve the
1056    /// crossing-offer tiebreak with [`keeps_own_offer`] before calling this: a side that keeps
1057    /// its own offer must not overwrite it by recording the peer's.
1058    pub fn receive_offer(&mut self, bundle: PreKeyBundle) -> Fingerprint {
1059        let offered = Fingerprint::of_signing_key(&bundle.sik);
1060        self.state = SessionState::OfferReceived { bundle };
1061        offered
1062    }
1063
1064    /// Accept a recorded offer: verify it, derive the session secret, and answer with a
1065    /// `HandshakeResponse`. Lands in [`SessionState::AwaitingAck`], not established.
1066    pub fn accept(
1067        &mut self,
1068        identity: &Identity,
1069        rng: &mut impl RandomSource,
1070    ) -> Result<HandshakeResponse, Error> {
1071        let SessionState::OfferReceived { bundle } = &self.state else {
1072            return Err(Error::WrongState);
1073        };
1074        let (response, ratchet) = accept_offer(identity, bundle, rng)?;
1075
1076        let fingerprint = Fingerprint::of_signing_key(&bundle.sik);
1077        if let PinOutcome::Changed { previous } = self.trust.observe(fingerprint) {
1078            return Err(Error::FingerprintChanged {
1079                previous,
1080                current: fingerprint,
1081            });
1082        }
1083
1084        self.role = Some(Role::Responder);
1085        self.state = SessionState::AwaitingAck { ratchet };
1086        Ok(response)
1087    }
1088
1089    /// Reject a recorded offer.
1090    pub fn reject(&mut self, reason: Option<String>) -> Frame {
1091        self.state = SessionState::Rejected;
1092        Frame::Reject { reason }
1093    }
1094
1095    /// Record an inbound `reject` for an offer this side sent.
1096    pub fn receive_reject(&mut self) {
1097        self.state = SessionState::Rejected;
1098    }
1099
1100    /// Complete the handshake as the initiator: verify the responder's signature before pinning
1101    /// its fingerprint, derive the session secret, and decrypt `boot`. Reaches
1102    /// [`SessionState::Established`] directly, since the initiator has no further frame to wait
1103    /// for.
1104    pub fn receive_accept(
1105        &mut self,
1106        identity: &Identity,
1107        response: &HandshakeResponse,
1108        rng: &mut impl RandomSource,
1109    ) -> Result<(), Error> {
1110        let SessionState::Offered { pending } = &self.state else {
1111            return Err(Error::WrongState);
1112        };
1113        let ratchet = complete_handshake(identity, pending, response, rng)?;
1114
1115        let fingerprint = Fingerprint::of_signing_key(&response.sik);
1116        if let PinOutcome::Changed { previous } = self.trust.observe(fingerprint) {
1117            return Err(Error::FingerprintChanged {
1118                previous,
1119                current: fingerprint,
1120            });
1121        }
1122
1123        self.role = Some(Role::Initiator);
1124        self.state = SessionState::Established { ratchet };
1125        Ok(())
1126    }
1127
1128    /// Produce the initiator's `ack`: an empty-plaintext ratchet message proving the session
1129    /// works.
1130    pub fn make_ack(&mut self) -> Result<RatchetMessage, Error> {
1131        self.send(&[])
1132    }
1133
1134    /// Decrypt the initiator's `ack`. Only this call moves a responder from
1135    /// [`SessionState::AwaitingAck`] to [`SessionState::Established`]; a lost or not-yet-arrived
1136    /// `ack` leaves the session exactly where it was, never showing established early.
1137    pub fn receive_ack(
1138        &mut self,
1139        ct: &RatchetMessage,
1140        rng: &mut impl RandomSource,
1141    ) -> Result<(), Error> {
1142        let SessionState::AwaitingAck { ratchet } = &mut self.state else {
1143            return Err(Error::WrongState);
1144        };
1145        ratchet.decrypt(ct, rng)?;
1146        let SessionState::AwaitingAck { ratchet } =
1147            mem::replace(&mut self.state, SessionState::Idle)
1148        else {
1149            return Err(Error::WrongState);
1150        };
1151        self.state = SessionState::Established { ratchet };
1152        Ok(())
1153    }
1154
1155    /// Encrypt a `msg`/`media` payload. Only available once established.
1156    pub fn send(&mut self, plaintext: &[u8]) -> Result<RatchetMessage, Error> {
1157        let SessionState::Established { ratchet } = &mut self.state else {
1158            return Err(Error::WrongState);
1159        };
1160        ratchet.encrypt(plaintext)
1161    }
1162
1163    /// Decrypt a `msg`/`media` payload. Only available once established: there is no state from
1164    /// which this call can reach a receiving chain before then.
1165    pub fn receive(
1166        &mut self,
1167        ct: &RatchetMessage,
1168        rng: &mut impl RandomSource,
1169    ) -> Result<Vec<u8>, Error> {
1170        let SessionState::Established { ratchet } = &mut self.state else {
1171            return Err(Error::WrongState);
1172        };
1173        ratchet.decrypt(ct, rng)
1174    }
1175
1176    /// End the session locally.
1177    pub fn close(&mut self) -> Frame {
1178        self.state = SessionState::Closed;
1179        Frame::Close
1180    }
1181
1182    /// Record an inbound `close`.
1183    pub fn receive_close(&mut self) {
1184        self.state = SessionState::Closed;
1185    }
1186
1187    /// Whether this session can currently send or receive content.
1188    pub const fn is_established(&self) -> bool {
1189        matches!(self.state, SessionState::Established { .. })
1190    }
1191
1192    /// The peer's pinned fingerprint, once one has been observed.
1193    pub fn peer_fingerprint(&self) -> Option<Fingerprint> {
1194        self.trust.pinned()
1195    }
1196
1197    /// Whether the pinned fingerprint has been confirmed out of band.
1198    pub fn is_peer_verified(&self) -> bool {
1199        self.trust.is_verified()
1200    }
1201
1202    /// Mark the pinned fingerprint as confirmed out of band.
1203    pub fn mark_peer_verified(&mut self) {
1204        self.trust.set_verified(true);
1205    }
1206
1207    /// Accept a fingerprint change the caller has explicitly decided to trust, so the operation
1208    /// that reported [`Error::FingerprintChanged`] can be retried and will succeed this time.
1209    pub fn confirm_fingerprint_change(&mut self, fingerprint: Fingerprint) {
1210        self.trust.repin(fingerprint);
1211    }
1212
1213    /// Which side of the handshake this session played, once negotiation has started.
1214    pub const fn role(&self) -> Option<Role> {
1215        self.role
1216    }
1217}
1218
1219#[cfg(test)]
1220mod tests {
1221    use super::*;
1222
1223    struct TestRng(u64);
1224
1225    impl TestRng {
1226        fn seeded(seed: u64) -> Self {
1227            Self(seed)
1228        }
1229    }
1230
1231    impl RandomSource for TestRng {
1232        fn fill_bytes(&mut self, dest: &mut [u8]) {
1233            for chunk in dest.chunks_mut(8) {
1234                self.0 = self.0.wrapping_add(0x9E37_79B9_7F4A_7C15);
1235                let mut z = self.0;
1236                z = (z ^ (z >> 30)).wrapping_mul(0xBF58_476D_1CE4_E5B9);
1237                z = (z ^ (z >> 27)).wrapping_mul(0x94D0_49BB_1331_11EB);
1238                z ^= z >> 31;
1239                let bytes = z.to_le_bytes();
1240                chunk.copy_from_slice(&bytes[..chunk.len()]);
1241            }
1242        }
1243    }
1244
1245    struct Pair {
1246        alice_identity: Identity,
1247        bob_identity: Identity,
1248        alice: Session,
1249        bob: Session,
1250    }
1251
1252    fn establish() -> Pair {
1253        let mut rng = TestRng::seeded(1);
1254        let alice_identity = Identity::generate(&mut rng);
1255        let bob_identity = Identity::generate(&mut rng);
1256        let mut alice = Session::new();
1257        let mut bob = Session::new();
1258
1259        let bundle = alice.start(&alice_identity, &mut rng).unwrap();
1260        bob.receive_offer(bundle);
1261        let response = bob.accept(&bob_identity, &mut rng).unwrap();
1262        assert!(!bob.is_established());
1263
1264        alice
1265            .receive_accept(&alice_identity, &response, &mut rng)
1266            .unwrap();
1267        assert!(alice.is_established());
1268
1269        let ack = alice.make_ack().unwrap();
1270        bob.receive_ack(&ack, &mut rng).unwrap();
1271        assert!(bob.is_established());
1272
1273        Pair {
1274            alice_identity,
1275            bob_identity,
1276            alice,
1277            bob,
1278        }
1279    }
1280
1281    #[test]
1282    fn full_handshake_establishes_both_sides() {
1283        let pair = establish();
1284        assert_eq!(pair.alice.role(), Some(Role::Initiator));
1285        assert_eq!(pair.bob.role(), Some(Role::Responder));
1286        assert_eq!(
1287            pair.alice.peer_fingerprint(),
1288            Some(pair.bob_identity.fingerprint())
1289        );
1290        assert_eq!(
1291            pair.bob.peer_fingerprint(),
1292            Some(pair.alice_identity.fingerprint())
1293        );
1294    }
1295
1296    #[test]
1297    fn accept_alone_does_not_establish() {
1298        let mut rng = TestRng::seeded(2);
1299        let alice_identity = Identity::generate(&mut rng);
1300        let bob_identity = Identity::generate(&mut rng);
1301        let mut alice = Session::new();
1302        let mut bob = Session::new();
1303
1304        let bundle = alice.start(&alice_identity, &mut rng).unwrap();
1305        bob.receive_offer(bundle);
1306        bob.accept(&bob_identity, &mut rng).unwrap();
1307
1308        assert!(!bob.is_established());
1309        assert_eq!(bob.send(b"hello"), Err(Error::WrongState));
1310    }
1311
1312    #[test]
1313    fn established_session_encrypts_and_decrypts() {
1314        let mut pair = establish();
1315        let mut rng = TestRng::seeded(3);
1316
1317        let ct = pair.alice.send(b"hello bob").unwrap();
1318        let plaintext = pair.bob.receive(&ct, &mut rng).unwrap();
1319        assert_eq!(plaintext, b"hello bob");
1320
1321        let ct = pair.bob.send(b"hello alice").unwrap();
1322        let plaintext = pair.alice.receive(&ct, &mut rng).unwrap();
1323        assert_eq!(plaintext, b"hello alice");
1324    }
1325
1326    #[test]
1327    fn out_of_order_delivery_still_decrypts() {
1328        let mut pair = establish();
1329        let mut rng = TestRng::seeded(4);
1330
1331        let first = pair.alice.send(b"one").unwrap();
1332        let second = pair.alice.send(b"two").unwrap();
1333        let third = pair.alice.send(b"three").unwrap();
1334
1335        assert_eq!(pair.bob.receive(&third, &mut rng).unwrap(), b"three");
1336        assert_eq!(pair.bob.receive(&first, &mut rng).unwrap(), b"one");
1337        assert_eq!(pair.bob.receive(&second, &mut rng).unwrap(), b"two");
1338    }
1339
1340    #[test]
1341    fn skip_bound_is_enforced() {
1342        let mut pair = establish();
1343        let mut rng = TestRng::seeded(5);
1344
1345        let mut far = pair.alice.send(b"far").unwrap();
1346        far.n += MAX_SKIP + 1;
1347        assert_eq!(pair.bob.receive(&far, &mut rng), Err(Error::TooManySkipped));
1348    }
1349
1350    #[test]
1351    fn tampered_ciphertext_fails_to_decrypt() {
1352        let mut pair = establish();
1353        let mut rng = TestRng::seeded(6);
1354
1355        let mut ct = pair.alice.send(b"hello").unwrap();
1356        let last = ct.ct.len() - 1;
1357        if let Some(byte) = ct.ct.get_mut(last) {
1358            *byte ^= 0xFF;
1359        }
1360        assert_eq!(pair.bob.receive(&ct, &mut rng), Err(Error::Aead));
1361    }
1362
1363    #[test]
1364    fn forged_responder_signature_is_rejected() {
1365        let mut rng = TestRng::seeded(7);
1366        let alice_identity = Identity::generate(&mut rng);
1367        let bob_identity = Identity::generate(&mut rng);
1368        let mallory_identity = Identity::generate(&mut rng);
1369        let mut alice = Session::new();
1370        let mut bob = Session::new();
1371
1372        let bundle = alice.start(&alice_identity, &mut rng).unwrap();
1373        bob.receive_offer(bundle);
1374        let mut response = bob.accept(&bob_identity, &mut rng).unwrap();
1375
1376        // splice in an unrelated identity's signing key, simulating an attacker presenting a
1377        // real user's fingerprint over its own session
1378        response.sik = mallory_identity.public().signing;
1379
1380        let result = alice.receive_accept(&alice_identity, &response, &mut rng);
1381        assert_eq!(result, Err(Error::InvalidSignature));
1382        assert!(!alice.is_established());
1383        assert_eq!(alice.peer_fingerprint(), None);
1384    }
1385
1386    #[test]
1387    fn crossing_offers_lower_fingerprint_wins() {
1388        let mut rng = TestRng::seeded(8);
1389        let a = Identity::generate(&mut rng).fingerprint();
1390        let b = Identity::generate(&mut rng).fingerprint();
1391        let (lower, higher) = if a < b { (a, b) } else { (b, a) };
1392
1393        assert!(keeps_own_offer(lower, Some(higher)));
1394        assert!(!keeps_own_offer(higher, Some(lower)));
1395        assert!(!keeps_own_offer(lower, None));
1396    }
1397
1398    #[test]
1399    fn changed_fingerprint_is_refused() {
1400        let mut rng = TestRng::seeded(9);
1401        let alice_identity = Identity::generate(&mut rng);
1402        let bob_identity = Identity::generate(&mut rng);
1403        let mallory_identity = Identity::generate(&mut rng);
1404
1405        let mut alice = Session::new();
1406        alice.trust.observe(mallory_identity.fingerprint());
1407
1408        let mut bob = Session::new();
1409        let bundle = alice.start(&alice_identity, &mut rng).unwrap();
1410        bob.receive_offer(bundle);
1411        let response = bob.accept(&bob_identity, &mut rng).unwrap();
1412
1413        let result = alice.receive_accept(&alice_identity, &response, &mut rng);
1414        assert_eq!(
1415            result,
1416            Err(Error::FingerprintChanged {
1417                previous: mallory_identity.fingerprint(),
1418                current: bob_identity.fingerprint(),
1419            })
1420        );
1421        assert!(!alice.is_established());
1422
1423        alice.confirm_fingerprint_change(bob_identity.fingerprint());
1424        alice
1425            .receive_accept(&alice_identity, &response, &mut rng)
1426            .unwrap();
1427        assert!(alice.is_established());
1428    }
1429
1430    #[test]
1431    fn safety_number_is_eight_groups_of_four() {
1432        let mut rng = TestRng::seeded(10);
1433        let fingerprint = Identity::generate(&mut rng).fingerprint();
1434        let rendered = fingerprint.safety_number();
1435        let groups: Vec<&str> = rendered.split(' ').collect();
1436        assert_eq!(groups.len(), 8);
1437        for group in groups {
1438            assert_eq!(group.len(), 4);
1439            assert!(
1440                group
1441                    .chars()
1442                    .all(|c| c.is_ascii_hexdigit() && !c.is_ascii_lowercase())
1443            );
1444        }
1445    }
1446
1447    #[test]
1448    fn frag_reassembles_in_order_and_rejects_a_gap() {
1449        let whole = b"hello obby world".to_vec();
1450        let (first_half, second_half) = whole.split_at(8);
1451        let fragments = alloc::vec![
1452            Frag {
1453                id: "abc".into(),
1454                i: 0,
1455                n: 2,
1456                ct: first_half.to_vec(),
1457            },
1458            Frag {
1459                id: "abc".into(),
1460                i: 1,
1461                n: 2,
1462                ct: second_half.to_vec(),
1463            },
1464        ];
1465        assert_eq!(reassemble(&fragments).unwrap(), whole);
1466
1467        let missing_second = &fragments[..1];
1468        assert_eq!(reassemble(missing_second), Err(Error::Fragmentation));
1469    }
1470
1471    #[test]
1472    fn reject_and_close_transition_state_and_frame() {
1473        let mut rng = TestRng::seeded(11);
1474        let alice_identity = Identity::generate(&mut rng);
1475        let mut alice = Session::new();
1476        let mut bob = Session::new();
1477
1478        let bundle = alice.start(&alice_identity, &mut rng).unwrap();
1479        let offered = bob.receive_offer(bundle.clone());
1480        assert_eq!(offered, alice_identity.fingerprint());
1481
1482        let frame = bob.reject(Some("busy".into()));
1483        assert_eq!(
1484            frame,
1485            Frame::Reject {
1486                reason: Some("busy".into())
1487            }
1488        );
1489        assert!(!bob.is_established());
1490        alice.receive_reject();
1491        assert_eq!(alice.send(b"too late"), Err(Error::WrongState));
1492
1493        let mut pair = establish();
1494        let frame = pair.alice.close();
1495        assert_eq!(frame, Frame::Close);
1496        pair.bob.receive_close();
1497        assert_eq!(pair.alice.send(b"too late"), Err(Error::WrongState));
1498        assert_eq!(pair.bob.send(b"too late"), Err(Error::WrongState));
1499
1500        // the frame set also carries plain content and handshake frames under their own names
1501        let init = Frame::Init {
1502            bundle,
1503            account: Some("alice".into()),
1504        };
1505        assert!(matches!(init, Frame::Init { .. }));
1506        assert_eq!(PROTOCOL_VERSION, 1);
1507    }
1508
1509    #[test]
1510    fn frame_wraps_every_content_and_handshake_variant() {
1511        let mut pair = establish();
1512        let ct = pair.alice.send(b"hi").unwrap();
1513        let msg = Frame::Msg { ct: ct.clone() };
1514        let media = Frame::Media { ct: ct.clone() };
1515        let ack = Frame::Ack { ct };
1516        assert!(matches!(msg, Frame::Msg { .. }));
1517        assert!(matches!(media, Frame::Media { .. }));
1518        assert!(matches!(ack, Frame::Ack { .. }));
1519
1520        let mut rng = TestRng::seeded(12);
1521        let alice_identity = Identity::generate(&mut rng);
1522        let bob_identity = Identity::generate(&mut rng);
1523        let mut alice = Session::new();
1524        let mut bob = Session::new();
1525        let bundle = alice.start(&alice_identity, &mut rng).unwrap();
1526        bob.receive_offer(bundle);
1527        let response = bob.accept(&bob_identity, &mut rng).unwrap();
1528        let accept = Frame::Accept {
1529            response,
1530            account: None,
1531        };
1532        assert!(matches!(accept, Frame::Accept { .. }));
1533    }
1534
1535    #[test]
1536    fn peer_verification_tracks_out_of_band_confirmation() {
1537        let mut pair = establish();
1538        assert!(!pair.alice.is_peer_verified());
1539        pair.alice.mark_peer_verified();
1540        assert!(pair.alice.is_peer_verified());
1541        assert!(!pair.bob.is_peer_verified());
1542    }
1543}