Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
31 changes: 21 additions & 10 deletions doc/api/webcrypto.md
Original file line number Diff line number Diff line change
Expand Up @@ -1933,26 +1933,39 @@ added:

<!-- YAML
added: v24.7.0
changes:
- version: REPLACEME
pr-url: https://github.com/nodejs/node/pull/63988
description: Named cSHAKE variants are now accepted.
-->

* Type: {ArrayBuffer|TypedArray|DataView|Buffer|undefined}

The `functionName` member represents the function name, used by NIST to define
functions based on cSHAKE.
The Node.js Web Crypto API implementation only supports zero-length functionName
which is equivalent to not providing functionName at all.
The `functionName` member represents the NIST function-name byte string used to
domain-separate functions built on top of cSHAKE. Accepted values are:

* empty or `undefined`, in which case cSHAKE is equivalent to plain SHAKE
* the ASCII byte sequence `'KMAC'`
* the ASCII byte sequence `'TupleHash'`
* the ASCII byte sequence `'ParallelHash'`

#### `cShakeParams.customization`

<!-- YAML
added: v24.7.0
changes:
- version: REPLACEME
pr-url: https://github.com/nodejs/node/pull/63988
description: Non-empty customization is now supported.
-->

* Type: {ArrayBuffer|TypedArray|DataView|Buffer|undefined}

The `customization` member represents the customization string.
The Node.js Web Crypto API implementation only supports zero-length customization
which is equivalent to not providing customization at all.
The `customization` member represents the customization data. Accepted
values are:

* empty or `undefined`, in which case cSHAKE is equivalent to plain SHAKE
* up to 512 bytes of arbitrary data

### Class: `EcdhKeyDeriveParams`

Expand Down Expand Up @@ -2489,9 +2502,7 @@ added:
- v24.15.0
-->

* Type: {number}

The length of the output in bytes. This must be a positive integer.
* Type: {number} represents the requested output length in bits.

#### `kmacParams.customization`

Expand Down
26 changes: 9 additions & 17 deletions lib/internal/crypto/diffiehellman.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,10 +3,8 @@
const {
ArrayBufferPrototypeSlice,
FunctionPrototypeCall,
MathCeil,
ObjectDefineProperty,
TypedArrayPrototypeGetBuffer,
Uint8Array,
} = primordials;

const { Buffer } = require('buffer');
Expand Down Expand Up @@ -59,7 +57,9 @@ const {
getArrayBufferOrView,
jobPromise,
jobPromiseThen,
numBitsToBytes,
toBuf,
truncateToBitLength,
kHandle,
} = require('internal/crypto/util');

Expand Down Expand Up @@ -328,7 +328,6 @@ function diffieHellman(options, callback) {
job.run();
}

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

return jobPromiseThen(bits, (bits) => {
// If the length is not a multiple of 8 the nearest ceiled
// multiple of 8 is sliced.
const sliceLength = MathCeil(length / 8);
const sliceLength = numBitsToBytes(length);

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

const slice = ArrayBufferPrototypeSlice(bits, 0, sliceLength);
if (length % 8 === 0) {
if (byteLength === sliceLength)
return bits;
return ArrayBufferPrototypeSlice(bits, 0, sliceLength);
}

const mod = length % 8;
if (mod === 0)
return slice;

// eslint-disable-next-line no-sparse-arrays
masks ||= [, 0b10000000, 0b11000000, 0b11100000, 0b11110000, 0b11111000, 0b11111100, 0b11111110];

const masked = new Uint8Array(slice);
masked[sliceLength - 1] = masked[sliceLength - 1] & masks[mod];
return TypedArrayPrototypeGetBuffer(masked);
return TypedArrayPrototypeGetBuffer(truncateToBitLength(length, bits));
});
}

Expand Down
39 changes: 35 additions & 4 deletions lib/internal/crypto/hash.js
Original file line number Diff line number Diff line change
Expand Up @@ -6,9 +6,11 @@ const {
StringPrototypeReplace,
StringPrototypeToLowerCase,
Symbol,
TypedArrayPrototypeGetBuffer,
} = primordials;

