- kv-proto: email on setup, AccountInfo.email/is_admin, ServerInfo.registration, admin DTOs.
- kv-sync: prelogin by email or account id (setup QR / Emergency Kit), register with email,
set_email, /v1/admin/* client calls.
- kv-ffi: register_account(code, email), login(.., email), account_details, set_account_email,
admin_create_invite. kv-wasm: the same plus the full admin API for the web vault.
- Live test: two users on one server stay isolated; a new device signs in by email.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
VaultCore::delete_account verifies the password against the keyset (Secret Key from its sealed copy),
calls DELETE /v1/account when a server is configured (needs a session; a failure deletes nothing),
then wipes the local store and keeps only the device identity. Unit test plus a live test against a
real server.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
kv-sync: SyncError::DeviceRevoked for 403 device_revoked (drops the session);
the test server answers it after OPAQUE (before the ticket) and for valid
signatures by a revoked device's key.
kv-ffi: CoreError::DeviceRevoked; the foreign DeviceSigner gains reset() (new key
and id). On device_revoked the core resets the identity, forgets the session
and the kept OPAQUE input (no silent re-enrolment) and keeps the vault data; a
login carrying a ticket retries once as the new device. reset_device_identity()
runs the reset on demand. Signed calls (sync, devices, tickets, join) reset too.
kv-wasm: DeviceRevoked; the browser binding replaces the WebCrypto key and id in
IndexedDB (same name and platform), loginWithSetup retries once with a ticket;
resetDeviceIdentity().
Tests: end to end revoked -> reset -> re-enrol with the same ticket (kv-ffi and
web core), no-ticket path, in-browser IndexedDB reset.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
New crate crates/kv-wasm runs the same crypto, item model and sync engine in the browser:
- WebStore: in-memory replica of kv-store's Store, each changed entry written to IndexedDB
(ciphertext only) before the JS call resolves.
- Device key: non-extractable WebCrypto ECDSA P-256 key in IndexedDB (raw r||s signatures).
- WebCore mirrors kv-ffi's VaultCore; wasm-bindgen surface in camelCase with TypeScript types,
plus a Web Worker wrapper (connectVault) so Argon2id never blocks the page.
- Tests: Chrome (IndexedDB, WebCrypto, RFC 9421 verify), shared envelope fixtures both ways
with kv-ffi, native sync with kv-ffi via the test server, Node smoke of the built package.
Small changes to shared crates so they build for wasm32:
- kv-store: SqliteStore moved to a native-only module.
- kv-sync: rustls/pinning/tokio native only (fetch on wasm); DeviceSigner::sign_async (default
wraps sign) used by ApiClient; unix_now() instead of SystemTime/now_utc.
- kv-model: Public Suffix List behind a default `psl` feature (kv-ffi keeps it; the web build
drops ~0.9 MB of code and matches same-host only).
- wasm-release profile (opt-level z, Argon2 at 3) and a wasm CI job.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
* kv-proto: ticket and join wire types, LoginFinish.ticket, b64_opt.
* kv-crypto: pairing.rs (HPKE AuthPSK seal/open with the QR secret as PSK, SAS code).
* kv-sync: onboarding.rs (kestrove://setup/v1 and kestrove://join/v1 codec with
strict validation), ApiClient ticket/join calls and login_with_ticket, test
server support for tickets and rendezvous.
* kv-ffi: parse/encode_setup_payload, setup_payload_for_kit, add_device_payload,
revoke_enrollment_ticket, login_with_setup, join_start/poll/cancel,
join_approve/send. The Secret Key is kept locally, HPKE-sealed to enc_pk.
* Tests: codec round trips and rejections, AuthPSK tamper cases, end-to-end
Add-device QR and scan-to-join between two clients, forged bundles, bound
tickets; live test extended with scan-to-join.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
next_version() and the two merge paths in the engine computed max(seq) + 1 on seq
values read from decrypted plaintext. A version carrying seq = u64::MAX panicked in
debug builds and wrapped to 0 in release, so the next save looked like a replay to
every other device. Found while checking fuzz/item_replay; needs an authentic
plaintext, so low severity.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Tombstones carried no ciphertext, so the server could forge a delete (hiding
an item), and a device that pulled one accepted the next live version without
a seq comparison (delete-then-replay rollback).
* A delete is sealed like an item (same AAD and padding) with plaintext
{id, seq, deleted: true}; seq = working/base seq + 1. DELETE carries it as
the ItemPut body; the test server stores and serves it.
* replay: Base is None | LegacyTombstone | Sealed(plaintext). check_pulled
refuses a record without ciphertext, a deleted flag that disagrees with the
plaintext, or a tombstone naming another item, then requires seq to grow
for tombstones too. The `restores` exception is gone: edit beats delete
re-seals with max(edit, tombstone) + 1. next_version returns just seq.
* Legacy: a pulled tombstone without ciphertext is an integrity alarm (cannot
be told apart from a forgery). A pending legacy delete is sealed on push; a
legacy tombstone already held as base is accepted as unknown seq.
* engine: open_sealed, seal_tombstone; next_local_version -> u64. Merkle leaf
hashes a sealed tombstone's envelope like a live record's.
* kv-model Item: `restores` removed. kv-ffi delete_item seals the tombstone;
FFI surface unchanged.
* Tests: forged (unsealed and relabelled) tombstones raise an integrity alarm
(kv-sync and kv-ffi), replay after a tombstone, replays of the tombstone and
of the restore, legacy pending delete, shared item_replay.json table; the
live test now deletes an item through the real server.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
A malicious server could re-serve an old but valid envelope of an item under
a new, higher revision: the AAD does not bind the revision and the Merkle
root was only checked when head_revision == cursor. The client took it as
the latest version, rolled the item back and signed over it in its next
manifest.
* kv-model Item: `seq: u64` (serde default 0) and `restores: Option<u64>`.
* kv-sync replay module: pure decision rules (check_successor,
next_version, stamp, same_content). Every pulled live record must
authenticate and have seq > the base's seq, unless it carries
restores = t with base_revision < t < revision (edit beat a delete this
device never saw) or is identical to the base. Otherwise: Integrity alarm.
Tombstones and items without a base pass.
* engine: check on every pulled record; edit-beats-delete re-seals with
restores = tombstone revision; merges write seq = max + 1 and adopt the
remote version when only seq/restores differ. Public
engine::next_local_version gives the seq/restores for a local save
(used by kv-ffi save_item and the tests).
* verify_manifest checks the Merkle root whenever no held item is newer
than head_revision (previously only at head == cursor).
* Tests: replay alarm (clean and pending receivers, plus kv-ffi), harmless
replay of the current version, tampered envelope, concurrent equal-seq
edits across three devices, edit-beats-delete with a device that skipped
the tombstone and a later replay of the restore, legacy items without seq,
hidden item with cursor past the manifest head, and the shared
item_replay.json decision table.
* gen-vectors: envelope vector item payload now includes seq.
FFI surface unchanged.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
kv-store: SQLite (bundled) and in-memory Store holding only ciphertext
envelopes, with base copies for three-way merges and a 10-version history.
kv-ffi: VaultCore::open(path); persisted account record; local unlock key
(enable/unlock_with_local_key/disable); async sync API (configure_server,
set_device_signer, register_account, login, sync_now, logout, devices);
DeviceSigner foreign trait; IntegrityAlarm/PinMismatch/... error variants.
kv-sync: reqwest + rustls (TLS 1.3 only, ring) with SPKI pinning; OPAQUE
client on kv-proto's OpaqueSuite; RFC 9421 signing per kv_proto::httpsig;
pull/push with If-Match, field-level three-way merge, edit-beats-delete;
signed manifest (canonical CBOR, Merkle root, prev chain, Ed25519) with
rollback/fork/Merkle checks as hard integrity alarms. Tested against a
wiremock fake server, plus an #[ignore] test against kestrove-server.
kv-crypto: gen-vectors example and tests reproducing kestrove-spec/vectors.
Co-Authored-By: Claude Opus 5.5 <[email protected]>