feat(keys): X25519 recipient-addressed seal primitive (@nestm/crypto/keys) - #2
Merged
Conversation
Adds the @nestm/crypto/keys entry point: X25519 keypair generation with raw<->DER conversion, an HKDF-SHA256 helper, and sealTo/openFrom/openKeyFrom (ephemeral-static X25519 -> HKDF-SHA256 -> AES-256-GCM) for wrapping an existing secret to a recipient public key. The wire format is version- and suite-tagged; the recipient public key is bound into the HKDF key schedule by the library; the nonce is derived and never transmitted; callers may bind key-schedule info and AEAD aad. Independent of DataKeyProvider/CipherEngine, so the framework-neutral core stays free of NestJS, tenant, and cloud-SDK imports. This is the upstream primitive that SEC-05 (conceptadev/concepta-artifacts#220) consumes to seal per-artifact content keys to user/org principals.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
What
Adds a new subpath entry point
@nestm/crypto/keys:generateX25519KeyPair,x25519PublicKeyFromRaw/FromRaw,x25519PublicKeyBytes/PrivateKeyBytes).hkdfSha256— an RFC 5869 HKDF-SHA256 extract-and-expand helper.sealTo/openFrom/openKeyFrom/inspectSealed— a recipient-addressed seal: ephemeral-static X25519 → HKDF-SHA256 → AES-256-GCM. It wraps an existing 32-byte secret (or anyUint8Array≤ 64 KiB) to a recipient's public key. This is the "wrap this key to a public key" primitive thatDataKeyProvidercannot express (generateDataKeymints its own key).Design
Wire format is version- and suite-tagged (
0x01 ‖ 0x01 ‖ epk(32) ‖ ct ‖ tag(16); 50-byte fixed overhead). Key schedule:info(domain separation) and AEADaad(per-message binding); a mismatch fails asAUTHENTICATION_FAILED.AUTHENTICATION_FAILED.CryptoErrorcodes only — no newCryptoErrorCode.Boundaries
DataKeyProviderandCipherEngine.@nestm/crypto/corestays free of NestJS, tenant, and cloud-SDK imports.DataKeyProvideradapter is intentionally deferred (wrong shape for per-recipient wrapping);sealTo/openFromis the primitive callers need.Tests
tests/unit/keys.test.ts(16 tests): seal/open round-trips (bytes, raw keys,KeyObject); wrong-recipient / wrong-info/ wrong-aad/ tampered-ciphertext-tag-ephemeral →AUTHENTICATION_FAILED; truncated/oversized/unknown-version/unknown-suite framing errors; non-X25519 key rejection; fresh-ephemeral-per-seal; RFC 5869 SHA-256 HKDF KATs (cases 1 and 3). Format-frozen golden vectors live in the consuming app per this repo's CONTRIBUTING (no ciphertext fixtures upstream).pnpm run verifypasses: lint, format, typecheck, 143 unit tests, build,check-package, publint, attw (./keysresolves 🟢 node16-ESM / bundler), packed consumers.Context
Upstream primitive for conceptadev/concepta-artifacts#220 (SEC-05: per-user/per-workspace content keys). SEC-05 uses
sealToto wrap a per-artifact content key to user/org principal public keys, and to a sandbox service key for the DB-less byte server.A follow-up PR will fix the Azure Key Vault provider silently dropping
wrappingContext(unrelated to this change; tracked separately to keep this PR focused).