mirror of
https://github.com/nodejs/node.git
synced 2026-11-09 15:11:11 -05:00
Enumerate usable ciphers and aliases from activated OpenSSL 3 providers instead of maintaining lists of provider-only algorithms. Skip numeric OID aliases and filter NULL, TLS composite, multiblock, and encrypt-then-MAC implementations that the Cipher APIs cannot use. Preserve the OpenSSL 1.1.1 and BoringSSL paths. Expose CBC-CTS, SM4-GCM, SM4-CCM, SM4-XTS, and additional AES key wrap implementations. Add `ctsMode` (CS1/CS2/CS3) and `xtsStandard` (GB/IEEE) options for selecting provider CTS and SM4-XTS variants. Keep ordinary cipher construction on the original binding and legacy lookup paths. Lazily cache successful provider fetches per Environment for string initialization and `getCipherInfo()`. Index entries by case-insensitive query, canonical, and alias names. Deduplicate owners by provider and canonical identity. Return borrowed pointers on warm hits. Use the shared process-wide FIPS-state generation to invalidate per-Environment cipher caches and refresh `getCiphers()` snapshots in the main thread and workers. Existing cipher contexts retain their implementation and can finish across a transition. Release provider owners before unloading worker addon DSOs. Enforce one-shot updates for CBC-CTS, AES key wrap, SIV/GCM-SIV, and CCM decryption. Reject finalization without required input or CCM tags, and defer authentication failures to `final()`. Document streaming and XTS data-unit constraints. Add known-answer vectors, option validation, provider round trips, cache, worker, snapshot, FIPS transition, and construction benchmark coverage. Fixes: https://github.com/nodejs/node/issues/43040 Fixes: https://github.com/nodejs/node/issues/64866 Refs: https://github.com/nodejs/node/issues/62982 Signed-off-by: Filip Skokan <panva.ip@gmail.com> PR-URL: https://github.com/nodejs/node/pull/65484 Reviewed-By: James M Snell <jasnell@gmail.com> Reviewed-By: Yagiz Nizipli <yagiz@nizipli.com>
331 lines
9.5 KiB
JavaScript
331 lines
9.5 KiB
JavaScript
'use strict';
|
|
|
|
const {
|
|
FunctionPrototypeCall,
|
|
NumberIsInteger,
|
|
ObjectSetPrototypeOf,
|
|
} = primordials;
|
|
|
|
const {
|
|
CipherBase,
|
|
privateDecrypt: _privateDecrypt,
|
|
privateEncrypt: _privateEncrypt,
|
|
publicDecrypt: _publicDecrypt,
|
|
publicEncrypt: _publicEncrypt,
|
|
getCipherInfo: _getCipherInfo,
|
|
} = internalBinding('crypto');
|
|
|
|
const {
|
|
crypto: {
|
|
RSA_PKCS1_OAEP_PADDING,
|
|
RSA_PKCS1_PADDING,
|
|
},
|
|
} = internalBinding('constants');
|
|
|
|
const {
|
|
codes: {
|
|
ERR_CRYPTO_INVALID_STATE,
|
|
ERR_INVALID_ARG_TYPE,
|
|
ERR_INVALID_ARG_VALUE,
|
|
ERR_UNKNOWN_ENCODING,
|
|
},
|
|
} = require('internal/errors');
|
|
|
|
const {
|
|
validateEncoding,
|
|
validateOneOf,
|
|
validateUint32,
|
|
validateObject,
|
|
validateString,
|
|
} = require('internal/validators');
|
|
|
|
const {
|
|
preparePrivateKey,
|
|
preparePublicOrPrivateKey,
|
|
prepareSecretKey,
|
|
} = require('internal/crypto/keys');
|
|
|
|
const {
|
|
getArrayBufferOrView,
|
|
getStringOption,
|
|
kHandle,
|
|
} = require('internal/crypto/util');
|
|
|
|
const {
|
|
isArrayBufferView,
|
|
} = require('internal/util/types');
|
|
|
|
const LazyTransform = require('internal/streams/lazy_transform');
|
|
|
|
const { normalizeEncoding, kEmptyObject } = require('internal/util');
|
|
|
|
const { StringDecoder } = require('string_decoder');
|
|
|
|
const kCtsModes = ['CS1', 'CS2', 'CS3'];
|
|
const kXtsStandards = ['GB', 'IEEE'];
|
|
|
|
function rsaFunctionFor(method, defaultPadding, keyType) {
|
|
const keyName = keyType === 'private' ? 'privateKey' : undefined;
|
|
return (key, buffer) => {
|
|
const { format, type, data, passphrase, namedCurve } =
|
|
keyType === 'private' ?
|
|
preparePrivateKey(key, keyName) :
|
|
preparePublicOrPrivateKey(key, keyName);
|
|
const padding = key.padding || defaultPadding;
|
|
const { oaepHash, mgf1Hash, encoding } = key;
|
|
let { oaepLabel } = key;
|
|
if (oaepHash !== undefined)
|
|
validateString(oaepHash, 'key.oaepHash');
|
|
if (mgf1Hash !== undefined)
|
|
validateString(mgf1Hash, 'key.mgf1Hash');
|
|
if (oaepLabel !== undefined)
|
|
oaepLabel = getArrayBufferOrView(oaepLabel, 'key.oaepLabel', encoding);
|
|
buffer = getArrayBufferOrView(buffer, 'buffer', encoding);
|
|
return method(data, format, type, passphrase, namedCurve, buffer,
|
|
padding, oaepHash, oaepLabel, mgf1Hash);
|
|
};
|
|
}
|
|
|
|
const publicEncrypt = rsaFunctionFor(_publicEncrypt, RSA_PKCS1_OAEP_PADDING,
|
|
'public');
|
|
const publicDecrypt = rsaFunctionFor(_publicDecrypt, RSA_PKCS1_PADDING,
|
|
'public');
|
|
const privateEncrypt = rsaFunctionFor(_privateEncrypt, RSA_PKCS1_PADDING,
|
|
'private');
|
|
const privateDecrypt = rsaFunctionFor(_privateDecrypt, RSA_PKCS1_OAEP_PADDING,
|
|
'private');
|
|
|
|
function getDecoder(decoder, encoding) {
|
|
const normalizedEncoding = normalizeEncoding(encoding);
|
|
decoder ||= new StringDecoder(encoding);
|
|
if (decoder.encoding !== normalizedEncoding) {
|
|
if (normalizedEncoding === undefined) {
|
|
throw new ERR_UNKNOWN_ENCODING(encoding);
|
|
}
|
|
throw new ERR_INVALID_ARG_VALUE(
|
|
'outputEncoding',
|
|
encoding,
|
|
`cannot be changed from '${decoder.encoding}'`,
|
|
);
|
|
}
|
|
return decoder;
|
|
}
|
|
|
|
function getUIntOption(options, key) {
|
|
let value;
|
|
if (options && (value = options[key]) != null) {
|
|
if (value >>> 0 !== value)
|
|
throw new ERR_INVALID_ARG_VALUE(`options.${key}`, value);
|
|
// Coerce -0 to +0.
|
|
return value + 0;
|
|
}
|
|
return -1;
|
|
}
|
|
|
|
function createCipherBase(cipher, credential, options, isEncrypt, iv) {
|
|
let authTagLength = -1;
|
|
let ctsMode;
|
|
let xtsStandard;
|
|
if (options != null) {
|
|
authTagLength = getUIntOption(options, 'authTagLength');
|
|
ctsMode = getStringOption(options, 'ctsMode') ?? undefined;
|
|
xtsStandard = getStringOption(options, 'xtsStandard') ?? undefined;
|
|
if (ctsMode !== undefined)
|
|
validateOneOf(ctsMode, 'options.ctsMode', kCtsModes);
|
|
if (xtsStandard !== undefined)
|
|
validateOneOf(xtsStandard, 'options.xtsStandard', kXtsStandards);
|
|
}
|
|
|
|
if (ctsMode === undefined && xtsStandard === undefined) {
|
|
this[kHandle] = new CipherBase(
|
|
isEncrypt, cipher, credential, iv, authTagLength);
|
|
} else {
|
|
this[kHandle] = new CipherBase(
|
|
isEncrypt,
|
|
cipher,
|
|
credential,
|
|
iv,
|
|
authTagLength,
|
|
ctsMode,
|
|
xtsStandard);
|
|
}
|
|
this._decoder = null;
|
|
|
|
FunctionPrototypeCall(LazyTransform, this, options);
|
|
}
|
|
|
|
function createCipherWithIV(cipher, key, options, isEncrypt, iv) {
|
|
validateString(cipher, 'cipher');
|
|
const encoding = getStringOption(options, 'encoding');
|
|
key = prepareSecretKey(key, encoding);
|
|
iv = iv === null ? null : getArrayBufferOrView(iv, 'iv');
|
|
FunctionPrototypeCall(createCipherBase, this, cipher, key, options, isEncrypt, iv);
|
|
}
|
|
|
|
// The Cipher class is part of the legacy Node.js crypto API. It exposes
|
|
// a stream-based encryption/decryption model. For backwards compatibility
|
|
// the Cipher class is defined using the legacy function syntax rather than
|
|
// ES6 classes.
|
|
|
|
function _transform(chunk, encoding, callback) {
|
|
this.push(this[kHandle].update(chunk, encoding));
|
|
callback();
|
|
};
|
|
|
|
function _flush(callback) {
|
|
try {
|
|
this.push(this[kHandle].final());
|
|
} catch (e) {
|
|
callback(e);
|
|
return;
|
|
}
|
|
callback();
|
|
};
|
|
|
|
function update(data, inputEncoding, outputEncoding) {
|
|
if (typeof data === 'string') {
|
|
validateEncoding(data, inputEncoding);
|
|
} else if (!isArrayBufferView(data)) {
|
|
throw new ERR_INVALID_ARG_TYPE(
|
|
'data', ['string', 'Buffer', 'TypedArray', 'DataView'], data);
|
|
}
|
|
|
|
const ret = this[kHandle].update(data, inputEncoding);
|
|
|
|
if (outputEncoding && outputEncoding !== 'buffer') {
|
|
this._decoder = getDecoder(this._decoder, outputEncoding);
|
|
return this._decoder.write(ret);
|
|
}
|
|
|
|
return ret;
|
|
};
|
|
|
|
function final(outputEncoding) {
|
|
const ret = this[kHandle].final();
|
|
|
|
if (outputEncoding && outputEncoding !== 'buffer') {
|
|
this._decoder = getDecoder(this._decoder, outputEncoding);
|
|
return this._decoder.end(ret);
|
|
}
|
|
|
|
return ret;
|
|
};
|
|
|
|
function setAutoPadding(ap) {
|
|
if (!this[kHandle].setAutoPadding(!!ap))
|
|
throw new ERR_CRYPTO_INVALID_STATE('setAutoPadding');
|
|
return this;
|
|
};
|
|
|
|
function getAuthTag() {
|
|
const ret = this[kHandle].getAuthTag();
|
|
if (ret === undefined)
|
|
throw new ERR_CRYPTO_INVALID_STATE('getAuthTag');
|
|
return ret;
|
|
};
|
|
|
|
function setAuthTag(tagbuf, encoding) {
|
|
tagbuf = getArrayBufferOrView(tagbuf, 'buffer', encoding);
|
|
if (!this[kHandle].setAuthTag(tagbuf))
|
|
throw new ERR_CRYPTO_INVALID_STATE('setAuthTag');
|
|
return this;
|
|
}
|
|
|
|
function setAAD(aadbuf, options) {
|
|
const encoding = getStringOption(options, 'encoding');
|
|
const plaintextLength = getUIntOption(options, 'plaintextLength');
|
|
aadbuf = getArrayBufferOrView(aadbuf, 'aadbuf', encoding);
|
|
if (!this[kHandle].setAAD(aadbuf, plaintextLength))
|
|
throw new ERR_CRYPTO_INVALID_STATE('setAAD');
|
|
return this;
|
|
};
|
|
|
|
// The Cipheriv class is part of the legacy Node.js crypto API. It exposes
|
|
// a stream-based encryption/decryption model. For backwards compatibility
|
|
// the Cipheriv class is defined using the legacy function syntax rather than
|
|
// ES6 classes.
|
|
|
|
function Cipheriv(cipher, key, iv, options) {
|
|
if (!(this instanceof Cipheriv))
|
|
return new Cipheriv(cipher, key, iv, options);
|
|
|
|
FunctionPrototypeCall(createCipherWithIV, this, cipher, key, options, true, iv);
|
|
}
|
|
|
|
function addCipherPrototypeFunctions(constructor) {
|
|
constructor.prototype._transform = _transform;
|
|
constructor.prototype._flush = _flush;
|
|
constructor.prototype.update = update;
|
|
constructor.prototype.final = final;
|
|
constructor.prototype.setAutoPadding = setAutoPadding;
|
|
if (constructor === Cipheriv) {
|
|
constructor.prototype.getAuthTag = getAuthTag;
|
|
} else {
|
|
constructor.prototype.setAuthTag = setAuthTag;
|
|
}
|
|
constructor.prototype.setAAD = setAAD;
|
|
}
|
|
|
|
ObjectSetPrototypeOf(Cipheriv.prototype, LazyTransform.prototype);
|
|
ObjectSetPrototypeOf(Cipheriv, LazyTransform);
|
|
addCipherPrototypeFunctions(Cipheriv);
|
|
|
|
// The Decipheriv class is part of the legacy Node.js crypto API. It exposes
|
|
// a stream-based encryption/decryption model. For backwards compatibility
|
|
// the Decipheriv class is defined using the legacy function syntax rather than
|
|
// ES6 classes.
|
|
function Decipheriv(cipher, key, iv, options) {
|
|
if (!(this instanceof Decipheriv))
|
|
return new Decipheriv(cipher, key, iv, options);
|
|
|
|
FunctionPrototypeCall(createCipherWithIV, this, cipher, key, options, false, iv);
|
|
}
|
|
|
|
ObjectSetPrototypeOf(Decipheriv.prototype, LazyTransform.prototype);
|
|
ObjectSetPrototypeOf(Decipheriv, LazyTransform);
|
|
addCipherPrototypeFunctions(Decipheriv);
|
|
|
|
const kMinNid = 1;
|
|
const kMaxNid = 2_147_483_647;
|
|
function getCipherInfo(nameOrNid, options = kEmptyObject) {
|
|
validateObject(options, 'options');
|
|
let { keyLength, ivLength } = options;
|
|
if (keyLength !== undefined) {
|
|
validateUint32(keyLength, 'options.keyLength');
|
|
// Coerce -0 to +0.
|
|
keyLength += 0;
|
|
}
|
|
if (ivLength !== undefined) {
|
|
validateUint32(ivLength, 'options.ivLength');
|
|
// Coerce -0 to +0.
|
|
ivLength += 0;
|
|
}
|
|
|
|
const type = typeof nameOrNid;
|
|
|
|
if (type === 'string') {
|
|
if (nameOrNid.length === 0) return undefined;
|
|
return _getCipherInfo(nameOrNid, keyLength, ivLength);
|
|
} else if (type === 'number') {
|
|
if (!NumberIsInteger(nameOrNid) || nameOrNid < kMinNid || nameOrNid > kMaxNid) {
|
|
return undefined;
|
|
}
|
|
return _getCipherInfo(nameOrNid, keyLength, ivLength);
|
|
}
|
|
|
|
throw new ERR_INVALID_ARG_TYPE(
|
|
'nameOrNid',
|
|
['string', 'number'],
|
|
nameOrNid);
|
|
}
|
|
|
|
module.exports = {
|
|
Cipheriv,
|
|
Decipheriv,
|
|
privateDecrypt,
|
|
privateEncrypt,
|
|
publicDecrypt,
|
|
publicEncrypt,
|
|
getCipherInfo,
|
|
};
|