import type { LibCrypto } from "./libcrypto"; import type { RatchetRootSuite } from "../utils/constants"; /** * Live Double Ratchet state for one `(roomId, peerPublicKey)` edge. Every key * field is a plain-JS `Uint8Array` secret (the WASM heap holds no ratchet state * — see design §9): `rootKey`, both chain keys, `dhSelfSec`, and each value in * `skipped`. Stage 3 wraps these at rest; here they are live plaintext. * * The ratchet advances **per logical message**, never per chunk. `Ns`/`Nr` are * the send/receive message counters in the current chains; `PN` is the length * of the previous sending chain (folded into inbound headers so the peer can * skip the tail of a superseded chain). */ export interface RatchetState { /** Authenticated bootstrap-suite provenance; immutable for this edge. */ rootSuite: RatchetRootSuite; rootKey: Uint8Array; sendingChainKey: Uint8Array | null; receivingChainKey: Uint8Array | null; dhSelfPub: Uint8Array; dhSelfSec: Uint8Array; dhRemotePub: Uint8Array | null; Ns: number; Nr: number; PN: number; skipped: Map; } /** * Cleartext per-message ratchet header. Rides the frame so the receiver can * derive the message key before decrypting: `dhPub` drives the DH ratchet, `N` * is this message's index in the sender's current chain, `PN` is the length of * the sender's previous chain (for skip-on-DH-step). */ export interface RatchetHeader { dhPub: Uint8Array; N: number; PN: number; } /** * Serializable projection of a `RatchetState` — maps 1:1 onto the wrapped * secret fields of the `RatchetSession` IndexedDB row (Stage 3). All buffers * are owned copies; the skipped map is flattened to an array keyed by * `(dhPub, n)`. */ export interface RatchetSessionSecrets { rootSuite: RatchetRootSuite; rootKey: ArrayBuffer; sendingChainKey: ArrayBuffer | null; receivingChainKey: ArrayBuffer | null; dhSelfPub: ArrayBuffer; dhSelfSec: ArrayBuffer; dhRemotePub: ArrayBuffer | null; Ns: number; Nr: number; PN: number; skippedMessageKeys: Array<{ dhPub: ArrayBuffer; n: number; messageKey: ArrayBuffer; }>; } /** * Seed a ratchet from the handshake's 32-byte, transcript-bound hybrid root. * * Initiator/responder asymmetry (must match so the two sides interoperate): * - The **responder** (`amInitiator=false`, `remoteDhPub=null`) generates its * DH keypair and stops: both chains stay `null`. After the initiator's * initial DH pub is authenticated, `primeResponderRatchet` performs the * receive-side DH step without consuming a message key and opens both * chains. A lower-level caller that does not prime retains the legacy * first-inbound-message behavior. * - The **initiator** (`amInitiator=true`) is handed the responder's * `dhSelfPub` as `remoteDhPub`, runs one `kdf_rk` over `DH(selfSec, remote)` * to advance the root and open its **sending** chain immediately, so it can * send message 0 before ever hearing back. Its receiving chain opens on the * responder's first reply (a DH step against the responder's fresh pub). */ export declare const initRatchet: (rootSeed: Uint8Array, amInitiator: boolean, remoteDhPub: Uint8Array | null, module: LibCrypto, rootSuite?: RatchetRootSuite) => RatchetState; /** * Advance the sending chain by one message. Returns the message key (caller's to * consume/wipe) and the cleartext header to put on the wire. The consumed chain * key is wiped once its successor is derived. */ export declare const ratchetEncrypt: (state: RatchetState, module: LibCrypto) => { messageKey: Uint8Array; header: RatchetHeader; }; /** * Complete the responder's handshake-time ratchet bootstrap after the * initiator's initial DH public key has been authenticated by key confirmation. * * This is the receive-side DH step that an unprimed responder would otherwise * perform on the initiator's first message, deliberately stopped before * `kdfCk`: it opens the receiving chain from * `DH(initialResponder, initialInitiator)`, rotates the responder DH keypair, * and opens the sending chain from `DH(rotatedResponder, initialInitiator)`. * Consequently both peers can send message 0 immediately after the handshake, * including simultaneously, without consuming or skipping a message key. */ export declare const primeResponderRatchet: (state: RatchetState, initiatorDhPub: Uint8Array, module: LibCrypto) => void; /** * Advance the receiving side for an inbound header and return its message key. * * CAUTION — this function is UNAUTHENTICATED: it mutates `state` (and may fire * a DH-ratchet step) from the cleartext header alone, before the AEAD tag is * ever checked (authentication happens later, in Stage 5). A duplicate or * replayed message that is NOT a stored skipped key — e.g. a retransmit of a * message from a chain the session has since stepped past — is NOT detected * here: it falls through to the "new peer DH pub" case below and can fire a * spurious backward DH-step, permanently desyncing the session. This is * reachable by our own retransmit/reconcile layer, so callers MUST: * 1. Dedup already-seen `(header.dhPub, header.N)` pairs BEFORE calling this * function — never invoke it a second time for a message already * processed. * 2. Call `ratchetDecrypt` on a CLONE of `state` (via * `deserializeRatchet(serializeRatchet(state))`) and commit that clone as * the live state ONLY after the returned message key successfully * authenticates the AEAD ciphertext. If authentication fails, discard the * clone and leave the live state untouched. * `ratchetDecrypt` itself performs neither dedup nor authentication — the safe * integration of both rules is wired in Stage 4/5, not here. * * Against that (untrusted) header, handles three cases: (a) a stored skipped * key — out-of-order delivery within an already-seen chain, (b) a new peer DH * pub — skip the old chain's tail, then DH-step, (c) a forward message in the * current chain — skip any gap, then derive. The consumed chain key is wiped * once its successor is derived. */ export declare const ratchetDecrypt: (state: RatchetState, header: RatchetHeader, module: LibCrypto) => Uint8Array; /** * Snapshot all live state (including the skipped map + DH keys) to owned * buffers for at-rest persistence. Does NOT wipe the source state — the caller * keeps using it. */ export declare const serializeRatchet: (state: RatchetState) => RatchetSessionSecrets; /** Rebuild a live `RatchetState` from a persisted snapshot. */ export declare const deserializeRatchet: (s: RatchetSessionSecrets) => RatchetState; /** Deep-clone a live ratchet state into independently owned key buffers. */ export declare const cloneRatchet: (state: RatchetState) => RatchetState; /** Wipe every secret-bearing buffer owned by a ratchet state. */ export declare const wipeRatchet: (state: RatchetState) => void; /** * Replace a live state with an independently owned authenticated successor. * The superseded live secrets are wiped before ownership moves from `next`. */ export declare const adoptRatchet: (live: RatchetState, next: RatchetState) => void;