Skip to content

Commit 8c7346a

Browse files
panvanodejs-github-bot
authored andcommitted
crypto: reject short AES-KW inputs
Enforce the minimum key-wrap input lengths before the empty-input shortcut, including the integrity-check block when unwrapping. Signed-off-by: Filip Skokan <panva.ip@gmail.com> Assisted-by: Codex PR-URL: #66237 Reviewed-By: James M Snell <jasnell@gmail.com> Reviewed-By: Aviv Keller <me@aviv.sh>
1 parent f0811ca commit 8c7346a

2 files changed

Lines changed: 56 additions & 2 deletions

File tree

‎src/crypto/crypto_aes.cc‎

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,13 @@ WebCryptoCipherStatus AES_Cipher(Environment* env,
4747
ByteSource* out) {
4848
CHECK_EQ(key_data.GetKeyType(), kKeyTypeSecret);
4949

50+
const bool encrypt = cipher_mode == kWebCryptoCipherEncrypt;
51+
// AES-KW requires at least two 64-bit plaintext blocks, plus the
52+
// 64-bit integrity check value when unwrapping.
53+
if (params.cipher.isWrapMode() && in.size() < (encrypt ? 16u : 24u)) {
54+
return WebCryptoCipherStatus::FAILED;
55+
}
56+
5057
auto ctx = CipherCtxPointer::New();
5158
if (!ctx) {
5259
return WebCryptoCipherStatus::FAILED;
@@ -56,8 +63,6 @@ WebCryptoCipherStatus AES_Cipher(Environment* env,
5663
ctx.setAllowWrap();
5764
}
5865

59-
const bool encrypt = cipher_mode == kWebCryptoCipherEncrypt;
60-
6166
if (!ctx.init(params.cipher, encrypt)) {
6267
// Cipher init failed
6368
return WebCryptoCipherStatus::FAILED;
Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
1+
'use strict';
2+
3+
const common = require('../common');
4+
5+
if (!common.hasCrypto)
6+
common.skip('missing crypto');
7+
8+
const assert = require('assert');
9+
const { getFips } = require('crypto');
10+
const { hasOpenSSL } = require('../common/crypto');
11+
const { subtle } = globalThis.crypto;
12+
13+
(async () => {
14+
const keyToWrap = await subtle.importKey(
15+
'raw', new Uint8Array(16), 'AES-GCM', true, ['encrypt']);
16+
let emptyKey;
17+
if (hasOpenSSL(3) && getFips() !== 1) {
18+
emptyKey = await subtle.importKey(
19+
'raw-secret', new Uint8Array(0), 'KMAC128', true, ['sign']);
20+
}
21+
22+
for (const length of [128, 192, 256]) {
23+
const wrappingKey = await subtle.generateKey(
24+
{ name: 'AES-KW', length }, false, ['wrapKey', 'unwrapKey']);
25+
26+
for (const byteLength of [0, 8, 16, 23]) {
27+
// HKDF accepts an empty key, so the unwrap operation must reject
28+
// before attempting to import the plaintext as a key.
29+
await assert.rejects(subtle.unwrapKey(
30+
'raw', new Uint8Array(byteLength), wrappingKey, 'AES-KW',
31+
'HKDF', false, ['deriveBits']), { name: 'OperationError' });
32+
}
33+
34+
if (emptyKey !== undefined) {
35+
await assert.rejects(subtle.wrapKey(
36+
'raw-secret', emptyKey, wrappingKey, 'AES-KW'),
37+
{ name: 'OperationError' });
38+
}
39+
40+
const wrapped = await subtle.wrapKey(
41+
'raw', keyToWrap, wrappingKey, 'AES-KW');
42+
assert.strictEqual(wrapped.byteLength, 24);
43+
const unwrapped = await subtle.unwrapKey(
44+
'raw', wrapped, wrappingKey, 'AES-KW', 'AES-GCM', true, ['encrypt']);
45+
assert.deepStrictEqual(
46+
new Uint8Array(await subtle.exportKey('raw', unwrapped)),
47+
new Uint8Array(16));
48+
}
49+
})().then(common.mustCall());

0 commit comments

Comments
 (0)