node/lib/internal/crypto/diffiehellman.js
Filip Skokan 8ec37ebbd6
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: https://github.com/nodejs/node/pull/63988
Reviewed-By: James M Snell <jasnell@gmail.com>
Reviewed-By: Yagiz Nizipli <yagiz@nizipli.com>
2026-06-20 18:42:04 +02:00

397 lines
9.9 KiB
JavaScript

'use strict';
const {
ArrayBufferPrototypeSlice,
FunctionPrototypeCall,
ObjectDefineProperty,
TypedArrayPrototypeGetBuffer,
} = primordials;
const { Buffer } = require('buffer');
const {
DHBitsJob,
DiffieHellman: _DiffieHellman,
DiffieHellmanGroup: _DiffieHellmanGroup,
ECDH: _ECDH,
ECDHConvertKey: _ECDHConvertKey,
kCryptoJobAsync,
kCryptoJobSync,
kCryptoJobWebCrypto,
} = internalBinding('crypto');
const {
codes: {
ERR_CRYPTO_ECDH_INVALID_FORMAT,
ERR_CRYPTO_ECDH_INVALID_PUBLIC_KEY,
ERR_INVALID_ARG_TYPE,
},
} = require('internal/errors');
const {
validateFunction,
validateInt32,
validateObject,
validateString,
} = require('internal/validators');
const {
isArrayBufferView,
isAnyArrayBuffer,
} = require('internal/util/types');
const {
deprecate,
lazyDOMException,
} = require('internal/util');
const {
getCryptoKeyAlgorithm,
getCryptoKeyHandle,
getCryptoKeyType,
preparePrivateKey,
preparePublicOrPrivateKey,
} = require('internal/crypto/keys');
const {
getArrayBufferOrView,
jobPromise,
jobPromiseThen,
numBitsToBytes,
toBuf,
truncateToBitLength,
kHandle,
} = require('internal/crypto/util');
const {
crypto: {
POINT_CONVERSION_COMPRESSED,
POINT_CONVERSION_HYBRID,
POINT_CONVERSION_UNCOMPRESSED,
},
} = internalBinding('constants');
const DH_GENERATOR = 2;
function DiffieHellman(sizeOrKey, keyEncoding, generator, genEncoding) {
if (!(this instanceof DiffieHellman))
return new DiffieHellman(sizeOrKey, keyEncoding, generator, genEncoding);
if (typeof sizeOrKey !== 'number' &&
typeof sizeOrKey !== 'string' &&
!isArrayBufferView(sizeOrKey) &&
!isAnyArrayBuffer(sizeOrKey)) {
throw new ERR_INVALID_ARG_TYPE(
'sizeOrKey',
['number', 'string', 'ArrayBuffer', 'Buffer', 'TypedArray', 'DataView'],
sizeOrKey,
);
}
// Sizes < 0 don't make sense but they _are_ accepted (and subsequently
// rejected with ERR_OSSL_BN_BITS_TOO_SMALL) by OpenSSL. The glue code
// in node_crypto.cc accepts values that are IsInt32() for that reason
// and that's why we do that here too.
if (typeof sizeOrKey === 'number') {
validateInt32(sizeOrKey, 'sizeOrKey');
// Coerce -0 to +0.
sizeOrKey += 0;
}
if (keyEncoding && !Buffer.isEncoding(keyEncoding) &&
keyEncoding !== 'buffer') {
genEncoding = generator;
generator = keyEncoding;
keyEncoding = false;
}
if (typeof sizeOrKey !== 'number')
sizeOrKey = toBuf(sizeOrKey, keyEncoding);
if (!generator) {
generator = DH_GENERATOR;
} else if (typeof generator === 'number') {
validateInt32(generator, 'generator');
} else if (typeof generator === 'string') {
generator = toBuf(generator, genEncoding);
} else if (!isArrayBufferView(generator) && !isAnyArrayBuffer(generator)) {
throw new ERR_INVALID_ARG_TYPE(
'generator',
['number', 'string', 'ArrayBuffer', 'Buffer', 'TypedArray', 'DataView'],
generator,
);
}
this[kHandle] = new _DiffieHellman(sizeOrKey, generator);
ObjectDefineProperty(this, 'verifyError', {
__proto__: null,
enumerable: true,
value: this[kHandle].verifyError,
writable: false,
});
}
function DiffieHellmanGroup(name) {
if (!(this instanceof DiffieHellmanGroup))
return new DiffieHellmanGroup(name);
this[kHandle] = new _DiffieHellmanGroup(name);
ObjectDefineProperty(this, 'verifyError', {
__proto__: null,
enumerable: true,
value: this[kHandle].verifyError,
writable: false,
});
}
DiffieHellmanGroup.prototype.generateKeys =
DiffieHellman.prototype.generateKeys =
dhGenerateKeys;
function dhGenerateKeys(encoding) {
const keys = this[kHandle].generateKeys();
return encode(keys, encoding);
}
DiffieHellmanGroup.prototype.computeSecret =
DiffieHellman.prototype.computeSecret =
dhComputeSecret;
function dhComputeSecret(key, inEnc, outEnc) {
key = getArrayBufferOrView(key, 'key', inEnc);
const ret = this[kHandle].computeSecret(key);
if (typeof ret === 'string')
throw new ERR_CRYPTO_ECDH_INVALID_PUBLIC_KEY();
return encode(ret, outEnc);
}
DiffieHellmanGroup.prototype.getPrime =
DiffieHellman.prototype.getPrime =
dhGetPrime;
function dhGetPrime(encoding) {
const prime = this[kHandle].getPrime();
return encode(prime, encoding);
}
DiffieHellmanGroup.prototype.getGenerator =
DiffieHellman.prototype.getGenerator =
dhGetGenerator;
function dhGetGenerator(encoding) {
const generator = this[kHandle].getGenerator();
return encode(generator, encoding);
}
DiffieHellmanGroup.prototype.getPublicKey =
DiffieHellman.prototype.getPublicKey =
dhGetPublicKey;
function dhGetPublicKey(encoding) {
const key = this[kHandle].getPublicKey();
return encode(key, encoding);
}
DiffieHellmanGroup.prototype.getPrivateKey =
DiffieHellman.prototype.getPrivateKey =
dhGetPrivateKey;
function dhGetPrivateKey(encoding) {
const key = this[kHandle].getPrivateKey();
return encode(key, encoding);
}
DiffieHellman.prototype.setPublicKey = function setPublicKey(key, encoding) {
key = getArrayBufferOrView(key, 'key', encoding);
this[kHandle].setPublicKey(key);
return this;
};
DiffieHellman.prototype.setPrivateKey = function setPrivateKey(key, encoding) {
key = getArrayBufferOrView(key, 'key', encoding);
this[kHandle].setPrivateKey(key);
return this;
};
function ECDH(curve) {
if (!(this instanceof ECDH))
return new ECDH(curve);
validateString(curve, 'curve');
this[kHandle] = new _ECDH(curve);
}
ECDH.prototype.computeSecret = DiffieHellman.prototype.computeSecret;
ECDH.prototype.setPrivateKey = DiffieHellman.prototype.setPrivateKey;
ECDH.prototype.setPublicKey = deprecate(DiffieHellman.prototype.setPublicKey,
'ecdh.setPublicKey() is deprecated.',
'DEP0031');
ECDH.prototype.getPrivateKey = DiffieHellman.prototype.getPrivateKey;
ECDH.prototype.generateKeys = function generateKeys(encoding, format) {
this[kHandle].generateKeys();
return this.getPublicKey(encoding, format);
};
ECDH.prototype.getPublicKey = function getPublicKey(encoding, format) {
const f = getFormat(format);
const key = this[kHandle].getPublicKey(f);
return encode(key, encoding);
};
ECDH.convertKey = function convertKey(key, curve, inEnc, outEnc, format) {
validateString(curve, 'curve');
key = getArrayBufferOrView(key, 'key', inEnc);
const f = getFormat(format);
const convertedKey = _ECDHConvertKey(key, curve, f);
return encode(convertedKey, outEnc);
};
function encode(buffer, encoding) {
if (encoding && encoding !== 'buffer')
buffer = buffer.toString(encoding);
return buffer;
}
function getFormat(format) {
if (format) {
if (format === 'compressed')
return POINT_CONVERSION_COMPRESSED;
if (format === 'hybrid')
return POINT_CONVERSION_HYBRID;
if (format !== 'uncompressed')
throw new ERR_CRYPTO_ECDH_INVALID_FORMAT(format);
}
return POINT_CONVERSION_UNCOMPRESSED;
}
function diffieHellman(options, callback) {
validateObject(options, 'options');
if (callback !== undefined)
validateFunction(callback, 'callback');
const { privateKey, publicKey } = options;
const {
data: pubData,
format: pubFormat,
type: pubType,
passphrase: pubPassphrase,
namedCurve: pubNamedCurve,
} = preparePublicOrPrivateKey(publicKey, 'options.publicKey');
const {
data: privData,
format: privFormat,
type: privType,
passphrase: privPassphrase,
namedCurve: privNamedCurve,
} = preparePrivateKey(privateKey, 'options.privateKey');
const job = new DHBitsJob(
callback ? kCryptoJobAsync : kCryptoJobSync,
pubData,
pubFormat,
pubType,
pubPassphrase,
pubNamedCurve,
privData,
privFormat,
privType,
privPassphrase,
privNamedCurve);
if (!callback) {
const { 0: err, 1: secret } = job.run();
if (err !== undefined)
throw err;
return Buffer.from(secret);
}
job.ondone = (error, secret) => {
if (error) return FunctionPrototypeCall(callback, job, error);
FunctionPrototypeCall(callback, job, null, Buffer.from(secret));
};
job.run();
}
// The ecdhDeriveBits function is part of the Web Crypto API and serves both
// deriveKeys and deriveBits functions.
function ecdhDeriveBits(algorithm, baseKey, length) {
const { 'public': key } = algorithm;
if (getCryptoKeyType(baseKey) !== 'private') {
throw lazyDOMException(
'baseKey must be a private key', 'InvalidAccessError');
}
const keyAlgorithm = getCryptoKeyAlgorithm(key);
const baseKeyAlgorithm = getCryptoKeyAlgorithm(baseKey);
if (keyAlgorithm.name !== baseKeyAlgorithm.name) {
throw lazyDOMException(
'The public and private keys must be of the same type',
'InvalidAccessError');
}
if (
keyAlgorithm.name === 'ECDH' &&
keyAlgorithm.namedCurve !== baseKeyAlgorithm.namedCurve
) {
throw lazyDOMException('Named curve mismatch', 'InvalidAccessError');
}
const bits = jobPromise(() => new DHBitsJob(
kCryptoJobWebCrypto,
getCryptoKeyHandle(key),
undefined,
undefined,
undefined,
undefined,
getCryptoKeyHandle(baseKey),
undefined,
undefined,
undefined,
undefined));
// If a length is not specified, return the full derived secret
if (length === null)
return bits;
return jobPromiseThen(bits, (bits) => {
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');
if (length % 8 === 0) {
if (byteLength === sliceLength)
return bits;
return ArrayBufferPrototypeSlice(bits, 0, sliceLength);
}
return TypedArrayPrototypeGetBuffer(truncateToBitLength(length, bits));
});
}
module.exports = {
DiffieHellman,
DiffieHellmanGroup,
ECDH,
diffieHellman,
ecdhDeriveBits,
};