Internal review

Triage — all findings

The state-of-everything table: one row per finding with severity, status, affected component, and the remediation that landed it.

Internal review — finding triage

This file tracks the status of every internal review finding through to remediation or accepted-as-is. Findings are addressed in priority order (critical > high > medium > low). Informational findings may be addressed or accepted as-documented.

Status legend:

  • Open — finding stands; no fix landed yet.
  • Open, Phase 3 prerequisite — must be addressed before the PQ-hybrid default ships in Phase 3.
  • In progress — fix PR exists.
  • Fixed — fix landed; PR linked.
  • Accepted — no fix planned; rationale recorded.
  • Document — to be reflected in the spec / public docs only.

Findings

FindingTaskSeverityStatusComponentRecommended fixPR
1.1 — Spec prose contradicts AAD definition (file_id not in AAD)1MediumFixedspecFix prose + add AAD test vectorcrypto#1
1.2 — HKDF salt="" ambiguity in nonce-prefix derivation1LowFixedspec + implTighten spec to salt=zeros(32) + add nonce-prefix test vector. See 1.2 severity recalibration below.crypto#1
1.3 — Spec uses redundant truncateN(...)[0..k] notation1LowFixedspecReplace with HKDF(..., L=k+1) directlycrypto#1
1.4a — AES-GCM-v1 decryptBlob parses suite_payload before MAC verify1LowFixedimplSwap call order in aes-gcm-v1/api.ts:208-212crypto#2
1.4b — XChaCha-v1 decryptBlob parses suite_payload before MAC verify1LowFixedimplSwap call order in xchacha-v1/api.ts:177-178; bundle PR with 1.4acrypto#2
1.4c — Spec contradiction: header § MUST is unimplementable for PQ-hybrid1MediumFixedspecScope header § MUST to non-KEM suites; add CCA-hygiene security argument to § "0x03"crypto#3
1.5 — validateHeaderInputs lacks (totalChunks × chunkSize, plaintextSize) cross-field check1LowFixedimplReplicate parseHeader's invariant, ideally via shared helpercrypto#2
1.6 — Web dispatcher silently defaults cipherVersion to v01LowFixedimplRemove ?? 1 default; Stage 2c is shipped (PRs #115/#116/#117)web#123
1.7 — Worker fileId byte comparison is non-constant-time1InformationalAcceptedimplNone; value is already public via cleartext header. Optional refactor to constantTimeEqual for code-style consistency
1.8 — Spec underspecifies suite_payload zero-fill convention for vault-stored wrapped keys1LowFixedspecSpec to state "all 72/96/N bytes are zero when wrapped key is stored out-of-band"crypto#1
1.9 — V0 trailing-bytes check rejects exactly-tag-size remainder1InformationalAcceptedimplNone; defense-in-depth, optional one-liner in v0 spec
1.10 — Versioning policy override path not exposed1InformationalAcceptedimplNone; intentional safer-default. Revisit if/when a minor-version bump ships
1.11 — V0/V1 isolation is structural (positive finding)1InformationalFixedspecAdd to format-v1.md § "Compatibility with v0" so future contributors don't add cross-format auto-detectioncrypto#1
2.1 — Web app uses PBKDF2-SHA-256 instead of Argon2id for vault-key wrap2MediumFixedimplMigrate vaultKeyClient to Argon2id MODERATE; additive via existing kdf column; new accounts immediately, legacy on next password changeweb#126
2.2 — Web app flat envelope scheme vs spec's HKDF hierarchy; rk used directly in 4 contexts2MediumOpenspec + implOption A preferred; see doc for Option A vs B decision + spec intent question
2.3 — Argon2id INTERACTIVE used on high-entropy inputs in metadata-key derivation2LowOpenimplSplit metadata-key derivation by input type: HKDF for folder-keyed paths (high-entropy), keep Argon2id for share-password path (low-entropy). Pairs with 2.2 Option A.
2.4 — Spec underspecifies ML-KEM-1024 seed (d, z) split convention2LowFixedspecAdd one paragraph documenting d = seed[0..32], z = seed[32..64] convention + add test vectorcrypto#3
2.5 — Two HKDF info strings constructed inline, contradicting library's own invariant2LowFixedimplAdd ML_KEM_1024_SEED and ARGON2ID_SALT_COMPRESSION to HKDF_INFO constants in crypto/src/internal/types.ts; update the two call sites to reference themcrypto#1
2.6 — Spec's "Forbidden cross-context usage" list omits shieldfive/v1/argon2id/salt-compression2LowFixedspecAdd to spec's forbidden-list block; pairs with 2.5crypto#1
2.7 — Spec doesn't document intentional XChaCha info-string reuse in PQ-hybrid2InformationalFixedspecReplace ambiguous paragraph in spec § "Per-file key derivation" with explicit reuse-is-safe argument (IKM space partitioned)crypto#1
2.8 — Salt-absent ambiguity inherits Task 1 Finding 1.2 into key-derivation.md2InformationalFixedspecBundle with 1.2 fix: same "salt absent = zeros(32) per RFC 5869" disambiguation, applied to key-derivation.md preamblecrypto#1
3.1 — A2 splice prose says "file_id AAD" but cross-file binding is HKDF salt3MediumFixedspecReplace bullet; cross-link format-v1.md § "Suite payloads" and crypto#1 test vector. Mirrors Task 1 Finding 1.1 in the threat-model doccrypto#4
3.2 — A3 documented via suite 0x03 but web client emits classical 0x013MediumFixedspecAdd "Current deployment status" subsection under A3 + comparison-table footnote. Web wire-up of suite 0x03 tracked separately as Phase 3 workcrypto#4
3.3 — Threat model presents v1 AEAD invariants without v0 carve-out3MediumFixedspecAdd "Legacy v0 data" subsection under A2 referencing format-v0.md § "Limitations of v0"crypto#4
3.4 — Recovery-key compromise not in adversary model3LowFixedspecAdd "Recovery-key compromise" subsection under "Out of scope"crypto#4
3.5 — Share-link recipients not enumerated as a partial-access party3LowFixedspecNew "Trust principals" section with "Share-link recipient" subsectioncrypto#4
3.6 — Metadata leakage list understates what the deployed schema exposes3LowFixedspecReplace paragraph with non-exhaustive list (name_hash, per-share viewer geo, hashed IP, lockout state, download counts, last_active_at)crypto#4
3.7 — TLS termination boundary implicit in A2 "Cannot break TLS"3InformationalFixedspecAppend edge-termination paragraph to A2; AEAD layer is end-to-end client-to-blob, so an attacker controlling TLS termination is effectively A1crypto#4
5.1 — initDecryptV1 accepts non-AES-GCM-v1 suites without explicit refusal5LowFixedimplAdd explicit parsed.suite !== SUITE.AES_256_GCM_V1 reject between verifyHeaderMac and createAesGcmV1DecryptStreamweb#133
5.4 — processEncryptV1/processDecryptV1 lack index bound check5LowFixedimplReject state.totalChunks === 0 || index >= state.totalChunks at the top of each process functionweb#133
6.2 — verifyAesGcmV1Proof accepts chunk_0 length in structurally-invalid [1, 16] range6LowFixedimplReject chunkZeroLength < 1 + AES_GCM_TAG_BYTES between the zero-reject and upper-bound checkweb#133

Notes

1.2 severity recalibration

Severity recalibrated Medium→Low in PR #1: the original finding claimed salt=Uint8Array(0) and salt=zeros(32) produce different HKDF-Extract outputs for HMAC-SHA-256. This is false — HMAC zero-pads keys shorter than the 64-byte block size, so the two are computationally equivalent. Spec edit retained for clarity; canonical form is zeros(32) per RFC 5869, but the two are equivalent for this construction.