Skip to content

Commit e3e3719

Browse files
panvaaduh95
authored andcommitted
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> PR-URL: #63988 Backport-PR-URL: #64629 Reviewed-By: Antoine du Hamel <duhamelantoine1995@gmail.com>
1 parent cedc8f4 commit e3e3719

29 files changed

Lines changed: 1389 additions & 146 deletions

doc/api/webcrypto.md

Lines changed: 21 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1924,26 +1924,39 @@ added: v24.15.0
19241924
19251925
<!-- YAML
19261926
added: v24.7.0
1927+
changes:
1928+
- version: REPLACEME
1929+
pr-url: https://github.com/nodejs/node/pull/63988
1930+
description: Named cSHAKE variants are now accepted.
19271931
-->
19281932
19291933
* Type: {ArrayBuffer|TypedArray|DataView|Buffer|undefined}
19301934
1931-
The `functionName` member represents the function name, used by NIST to define
1932-
functions based on cSHAKE.
1933-
The Node.js Web Crypto API implementation only supports zero-length functionName
1934-
which is equivalent to not providing functionName at all.
1935+
The `functionName` member represents the NIST function-name byte string used to
1936+
domain-separate functions built on top of cSHAKE. Accepted values are:
1937+
1938+
* empty or `undefined`, in which case cSHAKE is equivalent to plain SHAKE
1939+
* the ASCII byte sequence `'KMAC'`
1940+
* the ASCII byte sequence `'TupleHash'`
1941+
* the ASCII byte sequence `'ParallelHash'`
19351942
19361943
#### `cShakeParams.customization`
19371944
19381945
<!-- YAML
19391946
added: v24.7.0
1947+
changes:
1948+
- version: REPLACEME
1949+
pr-url: https://github.com/nodejs/node/pull/63988
1950+
description: Non-empty customization is now supported.
19401951
-->
19411952
19421953
* Type: {ArrayBuffer|TypedArray|DataView|Buffer|undefined}
19431954
1944-
The `customization` member represents the customization string.
1945-
The Node.js Web Crypto API implementation only supports zero-length customization
1946-
which is equivalent to not providing customization at all.
1955+
The `customization` member represents the customization data. Accepted
1956+
values are:
1957+
1958+
* empty or `undefined`, in which case cSHAKE is equivalent to plain SHAKE
1959+
* up to 512 bytes of arbitrary data
19471960
19481961
### Class: `EcdhKeyDeriveParams`
19491962
@@ -2472,9 +2485,7 @@ added: v24.8.0
24722485
added: v24.15.0
24732486
-->
24742487
2475-
* Type: {number}
2476-
2477-
The length of the output in bytes. This must be a positive integer.
2488+
* Type: {number} represents the requested output length in bits.
24782489
24792490
#### `kmacParams.customization`
24802491

lib/internal/crypto/diffiehellman.js

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

1311
const { Buffer } = require('buffer');
@@ -65,7 +63,9 @@ const {
6563
getArrayBufferOrView,
6664
jobPromise,
6765
jobPromiseThen,
66+
numBitsToBytes,
6867
toBuf,
68+
truncateToBitLength,
6969
kHandle,
7070
} = require('internal/crypto/util');
7171

@@ -365,7 +365,6 @@ function diffieHellman(options, callback) {
365365
job.run();
366366
}
367367

