ChromVoid Blog

How ChromVoid's Encryption Architecture Supports Plausible Deniability

ChromVoid's deniability model starts in the storage architecture: every password maps to its own encrypted namespace, while no vault list tells an observer what else exists.

By ChromVoid Team ·

How ChromVoid's Encryption Architecture Supports Plausible Deniability

Most encrypted vaults treat unlock as a yes-or-no question.

You enter a password. If it is correct, the vault opens. If it is wrong, the app tells you.

That model is familiar, but it creates a signal: the software knows there is one real vault behind one real password. In a normal password manager, that is fine. In a coercion scenario, it can be a problem.

ChromVoid is designed around a different question:

What if a password does not prove whether "the vault" exists, but selects one encrypted namespace inside the same storage?

That question is the core of ChromVoid's plausible deniability model.

The result is not magic privacy. It does not make device compromise irrelevant. It does not erase timestamps, total data size, operating-system traces, or user habits. It is a specific storage and unlock design that supports controlled disclosure: a decoy password can open a real, plausible low-risk vault, while other passwords open other vaults without an obvious "wrong password" signal.

This article explains how that works.

The Short Version

ChromVoid uses public cryptographic primitives and keeps the storage model intentionally simple.

The vault password is stretched with Argon2id. The salt used for derivation is hardened with a separate 32-byte storage pepper. That pepper is stored outside the chunk directory, for example in a platform keystore, so a copied storage folder is not automatically enough to test password guesses conveniently.

The resulting 32-byte vault key is used for two things:

  1. Encrypting and authenticating vault data chunks with ChaCha20-Poly1305.
  2. Deriving deterministic chunk names with BLAKE3, so each password maps to a different set of filenames.

There is no plaintext list of vaults.

There is no durable "this password is wrong" marker.

There is no central file that says "the real vault is here."

Instead, unlock derives a key from the supplied password and looks for catalog chunks whose names can only be derived from that key. If they exist, ChromVoid loads that vault. If they do not exist, ChromVoid opens an empty vault for that password.

That behavior is the basis for decoy and hidden vaults.

Key Derivation: Password, Salt, Pepper

ChromVoid's unlock path starts with three inputs:

  1. The user's vault password.
  2. A per-storage vault salt.
  3. A storage pepper stored separately from the encrypted chunk directory.

The current portable KDF path first stretches the salt with BLAKE3:

stretched_salt = BLAKE3(vault_salt || storage_pepper || "vault-salt-v2")[0..32]

Then ChromVoid derives the vault key with Argon2id:

vault_key = Argon2id(password, stretched_salt)

The output is a 32-byte key.

chromvoid encryption key derivation flow

Production parameters are intentionally memory-hard. Desktop builds use a larger Argon2id memory cost than mobile builds; mobile devices use a smaller profile to stay practical on battery and memory-constrained hardware. Test builds can opt into fast KDF settings, but production builds do not use those test parameters.

The storage pepper is important because it changes what an attacker can do with a copied folder. If an attacker only has the encrypted chunks, they still need the hardening material that was kept outside that folder. This is not a replacement for a strong passphrase. It is offline hardening: it increases the cost and inconvenience of password testing when the storage directory has been copied without the keystore material.

If storage chunks already exist but the storage pepper is missing, ChromVoid does not silently create a new pepper and pretend nothing happened. Existing encrypted data depends on the original pepper, so the unlock path treats that as a required-material error.

Chunk Storage: Flat Files, Independent Encryption

ChromVoid stores vault data as encrypted chunks.

The default chunk size is 16 KB. Each chunk is encrypted independently with ChaCha20-Poly1305. The serialized format is:

nonce || ciphertext || authentication_tag

The nonce is 12 bytes. The Poly1305 authentication tag is 16 bytes. Decryption requires the same additional authenticated data that was used during encryption.

ChromVoid uses the chunk name as AAD.

That detail matters. If an attacker copies ciphertext from one chunk name to another, the authentication check should fail because the AAD no longer matches. Encryption is not only about hiding bytes; it also needs to bind those bytes to the place where the storage layer expects them.

Catalog data is sharded. Blob data, catalog root indexes, catalog snapshots, deltas, OTP data, derivatives, and transaction markers all use key-derived chunk names. The storage directory sees opaque chunk files. It does not see a readable tree of "passwords", "wallet", "documents", or "hidden vault".

Writes are designed to be crash-tolerant: data is written to a temporary file, fsynced, and then moved into place. That protects durability. It is separate from deniability, but both properties matter because an encrypted vault that corrupts itself under normal failure is not a secure system in practice.

Chunk Names: Where Deniability Starts

The most important storage trick is not exotic encryption. It is naming.

ChromVoid derives chunk names from the vault key, a context string, and an index:

chunk_name = hex(BLAKE3(vault_key || context || chunk_index))

Different contexts are used for different kinds of storage records:

catalog
catalog:root
catalog:commit
blob
shard:<id>
delta:<id>
otp
derivative-index

Because the vault key changes when the password changes, the filenames change too.

Password A maps to one namespace.

Password B maps to another namespace.

The storage directory does not need a plaintext registry that says which namespaces are real. The only way to know whether a password has an existing catalog is to derive the key and check whether the corresponding key-derived root chunk exists.

That is what lets ChromVoid avoid a normal wrong-password oracle.

chromvoid encryption deniable namespaces

Unlock Is Not "Correct Or Wrong"

The unlock path is deliberately shaped around deniability.

