Every encrypted file format has to answer a quiet question: what stops an attacker from editing the parts that aren't secret? StenVault's file format, CVEF, learned the answer the hard way — and v1.4 is the fix.
A CVEF file is a small header, a block of JSON metadata, and the encrypted payload. The metadata declares the cryptographic suite: the algorithm, the wrapped file key, and — from v1.3 — a hybrid signature (Ed25519 + ML-DSA-65) that proves who sealed the file.
The gap: metadata nobody authenticated
Through v1.3, that JSON metadata was neither encrypted nor authenticated. The AES-256-GCM tag covered the payload, not the header around it. For most fields this fails safely — change the wrapped key and decryption simply produces garbage and aborts. One field did not fail safely: the version.
Signatures in CVEF are metadata-only — they aren't needed to decrypt the file. So an attacker with write access to a stored file could take a signed v1.3 file, strip the `signatureParams` field and change `version` from `1.3` to `1.2`. The result is a perfectly valid v1.2 file. It decrypts correctly. And it carries no signature — with no way for the recipient to know one was ever there.
This is the classic downgrade shape: don't break the crypto, just talk the reader down into a weaker mode. It's the same family as TLS signature-stripping, and it quietly undoes the non-repudiation that v1.3 signatures were meant to provide.
The fix: bind the version into the ciphertext
v1.4 closes the gap by authenticating the metadata itself. The serialized header — including the version and the signature parameters — is fed into the AEAD as Additional Authenticated Data (AAD). The GCM tag now covers both the payload and the metadata that describes it.
AES-256-GCM(
key: fileKey,
iv: iv,
plaintext: fileBytes,
aad: canonical(metadata) ← new in v1.4
)Flip a single byte of the header — downgrade the version, drop the signature, swap an algorithm name — and the tag no longer verifies. Decryption aborts before any plaintext is produced. The downgrade isn't detected after the fact; it is structurally impossible to pass off as valid.
It's the same defense Age (HMAC over the header), JWE (protected header as AAD) and COSE all use: binding the description of the ciphertext to the ciphertext is the only way to make tampering with it fail.
Backward compatibility, without the foot-gun
CVEF is versioned for exactly this reason. New files are written as v1.4; the reader still accepts v1.2, v1.3 and v1.4 so existing vaults keep opening. What it will not do is silently accept a v1.3 file whose signature has been peeled away — the AAD binding makes the version part of the proof, not a hint the reader has to trust.
Good cryptography is rarely about a cleverer algorithm. More often it's about leaving no unauthenticated surface for an attacker to lean on. v1.4 removes the last one CVEF had.