Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 3 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
4 changes: 2 additions & 2 deletions TECHNICAL_DOCUMENTATION.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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

Expand Down
6 changes: 3 additions & 3 deletions src/value-objects/crypto/PrivateKey.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand All @@ -114,6 +111,9 @@ export class PrivateKey extends Key {
PrivateKey.MAX_CIPHERTEXT_LENGTH,
),
);
PrivateKey.ensureIsBase64(cipherTextB64, encryptedPayload, {
allowEmpty: true,
});

PrivateKey.ensureBase64DecodedLength(
ephPubB64,
Expand Down
2 changes: 1 addition & 1 deletion src/value-objects/crypto/StrictBase64.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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(
Expand Down
8 changes: 4 additions & 4 deletions src/value-objects/crypto/SymmetricKey.ts
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@ export class SymmetricKey extends ValueObject<string> {
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;
Expand Down Expand Up @@ -230,14 +230,14 @@ export class SymmetricKey extends ValueObject<string> {
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,
Expand Down
12 changes: 10 additions & 2 deletions tests/value-objects/crypto/SymmetricKey.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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,
);
});
Expand Down Expand Up @@ -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',
Expand Down
Loading