From be725e26d84d201a1850e7d8489659d8dba3287c Mon Sep 17 00:00:00 2001 From: Hasko Date: Wed, 10 Jun 2026 18:24:55 +0200 Subject: [PATCH] =?UTF-8?q?feat(crypto):=20=E2=9C=A8=20Raise=20symmetric?= =?UTF-8?q?=20payload=20limit?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- README.md | 5 +++-- TECHNICAL_DOCUMENTATION.md | 4 ++-- src/value-objects/crypto/PrivateKey.ts | 6 +++--- src/value-objects/crypto/StrictBase64.ts | 2 +- src/value-objects/crypto/SymmetricKey.ts | 8 ++++---- tests/value-objects/crypto/SymmetricKey.spec.ts | 12 ++++++++++-- 6 files changed, 23 insertions(+), 14 deletions(-) diff --git a/README.md b/README.md index d8855e2..ec515c9 100644 --- a/README.md +++ b/README.md @@ -125,8 +125,9 @@ Technical notes: either a high-entropy random key or a strong password with a unique, non-empty salt. AES-GCM also requires that IVs do not repeat for the same key; the library generates a random 96-bit IV for each encryption. -- Payload encryption is intended for small payloads and is currently capped at - 1 MiB before encryption. +- Asymmetric payload encryption is intended for small payloads and is currently + capped at 1 MiB before encryption. Symmetric payload encryption is capped at + 8 MiB before encryption. - New encrypted private keys use `v3.scrypt.N16384.r8.p5` with a 16-byte salt, then AES-256-GCM with a 12-byte IV and 16-byte authentication tag. The older v2 scrypt profile and legacy PBKDF2 format still decrypt, and diff --git a/TECHNICAL_DOCUMENTATION.md b/TECHNICAL_DOCUMENTATION.md index 9693b65..4f957c8 100644 --- a/TECHNICAL_DOCUMENTATION.md +++ b/TECHNICAL_DOCUMENTATION.md @@ -1056,7 +1056,7 @@ console.log(decrypted.toString()); // 'hello world' 2. `encrypt()` generates a fresh 12-byte random IV 3. AES-256-GCM encrypts the payload, authenticates `v1.aes-256-gcm` as AAD by default, and produces a 16-byte authentication tag 4. The output is `v1.aes-256-gcm.iv.cipherText.tag` with Base64-encoded IV, ciphertext, and tag fields -5. `decrypt()` validates the version, algorithm, IV length, tag length, Base64 fields, and 1 MiB ciphertext limit before decrypting +5. `decrypt()` validates the version, algorithm, IV length, tag length, Base64 fields, and 8 MiB ciphertext limit before decrypting **Random key:** ```typescript @@ -1094,7 +1094,7 @@ const decrypted = key.decrypt(encrypted, { aad: 'orders.v1' }); - `decrypt()` falls back to no-AAD decryption only when no custom AAD is supplied, so symmetric payloads created before header AAD support remain readable. - AES-GCM requires IV uniqueness for a given key. The library generates a random IV for each encryption; avoid manually reusing serialized payload internals as new encryption inputs. - Symmetric payload encryption provides confidentiality and ciphertext integrity for holders of the same key. It does not identify which holder encrypted the payload. It is not a post-quantum cryptography scheme; AES-256 is considered to retain a large security margin against Grover-style quadratic speedups, but the KDF and password entropy still matter. -- Symmetric payload encryption is capped at 1 MiB before encryption. +- Symmetric payload encryption is capped at 8 MiB before encryption. #### CryptoPayload diff --git a/src/value-objects/crypto/PrivateKey.ts b/src/value-objects/crypto/PrivateKey.ts index fd25353..a8bed43 100644 --- a/src/value-objects/crypto/PrivateKey.ts +++ b/src/value-objects/crypto/PrivateKey.ts @@ -103,9 +103,6 @@ export class PrivateKey extends Key { tagB64: string, options: { useHkdf: boolean; aad?: Buffer }, ): Buffer { - PrivateKey.ensureIsBase64(cipherTextB64, encryptedPayload, { - allowEmpty: true, - }); const cipherTextLength = StrictBase64.getDecodedLength(cipherTextB64); assert( cipherTextLength <= PrivateKey.MAX_CIPHERTEXT_LENGTH, @@ -114,6 +111,9 @@ export class PrivateKey extends Key { PrivateKey.MAX_CIPHERTEXT_LENGTH, ), ); + PrivateKey.ensureIsBase64(cipherTextB64, encryptedPayload, { + allowEmpty: true, + }); PrivateKey.ensureBase64DecodedLength( ephPubB64, diff --git a/src/value-objects/crypto/StrictBase64.ts b/src/value-objects/crypto/StrictBase64.ts index 0dece97..3dac2c6 100644 --- a/src/value-objects/crypto/StrictBase64.ts +++ b/src/value-objects/crypto/StrictBase64.ts @@ -35,8 +35,8 @@ export class StrictBase64 { length: number, options: StrictBase64Options = {}, ): void { - StrictBase64.ensure(value, error, options); assert(StrictBase64.getDecodedLength(value) === length, error); + StrictBase64.ensure(value, error, options); } public static decode( diff --git a/src/value-objects/crypto/SymmetricKey.ts b/src/value-objects/crypto/SymmetricKey.ts index 229feb3..00b835c 100644 --- a/src/value-objects/crypto/SymmetricKey.ts +++ b/src/value-objects/crypto/SymmetricKey.ts @@ -32,7 +32,7 @@ export class SymmetricKey extends ValueObject { private static readonly ALGORITHM = 'aes-256-gcm'; private static readonly IV_LENGTH = 12; private static readonly KEY_LENGTH = 32; - private static readonly MAX_PAYLOAD_LENGTH = 1024 * 1024; + private static readonly MAX_PAYLOAD_LENGTH = 8 * 1024 * 1024; private static readonly PAYLOAD_PARTS = 5; private static readonly SCRYPT_N = 16384; private static readonly SCRYPT_P = 1; @@ -230,14 +230,14 @@ export class SymmetricKey extends ValueObject { encryptedPayload, SymmetricKey.IV_LENGTH, ); - SymmetricKey.ensureIsBase64(cipherTextB64, encryptedPayload, { - allowEmpty: true, - }); const cipherTextLength = StrictBase64.getDecodedLength(cipherTextB64); assert( cipherTextLength <= SymmetricKey.MAX_PAYLOAD_LENGTH, new InvalidLengthError(cipherTextLength, SymmetricKey.MAX_PAYLOAD_LENGTH), ); + SymmetricKey.ensureIsBase64(cipherTextB64, encryptedPayload, { + allowEmpty: true, + }); SymmetricKey.ensureBase64DecodedLength( tagB64, encryptedPayload, diff --git a/tests/value-objects/crypto/SymmetricKey.spec.ts b/tests/value-objects/crypto/SymmetricKey.spec.ts index 0995e55..bd23c9a 100644 --- a/tests/value-objects/crypto/SymmetricKey.spec.ts +++ b/tests/value-objects/crypto/SymmetricKey.spec.ts @@ -226,10 +226,18 @@ describe('SymmetricKey', () => { expect(key.decrypt(encrypted)).toHaveLength(0); }); + it('should encrypt and decrypt payloads larger than the asymmetric limit', () => { + const key = new SymmetricKey(keyBase64); + const payload = Buffer.alloc(1024 * 1024 + 1, 7); + const encrypted = key.encrypt(payload); + + expect(key.decrypt(encrypted)).toEqual(payload); + }); + it('should throw InvalidLengthError for oversized payloads', () => { const key = new SymmetricKey(keyBase64); - expect(() => key.encrypt(Buffer.alloc(1024 * 1024 + 1))).toThrow( + expect(() => key.encrypt(Buffer.alloc(8 * 1024 * 1024 + 1))).toThrow( InvalidLengthError, ); }); @@ -387,7 +395,7 @@ describe('SymmetricKey', () => { it('should throw InvalidLengthError for oversized ciphertext', () => { const key = new SymmetricKey(keyBase64); const oversizedCipher = 'A'.repeat( - Math.ceil((1024 * 1024 + 1) / 3) * 4, + Math.ceil((8 * 1024 * 1024 + 1) / 3) * 4, ); const payload = [ 'v1',