'use strict';
const {
FunctionPrototypeCall,
ObjectSetPrototypeOf,
StringPrototypeToLowerCase,
Symbol,
TypedArrayPrototypeIncludes,
} = primordials;
const {
Hash: _Hash,
HashJob,
Hmac: _Hmac,
kCryptoJobWebCrypto,
oneShotDigest,
TurboShakeJob,
KangarooTwelveJob,
} = internalBinding('crypto');
const {
getStringOption,
jobPromise,
normalizeHashName,
validateAlgorithm,
validateMaxBufferLength,
kHandle,
getCachedHashId,
getHashCache,
getArrayBufferOrView,
getBufferSourceBytes,
getOptionalByteLength,
} = require('internal/crypto/util');
const {
prepareSecretKey,
} = require('internal/crypto/keys');
const {
normalizeEncoding,
encodingsMap,
getDeprecationWarningEmitter,
} = require('internal/util');
const {
Buffer,
} = require('buffer');
const {
codes: {
ERR_CRYPTO_HASH_FINALIZED,
ERR_CRYPTO_HASH_UPDATE_FAILED,
ERR_INVALID_ARG_TYPE,
ERR_INVALID_ARG_VALUE,
ERR_UNKNOWN_ENCODING,
},
} = require('internal/errors');
const {
validateEncoding,
validateString,
validateObject,
validateUint32,
} = require('internal/validators');
const {
isArrayBufferView,
} = require('internal/util/types');
const LazyTransform = require('internal/streams/lazy_transform');
const kState = Symbol('kState');
const kFinalized = Symbol('kFinalized');
const emitHmacDigestDeprecation = getDeprecationWarningEmitter(
'DEP0206',
'Calling Hmac.digest() more than once is deprecated.',
);
function normalizeCShakeParameter(value, name) {
if (value === undefined) return undefined;
value = getArrayBufferOrView(value, name);
if (TypedArrayPrototypeIncludes(getBufferSourceBytes(value), 0)) {
throw new ERR_INVALID_ARG_VALUE(
name, value, 'must not contain NUL bytes');
}
return value;
}
function Hash(algorithm, options) {
if (!new.target)
return new Hash(algorithm, options);
const isCopy = algorithm instanceof _Hash;
if (!isCopy)
validateString(algorithm, 'algorithm');
let xofLen = typeof options === 'object' && options !== null ?
options.outputLength : undefined;
if (xofLen !== undefined) {
validateUint32(xofLen, 'options.outputLength');
// Coerce -0 to +0.
xofLen += 0;
}
let functionName;
let customization;
if (!isCopy && options !== undefined && options !== null) {
const functionNameOption = options.functionName;
if (functionNameOption !== undefined) {
functionName = normalizeCShakeParameter(
functionNameOption, 'options.functionName');
}
const customizationOption = options.customization;
if (customizationOption !== undefined) {
customization = normalizeCShakeParameter(
customizationOption, 'options.customization');
}
}
// Lookup the cached ID from JS land because it's faster than decoding
// the string in C++ land.
const algorithmId = isCopy ? -1 : getCachedHashId(algorithm);
if (functionName === undefined && customization === undefined) {
this[kHandle] = new _Hash(
algorithm, xofLen, algorithmId, getHashCache());
} else {
this[kHandle] = new _Hash(
algorithm,
xofLen,
algorithmId,
getHashCache(),
functionName,
customization,
);
}
this[kState] = {
[kFinalized]: false,
};
FunctionPrototypeCall(LazyTransform, this, options);
}
ObjectSetPrototypeOf(Hash.prototype, LazyTransform.prototype);
ObjectSetPrototypeOf(Hash, LazyTransform);
Hash.prototype.copy = function copy(options) {
const state = this[kState];
if (state[kFinalized])
throw new ERR_CRYPTO_HASH_FINALIZED();
return new Hash(this[kHandle], options);
};
Hash.prototype._transform = function _transform(chunk, encoding, callback) {
if (!this[kHandle].update(chunk, encoding))
return callback(new ERR_CRYPTO_HASH_UPDATE_FAILED());
callback();
};
Hash.prototype._flush = function _flush(callback) {
this.push(this[kHandle].digest());
callback();
};
Hash.prototype.update = function update(data, encoding) {
const state = this[kState];
if (state[kFinalized])
throw new ERR_CRYPTO_HASH_FINALIZED();
if (typeof data === 'string') {
validateEncoding(data, encoding);
} else if (!isArrayBufferView(data)) {
throw new ERR_INVALID_ARG_TYPE(
'data', ['string', 'Buffer', 'TypedArray', 'DataView'], data);
}
if (!this[kHandle].update(data, encoding))
throw new ERR_CRYPTO_HASH_UPDATE_FAILED();
return this;
};
function validateDigestEncoding(outputEncoding) {
// Explicit conversion of truthy values for backward compatibility.
outputEncoding &&= `${outputEncoding}`;
if (outputEncoding &&
normalizeEncoding(outputEncoding) === undefined &&
StringPrototypeToLowerCase(outputEncoding) !== 'buffer') {
throw new ERR_UNKNOWN_ENCODING(outputEncoding);
}
return outputEncoding;
}
Hash.prototype.digest = function digest(outputEncoding) {
const state = this[kState];
if (state[kFinalized])
throw new ERR_CRYPTO_HASH_FINALIZED();
const ret = this[kHandle].digest(validateDigestEncoding(outputEncoding));
state[kFinalized] = true;
return ret;
};
function Hmac(hmac, key, options) {
if (!(this instanceof Hmac))
return new Hmac(hmac, key, options);
validateString(hmac, 'hmac');
const encoding = getStringOption(options, 'encoding');
key = prepareSecretKey(key, encoding);
this[kHandle] = new _Hmac();
this[kHandle].init(hmac, key);
this[kState] = {
[kFinalized]: false,
};
FunctionPrototypeCall(LazyTransform, this, options);
}
ObjectSetPrototypeOf(Hmac.prototype, LazyTransform.prototype);
ObjectSetPrototypeOf(Hmac, LazyTransform);
Hmac.prototype.update = Hash.prototype.update;
Hmac.prototype.digest = function digest(outputEncoding) {
const state = this[kState];
if (state[kFinalized]) {
emitHmacDigestDeprecation();
const buf = Buffer.from('');
if (outputEncoding && outputEncoding !== 'buffer')
return buf.toString(outputEncoding);
return buf;
}
const ret = this[kHandle].digest(validateDigestEncoding(outputEncoding));
state[kFinalized] = true;
return ret;
};
Hmac.prototype._flush = function _flush(callback) {
this.push(this[kHandle].digest());
this[kState][kFinalized] = true; // This diverges from Hash.prototype._flush:
// Hash instances are still usable after a flush, Hmac are not, see DEP0206.
callback();
};
Hmac.prototype._transform = Hash.prototype._transform;
// Implementation for WebCrypto subtle.digest()
function asyncDigest(algorithm, data) {
validateMaxBufferLength(data, 'data');
validateAlgorithm(algorithm, 'digest');
switch (algorithm.name) {
case 'SHA-1':
// Fall through
case 'SHA-256':
// Fall through
case 'SHA-384':
// Fall through
case 'SHA-512':
// Fall through
case 'SHA3-256':
// Fall through
case 'SHA3-384':
// Fall through
case 'SHA3-512':
return jobPromise(() => new HashJob(
kCryptoJobWebCrypto,
normalizeHashName(algorithm.name),
data));
case 'cSHAKE128':
// Fall through
case 'cSHAKE256': {
const outputLength = algorithm.outputLength;
if (getOptionalByteLength(algorithm.functionName) ||
getOptionalByteLength(algorithm.customization)) {
return jobPromise(() => new HashJob(
kCryptoJobWebCrypto,
StringPrototypeToLowerCase(algorithm.name),
data,
outputLength,
algorithm.functionName,
algorithm.customization));
}
return jobPromise(() => new HashJob(
kCryptoJobWebCrypto,
normalizeHashName(algorithm.name),
data,
outputLength));
}
case 'TurboSHAKE128':
// Fall through
case 'TurboSHAKE256':
return jobPromise(() => new TurboShakeJob(
kCryptoJobWebCrypto,
algorithm.name,
algorithm.domainSeparation ?? 0x1f,
algorithm.outputLength / 8,
data));
case 'KT128':
// Fall through
case 'KT256':
return jobPromise(() => new KangarooTwelveJob(
kCryptoJobWebCrypto,
algorithm.name,
algorithm.customization,
algorithm.outputLength / 8,
data));
/* c8 ignore start */
default: {
const assert = require('internal/assert');
assert.fail('Unreachable code');
}
/* c8 ignore stop */
}
}
function hash(algorithm, input, options) {
validateString(algorithm, 'algorithm');
if (typeof input !== 'string' && !isArrayBufferView(input)) {
throw new ERR_INVALID_ARG_TYPE('input', ['Buffer', 'TypedArray', 'DataView', 'string'], input);
}
if (options === undefined) {
return oneShotDigest(algorithm, getCachedHashId(algorithm), getHashCache(),
input, 'hex', encodingsMap.hex, undefined);
}
let outputEncoding;
let outputLength;
let functionName;
let customization;
if (typeof options === 'string') {
outputEncoding = options;
} else if (options !== undefined) {
validateObject(options, 'options');
outputLength = options.outputLength;
outputEncoding = options.outputEncoding;
const functionNameOption = options.functionName;
if (functionNameOption !== undefined) {
functionName = normalizeCShakeParameter(
functionNameOption, 'options.functionName');
}
const customizationOption = options.customization;
if (customizationOption !== undefined) {
customization = normalizeCShakeParameter(
customizationOption, 'options.customization');
}
}
outputEncoding ??= 'hex';
let normalized = outputEncoding;
// Fast case: if it's 'hex', we don't need to validate it further.
if (normalized !== 'hex') {
validateString(outputEncoding, 'outputEncoding');
normalized = normalizeEncoding(outputEncoding);
// If the encoding is invalid, normalizeEncoding() returns undefined.
if (normalized === undefined) {
// normalizeEncoding() doesn't handle 'buffer'.
if (StringPrototypeToLowerCase(outputEncoding) === 'buffer') {
normalized = 'buffer';
} else {
throw new ERR_INVALID_ARG_VALUE('outputEncoding', outputEncoding);
}
}
}
if (outputLength !== undefined) {
validateUint32(outputLength, 'outputLength');
// Coerce -0 to +0.
outputLength += 0;
}
if (functionName === undefined && customization === undefined) {
return oneShotDigest(algorithm, getCachedHashId(algorithm), getHashCache(),
input, normalized, encodingsMap[normalized], outputLength);
}
return oneShotDigest(algorithm, getCachedHashId(algorithm), getHashCache(),
input, normalized, encodingsMap[normalized], outputLength,
functionName, customization);
}
module.exports = {
Hash,
Hmac,
asyncDigest,
hash,
};