node/lib/internal/crypto/webcrypto.js
James M Snell dae283d96f
crypto: refactoring internals, add WebCrypto
Fixes: https://github.com/nodejs/node/issues/678
Refs: https://github.com/nodejs/node/issues/26854

Signed-off-by: James M Snell <jasnell@gmail.com>

PR-URL: https://github.com/nodejs/node/pull/35093
Reviewed-By: Matteo Collina <matteo.collina@gmail.com>
Reviewed-By: Anna Henningsen <anna@addaleax.net>
Reviewed-By: Michaël Zasso <targos@protonmail.com>
Reviewed-By: Rich Trott <rtrott@gmail.com>
2020-10-07 17:27:05 -07:00

761 lines
21 KiB
JavaScript

'use strict';
const {
ArrayPrototypeIncludes,
JSONParse,
JSONStringify,
ObjectDefineProperties,
SafeSet,
SymbolToStringTag,
} = primordials;
const {
kWebCryptoKeyFormatRaw,
kWebCryptoKeyFormatPKCS8,
kWebCryptoKeyFormatSPKI,
kWebCryptoCipherEncrypt,
kWebCryptoCipherDecrypt,
} = internalBinding('crypto');
const {
validateArray,
validateBoolean,
validateObject,
validateOneOf,
validateString,
} = require('internal/validators');
const { TextDecoder } = require('internal/encoding');
const {
codes: {
ERR_INVALID_ARG_TYPE,
}
} = require('internal/errors');
const {
CryptoKey,
InternalCryptoKey,
createSecretKey,
isCryptoKey,
isKeyObject,
} = require('internal/crypto/keys');
const {
asyncDigest,
} = require('internal/crypto/hash');
const {
getArrayBufferOrView,
hasAnyNotIn,
lazyDOMException,
lazyRequire,
normalizeAlgorithm,
normalizeHashName,
validateMaxBufferLength,
kExportFormats,
kHandle,
kKeyObject,
} = require('internal/crypto/util');
const {
getRandomValues,
} = require('internal/crypto/random');
async function generateKey(
algorithm,
extractable,
keyUsages) {
algorithm = normalizeAlgorithm(algorithm);
validateBoolean(extractable, 'extractable');
validateArray(keyUsages, 'keyUsages');
switch (algorithm.name) {
case 'RSASSA-PKCS1-V1_5':
// Fall through
case 'RSA-PSS':
// Fall through
case 'RSA-OAEP':
return lazyRequire('internal/crypto/rsa')
.rsaKeyGenerate(algorithm, extractable, keyUsages);
case 'ECDSA':
// Fall through
case 'ECDH':
return lazyRequire('internal/crypto/ec')
.ecGenerateKey(algorithm, extractable, keyUsages);
case 'HMAC':
return lazyRequire('internal/crypto/mac')
.hmacGenerateKey(algorithm, extractable, keyUsages);
case 'AES-CTR':
// Fall through
case 'AES-CBC':
// Fall through
case 'AES-GCM':
// Fall through
case 'AES-KW':
return lazyRequire('internal/crypto/aes')
.aesGenerateKey(algorithm, extractable, keyUsages);
// Following are Node.js specific extensions. Names must be prefixed
// with the `NODE-`
case 'NODE-DSA':
return lazyRequire('internal/crypto/dsa')
.dsaGenerateKey(algorithm, extractable, keyUsages);
case 'NODE-DH':
return lazyRequire('internal/crypto/diffiehellman')
.dhGenerateKey(algorithm, extractable, keyUsages);
default:
throw lazyDOMException('Unrecognized name.');
}
}
async function deriveBits(algorithm, baseKey, length) {
algorithm = normalizeAlgorithm(algorithm);
if (!isCryptoKey(baseKey))
throw new ERR_INVALID_ARG_TYPE('baseKey', 'CryptoKey', baseKey);
if (!ArrayPrototypeIncludes(baseKey.usages, 'deriveBits')) {
throw lazyDOMException(
'baseKey does not have deriveBits usage',
'InvalidAccessError');
}
if (baseKey.algorithm.name !== algorithm.name)
throw lazyDOMException('Key algorithm mismatch', 'InvalidAccessError');
switch (algorithm.name) {
case 'ECDH':
return lazyRequire('internal/crypto/diffiehellman')
.asyncDeriveBitsECDH(algorithm, baseKey, length);
case 'HKDF':
return lazyRequire('internal/crypto/hkdf')
.hkdfDeriveBits(algorithm, baseKey, length);
case 'PBKDF2':
return lazyRequire('internal/crypto/pbkdf2')
.pbkdf2DeriveBits(algorithm, baseKey, length);
case 'NODE-SCRYPT':
return lazyRequire('internal/crypto/scrypt')
.asyncScryptDeriveBits(algorithm, baseKey, length);
case 'NODE-DH':
return lazyRequire('internal/crypto/diffiehellman')
.asyncDeriveBitsDH(algorithm, baseKey, length);
}
throw lazyDOMException('Unrecognized name.');
}
async function deriveKey(
algorithm,
baseKey,
derivedKeyAlgorithm,
extractable,
keyUsages) {
algorithm = normalizeAlgorithm(algorithm);
derivedKeyAlgorithm = normalizeAlgorithm(derivedKeyAlgorithm);
if (!isCryptoKey(baseKey))
throw new ERR_INVALID_ARG_TYPE('baseKey', 'CryptoKey', baseKey);
if (!ArrayPrototypeIncludes(baseKey.usages, 'deriveKey')) {
throw lazyDOMException(
'baseKey does not have deriveKey usage',
'InvalidAccessError');
}
if (baseKey.algorithm.name !== algorithm.name)
throw lazyDOMException('Key algorithm mismatch', 'InvalidAccessError');
validateObject(derivedKeyAlgorithm, 'derivedKeyAlgorithm');
validateBoolean(extractable, 'extractable');
validateArray(keyUsages, 'keyUsages');
const { length } = derivedKeyAlgorithm;
let bits;
switch (algorithm.name) {
case 'ECDH':
bits = await lazyRequire('internal/crypto/diffiehellman')
.asyncDeriveBitsECDH(algorithm, baseKey, length);
break;
case 'HKDF':
bits = await lazyRequire('internal/crypto/hkdf')
.hkdfDeriveBits(algorithm, baseKey, length);
break;
case 'PBKDF2':
bits = await lazyRequire('internal/crypto/pbkdf2')
.pbkdf2DeriveBits(algorithm, baseKey, length);
break;
case 'NODE-SCRYPT':
bits = await lazyRequire('internal/crypto/scrypt')
.scryptDeriveBits(algorithm, baseKey, length);
break;
case 'NODE-DH':
bits = await lazyRequire('internal/crypto/diffiehellman')
.asyncDeriveBitsDH(algorithm, baseKey, length);
break;
default:
throw lazyDOMException('Unrecognized name.');
}
return importKey('raw', bits, derivedKeyAlgorithm, extractable, keyUsages);
}
async function exportKeySpki(key) {
switch (key.algorithm.name) {
case 'RSASSA-PKCS1-V1_5':
// Fall through
case 'RSA-PSS':
// Fall through
case 'RSA-OAEP':
if (key.type === 'public') {
return lazyRequire('internal/crypto/rsa')
.rsaExportKey(key, kWebCryptoKeyFormatSPKI);
}
break;
case 'ECDSA':
// Fall through
case 'ECDH':
if (key.type === 'public') {
return lazyRequire('internal/crypto/ec')
.ecExportKey(key, kWebCryptoKeyFormatSPKI);
}
break;
case 'NODE-DSA':
if (key.type === 'public') {
return lazyRequire('internal/crypto/dsa')
.dsaExportKey(key, kWebCryptoKeyFormatSPKI);
}
break;
case 'NODE-DH':
if (key.type === 'public') {
return lazyRequire('internal/crypto/diffiehellman')
.dhExportKey(key, kWebCryptoKeyFormatSPKI);
}
break;
}
throw lazyDOMException(
`Unable to export a raw ${key.algorithm.name} ${key.type} key`,
'InvalidAccessError');
}
async function exportKeyPkcs8(key) {
switch (key.algorithm.name) {
case 'RSASSA-PKCS1-V1_5':
// Fall through
case 'RSA-PSS':
// Fall through
case 'RSA-OAEP':
if (key.type === 'private') {
return lazyRequire('internal/crypto/rsa')
.rsaExportKey(key, kWebCryptoKeyFormatPKCS8);
}
break;
case 'ECDSA':
// Fall through
case 'ECDH':
if (key.type === 'private') {
return lazyRequire('internal/crypto/ec')
.ecExportKey(key, kWebCryptoKeyFormatPKCS8);
}
break;
case 'NODE-DSA':
if (key.type === 'private') {
return lazyRequire('internal/crypto/dsa')
.dsaExportKey(key, kWebCryptoKeyFormatPKCS8);
}
break;
case 'NODE-DH':
if (key.type === 'private') {
return lazyRequire('internal/crypto/diffiehellman')
.dhExportKey(key, kWebCryptoKeyFormatPKCS8);
}
break;
}
throw lazyDOMException(
`Unable to export a pkcs8 ${key.algorithm.name} ${key.type} key`,
'InvalidAccessError');
}
async function exportKeyRaw(key) {
switch (key.algorithm.name) {
case 'ECDSA':
// Fall through
case 'ECDH':
if (key.type === 'public') {
return lazyRequire('internal/crypto/ec')
.ecExportKey(key, kWebCryptoKeyFormatRaw);
}
break;
case 'AES-CTR':
// Fall through
case 'AES-CBC':
// Fall through
case 'AES-GCM':
// Fall through
case 'AES-KW':
// Fall through
case 'HMAC':
return key[kKeyObject].export().buffer;
}
throw lazyDOMException(
`Unable to export a raw ${key.algorithm.name} ${key.type} key`,
'InvalidAccessError');
}
async function exportKeyJWK(key) {
const jwk = key[kKeyObject][kHandle].exportJwk({
key_ops: key.usages,
ext: key.extractable,
});
switch (key.algorithm.name) {
case 'RSASSA-PKCS1-V1_5':
jwk.alg = normalizeHashName(
key.algorithm.hash.name,
normalizeHashName.kContextJwkRsa);
return jwk;
case 'RSA-PSS':
jwk.alg = normalizeHashName(
key.algorithm.hash.name,
normalizeHashName.kContextJwkRsaPss);
return jwk;
case 'RSA-OAEP':
jwk.alg = normalizeHashName(
key.algorithm.hash.name,
normalizeHashName.kContextJwkRsaOaep);
return jwk;
case 'ECDSA':
// Fall through
case 'ECDH':
jwk.crv = key.algorithm.namedCurve;
return jwk;
case 'AES-CTR':
// Fall through
case 'AES-CBC':
// Fall through
case 'AES-GCM':
// Fall through
case 'AES-KW':
jwk.alg = lazyRequire('internal/crypto/aes')
.getAlgorithmName(key.algorithm.name, key.algorithm.length);
return jwk;
case 'HMAC':
jwk.alg = normalizeHashName(
key.algorithm.hash.name,
normalizeHashName.kContextJwkHmac);
return jwk;
case 'NODE-DSA':
jwk.alg = normalizeHashName(
key.algorithm.hash.name,
normalizeHashName.kContextJwkDsa);
return jwk;
default:
// Fall through
}
throw lazyDOMException('Not yet supported', 'NotSupportedError');
}
async function exportKey(format, key) {
validateString(format, 'format');
validateOneOf(format, 'format', kExportFormats);
if (!isCryptoKey(key))
throw new ERR_INVALID_ARG_TYPE('key', 'CryptoKey', key);
if (!key.extractable)
throw lazyDOMException('key is not extractable', 'InvalidAccessException');
switch (format) {
case 'node.keyObject': return key[kKeyObject];
case 'spki': return exportKeySpki(key);
case 'pkcs8': return exportKeyPkcs8(key);
case 'jwk': return exportKeyJWK(key);
case 'raw': return exportKeyRaw(key);
}
throw lazyDOMException(
'Export format is unsupported', 'NotSupportedError');
}
async function importGenericSecretKey(
{ name, length },
format,
keyData,
extractable,
keyUsages) {
const usagesSet = new SafeSet(keyUsages);
if (extractable)
throw lazyDOMException(`${name} keys are not extractable`, 'SyntaxError');
if (hasAnyNotIn(usagesSet, 'deriveKey', 'deriveBits')) {
throw lazyDOMException(
`Unsupported key usage for a ${name} key`,
'SyntaxError');
}
switch (format) {
case 'node.keyObject': {
if (!isKeyObject(keyData))
throw new ERR_INVALID_ARG_TYPE('keyData', 'KeyObject', keyData);
if (keyData.type === 'secret')
return new InternalCryptoKey(keyData, { name }, keyUsages, extractable);
break;
}
case 'raw':
if (hasAnyNotIn(usagesSet, 'deriveKey', 'deriveBits')) {
throw lazyDOMException(
`Unsupported key usage for a ${name} key`,
'SyntaxError');
}
const checkLength = keyData.byteLength * 8;
if (checkLength === 0 || length === 0)
throw lazyDOMException('Zero-length key is not supported', 'DataError');
// The Web Crypto spec allows for key lengths that are not multiples of
// 8. We don't. Our check here is stricter than that defined by the spec
// in that we require that algorithm.length match keyData.length * 8 if
// algorithm.length is specified.
if (length !== undefined && length !== checkLength) {
throw lazyDOMException('Invalid key length', 'DataError');
}
const keyObject = createSecretKey(keyData);
return new InternalCryptoKey(keyObject, { name }, keyUsages, false);
}
throw lazyDOMException(
`Unable to import ${name} key with format ${format}`,
'NotSupportedError');
}
async function importKey(
format,
keyData,
algorithm,
extractable,
keyUsages) {
validateString(format, 'format');
validateOneOf(format, 'format', kExportFormats);
if (format !== 'node.keyObject' && format !== 'jwk')
keyData = getArrayBufferOrView(keyData, 'keyData');
algorithm = normalizeAlgorithm(algorithm);
validateBoolean(extractable, 'extractable');
validateArray(keyUsages, 'keyUsages');
switch (algorithm.name) {
case 'RSASSA-PKCS1-V1_5':
// Fall through
case 'RSA-PSS':
// Fall through
case 'RSA-OAEP':
return lazyRequire('internal/crypto/rsa')
.rsaImportKey(format, keyData, algorithm, extractable, keyUsages);
case 'ECDSA':
// Fall through
case 'ECDH':
return lazyRequire('internal/crypto/ec')
.ecImportKey(format, keyData, algorithm, extractable, keyUsages);
case 'HMAC':
return lazyRequire('internal/crypto/mac')
.hmacImportKey(format, keyData, algorithm, extractable, keyUsages);
case 'AES-CTR':
// Fall through
case 'AES-CBC':
// Fall through
case 'AES-GCM':
// Fall through
case 'AES-KW':
return lazyRequire('internal/crypto/aes')
.aesImportKey(algorithm, format, keyData, extractable, keyUsages);
case 'HKDF':
// Fall through
case 'NODE-SCRYPT':
// Fall through
case 'PBKDF2':
return importGenericSecretKey(
algorithm,
format,
keyData,
extractable,
keyUsages);
case 'NODE-DSA':
return lazyRequire('internal/crypto/dsa')
.dsaImportKey(format, keyData, algorithm, extractable, keyUsages);
case 'NODE-DH':
return lazyRequire('internal/crypto/diffiehellman')
.dhImportKey(format, keyData, algorithm, extractable, keyUsages);
}
throw lazyDOMException('Unrecognized name.', 'NotSupportedError');
}
// subtle.wrapKey() is essentially a subtle.exportKey() followed
// by a subtle.encrypt().
async function wrapKey(format, key, wrappingKey, algorithm) {
algorithm = normalizeAlgorithm(algorithm);
let keyData = await exportKey(format, key);
if (format === 'jwk') {
if (keyData == null || typeof keyData !== 'object')
throw lazyDOMException('Invalid exported JWK key', 'DataError');
const ec = new TextEncoder();
const raw = JSONStringify(keyData);
keyData = ec.encode(raw + ' '.repeat(8 - (raw.length % 8)));
}
return cipherOrWrap(
kWebCryptoCipherEncrypt,
algorithm,
wrappingKey,
keyData,
'wrapKey');
}
// subtle.unwrapKey() is essentially a subtle.decrypt() followed
// by a subtle.importKey().
async function unwrapKey(
format,
wrappedKey,
unwrappingKey,
unwrapAlgo,
unwrappedKeyAlgo,
extractable,
keyUsages) {
wrappedKey = getArrayBufferOrView(wrappedKey, 'wrappedKey');
let keyData = await cipherOrWrap(
kWebCryptoCipherDecrypt,
normalizeAlgorithm(unwrapAlgo),
unwrappingKey,
wrappedKey,
'unwrapKey');
if (format === 'jwk') {
// The fatal: true option is only supported in builds that have ICU.
const options = process.versions.icu !== undefined ?
{ fatal: true } : undefined;
const dec = new TextDecoder('utf-8', options);
try {
keyData = JSONParse(dec.decode(keyData));
} catch {
throw lazyDOMException('Invalid imported JWK key', 'DataError');
}
}
return importKey(format, keyData, unwrappedKeyAlgo, extractable, keyUsages);
}
function signVerify(algorithm, key, data, signature) {
algorithm = normalizeAlgorithm(algorithm);
if (!isCryptoKey(key))
throw new ERR_INVALID_ARG_TYPE('key', 'CryptoKey', key);
data = getArrayBufferOrView(data, 'data');
let usage = 'sign';
if (signature !== undefined) {
signature = getArrayBufferOrView(signature, 'signature');
usage = 'verify';
}
if (!ArrayPrototypeIncludes(key.usages, usage) ||
algorithm.name !== key.algorithm.name) {
throw lazyDOMException(
`Unable to use this key to ${usage}`,
'InvalidAccessError');
}
switch (algorithm.name) {
case 'RSA-PSS':
// Fall through
case 'RSASSA-PKCS1-V1_5':
return lazyRequire('internal/crypto/rsa')
.rsaSignVerify(key, data, algorithm, signature);
case 'ECDSA':
return lazyRequire('internal/crypto/ec')
.ecdsaSignVerify(key, data, algorithm, signature);
case 'HMAC':
return lazyRequire('internal/crypto/mac')
.hmacSignVerify(key, data, algorithm, signature);
// The following are Node.js specific extensions. They must begin with
// the `NODE-` prefix
case 'NODE-DSA':
return lazyRequire('internal/crypto/dsa')
.dsaSignVerify(key, data, algorithm, signature);
}
throw lazyDOMException('Unrecognized named.', 'NotSupportedError');
}
async function sign(algorithm, key, data) {
return signVerify(algorithm, key, data);
}
async function verify(algorithm, key, signature, data) {
return signVerify(algorithm, key, data, signature);
}
async function cipherOrWrap(mode, algorithm, key, data, op) {
algorithm = normalizeAlgorithm(algorithm);
// We use a Node.js style error here instead of a DOMException because
// the WebCrypto spec is not specific what kind of error is to be thrown
// in this case. Both Firefox and Chrome throw simple TypeErrors here.
if (!isCryptoKey(key))
throw new ERR_INVALID_ARG_TYPE('key', 'CryptoKey', key);
// The key algorithm and cipher algorithm must match, and the
// key must have the proper usage.
if (key.algorithm.name !== algorithm.name ||
!ArrayPrototypeIncludes(key.usages, op)) {
throw lazyDOMException(
'The requested operation is not valid for the provided key',
'InvalidAccessError');
}
// For the Web Crypto API, the input data can be any ArrayBuffer,
// TypedArray, or DataView.
data = getArrayBufferOrView(data, 'data');
// While WebCrypto allows for larger input buffer sizes, we limit
// those to sizes that can fit within uint32_t because of limitations
// in the OpenSSL API.
validateMaxBufferLength(data, 'data');
switch (algorithm.name) {
case 'RSA-OAEP':
return lazyRequire('internal/crypto/rsa')
.rsaCipher(mode, key, data, algorithm);
case 'AES-CTR':
// Fall through
case 'AES-CBC':
// Fall through
case 'AES-GCM':
return lazyRequire('internal/crypto/aes')
.aesCipher(mode, key, data, algorithm);
case 'AES-KW':
if (op === 'wrapKey' || op === 'unwrapKey') {
return lazyRequire('internal/crypto/aes')
.aesCipher(mode, key, data, algorithm);
}
}
throw lazyDOMException('Unrecognized name.', 'NotSupportedError');
}
async function encrypt(algorithm, key, data) {
return cipherOrWrap(kWebCryptoCipherEncrypt, algorithm, key, data, 'encrypt');
}
async function decrypt(algorithm, key, data) {
return cipherOrWrap(kWebCryptoCipherDecrypt, algorithm, key, data, 'decrypt');
}
// The SubtleCrypto and Crypto classes are defined as part of the
// Web Crypto API standard: https://www.w3.org/TR/WebCryptoAPI/
class SubtleCrypto {}
const subtle = new SubtleCrypto();
class Crypto {}
const crypto = new Crypto();
ObjectDefineProperties(
Crypto.prototype, {
[SymbolToStringTag]: {
enumerable: false,
configurable: true,
writable: false,
value: 'Crypto',
},
subtle: {
enumerable: true,
configurable: false,
value: subtle,
},
getRandomValues: {
enumerable: true,
configurable: true,
writable: true,
value: getRandomValues,
},
CryptoKey: {
enumerable: true,
configurable: true,
writable: true,
value: CryptoKey,
}
});
ObjectDefineProperties(
SubtleCrypto.prototype, {
[SymbolToStringTag]: {
enumerable: false,
configurable: true,
writable: false,
value: 'SubtleCrypto',
},
encrypt: {
enumerable: true,
configurable: true,
writable: true,
value: encrypt,
},
decrypt: {
enumerable: true,
configurable: true,
writable: true,
value: decrypt,
},
sign: {
enumerable: true,
configurable: true,
writable: true,
value: sign,
},
verify: {
enumerable: true,
configurable: true,
writable: true,
value: verify,
},
digest: {
enumerable: true,
configurable: true,
writable: true,
value: asyncDigest,
},
generateKey: {
enumerable: true,
configurable: true,
writable: true,
value: generateKey,
},
deriveKey: {
enumerable: true,
configurable: true,
writable: true,
value: deriveKey,
},
deriveBits: {
enumerable: true,
configurable: true,
writable: true,
value: deriveBits,
},
importKey: {
enumerable: true,
configurable: true,
writable: true,
value: importKey,
},
exportKey: {
enumerable: true,
configurable: true,
writable: true,
value: exportKey,
},
wrapKey: {
enumerable: true,
configurable: true,
writable: true,
value: wrapKey,
},
unwrapKey: {
enumerable: true,
configurable: true,
writable: true,
value: unwrapKey,
}
});
module.exports = crypto;