Skip to content
5 changes: 5 additions & 0 deletions packages/cryptography/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,11 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

## [Unreleased]

### Added

- Add an `additionalData` option to AES-GCM `encrypt` and `decrypt` ([#10628](https://github.com/MetaMask/core/pull/10628))
- Allow HKDF functions to derive from an empty input when `unsafeInputKeyingMaterial` is set ([#10628](https://github.com/MetaMask/core/pull/10628))

### Fixed

- Narrow return type for AES `encrypt` function to `Uint8Array<ArrayBuffer>` ([#10623](https://github.com/MetaMask/core/pull/10623))
Expand Down
66 changes: 66 additions & 0 deletions packages/cryptography/src/aes-gcm.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,19 @@ const nistCiphertext15 = hexToBytes(
'0x522dc1f099567d07f47f37a32a84427d643a8cdcbfe5c0c97598a2bd2555d1aa8cb08e48590dbb3da7b08b1056828838c5f61e6393ba7a0abcc9f662898015adb094dac5d93471bdec1a502270e3cc6c',
);

// Test Case 4: 128-bit key with associated data
const nistKey4 = hexToBytes('0xfeffe9928665731c6d6a8f9467308308');
const nistIv4 = hexToBytes('0xcafebabefacedbaddecaf888');
const nistPlaintext4 = hexToBytes(
'0xd9313225f88406e5a55909c5aff5269a86a7a9531534f7da2e4c303d8a318a721c3c0c95956809532fcf0e2449a6b525b16aedf5aa0de657ba637b39',
);
const nistAdditionalData4 = hexToBytes(
'0xfeedfacedeadbeeffeedfacedeadbeefabaddad2',
);
const nistCiphertext4 = hexToBytes(
'0x42831ec2217774244b7221b784d0d49ce3aa212f2c02a4e035c17e2329aca12e21d514b25466931c7d8f6a5aac84aa051ba30b396a0aac973d58e0915bc94fbc3221a5db94fae95ae7121a47',
);

describe('encrypt', () => {
it('matches test case 2', async () => {
const result = await encrypt(nistKey2, nistPlaintext2, {
Expand Down Expand Up @@ -96,6 +109,30 @@ describe('encrypt', () => {
'Unsafe IV length: IV must be exactly 12 bytes for AES-GCM. To bypass this check, set the `unsafeIvLength` option to `true`.',
);
});

it('matches test case 4 with associated data', async () => {
const result = await encrypt(nistKey4, nistPlaintext4, {
unsafeIv: nistIv4,
additionalData: nistAdditionalData4,
});
expect(bytesToHex(result.ciphertext)).toBe(bytesToHex(nistCiphertext4));
});

it('accepts an ArrayBuffer of associated data', async () => {
const result = await encrypt(nistKey4, nistPlaintext4, {
unsafeIv: nistIv4,
additionalData: nistAdditionalData4.buffer,
});
expect(bytesToHex(result.ciphertext)).toBe(bytesToHex(nistCiphertext4));
});

it('accepts a DataView of associated data', async () => {
const result = await encrypt(nistKey4, nistPlaintext4, {
unsafeIv: nistIv4,
additionalData: new DataView(nistAdditionalData4.buffer),
});
expect(bytesToHex(result.ciphertext)).toBe(bytesToHex(nistCiphertext4));
});
});

describe('decrypt', () => {
Expand Down Expand Up @@ -150,4 +187,33 @@ describe('decrypt', () => {
'Unsafe IV length: IV must be exactly 12 bytes for AES-GCM. To bypass this check, set the `unsafeIvLength` option to `true`.',
);
});

it('matches test case 4 with associated data', async () => {
Comment thread
matthiasgeihs marked this conversation as resolved.
const result = await decrypt(nistKey4, nistIv4, nistCiphertext4, {
additionalData: nistAdditionalData4,
});
expect(bytesToHex(result)).toBe(bytesToHex(nistPlaintext4));
});

it('accepts an ArrayBuffer of associated data', async () => {
const result = await decrypt(nistKey4, nistIv4, nistCiphertext4, {
additionalData: nistAdditionalData4.buffer,
});
expect(bytesToHex(result)).toBe(bytesToHex(nistPlaintext4));
});

it('accepts a DataView of associated data', async () => {
const result = await decrypt(nistKey4, nistIv4, nistCiphertext4, {
additionalData: new DataView(nistAdditionalData4.buffer),
});
expect(bytesToHex(result)).toBe(bytesToHex(nistPlaintext4));
});

it('rejects ciphertext authenticated with different associated data', async () => {
await expect(
decrypt(nistKey4, nistIv4, nistCiphertext4, {
additionalData: new Uint8Array(nistAdditionalData4.length),
}),
).rejects.toThrow(DOMException);
});
});
20 changes: 18 additions & 2 deletions packages/cryptography/src/aes-gcm.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,12 @@ export type AesGcmEncryptOptions = {
} & AesGcmDecryptOptions;

export type AesGcmDecryptOptions = {
/**
* Associated data authenticated with the ciphertext but not encrypted.
* Encryption and decryption must use the same value.
*/
additionalData?: BufferSource;

/**
* Skip the IV length check. Using an IV other than 12 bytes deviates from
* the standard and is considered less safe.
Expand Down Expand Up @@ -71,7 +77,12 @@ export async function encrypt(

const ciphertext = await globalThis.crypto.subtle.encrypt(
// Converting to Uint8Array to work around a Node 22 bug, we may be able to remove in the future.
{ name: 'AES-GCM', iv },
{
name: 'AES-GCM',
iv,
additionalData:
options?.additionalData && toUint8Array(options.additionalData),
},
subtleKey,
toUint8Array(plaintext),
);
Expand Down Expand Up @@ -122,7 +133,12 @@ export async function decrypt(

const plaintext = await globalThis.crypto.subtle.decrypt(
// Converting to Uint8Array to work around a Node 22 bug, we may be able to remove in the future.
{ name: 'AES-GCM', iv: toUint8Array(iv) },
{
name: 'AES-GCM',
iv: toUint8Array(iv),
additionalData:
options?.additionalData && toUint8Array(options.additionalData),
},
subtleKey,
toUint8Array(ciphertext),
);
Expand Down
21 changes: 18 additions & 3 deletions packages/cryptography/src/hkdf.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -69,7 +69,22 @@ describe('hkdfSha256', () => {

it('throws if the IKM is empty', async () => {
await expect(hkdfSha256(new Uint8Array(0), salt, info, 32)).rejects.toThrow(
'Unsafe input keying material length: IKM must not be zero bytes for HKDF-SHA-256.',
'Unsafe input keying material length: IKM must not be zero bytes for HKDF-SHA-256. To bypass this check, set the `unsafeInputKeyingMaterial` option to `true`.',
);
});

it('derives key material from an empty IKM when the length check is skipped', async () => {
// HKDF-SHA-256, 32-byte salt of 0x01, empty IKM and info, 64-byte output.
// Matches Node crypto.hkdfSync and the Noise Split input shape.
const key = await hkdfSha256(
new Uint8Array(0),
new Uint8Array(32).fill(1),
new Uint8Array(0),
64,
{ unsafeInputKeyingMaterial: true },
);
expect(bytesToHex(key)).toBe(
'0xb5a5789af1d00c74773ef327dc63f0f11c7041252d4916c43a252b92e3358ad272d7e204fba7b0ac19964ebcba5e88e3609cb8c3cafa4b940945df49da83680f',
);
});
});
Expand Down Expand Up @@ -129,7 +144,7 @@ describe('hkdfSha384', () => {

it('throws if the IKM is empty', async () => {
await expect(hkdfSha384(new Uint8Array(0), salt, info, 48)).rejects.toThrow(
'Unsafe input keying material length: IKM must not be zero bytes for HKDF-SHA-384.',
'Unsafe input keying material length: IKM must not be zero bytes for HKDF-SHA-384. To bypass this check, set the `unsafeInputKeyingMaterial` option to `true`.',
);
});
});
Expand Down Expand Up @@ -189,7 +204,7 @@ describe('hkdfSha512', () => {

it('throws if the IKM is empty', async () => {
await expect(hkdfSha512(new Uint8Array(0), salt, info, 64)).rejects.toThrow(
'Unsafe input keying material length: IKM must not be zero bytes for HKDF-SHA-512.',
'Unsafe input keying material length: IKM must not be zero bytes for HKDF-SHA-512. To bypass this check, set the `unsafeInputKeyingMaterial` option to `true`.',
);
});
});
4 changes: 2 additions & 2 deletions packages/cryptography/src/hkdf.ts
Original file line number Diff line number Diff line change
Expand Up @@ -89,9 +89,9 @@ async function hkdf(
keyLength: number,
options: HkdfOptions = {},
): Promise<Uint8Array<ArrayBuffer>> {
if (ikm.byteLength === 0) {
if (ikm.byteLength === 0 && !options.unsafeInputKeyingMaterial) {
Comment thread
FrederikBolding marked this conversation as resolved.
throw new Error(
`Unsafe input keying material length: IKM must not be zero bytes for HKDF-${hash}.`,
`Unsafe input keying material length: IKM must not be zero bytes for HKDF-${hash}. To bypass this check, set the \`unsafeInputKeyingMaterial\` option to \`true\`.`,
);
}

Expand Down
Loading