Envelopes

What a sealed record is, byte by byte, and how a second passkey opens the same vault.

The sealed record

SAT1 layout
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.