Skip to content

Commit 348cf8c

Browse files
committed
crypto: support non-byte WebCrypto lengths
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. Signed-off-by: Filip Skokan <panva.ip@gmail.com>
1 parent d1ff9a0 commit 348cf8c

25 files changed

Lines changed: 705 additions & 116 deletions

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: 17 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ const {
66
StringPrototypeReplace,
77
StringPrototypeToLowerCase,
88
Symbol,
9+
TypedArrayPrototypeGetBuffer,
910
} = primordials;
1011

1112
const {
@@ -21,7 +22,10 @@ const {
2122
const {
2223
getStringOption,
2324
jobPromise,
25+
jobPromiseThen,
2426
normalizeHashName,
27+
numBitsToBytes,
28+
truncateToBitLength,
2529
validateMaxBufferLength,
2630
kHandle,
2731
getCachedHashId,
@@ -227,15 +231,24 @@ function asyncDigest(algorithm, data) {
227231
case 'SHA3-384':
228232
// Fall through
229233
case 'SHA3-512':
230-
// Fall through
234+
return jobPromise(() => new HashJob(
235+
kCryptoJobWebCrypto,
236+
normalizeHashName(algorithm.name),
237+
data));
231238
case 'cSHAKE128':
232239
// Fall through
233-
case 'cSHAKE256':
234-
return jobPromise(() => new HashJob(
240+
case 'cSHAKE256': {
241+
const outputLength = algorithm.outputLength;
242+
const bits = jobPromise(() => new HashJob(
235243
kCryptoJobWebCrypto,
236244
normalizeHashName(algorithm.name),
237245
data,
238-
algorithm.outputLength));
246+
numBitsToBytes(outputLength) * 8));
247+
if (outputLength % 8 === 0)
248+
return bits;
249+
return jobPromiseThen(bits, (bits) =>
250+
TypedArrayPrototypeGetBuffer(truncateToBitLength(outputLength, bits)));
251+
}
239252
case 'TurboSHAKE128':
240253
// Fall through
241254
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: 42 additions & 0 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,45 @@ 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 result = TypedArrayPrototypeSlice(
575+
new Uint8Array(
576+
isView ? getDataViewOrTypedArrayBuffer(bytes) : bytes,
577+
isView ? getDataViewOrTypedArrayByteOffset(bytes) : 0,
578+
isView ?
579+
getDataViewOrTypedArrayByteLength(bytes) :
580+
ArrayBufferPrototypeGetByteLength(bytes),
581+
),
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+
553593
let webidl;
554594

555595
// Keep this as a regular object. The WebIDL converters read and spread these
@@ -971,6 +1011,8 @@ module.exports = {
9711011
cleanupWebCryptoResult,
9721012
prepareWebCryptoResult,
9731013
validateMaxBufferLength,
1014+
numBitsToBytes,
1015+
truncateToBitLength,
9741016
bigIntArrayToUnsignedBigInt,
9751017
bigIntArrayToUnsignedInt,
9761018
getBlockSize,

lib/internal/crypto/webcrypto.js

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -65,6 +65,7 @@ const {
6565
jobPromiseThen,
6666
normalizeAlgorithm,
6767
normalizeHashName,
68+
numBitsToBytes,
6869
prepareWebCryptoResult,
6970
validateMaxBufferLength,
7071
} = require('internal/crypto/util');
@@ -1914,7 +1915,7 @@ class SubtleCrypto {
19141915
case 'KMAC128':
19151916
case 'KMAC256':
19161917
if (normalizedAdditionalAlgorithm.length === undefined ||
1917-
normalizedAdditionalAlgorithm.length === 256) {
1918+
numBitsToBytes(normalizedAdditionalAlgorithm.length) === 32) {
19181919
break;
19191920
}
19201921
return false;

lib/internal/crypto/webidl.js

Lines changed: 9 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -269,6 +269,13 @@ converters.EcdsaParams = createDictionaryConverter(
269269
],
270270
]);
271271

272+
function validateHmacKeyLength(parameterName, zeroError) {
273+
return (V) => {
274+
if (V === 0)
275+
throw lazyDOMException(`${parameterName} cannot be 0`, zeroError);
276+
};
277+
}
278+
272279
for (const { 0: name, 1: zeroError } of [['HmacKeyGenParams', 'OperationError'], ['HmacImportParams', 'DataError']]) {
273280
converters[name] = createDictionaryConverter(
274281
name, [
@@ -284,7 +291,7 @@ for (const { 0: name, 1: zeroError } of [['HmacKeyGenParams', 'OperationError'],
284291
key: 'length',
285292
converter: (V, opts) =>
286293
converters['unsigned long'](V, enforceRangeOptions(opts)),
287-
validator: validateMacKeyLength(`${name}.length`, zeroError),
294+
validator: validateHmacKeyLength(`${name}.length`, zeroError),
288295
},
289296
],
290297
]);
@@ -366,12 +373,6 @@ converters.CShakeParams = createDictionaryConverter(
366373
key: 'outputLength',
367374
converter: (V, opts) =>
368375
converters['unsigned long'](V, enforceRangeOptions(opts)),
369-
validator: (V, opts) => {
370-
// The Web Crypto spec allows for SHAKE output length that are not multiples of
371-
// 8. We don't.
372-
if (V % 8)
373-
throw lazyDOMException('Unsupported CShakeParams outputLength', 'NotSupportedError');
374-
},
375376
required: true,
376377
},
377378
{
@@ -656,18 +657,7 @@ converters.Argon2Params = createDictionaryConverter(
656657
],
657658
]);
658659

659-
function validateMacKeyLength(parameterName, zeroError) {
660-
return (V) => {
661-
if (V === 0)
662-
throw lazyDOMException(`${parameterName} cannot be 0`, zeroError);
663-
664-
// The Web Crypto spec allows for key lengths that are not multiples of 8. We don't.
665-
if (V % 8)
666-
throw lazyDOMException(`Unsupported ${parameterName}`, 'NotSupportedError');
667-
};
668-
}
669-
670-
for (const { 0: name, 1: zeroError } of [['KmacKeyGenParams', 'OperationError'], ['KmacImportParams', 'DataError']]) {
660+
for (const name of ['KmacKeyGenParams', 'KmacImportParams']) {
671661
converters[name] = createDictionaryConverter(
672662
name, [
673663
dictAlgorithm,
@@ -676,7 +666,6 @@ for (const { 0: name, 1: zeroError } of [['KmacKeyGenParams', 'OperationError'],
676666
key: 'length',
677667
converter: (V, opts) =>
678668
converters['unsigned long'](V, enforceRangeOptions(opts)),
679-
validator: validateMacKeyLength(`${name}.length`, zeroError),
680669
},
681670
],
682671
]);
@@ -690,11 +679,6 @@ converters.KmacParams = createDictionaryConverter(
690679
key: 'outputLength',
691680
converter: (V, opts) =>
692681
converters['unsigned long'](V, enforceRangeOptions(opts)),
693-
validator: (V, opts) => {
694-
// The Web Crypto spec allows for KMAC output length that are not multiples of 8. We don't.
695-
if (V % 8)
696-
throw lazyDOMException('Unsupported KmacParams outputLength', 'NotSupportedError');
697-
},
698682
required: true,
699683
},
700684
{

src/crypto/crypto_keygen.cc

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -64,7 +64,13 @@ Maybe<void> SecretKeyGenTraits::AdditionalConfig(
6464
SecretKeyGenConfig* params) {
6565
CHECK(args[*offset]->IsUint32());
6666
uint32_t bits = args[*offset].As<Uint32>()->Value();
67-
params->length = bits / CHAR_BIT;
67+
params->length_bits = bits;
68+
if (mode == kCryptoJobWebCrypto) {
69+
params->length = NumBitsToBytes(static_cast<size_t>(bits));
70+
params->truncate_to_bit_length = bits % CHAR_BIT != 0;
71+
} else {
72+
params->length = bits / CHAR_BIT;
73+
}
6874
*offset += 1;
6975
return JustVoid();
7076
}
@@ -77,6 +83,8 @@ KeyGenJobStatus SecretKeyGenTraits::DoKeyGen(Environment* env,
7783
return KeyGenJobStatus::FAILED;
7884
}
7985
params->out = ByteSource::Allocated(bytes.release());
86+
if (params->truncate_to_bit_length)
87+
TruncateToBitLength(params->length_bits, &params->out);
8088
return KeyGenJobStatus::OK;
8189
}
8290

src/crypto/crypto_keygen.h

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -284,8 +284,10 @@ struct KeyPairGenTraits final {
284284
};
285285

286286
struct SecretKeyGenConfig final : public MemoryRetainer {
287-
size_t length; // In bytes.
288-
ByteSource out; // Placeholder for the generated key bytes.
287+
size_t length = 0; // In bytes.
288+
size_t length_bits = 0;
289+
bool truncate_to_bit_length = false;
290+
ByteSource out; // Placeholder for the generated key bytes.
289291

290292
void MemoryInfo(MemoryTracker* tracker) const override;
291293
SET_MEMORY_INFO_NAME(SecretKeyGenConfig)

0 commit comments

Comments
 (0)