Fordan Vault Format Specification
Version: 1 | Status: Normative | Applies to: fordan-core vault reader/writer
This document is the byte-exact contract for the Fordan vault format. Every implementation must conform to this spec. A format change that is not reflected here is a bug.
1. Overview
A vault is a self-contained directory tree on disk. It holds:
- A vault header file (
vault.fvh) at the vault root. - One encrypted file blob per imported file (arbitrary nesting).
- An encrypted directory tree manifest (
vault.fvm) at the vault root.
All multi-byte integer fields are little-endian (fixed, regardless of host architecture) so the vault is portable across x86, ARM, big-endian MIPS, etc.
2. Vault Header (vault.fvh)
2.1 Binary Layout
Offset Size Type Field
------ ---- ------ -----
0 8 [u8;8] MAGIC
8 2 u16 VERSION
10 1 u8 KDF_ID
11 4 u32 ARGON2_M_COST (memory, KiB)
15 4 u32 ARGON2_T_COST (iterations)
19 4 u32 ARGON2_P_COST (parallelism)
23 16 [u8;16] VAULT_ID (random UUID, fixed at vault creation)
39 32 [u8;32] KDF_SALT (random, fixed at vault creation)
71 32 [u8;32] WRAPPED_KEY_NONCE (nonce for the key-wrapping AEAD)
103 48 [u8;48] WRAPPED_MASTER_KEY (master key encrypted under KEK)
151 END
Total header size: 151 bytes.
2.2 MAGIC
46 4F 52 44 41 4E 56 31 ("FORDANV1" in ASCII)
Readers must reject any file whose first 8 bytes do not match exactly.
2.3 VERSION
Current version: 0x0001 (little-endian 01 00). Readers encountering an unknown VERSION must return a hard error, not silently downgrade.
2.4 KDF_ID
| Value | KDF |
|---|---|
0x01 | Argon2id |
2.5 Argon2id Parameters
The key-derivation function is Argon2id as specified in RFC 9106.
Minimum parameter floor (any vault with weaker parameters must be rejected):
| Parameter | Field | Minimum (RFC 9106 §4) |
|---|---|---|
| Memory cost | ARGON2_M_COST | 19 456 KiB (≈ 19 MiB) |
| Iterations | ARGON2_T_COST | 2 |
| Parallelism | ARGON2_P_COST | 1 |
Recommended defaults for new vaults:
| Parameter | Value |
|---|---|
| ARGON2_M_COST | 65 536 KiB |
| ARGON2_T_COST | 3 |
| ARGON2_P_COST | 4 |
2.6 VAULT_ID
A 16-byte random identifier assigned once at vault creation and never changed. Used as a domain-separation constant in associated data.
2.7 KDF_SALT
A 32-byte cryptographically random salt, generated with getrandom at vault creation. The salt is stored in the header and is never secret. It is unique per vault.
3. Key Hierarchy
password
|
| Argon2id(password, salt, m, t, p) → 32 bytes
▼
KEK (Key Encryption Key, ephemeral — never stored directly)
|
| XChaCha20-Poly1305 encrypt(master_key, nonce=WRAPPED_KEY_NONCE)
▼
WRAPPED_MASTER_KEY (stored in vault header)
|
| HKDF-SHA256(master_key, salt=vault_id, info="fordan/dir/" + dir_path)
▼
dir_key[dir_path] (one per directory)
|
| HKDF-SHA256(dir_key, salt=file_id, info="fordan/file/" + file_name)
▼
file_key[file_id] (one per file)
|
| file_key used as AEAD key for each chunk
▼
ciphertext[chunk]
3.1 KEK Derivation
KEK = Argon2id(
password = user_password_as_utf8_bytes,
salt = KDF_SALT (from header),
m_cost = ARGON2_M_COST,
t_cost = ARGON2_T_COST,
p_cost = ARGON2_P_COST,
output_len = 32,
)
3.2 Master Key Storage and Wrapping
WRAPPED_MASTER_KEY = XChaCha20-Poly1305.seal(
key = KEK,
nonce = WRAPPED_KEY_NONCE (24-byte random, stored in header),
plaintext = master_key,
aad = VAULT_ID,
)
3.3 Per-Directory Key Derivation
dir_key = HKDF-SHA256(
ikm = master_key,
salt = VAULT_ID,
info = b"fordan/dir/" + canonical_dir_path_utf8,
)
3.4 Per-File Key Derivation
file_key = HKDF-SHA256(
ikm = dir_key,
salt = file_id,
info = b"fordan/file/" + file_name_utf8,
)
3.5 Per-Chunk Nonce
Files are encrypted in chunks of at most 65 536 bytes (64 KiB). Each chunk gets a unique 12-byte nonce derived deterministically from the chunk index:
nonce[i] = little_endian_u64(i as u64) || 0x00 0x00 0x00 0x00
4. Encrypted File Blob (.fvb)
Offset Size Type Field
------ ---- ------- -----
0 8 [u8;8] MAGIC ("FORDANFB")
8 2 u16 VERSION (current: 0x0001)
10 1 u8 CIPHER_ID (0x01 = ChaCha20-Poly1305, 0x02 = AES-256-GCM)
11 16 [u8;16] FILE_ID (random UUID, assigned at import)
27 8 u64 ORIGINAL_SIZE (plaintext byte count)
35 32 [u8;32] CONTENT_HASH (BLAKE3 hash of plaintext)
67 4 u32 CHUNK_COUNT
71 CHUNKS...
Each chunk:
Offset Size Type Field
------ ---- ------ -----
0 4 u32 CHUNK_LEN (ciphertext byte count, including 16-byte tag)
4 CHUNK_LEN [u8] CIPHERTEXT (encrypted + authenticated chunk)
5. Associated Data (AEAD Binding)
associated data = VAULT_ID (16 bytes)
|| FILE_ID (16 bytes)
|| chunk_index as little_endian_u64 (8 bytes)
This construction guarantees:
- A chunk from vault A cannot be transplanted into vault B (VAULT_ID differs).
- A chunk from file X cannot be substituted into file Y (FILE_ID differs).
- Chunks within a file cannot be reordered (chunk_index differs).
- Truncation is detected: missing chunks cause decryption to fail.
6. Encrypted Directory Tree Manifest (vault.fvm)
The manifest lists the vault's logical directory tree with encrypted filenames. It is itself an encrypted blob using the master key directly:
manifest_key = HKDF-SHA256(
ikm = master_key,
salt = VAULT_ID,
info = b"fordan/manifest",
)
The manifest plaintext is a sequence of length-prefixed UTF-8 JSON records, one per file entry:
{
"file_id": "<uuid>",
"dir_path": "<canonical vault-relative directory path>",
"file_name": "<original filename, UTF-8>",
"blob_path": "<relative path to .fvb file inside the vault directory>",
"imported": "<ISO-8601 timestamp>"
}
7. Password Change — re-wrap
Changing the vault password does not regenerate the master key. It performs a re-wrap:
- Open the vault with the old password: derive old KEK → unseal master key.
- Derive a new KEK from the new password (same salt, new parameters if upgraded).
- Generate a fresh
WRAPPED_KEY_NONCE. - Seal the same master key under the new KEK with the new nonce.
- Overwrite
WRAPPED_KEY_NONCEandWRAPPED_MASTER_KEYinvault.fvh. - Flush and fsync
vault.fvh.
8. Portability
- All integer fields are little-endian.
- File paths inside the manifest use
/as separator, never\. - Filenames are stored as raw UTF-8 bytes.
- Timestamps use ISO-8601 with UTC offset (
Z).
9. Cryptographic Primitive Summary
| Role | Algorithm | Crate |
|---|---|---|
| Password KDF | Argon2id (RFC 9106) | argon2 |
| Key wrapping AEAD | XChaCha20-Poly1305 | chacha20poly1305 |
| File encryption AEAD | ChaCha20-Poly1305 (default) | chacha20poly1305 |
| File encryption AEAD | AES-256-GCM (alt) | aes-gcm |
| Key derivation (tree) | HKDF-SHA256 | hkdf + sha2 |
| Content integrity | BLAKE3 | blake3 |
| Randomness | getrandom | getrandom |
11. Version History
| Version | Date | Change |
|---|---|---|
| 0x0001 | 2026-06-02 | Initial format (this document) |