Skip to content

Commit babb6c6

Browse files
authored
fix(aes128): validate PKCS#7 padding in decryptBufferCBC (#22179)
## Summary - `decryptBufferCBC` previously stripped padding by blindly trusting the last byte value with no validation - A corrupt or maliciously crafted ciphertext could silently produce wrong plaintext (wrong number of bytes stripped) instead of an error - Adds full PKCS#7 validation: padding length must be in [1, 16] and all padding bytes must equal the declared length Fixes [A-767](https://linear.app/aztec-labs/issue/A-767/audit-101-aes-128-decryption-has-no-pkcs7-padding-validation-padding) Made with [Cursor](https://cursor.com)
1 parent bd63929 commit babb6c6

2 files changed

Lines changed: 61 additions & 21 deletions

File tree

yarn-project/foundation/src/crypto/aes128/index.test.ts

Lines changed: 50 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -18,47 +18,78 @@ describe('aes128', () => {
1818
return Buffer.concat([data, paddingBuffer]);
1919
};
2020

21-
// PKCS#7 padding removal
22-
const removePadding = (paddedBuffer: Buffer): Buffer => {
23-
// We get padding length from the last byte - in PKCS#7 all the padded bytes contain padding length
24-
// and there is always some padding.
25-
const paddingToRemove = paddedBuffer[paddedBuffer.length - 1];
26-
return paddedBuffer.subarray(0, paddedBuffer.length - paddingToRemove);
21+
// Encrypt data with Node's built-in AES-128-CBC
22+
const encrypt = (data: Buffer, key: Buffer, iv: Buffer): Buffer => {
23+
const cipher = createCipheriv('aes-128-cbc', key, iv);
24+
cipher.setAutoPadding(false);
25+
return Buffer.concat([cipher.update(pad(data)), cipher.final()]);
2726
};
2827

2928
it('should correctly encrypt input', async () => {
3029
const data = randomBytes(32);
3130
const key = randomBytes(16);
3231
const iv = randomBytes(16);
3332

34-
const paddedData = pad(data);
35-
36-
const cipher = createCipheriv('aes-128-cbc', key, iv);
37-
cipher.setAutoPadding(false);
38-
const expected = Buffer.concat([cipher.update(paddedData), cipher.final()]);
39-
4033
const result: Buffer = await aes128.encryptBufferCBC(data, iv, key);
4134

42-
expect(result).toEqual(expected);
35+
expect(result).toEqual(encrypt(data, key, iv));
4336
});
4437

4538
it('should correctly decrypt input', async () => {
4639
const data = randomBytes(32);
4740
const key = randomBytes(16);
4841
const iv = randomBytes(16);
4942

50-
const paddedData = pad(data);
51-
52-
const cipher = createCipheriv('aes-128-cbc', key, iv);
53-
cipher.setAutoPadding(false);
54-
const ciphertext = Buffer.concat([cipher.update(paddedData), cipher.final()]);
43+
const ciphertext = encrypt(data, key, iv);
5544

5645
const decipher = createDecipheriv('aes-128-cbc', key, iv);
5746
decipher.setAutoPadding(false);
58-
const expected = removePadding(Buffer.concat([decipher.update(ciphertext), decipher.final()]));
47+
const decrypted = Buffer.concat([decipher.update(ciphertext), decipher.final()]);
48+
const expected = decrypted.subarray(0, decrypted.length - decrypted[decrypted.length - 1]);
5949

6050
const result: Buffer = await aes128.decryptBufferCBC(ciphertext, iv, key);
6151

6252
expect(result).toEqual(expected);
6353
});
54+
55+
it('should throw on invalid PKCS#7 padding length (0)', async () => {
56+
const key = randomBytes(16);
57+
const iv = randomBytes(16);
58+
// Craft a ciphertext whose last plaintext byte decrypts to 0x00 (invalid padding length).
59+
// We encrypt a block where the last byte is 0x00 using raw (no-padding) encryption.
60+
const plaintext = Buffer.alloc(16, 0x00);
61+
const cipher = createCipheriv('aes-128-cbc', key, iv);
62+
cipher.setAutoPadding(false);
63+
const ciphertext = Buffer.concat([cipher.update(plaintext), cipher.final()]);
64+
65+
await expect(aes128.decryptBufferCBC(ciphertext, iv, key)).rejects.toThrow('Invalid PKCS#7 padding length: 0');
66+
});
67+
68+
it('should throw on invalid PKCS#7 padding length (> 16)', async () => {
69+
const key = randomBytes(16);
70+
const iv = randomBytes(16);
71+
// Craft a ciphertext whose last plaintext byte is 0x11 = 17, which exceeds the block size.
72+
const plaintext = Buffer.alloc(16, 0x11);
73+
const cipher = createCipheriv('aes-128-cbc', key, iv);
74+
cipher.setAutoPadding(false);
75+
const ciphertext = Buffer.concat([cipher.update(plaintext), cipher.final()]);
76+
77+
await expect(aes128.decryptBufferCBC(ciphertext, iv, key)).rejects.toThrow('Invalid PKCS#7 padding length: 17');
78+
});
79+
80+
it('should throw when padding bytes are inconsistent', async () => {
81+
const key = randomBytes(16);
82+
const iv = randomBytes(16);
83+
// Last byte says padding length is 4, but the 3 bytes before it are not 0x04.
84+
const plaintext = Buffer.alloc(16, 0x00);
85+
plaintext[15] = 0x04;
86+
plaintext[14] = 0x04;
87+
plaintext[13] = 0x04;
88+
plaintext[12] = 0x01; // should be 0x04 — inconsistent
89+
const cipher = createCipheriv('aes-128-cbc', key, iv);
90+
cipher.setAutoPadding(false);
91+
const ciphertext = Buffer.concat([cipher.update(plaintext), cipher.final()]);
92+
93+
await expect(aes128.decryptBufferCBC(ciphertext, iv, key)).rejects.toThrow('Invalid PKCS#7 padding');
94+
});
6495
});

yarn-project/foundation/src/crypto/aes128/index.ts

Lines changed: 11 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -59,10 +59,19 @@ export class Aes128 {
5959
* @param iv - AES initialization vector.
6060
* @param key - Key to decrypt with.
6161
* @returns Decrypted data.
62+
* @throws If the decrypted buffer has invalid PKCS#7 padding.
6263
*/
6364
public async decryptBufferCBC(data: Uint8Array, iv: Uint8Array, key: Uint8Array) {
6465
const paddedBuffer = await this.decryptBufferCBCKeepPadding(data, iv, key);
65-
const paddingToRemove = paddedBuffer[paddedBuffer.length - 1];
66-
return paddedBuffer.subarray(0, paddedBuffer.length - paddingToRemove);
66+
const paddingLen = paddedBuffer[paddedBuffer.length - 1];
67+
if (paddingLen === 0 || paddingLen > 16) {
68+
throw new Error(`Invalid PKCS#7 padding length: ${paddingLen}`);
69+
}
70+
for (let i = paddedBuffer.length - paddingLen; i < paddedBuffer.length; i++) {
71+
if (paddedBuffer[i] !== paddingLen) {
72+
throw new Error('Invalid PKCS#7 padding');
73+
}
74+
}
75+
return paddedBuffer.subarray(0, paddedBuffer.length - paddingLen);
6776
}
6877
}

0 commit comments

Comments
 (0)