Skip to content

Commit ee1069c

Browse files
committed
crypto: support non-byte WebCrypto lengths and cSHAKE
Add shared bit-length helpers for WebCrypto operations that accept bit sequences whose length is not byte-aligned. Use the helpers for cSHAKE output, ECDH-derived bits, HMAC/KMAC key generation/import/derivation, and KMAC sign/verify output. Preserve the requested bit length in CryptoKey algorithm metadata while storing and exporting rounded-up byte material with unused low bits cleared. Keep byte-multiple validation for algorithms whose specs require it. Extend the lower-end of KMAC's key length support. Enable cSHAKE customization and functionName parameters. Signed-off-by: Filip Skokan <panva.ip@gmail.com>
1 parent 8fa5954 commit ee1069c

29 files changed

Lines changed: 1386 additions & 148 deletions

doc/api/webcrypto.md

Lines changed: 21 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1933,26 +1933,39 @@ added:
19331933
19341934
<!-- YAML
19351935
added: v24.7.0
1936+
changes:
1937+
- version: REPLACEME
1938+
pr-url: https://github.com/nodejs/node/pull/63988
1939+
description: Named cSHAKE variants are now accepted.
19361940
-->
19371941
19381942
* Type: {ArrayBuffer|TypedArray|DataView|Buffer|undefined}
19391943
1940-
The `functionName` member represents the function name, used by NIST to define
1941-
functions based on cSHAKE.
1942-
The Node.js Web Crypto API implementation only supports zero-length functionName
1943-
which is equivalent to not providing functionName at all.
1944+
The `functionName` member represents the NIST function-name byte string used to
1945+
domain-separate functions built on top of cSHAKE. Accepted values are:
1946+
1947+
* empty or `undefined`, in which case cSHAKE is equivalent to plain SHAKE
1948+
* the ASCII byte sequence `'KMAC'`
1949+
* the ASCII byte sequence `'TupleHash'`
1950+
* the ASCII byte sequence `'ParallelHash'`
19441951
19451952
#### `cShakeParams.customization`
19461953
19471954
<!-- YAML
19481955
added: v24.7.0
1956+
changes:
1957+
- version: REPLACEME
1958+
pr-url: https://github.com/nodejs/node/pull/63988
1959+
description: Non-empty customization is now supported.
19491960
-->
19501961
19511962
* Type: {ArrayBuffer|TypedArray|DataView|Buffer|undefined}
19521963
1953-
The `customization` member represents the customization string.
1954-
The Node.js Web Crypto API implementation only supports zero-length customization
1955-
which is equivalent to not providing customization at all.
1964+
The `customization` member represents the customization data. Accepted
1965+
values are:
1966+
1967+
* empty or `undefined`, in which case cSHAKE is equivalent to plain SHAKE
1968+
* up to 512 bytes of arbitrary data
19561969
19571970
### Class: `EcdhKeyDeriveParams`
19581971
@@ -2489,9 +2502,7 @@ added:
24892502
- v24.15.0
24902503
-->
24912504
2492-
* Type: {number}
2493-
2494-
The length of the output in bytes. This must be a positive integer.
2505+
* Type: {number} represents the requested output length in bits.
24952506
24962507
#### `kmacParams.customization`
24972508

lib/internal/crypto/diffiehellman.js

Lines changed: 9 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -3,10 +3,8 @@
33
const {
44
ArrayBufferPrototypeSlice,
55
FunctionPrototypeCall,
6-
MathCeil,
76
ObjectDefineProperty,
87
TypedArrayPrototypeGetBuffer,
9-
Uint8Array,
108
} = primordials;
119

1210
const { Buffer } = require('buffer');
@@ -59,7 +57,9 @@ const {
5957
getArrayBufferOrView,
6058
jobPromise,
6159
jobPromiseThen,
60+
numBitsToBytes,
6261
toBuf,
62+
truncateToBitLength,
6363
kHandle,
6464
} = require('internal/crypto/util');
6565

@@ -328,7 +328,6 @@ function diffieHellman(options, callback) {
328328
job.run();
329329
}
330330

