docs/crypto.md is now the normative, frozen specification. This had to land before
anything else in M1: the server holds ciphertext and no keys, so it can never
re-encrypt, and a format change after users hold data is a coordinated client rewrite
with no rollback.
Specification:
- DSH1 envelope layout, canonical 64-byte AAD encoding, SealTo construction, key
hierarchy, Argon2id profiles, fingerprints, and the change rules for each version field.
- AAD encoding is fixed-width binary rather than delimited string concatenation, so no
field value can forge a field boundary. This supersedes the illustrative form sketched
in ADR 0001, which now points here.
- UUIDs are RFC 4122 big-endian. Guid.ToByteArray() emits the first three groups
little-endian and would have made our ciphertext unreadable by any other implementation
of this spec, failing only at a cross-implementation boundary.
Verified rather than assumed:
- PrimitiveAvailabilityTests proves X25519, Ed25519, XChaCha20-Poly1305, Argon2id and
HKDF-SHA512 all function on net10.0. NSec 26.4.0 targets net9.0 and is consumed by
forward compatibility; this closes one of the two package questions the plan flagged.
- Argon2Profile exists because NSec's MemorySize is in KIBIBYTES, not bytes. Passing bytes
gives either a 256 GiB allocation or a 256 KiB KDF that cracks instantly. The type takes
mebibytes so the unit cannot be got wrong at a call site. Found by benchmarking: the
first measurements were ~1000x too slow, which turned out to be 19 GiB of work.
- Parameters measured, not guessed: 256 MiB/t=4 is 323 ms on this machine; the table of
candidates is in the spec.
Implementation and tests (83 total, up from 17):
- AadDescriptor, DshEnvelope, DshCrypto (Seal/Open/SealTo/OpenSealed/fingerprints).
- Decryption returns null rather than throwing: ciphertext comes from a server that is
explicitly not trusted, so a failed tag is an expected outcome.
- Envelope readers reject unknown algorithms and any non-zero flag bit, so an envelope
that is not fully understood fails closed.
- Executable form of the spec's substitution claims: a server cannot move ciphertext
between resources, roll back a key generation or item version, repurpose a payload as
metadata, or confuse the two constructions.
- Golden vectors in tests/fixtures/crypto/vectors.json guard the format. Mutation-checked:
a one-byte schema version change trips four tests including the guard.
Two build-infrastructure bugs found and fixed along the way:
- .editorconfig forced camelCase on const and static readonly fields. PascalCase is the
.NET convention for both; the config was wrong, not the code.
- The golden fixture was resolved with [CallerFilePath], which ContinuousIntegrationBuild
rewrites to /_/... under deterministic source paths. It passed locally and would have
failed only in CI. Now copied to the output directory and read from there.