Persisted formats
Versioned V1/V2 encrypted-envelope and Recovery Kit formats, authenticated metadata, encoding rules, and parser rejection behavior.
All persisted structures are strict UTF-8 JSON. Binary fields use unpadded base64url and timestamps use UTC RFC 3339. Unknown fields and unsupported versions are rejected.
PersistedFormatSupport from @ownfold/core declares the versions each release reads and writes.
pnpm test:compatibility checks that declaration against immutable V1 and V2 cryptographic
fixtures. See Versioning and upgrades for the removal and migration policy.
PersistedCryptoCapabilities.current separately declares reader and writer suite identifiers for
every persisted cryptographic format. Its strict V1 document can be exchanged through a host-owned
rollout system, and checkCompatibility proves both releases can read the other’s writer. This
supports bridge releases for future algorithm changes without enabling automatic downgrade. See
ADR 0010.
Encrypted envelope V2
New encryption uses ownfold.encrypted-envelope V2. It retains the V1 field layout and
XChaCha20-Poly1305-IETF primitives, but separates the two authenticated-data purposes:
- Content authenticates the format, version, suite, vault ID, record context, encoding, and content nonce.
- Data-key wrapping authenticates those fields plus the root-key version.
The content tag deliberately does not depend on the wrapped-data-key object or root-key version. Root-key rotation can therefore authenticate and unwrap the random record key with the old root, wrap it with the new root and a fresh nonce, and leave the content nonce and ciphertext unchanged. Changing any routing context or key version still makes one of the authenticated operations fail.
Encrypted envelope V1 compatibility
ownfold.encrypted-envelope V1 uses a random 256-bit data key per record. The data key is
wrapped by the root key. Both operations use XChaCha20-Poly1305-IETF with independent random
192-bit nonces.
Wrapping additional data authenticates the format, version, suite, vault ID, key version, record context, content encoding, and content nonce. Content additional data authenticates the same values plus the wrapped-data-key object.
V1 remains parseable and decryptable. Because its content tag depends on the wrapped key, rotating
a V1 record requires a one-time browser-side migration: authenticate and decrypt the content, use a
fresh content nonce, emit V2, and erase temporary plaintext buffers. No server receives plaintext.
CryptoEngine.rewrapRecordDataKey performs this migration. V2 inputs take the fast path and retain
their exact content object.
The compatibility suite creates actual V1 records with
withEnvelopeVersionForTesting(createSodiumCryptoEngine(), 1), rotates them through the browser
client, and verifies the resulting V2 record against its original context and plaintext. The
version override is exported only from @ownfold/crypto/testing; application code should always
write the engine’s current default version.
See ADR 0008: Rotation-friendly envelope for the protocol rationale and compatibility decision.
Recovery Kit V1
ownfold.recovery-kit V1 derives a 256-bit wrapping key with Argon2id13 using a 16-byte salt,
three operations, and 64 MiB memory. XChaCha20-Poly1305-IETF encrypts the root key. All public
kit fields are authenticated.
Password bytes are exact UTF-8. Ownfold does not trim or normalize them. A valid-looking kit whose authentication fails may have the wrong secret or may have been modified; the SDK does not claim to distinguish those cases.
Device envelope V1
ownfold.device-envelope version 1 uses X25519-HKDF-SHA-256+A256GCM. It contains the device and
vault identifiers, key version, creation time, recipient and ephemeral X25519 public keys, HKDF
salt, AES-GCM nonce, and the encrypted 32-byte root key. Binary fields use unpadded base64url.
Every field except encryption.wrappedRootKey is canonicalized as authenticated additional data.
The HKDF info separately binds the derivation purpose, format, version, device, vault, key version,
and both public keys. Changing ownership or key-routing metadata makes decryption fail.
Protected device private key V1
ownfold.protected-device-private-key V1 is local-only and never crosses the configured transport.
It contains an X25519 private key encrypted with AES-256-GCM under a non-extractable browser
CryptoKey. Its authenticated data binds the storage purpose, format, version, algorithm, X25519
public key, encryption algorithm, and nonce. The AES key and encrypted private-key bytes are stored
in IndexedDB; raw X25519 private bytes exist only briefly during device operations and are zeroed
on a best-effort basis.
The parser also accepts the earlier direct non-extractable X25519 CryptoKey representation for
upgrade compatibility. New writes always use the protected V1 structure because WebKit does not
reliably restore direct X25519 keys from IndexedDB.
Pairing offer V1
ownfold.pairing-offer version 1 is an expiring public structure containing the pairing request,
vault, and device identifiers; device label; X25519 public key; creation time; and expiry. It
contains no ciphertext or secret material. The complete strict JSON structure is transferred out
of band and compared exactly by the approving browser before it creates a device envelope.
Server-side pairing state is a discriminated union: pending contains only the offer and revision;
approved additionally requires a device envelope and approval time; cancelled requires a
cancellation time. Database constraints and parsers reject mixed or incomplete states.