v0.1.0
Minor Changes
-
#57
8115145Thanks @germ-mark! - Push-based persistence; the pullarchive()is removed from the FFI⚠️ Binding contract 12 → 13 — binding and binary MUST pair. Taketwo_mls_pq.swiftfrom this release. Persisted state is not portable:SESSION_ARCHIVE_VERSION→ 9,INVITATION_VERSION→ 3 — regenerate all persisted sessions and invitations.The pull
archive()onTwoMlsPqSessionandTwoMlsPqInvitationis removed from the exported surface. Its contract was a move, not a copy — using the live object after archiving, then restoring, rewound the sender ratchet and re-derived AEAD keys/nonces (security review finding H1: keystream reuse against a real transcript). The crate could not enforce the discipline while the app decided when to pull.The live object now pushes its state to a foreign persistence hook after every state-advancing mutation:
ArchiveSink(with_foreigntrait) withpersist(seq, kind: BlobKind, archive). Attach one per object with the newinstall_sink(pushes a baselineCheckpoint). The contract: enqueue-only, non-blocking, atomically upsert the one blob named bykind(never a multi-object write), newest-seq-wins per slot, and seal the plaintext bytes before writing.- Two-blob session model: a classical mutation rewrites
Core(identity + both classical halves + meta — the ML-KEM ratchet trees omitted); a PQ op (and the baseline) writes a fullCheckpoint. Every mutation is one atomic single-blob push, so the sink needs no cross-object transaction. Restore isTwoMlsPqSession.restore(core, checkpoint)(reconciles PQ-from-checkpoint, rest by higherstate_seq, fails closed on a manifest mismatch). The invitation is monolithic (no ML-KEM trees) and restores withTwoMlsPqInvitation.restore(archive:). Both restore constructors are namedrestore(renamed from the session'sfrom_persistedand the invitation'snew(archive:)) — the name signals materialising from serialised bytes, not minting fresh state. EncryptResult.depends_on_seq+ read-onlystate_seq()on both objects: the app waits until it has durably persisted the frame'sdepends_on_seqbefore transmitting a frame that publishes stored-private-key material (a routine app message re-staples an already-persisted commit, so it imposes no wait). Transmission stays entirely the app's concern.
Internals: the invitation's four mutexes are consolidated into one (removing a torn-archive class);
pq_bootstrap_beginnow persists its pending PQ key package (previously at risk of a restore-time strand). No protocol/wire changes to messages — only persistence and the removed pull surface.