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:

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

ValueKDF
0x01Argon2id

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):

ParameterFieldMinimum (RFC 9106 §4)
Memory costARGON2_M_COST19 456 KiB (≈ 19 MiB)
IterationsARGON2_T_COST2
ParallelismARGON2_P_COST1

Recommended defaults for new vaults:

ParameterValue
ARGON2_M_COST65 536 KiB
ARGON2_T_COST3
ARGON2_P_COST4

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:


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:

  1. Open the vault with the old password: derive old KEK → unseal master key.
  2. Derive a new KEK from the new password (same salt, new parameters if upgraded).
  3. Generate a fresh WRAPPED_KEY_NONCE.
  4. Seal the same master key under the new KEK with the new nonce.
  5. Overwrite WRAPPED_KEY_NONCE and WRAPPED_MASTER_KEY in vault.fvh.
  6. Flush and fsync vault.fvh.

8. Portability


9. Cryptographic Primitive Summary

RoleAlgorithmCrate
Password KDFArgon2id (RFC 9106)argon2
Key wrapping AEADXChaCha20-Poly1305chacha20poly1305
File encryption AEADChaCha20-Poly1305 (default)chacha20poly1305
File encryption AEADAES-256-GCM (alt)aes-gcm
Key derivation (tree)HKDF-SHA256hkdf + sha2
Content integrityBLAKE3blake3
Randomnessgetrandomgetrandom

11. Version History

VersionDateChange
0x00012026-06-02Initial format (this document)