At a high level, ChromVoid does this:

  1. Load or create the per-storage salt.
  2. Check the storage format version.
  3. Load the storage pepper from the configured keystore.
  4. Derive the vault key from password, salt, and pepper.
  5. Recover any incomplete rekey state for that key.
  6. Try to load the key-derived catalog root.
  7. If the catalog exists, load the vault.
  8. If no catalog chunks exist for that key, return an empty vault.

The important part is step 8.

ChromVoid does not return "invalid password" just because the supplied password does not find an existing catalog. Any password can derive a valid key. Any key maps to a possible storage namespace. Some namespaces already contain data. Some are empty.

That means a decoy password can open a real decoy vault with routine accounts and low-risk records. A different password can open a different hidden vault. Another password can open an empty namespace. The app behavior does not need to reveal which of those cases happened as a password-validation event.

The implementation still distinguishes absence from corruption.

If the key-derived root index chunk is missing, ChromVoid can treat that as "no catalog for this password" and open an empty vault. But if the root index chunk exists and then fails to read, decrypt, or parse, that is not a deniability case. It means the storage for that key is corrupt or temporarily unavailable, and the error must propagate.

That distinction prevents a dangerous failure mode: treating a real but damaged catalog as empty and then overwriting it on the next save.

Decoy Vaults Are Real Vaults

A decoy vault should not be a fake screen.

In ChromVoid's model, a decoy vault is an actual vault namespace produced by an actual password. It can contain real low-risk data: routine logins, notes, files, receipts, or other material that makes sense for the user to have.

That matters for two reasons.

First, a decoy that is obviously artificial is weak. Controlled disclosure depends partly on the user's operational security: the decoy needs to be believable, maintained, and consistent with the user's story.

Second, keeping decoy and hidden surfaces in the same storage model avoids a separate "fake mode" code path. Separate fake modes tend to create markers. A different UI state, a different metadata file, a different backup layout, or a different persistence path can become evidence.

ChromVoid's design is cleaner:

password -> key -> chunk namespace -> catalog

Every vault follows that shape.

The difference between decoy and hidden is not a special flag. It is which password the user discloses and which namespace that password selects.

What The Storage Does Not Reveal

The storage layer is designed to reduce obvious inventory markers.

It does not store a plaintext list of vault names.

It does not store "decoy" or "hidden" labels next to encrypted data.

It does not keep a single readable catalog that lists every vault.

It does not need to tell the caller that a password was wrong.

At the file level, chunks are opaque names derived from keys and contexts. Without the relevant key material, a copied directory does not conveniently explain which chunks belong to which password-derived namespace.

This is the difference between encryption and deniability.

Encryption hides content.

Deniability reduces explicit evidence about which encrypted namespace is the only real one.

ChromVoid needs both.

Remote Transport Is A Separate Layer

Storage encryption protects data at rest. Remote access needs a different boundary.

ChromVoid treats transport as untrusted by default. Remote RPC and data payloads are protected with Noise-based secure channels on approved remote paths. Pairing and reconnect flows authenticate peers cryptographically, so a relay or network intermediary should remain a byte-moving layer rather than a trusted source of truth.

This does not replace vault encryption. It wraps remote access to an already-defined vault boundary.

A useful mental model is:

storage encryption protects what is saved
Noise transport protects what is sent
capability grants limit what is allowed during a session

Those layers solve different problems. Keeping them separate makes the architecture easier to audit.

chromvoid encryption layer boundaries

What Plausible Deniability Does Not Promise

Plausible deniability is easy to overstate, so ChromVoid's model has explicit limits.

It does not hide that ChromVoid is installed.

It does not hide total encrypted data size.

It does not guarantee that a storage medium, filesystem, backup tool, or operating system left no traces.

It does not defeat malware on an unlocked host. If a compromised OS can read the screen, keyboard, clipboard, process memory, or open session, the threat model boundary is already broken.

It does not make a weak password safe.

It does not help if the storage pepper is exposed next to the copied chunks.

It does not remove the need for a believable decoy vault and disciplined user behavior.

It also depends on the attacker model. A single snapshot of encrypted chunk storage is different from repeated snapshots over time. Multi-snapshot analysis can reveal change patterns even when content remains encrypted. Device seizure while a vault is open is different from a cold copy of encrypted files.

The honest claim is narrower and more useful:

ChromVoid reduces explicit storage markers and wrong-password signals so controlled disclosure is possible within the documented threat model.

That is the claim the architecture is built to support.

Why This Architecture Matters

Security features are often described as if they were independent switches:

turn on encryption
turn on decoy mode
turn on remote access

ChromVoid does not treat them that way.

The deniability story depends on the storage story. The storage story depends on key derivation. Key derivation depends on keeping hardening material separate from copied chunks. Remote access depends on authenticated transport and short-lived grants. The UI can only be honest if those lower layers do not contradict it.

That is why the architecture is mechanism-first.

The core promise is not "trust us, it is private."

The promise is:

  1. The vault key is derived from password, salt, and separate hardening material.
  2. Data is stored as independently authenticated encrypted chunks.
  3. Chunk names are derived from the vault key, so each password selects its own namespace.
  4. Unlock does not expose a normal wrong-password signal when no catalog exists for that namespace.
  5. Decoy vaults are real vaults, not a fake UI overlay.
  6. The limits are documented because deniability only means something inside a threat model.

That combination is what makes ChromVoid different from a conventional encrypted vault.

It is not just encrypted data.

It is encrypted data arranged so the system does not have to confess which password was supposed to be the real one.