node/lib/internal/crypto/rsa.js
Filip Skokan 0d80e7344e
crypto: add causes to applicable webcrypto's OperationError
PR-URL: https://github.com/nodejs/node/pull/44890
Reviewed-By: Luigi Pinca <luigipinca@gmail.com>
Reviewed-By: Antoine du Hamel <duhamelantoine1995@gmail.com>
2022-10-10 01:12:28 +00:00

372 lines
8.8 KiB
JavaScript

'use strict';
const {
SafeSet,
Uint8Array,
} = primordials;
const {
KeyObjectHandle,
RSACipherJob,
RSAKeyExportJob,
SignJob,
kCryptoJobAsync,
kSignJobModeSign,
kSignJobModeVerify,
kKeyVariantRSA_SSA_PKCS1_v1_5,
kKeyVariantRSA_PSS,
kKeyVariantRSA_OAEP,
kKeyTypePrivate,
kWebCryptoCipherEncrypt,
RSA_PKCS1_PSS_PADDING,
} = internalBinding('crypto');
const {
codes: {
ERR_INVALID_ARG_TYPE,
ERR_MISSING_OPTION,
}
} = require('internal/errors');
const {
validateInt32,
validateUint32,
} = require('internal/validators');
const {
bigIntArrayToUnsignedInt,
getArrayBufferOrView,
getUsagesUnion,
hasAnyNotIn,
jobPromise,
normalizeHashName,
validateKeyOps,
validateMaxBufferLength,
kHandle,
kKeyObject,
} = require('internal/crypto/util');
const {
lazyDOMException,
promisify,
} = require('internal/util');
const {
isUint8Array,
} = require('internal/util/types');
const {
InternalCryptoKey,
PrivateKeyObject,
PublicKeyObject,
createPublicKey,
createPrivateKey,
} = require('internal/crypto/keys');
const {
generateKeyPair: _generateKeyPair,
} = require('internal/crypto/keygen');
const kRsaVariants = {
'RSASSA-PKCS1-v1_5': kKeyVariantRSA_SSA_PKCS1_v1_5,
'RSA-PSS': kKeyVariantRSA_PSS,
'RSA-OAEP': kKeyVariantRSA_OAEP,
};
const generateKeyPair = promisify(_generateKeyPair);
function verifyAcceptableRsaKeyUse(name, type, usages) {
let checkSet;
switch (name) {
case 'RSA-OAEP':
switch (type) {
case 'private':
checkSet = ['decrypt', 'unwrapKey'];
break;
case 'public':
checkSet = ['encrypt', 'wrapKey'];
break;
}
break;
default:
switch (type) {
case 'private':
checkSet = ['sign'];
break;
case 'public':
checkSet = ['verify'];
break;
}
}
if (hasAnyNotIn(usages, checkSet)) {
throw lazyDOMException(
`Unsupported key usage for an ${name} key`,
'SyntaxError');
}
}
function rsaOaepCipher(mode, key, data, { label }) {
const type = mode === kWebCryptoCipherEncrypt ? 'public' : 'private';
if (key.type !== type) {
throw lazyDOMException(
'The requested operation is not valid for the provided key',
'InvalidAccessError');
}
if (label !== undefined) {
label = getArrayBufferOrView(label, 'algorithm.label');
validateMaxBufferLength(label, 'algorithm.label');
}
return jobPromise(new RSACipherJob(
kCryptoJobAsync,
mode,
key[kKeyObject][kHandle],
data,
kKeyVariantRSA_OAEP,
normalizeHashName(key.algorithm.hash.name),
label));
}
async function rsaKeyGenerate(
algorithm,
extractable,
keyUsages) {
const {
name,
modulusLength,
publicExponent,
hash
} = algorithm;
if (hash === undefined)
throw new ERR_MISSING_OPTION('algorithm.hash');
validateUint32(modulusLength, 'algorithm.modulusLength');
if (!isUint8Array(publicExponent)) {
throw new ERR_INVALID_ARG_TYPE(
'algorithm.publicExponent',
'Uint8Array',
publicExponent);
}
const usageSet = new SafeSet(keyUsages);
const publicExponentConverted = bigIntArrayToUnsignedInt(publicExponent);
if (publicExponentConverted === undefined) {
throw lazyDOMException(
'The publicExponent must be equivalent to an unsigned 32-bit value',
'OperationError');
}
switch (name) {
case 'RSA-OAEP':
if (hasAnyNotIn(usageSet,
['encrypt', 'decrypt', 'wrapKey', 'unwrapKey'])) {
throw lazyDOMException(
'Unsupported key usage for a RSA key',
'SyntaxError');
}
break;
default:
if (hasAnyNotIn(usageSet, ['sign', 'verify'])) {
throw lazyDOMException(
'Unsupported key usage for a RSA key',
'SyntaxError');
}
}
const keypair = await generateKeyPair('rsa', {
modulusLength,
publicExponent: publicExponentConverted,
}).catch((err) => {
throw lazyDOMException(
'The operation failed for an operation-specific reason',
{ name: 'OperationError', cause: err });
});
const keyAlgorithm = {
name,
modulusLength,
publicExponent,
hash: { name: hash.name }
};
let publicUsages;
let privateUsages;
switch (name) {
case 'RSA-OAEP': {
publicUsages = getUsagesUnion(usageSet, 'encrypt', 'wrapKey');
privateUsages = getUsagesUnion(usageSet, 'decrypt', 'unwrapKey');
break;
}
default: {
publicUsages = getUsagesUnion(usageSet, 'verify');
privateUsages = getUsagesUnion(usageSet, 'sign');
break;
}
}
const publicKey =
new InternalCryptoKey(
keypair.publicKey,
keyAlgorithm,
publicUsages,
true);
const privateKey =
new InternalCryptoKey(
keypair.privateKey,
keyAlgorithm,
privateUsages,
extractable);
return { publicKey, privateKey };
}
function rsaExportKey(key, format) {
return jobPromise(new RSAKeyExportJob(
kCryptoJobAsync,
format,
key[kKeyObject][kHandle],
kRsaVariants[key.algorithm.name]));
}
async function rsaImportKey(
format,
keyData,
algorithm,
extractable,
keyUsages) {
const { hash } = algorithm;
if (hash === undefined)
throw new ERR_MISSING_OPTION('algorithm.hash');
const usagesSet = new SafeSet(keyUsages);
let keyObject;
switch (format) {
case 'spki': {
verifyAcceptableRsaKeyUse(algorithm.name, 'public', usagesSet);
keyObject = createPublicKey({
key: keyData,
format: 'der',
type: 'spki'
});
break;
}
case 'pkcs8': {
verifyAcceptableRsaKeyUse(algorithm.name, 'private', usagesSet);
keyObject = createPrivateKey({
key: keyData,
format: 'der',
type: 'pkcs8'
});
break;
}
case 'jwk': {
if (keyData == null || typeof keyData !== 'object')
throw lazyDOMException('Invalid JWK keyData', 'DataError');
verifyAcceptableRsaKeyUse(
algorithm.name,
keyData.d !== undefined ? 'private' : 'public',
usagesSet);
if (keyData.kty !== 'RSA')
throw lazyDOMException('Invalid key type', 'DataError');
if (usagesSet.size > 0 && keyData.use !== undefined) {
const checkUse = algorithm.name === 'RSA-OAEP' ? 'enc' : 'sig';
if (keyData.use !== checkUse)
throw lazyDOMException('Invalid use type', 'DataError');
}
validateKeyOps(keyData.key_ops, usagesSet);
if (keyData.ext !== undefined &&
keyData.ext === false &&
extractable === true) {
throw lazyDOMException('JWK is not extractable', 'DataError');
}
if (keyData.alg !== undefined) {
if (typeof keyData.alg !== 'string')
throw lazyDOMException('Invalid alg', 'DataError');
const hash =
normalizeHashName(keyData.alg, normalizeHashName.kContextWebCrypto);
if (hash !== algorithm.hash.name)
throw lazyDOMException('Hash mismatch', 'DataError');
}
const handle = new KeyObjectHandle();
const type = handle.initJwk(keyData);
if (type === undefined)
throw lazyDOMException('Invalid JWK keyData', 'DataError');
keyObject = type === kKeyTypePrivate ?
new PrivateKeyObject(handle) :
new PublicKeyObject(handle);
break;
}
default:
throw lazyDOMException(
`Unable to import RSA key with format ${format}`,
'NotSupportedError');
}
if (keyObject.asymmetricKeyType !== 'rsa') {
throw lazyDOMException('Invalid key type', 'DataError');
}
const {
modulusLength,
publicExponent,
} = keyObject[kHandle].keyDetail({});
return new InternalCryptoKey(keyObject, {
name: algorithm.name,
modulusLength,
publicExponent: new Uint8Array(publicExponent),
hash: algorithm.hash
}, keyUsages, extractable);
}
function rsaSignVerify(key, data, { saltLength }, signature) {
let padding;
if (key.algorithm.name === 'RSA-PSS') {
padding = RSA_PKCS1_PSS_PADDING;
// TODO(@jasnell): Validate maximum size of saltLength
// based on the key size:
// Math.ceil((keySizeInBits - 1)/8) - digestSizeInBytes - 2
validateInt32(saltLength, 'algorithm.saltLength', -2);
}
const mode = signature === undefined ? kSignJobModeSign : kSignJobModeVerify;
const type = mode === kSignJobModeSign ? 'private' : 'public';
if (key.type !== type)
throw lazyDOMException(`Key must be a ${type} key`, 'InvalidAccessError');
return jobPromise(new SignJob(
kCryptoJobAsync,
signature === undefined ? kSignJobModeSign : kSignJobModeVerify,
key[kKeyObject][kHandle],
undefined,
undefined,
undefined,
data,
normalizeHashName(key.algorithm.hash.name),
saltLength,
padding,
undefined,
signature));
}
module.exports = {
rsaCipher: rsaOaepCipher,
rsaExportKey,
rsaImportKey,
rsaKeyGenerate,
rsaSignVerify,
};