Envelopes
What a sealed record is, byte by byte, and how a second passkey opens the same vault.
The sealed record
offset size field
0 4 magic "SAT1"
4 1 version (1)
5 32 nsHash: keccak256("satchel/ns/" + path)
37 8 epoch, big endian
45 12 IV, random
57 n+16 AES-256-GCM ciphertext and tag
AAD = bytes 0 to 56 (the whole header)The header is plain on purpose. Routing a blob to a namespace, showing its epoch and refusing a foreign blob all happen without a key. Because the header is the authenticated data, changing any header byte makes decryption fail. The path itself is never in the bytes, only its hash. parseSealed reads the header.
Which key encrypts
The data key for a record is derived from the namespace key and the epoch: HKDF(nsKey, info "satchel/dek/" + epoch). A new epoch is a new key. Grants and epochs uses that to revoke.
A second passkey
addPasskey() creates another passkey and encrypts the root with a key derived from that passkey's own PRF output, with the rpId and credential id as authenticated data. The wrapped root goes to the Store you pass (IndexedDB by default). Opening with the second passkey unwraps the same root, so every namespace and every fingerprint is identical under either passkey.
The store holds ciphertext only. A vault that must open on a device that has never seen this browser needs a store both can reach, which is why Store is a two method interface rather than a fixed location.