One key per object: envelope encryption inside NT² Vault
9 min read By NT²
A vault can encrypt everything with one key and still claim that its data is encrypted. We wanted a more useful boundary: each item and attachment gets its own content encryption key, while the vault key protects those keys.
One key per object: envelope encryption inside NT² Vault
Encryption is often described as if it were a switch: data is either encrypted or it is not. Real storage design has another question underneath: what does each key encrypt?
The simplest answer is one key for everything. Derive a vault key when the user unlocks, feed every item and attachment through AES-GCM with that key, and store the resulting ciphertext. That protects plaintext at rest, provided the implementation handles keys and initialization vectors correctly.
But simplicity at the first write can create complexity everywhere else. Rotating a key, sharing one item, synchronizing one changed attachment, or recovering from damaged data all become entangled with a vault-wide encryption boundary.
NT² Vault uses envelope encryption instead. Every item and every attachment has its own randomly generated content encryption key, or CEK. The CEK encrypts that object's payload. The vault key does not directly encrypt every byte of user content; it wraps the CEKs.
That distinction adds fields and operations. It also gives the system a clearer unit of protection: one object, one content key, one encrypted payload.
The constraint: a vault is not one indivisible blob
A vault looks like one thing in the interface. Underneath, it is a collection of objects with different sizes, lifetimes, and destinations.
A login item may be edited several times in a week. A scanned passport may be a large attachment that rarely changes. A note may later be shared while the rest of the vault stays private. Another device may need only the objects changed since its last synchronization. Some data belongs in relational storage for querying; large binary ciphertext belongs in file-oriented storage.
Treating all of that as one vault-wide ciphertext blob creates awkward choices:
- A small edit can require rewriting or retransmitting a large encrypted unit.
- Key rotation can imply decrypting and re-encrypting every payload.
- Sharing one object can tempt the design to expose a key with a much wider scope than the object being shared.
- Partial synchronization becomes difficult because the cryptographic boundary does not match the synchronization boundary.
- Corruption or an implementation error can affect a larger portion of the vault at once.
Storing objects separately but encrypting every payload directly with one shared vault key avoids the single-file rewrite problem, yet changing that key still means touching every ciphertext. NT² Vault instead makes the encryption boundary match the product's unit of work: the item or attachment.
The design: data keys below, vault key above
Envelope encryption separates two jobs:
- A per-object CEK encrypts the object's content.
- The vault key encrypts, or wraps, that CEK.
When NT² Vault creates an item, the client generates a fresh random CEK. It encrypts the item's structured payload with AES-GCM and a fresh initialization vector. It then wraps the CEK with the vault key using another AES-GCM operation and another fresh IV.
Conceptually, the stored item contains:
- ciphertext for the item's payload;
- the IV used for payload encryption;
- the wrapped CEK;
- the IV used for CEK wrapping; and
- non-secret fields needed to locate and manage the record.
The same hierarchy applies to an attachment. A fresh CEK encrypts the binary content, and the vault key wraps that CEK. Attachment records include the metadata needed to locate, validate, and reconstruct the encrypted binary, without placing the plaintext file in the database.
flowchart LR
VK[Vault key in memory] -->|AES-GCM wraps| ICK[Item CEK]
VK -->|AES-GCM wraps| ACK[Attachment CEK]
ICK -->|AES-GCM encrypts| IP[Item payload ciphertext]
ACK -->|AES-GCM encrypts| AB[Attachment blob ciphertext]
Unlocking the vault makes the vault key available in memory. Opening an object then follows the hierarchy in reverse:
- Read the object's wrapped CEK and wrap IV.
- Use the vault key to unwrap the CEK.
- Use that CEK and the payload IV to decrypt the object.
- Keep plaintext and working keys out of persistent storage.
This is a key hierarchy, not a chain of passwords. The user does not manage a password per item. The application generates and manages CEKs locally, while the vault key remains the root that authorizes access to them.
Why AES-GCM appears twice
AES-GCM provides authenticated encryption. It hides the plaintext and verifies that the ciphertext has not been modified without detection. That property matters for both layers.
At the content layer, authentication helps detect a modified item payload or attachment chunk. At the wrapping layer, it helps detect a modified wrapped CEK. A corrupted key envelope should fail closed; it should not quietly produce plausible but incorrect plaintext.
Each AES-GCM encryption operation requires a globally unique IV for its key. The payload encryption and key wrapping are distinct operations, so each gets its own fresh IV. IVs are not secret and can be stored beside their ciphertexts, but they are not optional bookkeeping. Reusing an IV with the same AES-GCM key can undermine confidentiality and integrity.
Items in SQLite, attachment ciphertext in BlobStore
The key hierarchy also fits the storage hierarchy.
Structured item records and attachment metadata live in the vault's local SQLite database. SQLite is well suited to relationships and queries: which attachments belong to an item, what changed, what is archived, and which rows should appear in a paged list.
Large encrypted attachment bytes have different needs. In the browser, NT² Vault stores attachment ciphertext as files in the Origin Private File System (OPFS), accessed through a BlobStore boundary. A desktop build can provide the same BlobStore behavior over its local application files.
The database therefore holds authoritative metadata, including the attachment identifier, parent item, size and cryptographic envelope fields. BlobStore holds the ciphertext bytes. Neither side contains the plaintext attachment at rest.
This separation avoids forcing large binary objects through relational rows while preserving transactional, queryable metadata. It also keeps platform differences below one storage interface: the encryption model does not need to change because a browser uses OPFS and a desktop application uses local files.
Most importantly, storage placement does not weaken the boundary. Moving an encrypted attachment blob without its wrapped CEK does not reveal the file. Copying metadata without the vault key does not unwrap the CEK. The parts are useful only within the authorized key hierarchy.
Rotation without rewriting every payload
Envelope encryption makes key rotation more tractable because content encryption and root-key protection are separate operations.
If the vault key must change, the system can unwrap each CEK with the old vault key and wrap it with the new vault key. The large item payloads and attachment blobs do not need to be decrypted and encrypted again merely because their parent key changed.
This does not make rotation free. Every envelope must still be processed correctly, and the operation needs careful failure handling. But the amount of data touched is proportional to the number of small wrapped keys, not the total size of every encrypted attachment.
The distinction is especially meaningful for a vault containing many photos, PDFs, or other large files. Rewrapping a small CEK is a different operational task from streaming, decrypting, and re-encrypting gigabytes of content.
It also creates room for object-level sharing or replication without treating the whole vault as the unit of disclosure. A CEK is not a complete sharing protocol—recipient-bound encryption and authorization are still required—but it is the right starting boundary.
Partial sync with object-sized boundaries
Local-first software should not turn a one-field edit into a vault-sized upload. Per-object ciphertext gives synchronization a natural unit.
When an item changes, its encrypted record can be synchronized independently. When an attachment is added, its metadata and encrypted blob can travel through the appropriate channels without bundling unrelated vault content. A device that already has every other object does not need another copy of them.
This is both a performance property and a security property. Smaller units make retries, integrity checks, conflict handling, and failure diagnosis more specific. A failed attachment transfer can remain an attachment transfer problem instead of becoming an ambiguous failure of one monolithic vault file.
The trade-off: more metadata, clearer blast radius
There is no free abstraction. Per-object envelope encryption stores more than a single-key scheme:
- one CEK for each object;
- a payload IV for each encryption;
- a wrapped-key ciphertext;
- a wrap IV; and
- enough versioning and metadata to parse the envelope safely.
Creating or opening an object also requires an additional cryptographic operation. Backups must preserve envelope fields. Migrations must understand their formats. Tests must cover damaged ciphertext, missing blobs, invalid wraps, and interrupted writes.
That overhead is worthwhile because the extra structure is meaningful. It narrows the blast radius of each content key. It makes the relationship between an object and its encryption material explicit. It allows root-key changes to operate on envelopes instead of bulk content. It aligns encryption with sharing, synchronization, storage, and recovery boundaries.
Envelope encryption is not a cure for a compromised runtime. An attacker who controls an unlocked application may be able to use the vault key to unwrap many CEKs. Its value is reducing unnecessary storage and key-management coupling, not promising that compromise can never spread.
What we refuse to store
Two rules define the boundary more clearly than any diagram.
First, NT² Vault does not persist plaintext item payloads or plaintext attachments. Searchable and operational metadata must be designed narrowly; the sensitive payload remains ciphertext at rest. Temporary plaintext required for an active user action belongs in memory, not in SQLite, OPFS, or a server-side database.
Second, NT² Vault does not use one shared CEK for the whole vault. Calling a vault-wide content key a “CEK” would not create an object boundary; it would only rename the vault key problem. Each item and each attachment receives its own content key.
The vault key has a powerful role, but a focused one: protect the envelopes. That makes it possible to reason separately about the root of access, the key for one object, and the ciphertext for that object.
A small hierarchy with useful consequences
Good encryption architecture is less about adding cryptographic vocabulary and more about choosing boundaries that survive real product operations.
Items change independently. Attachments can be large. Synchronization is partial. Sharing should disclose less than an entire vault. Keys sometimes need to rotate. Storage can fail one object at a time. A per-object CEK fits those facts better than one vault-wide ciphertext or one shared content key.
The vault key still matters enormously. How it is derived and what supporting material stays on the device are separate parts of the design; for the local salt boundary, read Why the KDF salt stays on your device.
Envelope encryption gives NT² Vault a practical rule: encrypt content with the smallest durable key scope that matches the content, then protect those keys under the vault's root of access.
You do not need to manage that hierarchy yourself. Open NT² Vault or visit se.nt2.me to create a local-first vault whose encryption boundaries are built into the storage model.
Last updated 2026-07-25