368-
let masks;
369368
// The ecdhDeriveBits function is part of the Web Crypto API and serves both
370369
// deriveKeys and deriveBits functions.
371370
function ecdhDeriveBits(algorithm, baseKey, length) {
@@ -409,27 +408,20 @@ function ecdhDeriveBits(algorithm, baseKey, length) {
409408
return bits;
410409

411410
return jobPromiseThen(bits, (bits) => {
412-
// If the length is not a multiple of 8 the nearest ceiled
413-
// multiple of 8 is sliced.
414-
const sliceLength = MathCeil(length / 8);
411+
const sliceLength = numBitsToBytes(length);
415412

416413
const { byteLength } = bits;
417414
// If the length is larger than the derived secret, throw.
418415
if (byteLength < sliceLength)
419416
throw lazyDOMException('derived bit length is too small', 'OperationError');
420417

421-
const slice = ArrayBufferPrototypeSlice(bits, 0, sliceLength);
422-
423-
const mod = length % 8;
424-
if (mod === 0)
425-
return slice;
426-
427-
// eslint-disable-next-line no-sparse-arrays
428-
masks ||= [, 0b10000000, 0b11000000, 0b11100000, 0b11110000, 0b11111000, 0b11111100, 0b11111110];
418+
if (length % 8 === 0) {
419+
if (byteLength === sliceLength)
420+
return bits;
421+
return ArrayBufferPrototypeSlice(bits, 0, sliceLength);
422+
}
429423

430-
const masked = new Uint8Array(slice);
431-
masked[sliceLength - 1] = masked[sliceLength - 1] & masks[mod];
432-
return TypedArrayPrototypeGetBuffer(masked);
424+
return TypedArrayPrototypeGetBuffer(truncateToBitLength(length, bits));
433425
});
434426
}
435427

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,
@@ -222,15 +227,41 @@ function asyncDigest(algorithm, data) {
222227
case 'SHA3-384':
223228
// Fall through
224229
case 'SHA3-512':
225-
// Fall through
230+
return jobPromise(() => new HashJob(
231+
kCryptoJobWebCrypto,
232+
normalizeHashName(algorithm.name),
233+
data));
226234
case 'cSHAKE128':
227235
// Fall through
228-
case 'cSHAKE256':
229-
return jobPromise(() => new HashJob(
236+
case 'cSHAKE256': {
237+
const outputLength = algorithm.outputLength;
238+
if (algorithm.functionName?.byteLength ||
239+
algorithm.customization?.byteLength) {
240+
if (CShakeJob === undefined) {
241+
throw lazyDOMException(
242+
'Non-empty CShakeParams functionName or customization is not supported',
243+
'NotSupportedError');
244+
}
245+
246+
return jobPromise(() => new CShakeJob(
247+
kCryptoJobWebCrypto,
248+
algorithm.name,
249+
data,
250+
algorithm.functionName,
251+
algorithm.customization,
252+
outputLength));
253+
}
254+
255+
const bits = jobPromise(() => new HashJob(
230256
kCryptoJobWebCrypto,
231257
normalizeHashName(algorithm.name),
232258
data,
233-
algorithm.outputLength));
259+
numBitsToBytes(outputLength) * 8));
260+
if (outputLength % 8 === 0)
261+
return bits;
262+
return jobPromiseThen(bits, (bits) =>
263+
TypedArrayPrototypeGetBuffer(truncateToBitLength(outputLength, bits)));
264+
}
234265
case 'TurboSHAKE128':
235266
// Fall through
236267
case 'TurboSHAKE256':

lib/internal/crypto/mac.js

Lines changed: 26 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,8 @@ const {
1818
getUsagesMask,
1919
jobPromise,
2020
normalizeHashName,
21+
numBitsToBytes,
22+
truncateToBitLength,
2123
} = require('internal/crypto/util');
2224

2325
const {
@@ -40,6 +42,27 @@ const {
4042

4143
const kUsages = ['sign', 'verify'];
4244

45+
function normalizeKeyLength(handle, algorithm) {
46+
let length = handle.getSymmetricKeySize() * 8;
47+
if (length === 0 && algorithm.name === 'HMAC')
48+
throw lazyDOMException('Zero-length key is not supported', 'DataError');
49+
50+
if (algorithm.length !== undefined) {
51+
const byteLength = numBitsToBytes(algorithm.length);
52+
if (byteLength !== handle.getSymmetricKeySize())
53+
throw lazyDOMException('Invalid key length', 'DataError');
54+
55+
if (algorithm.length % 8 !== 0) {
56+
handle = importSecretKey(
57+
truncateToBitLength(algorithm.length, handle.export()));
58+
}
59+
60+
length = algorithm.length;
61+
}
62+
63+
return { handle, length };
64+
}
65+
4366
function hmacGenerateKey(algorithm, extractable, usages) {
4467
const {
4568
hash,
@@ -93,7 +116,6 @@ function macImportKey(
93116
let length;
94117
switch (format) {
95118
case 'KeyObjectHandle': {
96-
length = keyData.getSymmetricKeySize() * 8;
97119
handle = keyData;
98120
break;
99121
}
@@ -102,7 +124,6 @@ function macImportKey(
102124
if (format === 'raw' && !isHmac) {
103125
return undefined;
104126
}
105-
length = keyData.byteLength * 8;
106127
handle = importSecretKey(keyData);
107128
break;
108129
}
@@ -120,20 +141,13 @@ function macImportKey(
120141
}
121142

122143
handle = importJwkSecretKey(keyData);
123-
length = handle.getSymmetricKeySize() * 8;
124144
break;
125145
}
126146
default:
127147
return undefined;
128148
}
129149

130-
if (length === 0)
131-
throw lazyDOMException('Zero-length key is not supported', 'DataError');
132-
133-
if (algorithm.length !== undefined &&
134-
algorithm.length !== length) {
135-
throw lazyDOMException('Invalid key length', 'DataError');
136-
}
150+
({ handle, length } = normalizeKeyLength(handle, algorithm)); // eslint-disable-line prefer-const
137151

138152
const algorithmObject = {
139153
name: algorithm.name,
@@ -170,7 +184,8 @@ function kmacSignVerify(key, data, algorithm, signature) {
170184
getCryptoKeyHandle(key),
171185
algorithm.name,
172186
algorithm.customization,
173-
algorithm.outputLength / 8,
187+
getCryptoKeyAlgorithm(key).length,
188+
algorithm.outputLength,
174189
data,
175190
signature));
176191
}

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,
@@ -549,6 +550,46 @@ function validateMaxBufferLength(data, name, max = kMaxBufferLength) {
549550
}
550551
}
551552

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

554595
// Keep this as a regular object. The WebIDL converters read and spread these
@@ -608,12 +649,19 @@ function normalizeAlgorithm(algorithm, op) {
608649
// 3.
609650
if (idlType === 'BufferSource' && idlValue) {
610651
const isView = ArrayBufferIsView(idlValue);
611-
normalizedAlgorithm[member] = TypedArrayPrototypeSlice(
652+
const idlValueBytes = isView ?
612653
new Uint8Array(
613-
isView ? getDataViewOrTypedArrayBuffer(idlValue) : idlValue,
614-
isView ? getDataViewOrTypedArrayByteOffset(idlValue) : 0,
615-
isView ? getDataViewOrTypedArrayByteLength(idlValue) : ArrayBufferPrototypeGetByteLength(idlValue),
616-
),
654+
getDataViewOrTypedArrayBuffer(idlValue),
655+
getDataViewOrTypedArrayByteOffset(idlValue),
656+
getDataViewOrTypedArrayByteLength(idlValue),
657+
) :
658+
new Uint8Array(
659+
idlValue,
660+
0,
661+
ArrayBufferPrototypeGetByteLength(idlValue),
662+
);
663+
normalizedAlgorithm[member] = TypedArrayPrototypeSlice(
664+
idlValueBytes,
617665
);
618666
} else if (idlType === 'HashAlgorithmIdentifier') {
619667
normalizedAlgorithm[member] = normalizeAlgorithm(idlValue, 'digest');
@@ -1003,6 +1051,8 @@ module.exports = {
10031051
cleanupWebCryptoResult,
10041052
prepareWebCryptoResult,
10051053
validateMaxBufferLength,
1054+
numBitsToBytes,
1055+
truncateToBitLength,
10061056
bigIntArrayToUnsignedBigInt,
10071057
bigIntArrayToUnsignedInt,
10081058
getBlockSize,

0 commit comments

Comments
 (0)