331-
let masks;
332331
// The ecdhDeriveBits function is part of the Web Crypto API and serves both
333332
// deriveKeys and deriveBits functions.
334333
function ecdhDeriveBits(algorithm, baseKey, length) {
@@ -372,27 +371,20 @@ function ecdhDeriveBits(algorithm, baseKey, length) {
372371
return bits;
373372

374373
return jobPromiseThen(bits, (bits) => {
375-
// If the length is not a multiple of 8 the nearest ceiled
376-
// multiple of 8 is sliced.
377-
const sliceLength = MathCeil(length / 8);
374+
const sliceLength = numBitsToBytes(length);
378375

379376
const { byteLength } = bits;
380377
// If the length is larger than the derived secret, throw.
381378
if (byteLength < sliceLength)
382379
throw lazyDOMException('derived bit length is too small', 'OperationError');
383380

384-
const slice = ArrayBufferPrototypeSlice(bits, 0, sliceLength);
381+
if (length % 8 === 0) {
382+
if (byteLength === sliceLength)
383+
return bits;
384+
return ArrayBufferPrototypeSlice(bits, 0, sliceLength);
385+
}
385386

386-
const mod = length % 8;
387-
if (mod === 0)
388-
return slice;
389-
390-
// eslint-disable-next-line no-sparse-arrays
391-
masks ||= [, 0b10000000, 0b11000000, 0b11100000, 0b11110000, 0b11111000, 0b11111100, 0b11111110];
392-
393-
const masked = new Uint8Array(slice);
394-
masked[sliceLength - 1] = masked[sliceLength - 1] & masks[mod];
395-
return TypedArrayPrototypeGetBuffer(masked);
387+
return TypedArrayPrototypeGetBuffer(truncateToBitLength(length, bits));
396388
});
397389
}
398390

lib/internal/crypto/hash.js

Lines changed: 35 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -6,9 +6,11 @@ const {
66
StringPrototypeReplace,
77
StringPrototypeToLowerCase,
88
Symbol,
9+
TypedArrayPrototypeGetBuffer,
910
} = primordials;
1011

1112
const {
13+
CShakeJob,
1214
Hash: _Hash,
1315
HashJob,
1416
Hmac: _Hmac,
@@ -21,7 +23,10 @@ const {
2123
const {
2224
getStringOption,
2325
jobPromise,
26+
jobPromiseThen,
2427
normalizeHashName,
28+
numBitsToBytes,
29+
truncateToBitLength,
2530
validateMaxBufferLength,
2631
kHandle,
2732
getCachedHashId,
@@ -227,15 +232,41 @@ function asyncDigest(algorithm, data) {
227232
case 'SHA3-384':
228233
// Fall through
229234
case 'SHA3-512':
230-
// Fall through
235+
return jobPromise(() => new HashJob(
236+
kCryptoJobWebCrypto,
237+
normalizeHashName(algorithm.name),
238+
data));
231239
case 'cSHAKE128':
232240
// Fall through
233-
case 'cSHAKE256':
234-
return jobPromise(() => new HashJob(
241+
case 'cSHAKE256': {
242+
const outputLength = algorithm.outputLength;
243+
if (algorithm.functionName?.byteLength ||
244+
algorithm.customization?.byteLength) {
245+
if (CShakeJob === undefined) {
246+
throw lazyDOMException(
247+
'Non-empty CShakeParams functionName or customization is not supported',
248+
'NotSupportedError');
249+
}
250+
251+
return jobPromise(() => new CShakeJob(
252+
kCryptoJobWebCrypto,
253+
algorithm.name,
254+
data,
255+
algorithm.functionName,
256+
algorithm.customization,
257+
outputLength));
258+
}
259+
260+
const bits = jobPromise(() => new HashJob(
235261
kCryptoJobWebCrypto,
236262
normalizeHashName(algorithm.name),
237263
data,
238-
algorithm.outputLength));
264+
numBitsToBytes(outputLength) * 8));
265+
if (outputLength % 8 === 0)
266+
return bits;
267+
return jobPromiseThen(bits, (bits) =>
268+
TypedArrayPrototypeGetBuffer(truncateToBitLength(outputLength, bits)));
269+
}
239270
case 'TurboSHAKE128':
240271
// Fall through
241272
case 'TurboSHAKE256':

lib/internal/crypto/mac.js

Lines changed: 26 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,8 @@ const {
2020
hasAnyNotIn,
2121
jobPromise,
2222
normalizeHashName,
23+
numBitsToBytes,
24+
truncateToBitLength,
2325
} = require('internal/crypto/util');
2426

2527
const {
@@ -38,6 +40,27 @@ const {
3840
validateJwk,
3941
} = require('internal/crypto/webcrypto_util');
4042

43+
function normalizeKeyLength(handle, algorithm) {
44+
let length = handle.getSymmetricKeySize() * 8;
45+
if (length === 0 && algorithm.name === 'HMAC')
46+
throw lazyDOMException('Zero-length key is not supported', 'DataError');
47+
48+
if (algorithm.length !== undefined) {
49+
const byteLength = numBitsToBytes(algorithm.length);
50+
if (byteLength !== handle.getSymmetricKeySize())
51+
throw lazyDOMException('Invalid key length', 'DataError');
52+
53+
if (algorithm.length % 8 !== 0) {
54+
handle = importSecretKey(
55+
truncateToBitLength(algorithm.length, handle.export()));
56+
}
57+
58+
length = algorithm.length;
59+
}
60+
61+
return { handle, length };
62+
}
63+
4164
function hmacGenerateKey(algorithm, extractable, usages) {
4265
const {
4366
hash,
@@ -113,7 +136,6 @@ function macImportKey(
113136
let length;
114137
switch (format) {
115138
case 'KeyObjectHandle': {
116-
length = keyData.getSymmetricKeySize() * 8;
117139
handle = keyData;
118140
break;
119141
}
@@ -122,7 +144,6 @@ function macImportKey(
122144
if (format === 'raw' && !isHmac) {
123145
return undefined;
124146
}
125-
length = keyData.byteLength * 8;
126147
handle = importSecretKey(keyData);
127148
break;
128149
}
@@ -140,20 +161,13 @@ function macImportKey(
140161
}
141162

142163
handle = importJwkSecretKey(keyData);
143-
length = handle.getSymmetricKeySize() * 8;
144164
break;
145165
}
146166
default:
147167
return undefined;
148168
}
149169

150-
if (length === 0)
151-
throw lazyDOMException('Zero-length key is not supported', 'DataError');
152-
153-
if (algorithm.length !== undefined &&
154-
algorithm.length !== length) {
155-
throw lazyDOMException('Invalid key length', 'DataError');
156-
}
170+
({ handle, length } = normalizeKeyLength(handle, algorithm)); // eslint-disable-line prefer-const
157171

158172
const algorithmObject = {
159173
name: algorithm.name,
@@ -190,7 +204,8 @@ function kmacSignVerify(key, data, algorithm, signature) {
190204
getCryptoKeyHandle(key),
191205
algorithm.name,
192206
algorithm.customization,
193-
algorithm.outputLength / 8,
207+
getCryptoKeyAlgorithm(key).length,
208+
algorithm.outputLength,
194209
data,
195210
signature));
196211
}

lib/internal/crypto/util.js

Lines changed: 55 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ const {
99
DataViewPrototypeGetBuffer,
1010
DataViewPrototypeGetByteLength,
1111
DataViewPrototypeGetByteOffset,
12+
MathFloor,
1213
Number,
1314
ObjectDefineProperty,
1415
ObjectEntries,
@@ -550,6 +551,46 @@ function validateMaxBufferLength(data, name, max = kMaxBufferLength) {
550551
}
551552
}
552553

554+
/**
555+
* Converts a bit length to the number of bytes needed to contain it.
556+
* Non-byte lengths are rounded up to the next byte.
557+
* @param {number} length
558+
* @returns {number}
559+
*/
560+
function numBitsToBytes(length) {
561+
return MathFloor(length / 8) + MathFloor((7 + (length % 8)) / 8);
562+
}
563+
564+
/**
565+
* Copies `bytes` up to the byte length needed for `length` bits, then clears
566+
* unused least-significant bits in the final byte.
567+
* @param {number} length
568+
* @param {ArrayBuffer|ArrayBufferView} bytes
569+
* @returns {Uint8Array}
570+
*/
571+
function truncateToBitLength(length, bytes) {
572+
const lengthBytes = numBitsToBytes(length);
573+
const isView = ArrayBufferIsView(bytes);
574+
const byteView = isView ?
575+
new Uint8Array(
576+
getDataViewOrTypedArrayBuffer(bytes),
577+
getDataViewOrTypedArrayByteOffset(bytes),
578+
getDataViewOrTypedArrayByteLength(bytes),
579+
) :
580+
new Uint8Array(bytes, 0, ArrayBufferPrototypeGetByteLength(bytes));
581+
const result = TypedArrayPrototypeSlice(
582+
byteView,
583+
0,
584+
lengthBytes,
585+
);
586+
587+
const remainder = length % 8;
588+
if (remainder !== 0)
589+
result[lengthBytes - 1] &= (0xff << (8 - remainder)) & 0xff;
590+
591+
return result;
592+
}
593+
553594
let webidl;
554595

555596
// Keep this as a regular object. The WebIDL converters read and spread these
@@ -609,12 +650,19 @@ function normalizeAlgorithm(algorithm, op) {
609650
// 3.
610651
if (idlType === 'BufferSource' && idlValue) {
611652
const isView = ArrayBufferIsView(idlValue);
612-
normalizedAlgorithm[member] = TypedArrayPrototypeSlice(
653+
const idlValueBytes = isView ?
613654
new Uint8Array(
614-
isView ? getDataViewOrTypedArrayBuffer(idlValue) : idlValue,
615-
isView ? getDataViewOrTypedArrayByteOffset(idlValue) : 0,
616-
isView ? getDataViewOrTypedArrayByteLength(idlValue) : ArrayBufferPrototypeGetByteLength(idlValue),
617-
),
655+
getDataViewOrTypedArrayBuffer(idlValue),
656+
getDataViewOrTypedArrayByteOffset(idlValue),
657+
getDataViewOrTypedArrayByteLength(idlValue),
658+
) :
659+
new Uint8Array(
660+
idlValue,
661+
0,
662+
ArrayBufferPrototypeGetByteLength(idlValue),
663+
);
664+
normalizedAlgorithm[member] = TypedArrayPrototypeSlice(
665+
idlValueBytes,
618666
);
619667
} else if (idlType === 'HashAlgorithmIdentifier') {
620668
normalizedAlgorithm[member] = normalizeAlgorithm(idlValue, 'digest');
@@ -971,6 +1019,8 @@ module.exports = {
9711019
cleanupWebCryptoResult,
9721020
prepareWebCryptoResult,
9731021
validateMaxBufferLength,
1022+
numBitsToBytes,
1023+
truncateToBitLength,
9741024
bigIntArrayToUnsignedBigInt,
9751025
bigIntArrayToUnsignedInt,
9761026
getBlockSize,

0 commit comments

Comments
 (0)