const {
CShakeJob,
Hash: _Hash,
HashJob,
Hmac: _Hmac,
Expand All @@ -21,7 +23,10 @@ const {
const {
getStringOption,
jobPromise,
jobPromiseThen,
normalizeHashName,
numBitsToBytes,
truncateToBitLength,
validateMaxBufferLength,
kHandle,
getCachedHashId,
Expand Down Expand Up @@ -227,15 +232,41 @@ function asyncDigest(algorithm, data) {
case 'SHA3-384':
// Fall through
case 'SHA3-512':
// Fall through
return jobPromise(() => new HashJob(
kCryptoJobWebCrypto,
normalizeHashName(algorithm.name),
data));
case 'cSHAKE128':
// Fall through
case 'cSHAKE256':
return jobPromise(() => new HashJob(
case 'cSHAKE256': {
const outputLength = algorithm.outputLength;
if (algorithm.functionName?.byteLength ||
algorithm.customization?.byteLength) {
if (CShakeJob === undefined) {
throw lazyDOMException(
'Non-empty CShakeParams functionName or customization is not supported',
'NotSupportedError');
}

return jobPromise(() => new CShakeJob(
kCryptoJobWebCrypto,
algorithm.name,
data,
algorithm.functionName,
algorithm.customization,
outputLength));
}

const bits = jobPromise(() => new HashJob(
kCryptoJobWebCrypto,
normalizeHashName(algorithm.name),
data,
algorithm.outputLength));
numBitsToBytes(outputLength) * 8));
if (outputLength % 8 === 0)
return bits;
return jobPromiseThen(bits, (bits) =>
TypedArrayPrototypeGetBuffer(truncateToBitLength(outputLength, bits)));
}
case 'TurboSHAKE128':
// Fall through
case 'TurboSHAKE256':
Expand Down
37 changes: 26 additions & 11 deletions lib/internal/crypto/mac.js
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,8 @@ const {
hasAnyNotIn,
jobPromise,
normalizeHashName,
numBitsToBytes,
truncateToBitLength,
} = require('internal/crypto/util');

const {
Expand All @@ -38,6 +40,27 @@ const {
validateJwk,
} = require('internal/crypto/webcrypto_util');

function normalizeKeyLength(handle, algorithm) {
let length = handle.getSymmetricKeySize() * 8;
if (length === 0 && algorithm.name === 'HMAC')
throw lazyDOMException('Zero-length key is not supported', 'DataError');

if (algorithm.length !== undefined) {
const byteLength = numBitsToBytes(algorithm.length);
if (byteLength !== handle.getSymmetricKeySize())
throw lazyDOMException('Invalid key length', 'DataError');

if (algorithm.length % 8 !== 0) {
handle = importSecretKey(
truncateToBitLength(algorithm.length, handle.export()));
}

length = algorithm.length;
}

return { handle, length };
}

function hmacGenerateKey(algorithm, extractable, usages) {
const {
hash,
Expand Down Expand Up @@ -113,7 +136,6 @@ function macImportKey(
let length;
switch (format) {
case 'KeyObjectHandle': {
length = keyData.getSymmetricKeySize() * 8;
handle = keyData;
break;
}
Expand All @@ -122,7 +144,6 @@ function macImportKey(
if (format === 'raw' && !isHmac) {
return undefined;
}
length = keyData.byteLength * 8;
handle = importSecretKey(keyData);
break;
}
Expand All @@ -140,20 +161,13 @@ function macImportKey(
}

handle = importJwkSecretKey(keyData);
length = handle.getSymmetricKeySize() * 8;
break;
}
default:
return undefined;
}

if (length === 0)
throw lazyDOMException('Zero-length key is not supported', 'DataError');

if (algorithm.length !== undefined &&
algorithm.length !== length) {
throw lazyDOMException('Invalid key length', 'DataError');
}
({ handle, length } = normalizeKeyLength(handle, algorithm)); // eslint-disable-line prefer-const

const algorithmObject = {
name: algorithm.name,
Expand Down Expand Up @@ -190,7 +204,8 @@ function kmacSignVerify(key, data, algorithm, signature) {
getCryptoKeyHandle(key),
algorithm.name,
algorithm.customization,
algorithm.outputLength / 8,
getCryptoKeyAlgorithm(key).length,
algorithm.outputLength,
data,
signature));
}
Expand Down
60 changes: 55 additions & 5 deletions lib/internal/crypto/util.js
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ const {
DataViewPrototypeGetBuffer,
DataViewPrototypeGetByteLength,
DataViewPrototypeGetByteOffset,
MathFloor,
Number,
ObjectDefineProperty,
ObjectEntries,
Expand Down Expand Up @@ -550,6 +551,46 @@ function validateMaxBufferLength(data, name, max = kMaxBufferLength) {
}
}

/**
* Converts a bit length to the number of bytes needed to contain it.
* Non-byte lengths are rounded up to the next byte.
* @param {number} length
* @returns {number}
*/
function numBitsToBytes(length) {
return MathFloor(length / 8) + MathFloor((7 + (length % 8)) / 8);
}

/**
* Copies `bytes` up to the byte length needed for `length` bits, then clears
* unused least-significant bits in the final byte.
* @param {number} length
* @param {ArrayBuffer|ArrayBufferView} bytes
* @returns {Uint8Array}
*/
function truncateToBitLength(length, bytes) {
const lengthBytes = numBitsToBytes(length);
const isView = ArrayBufferIsView(bytes);
const byteView = isView ?
new Uint8Array(
getDataViewOrTypedArrayBuffer(bytes),
getDataViewOrTypedArrayByteOffset(bytes),
getDataViewOrTypedArrayByteLength(bytes),
) :
new Uint8Array(bytes, 0, ArrayBufferPrototypeGetByteLength(bytes));
const result = TypedArrayPrototypeSlice(
byteView,
0,
lengthBytes,
);

const remainder = length % 8;
if (remainder !== 0)
result[lengthBytes - 1] &= (0xff << (8 - remainder)) & 0xff;

return result;
}

let webidl;

// Keep this as a regular object. The WebIDL converters read and spread these
Expand Down Expand Up @@ -609,12 +650,19 @@ function normalizeAlgorithm(algorithm, op) {
// 3.
if (idlType === 'BufferSource' && idlValue) {
const isView = ArrayBufferIsView(idlValue);
normalizedAlgorithm[member] = TypedArrayPrototypeSlice(
const idlValueBytes = isView ?
new Uint8Array(
isView ? getDataViewOrTypedArrayBuffer(idlValue) : idlValue,
isView ? getDataViewOrTypedArrayByteOffset(idlValue) : 0,
isView ? getDataViewOrTypedArrayByteLength(idlValue) : ArrayBufferPrototypeGetByteLength(idlValue),
),
getDataViewOrTypedArrayBuffer(idlValue),
getDataViewOrTypedArrayByteOffset(idlValue),
getDataViewOrTypedArrayByteLength(idlValue),
) :
new Uint8Array(
idlValue,
0,
ArrayBufferPrototypeGetByteLength(idlValue),
);
normalizedAlgorithm[member] = TypedArrayPrototypeSlice(
idlValueBytes,
);
} else if (idlType === 'HashAlgorithmIdentifier') {
normalizedAlgorithm[member] = normalizeAlgorithm(idlValue, 'digest');
Expand Down Expand Up @@ -971,6 +1019,8 @@ module.exports = {
cleanupWebCryptoResult,
prepareWebCryptoResult,
validateMaxBufferLength,
numBitsToBytes,
truncateToBitLength,
bigIntArrayToUnsignedBigInt,
bigIntArrayToUnsignedInt,
getBlockSize,
Expand Down
Loading
Loading