You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
{{ message }}
RFC: container format v2 — Argon2id and chunked files #58
Design only; no code. Written so the decision can be made on the tradeoffs rather than on an implementation that already exists. Builds on #43 (password normalization is settled before any new KDF), #47 (one crypto core for both files, so v2 lands in one place) and the fixture discipline in #46 (v2 ciphertexts would be pinned from day one). Closes SECURITY-AUDIT.md Low #1 if adopted.
Why a v2 at all
Two limits of v1 are format-level, not fixable in the UI:
PBKDF2-SHA256 at 1,000,000 iterations is OWASP-compliant and GPU-friendly. A memory-hard KDF (Argon2id) is the "good → better" step the audit has carried as open since v2.2.0.
100 MB cap, because the whole file is one AES-GCM call held in memory three times over. Chunking removes the cap and allows streaming.
Both need a version byte; v1 already has one (IBTZ 0x01), so nothing about v0/v1 changes, ever.
Wire format
IBTZ 0x02
kdf-id 1 byte 0x01 PBKDF2-SHA256 · 0x02 Argon2id
kdf-params variable PBKDF2: iterations u32 · Argon2id: m_kib u32, t u8, p u8
salt 16
nonce-prefix 8 random per file
chunk-size u32 bytes of plaintext per chunk (1 MiB proposed)
chunks n × [ ciphertext || tag(16) ]
Per chunk i: nonce = nonce-prefix || u32(i); AAD = header || u32(i) || final-flag. The index in the AAD defeats reordering and replay of a chunk into another slot; the final flag defeats truncation (a stream that ends without a final = 1 chunk is corrupt); the whole header in the AAD binds the KDF choice and parameters, so an attacker cannot downgrade kdf-id on a stored file. 8 + 4 nonce bytes allow 2³² chunks per file — 4 PiB at 1 MiB — with no nonce reuse within a key, and the key is fresh per file anyway (random salt).
Last chunk may be short; empty files are one empty final chunk (a 16-byte tag), so "empty data" stops being an error.
Key derivation
Argon2idm = 64 MiB, t = 3, p = 1 as the opening proposal (the stronger of OWASP's two configurations; measure on the oldest phone you care about and adjust — the parameters are in the header, so they are free to change per release).
The hard part: Argon2 in a single file with a hash-pinned CSP
Web Crypto has no Argon2. The options:
WASM Argon2id (e.g. the reference C via Emscripten, ~20 KB base64 in the file)
Pure-JS Argon2id
64 MiB, t=3 on a 2020 laptop
≈ 0.4–0.8 s (argon2-browser's published figures)
≈ 4–8 s
same on a mid-range phone
≈ 1.5–3 s
15–30 s, and memory pressure
CSP
needs 'wasm-unsafe-eval' added to script-src (it permits only WebAssembly compilation, not JS eval; the hash pins stay)
no CSP change
Recovery tool
needs the same WASM block (≈ 27 KB → ≈ 50 KB)
needs the JS (≈ 27 KB → ≈ 35 KB)
Auditability
a binary blob in a file whose selling point is "read it in a text editor" — would need a reproducible build recipe and its own hash in the README
plain JS, readable
Both numbers are from public benchmarks and should be re-measured on your targets before deciding; mobile is the constraint.
Migration
v0 and v1 decrypt forever; scripts/crypto-regression.mts is unchanged, and fixtures:add pins v2 ciphertexts from the first release that writes them.
The app writes v2 by default, with a visible "compatible with older IttyBitz" switch that writes v1 (someone may need a file to open on a saved v3 copy).
The recovery tool must open v2 before any v2 file exists — ship the decrypt side first.
SECURITY-AUDIT.md Low Update README.md #1 → closed; README's "cryptography has never changed" sentence becomes "every version still decrypts".
smallest; leaves the KDF question open but representable
D v2: Argon2id (pure JS) + chunking
yes
yes
none
≈ +8 KB
too slow on phones at useful parameters
C is the smallest honest step: it fixes the format (chunking, AAD, HKDF key-file binding, parameters in the header) without taking the WASM decision, and leaves kdf-id 0x02 reserved so Argon2id can be switched on later as a parameter change, not a format change. B is the complete answer and the one the audit is asking for. A is defensible: PBKDF2 at 1M iterations is not broken, it is merely not the best available.
Happy to prototype whichever row is chosen behind the same regression gate; nothing here is a request to change the current format without that decision.
Design only; no code. Written so the decision can be made on the tradeoffs rather than on an implementation that already exists. Builds on #43 (password normalization is settled before any new KDF), #47 (one crypto core for both files, so v2 lands in one place) and the fixture discipline in #46 (v2 ciphertexts would be pinned from day one). Closes SECURITY-AUDIT.md Low #1 if adopted.
Why a v2 at all
Two limits of v1 are format-level, not fixable in the UI:
Both need a version byte; v1 already has one (
IBTZ 0x01), so nothing about v0/v1 changes, ever.Wire format
Per chunk i: nonce =
nonce-prefix || u32(i); AAD =header || u32(i) || final-flag. The index in the AAD defeats reordering and replay of a chunk into another slot; the final flag defeats truncation (a stream that ends without afinal = 1chunk is corrupt); the whole header in the AAD binds the KDF choice and parameters, so an attacker cannot downgradekdf-idon a stored file. 8 + 4 nonce bytes allow 2³² chunks per file — 4 PiB at 1 MiB — with no nonce reuse within a key, and the key is fresh per file anyway (random salt).Last chunk may be short; empty files are one empty final chunk (a 16-byte tag), so "empty data" stops being an error.
Key derivation
m = 64 MiB, t = 3, p = 1as the opening proposal (the stronger of OWASP's two configurations; measure on the oldest phone you care about and adjust — the parameters are in the header, so they are free to change per release).key = HKDF-SHA256(ikm = KDF(password, salt) || SHA-256(keyfile), info = "ittybitz-v2"). No boundary ambiguity, and an empty key file no longer silently equals "no key file" (Refuse an empty key file; hand binary results of text-mode decryption over as a file #50/Several files at once; a fingerprint for the key file #54 refuse it in the UI; v2 makes it structurally different).The hard part: Argon2 in a single file with a hash-pinned CSP
Web Crypto has no Argon2. The options:
'wasm-unsafe-eval'added toscript-src(it permits only WebAssembly compilation, not JSeval; the hash pins stay)Both numbers are from public benchmarks and should be re-measured on your targets before deciding; mobile is the constraint.
Migration
scripts/crypto-regression.mtsis unchanged, andfixtures:addpins v2 ciphertexts from the first release that writes them.SECURITY-AUDIT.mdLow Update README.md #1 → closed; README's "cryptography has never changed" sentence becomes "every version still decrypts".Decision matrix
'wasm-unsafe-eval'kdf-id 0x01)C is the smallest honest step: it fixes the format (chunking, AAD, HKDF key-file binding, parameters in the header) without taking the WASM decision, and leaves
kdf-id 0x02reserved so Argon2id can be switched on later as a parameter change, not a format change. B is the complete answer and the one the audit is asking for. A is defensible: PBKDF2 at 1M iterations is not broken, it is merely not the best available.Happy to prototype whichever row is chosen behind the same regression gate; nothing here is a request to change the current format without that decision.