node/lib/internal/crypto/rsa.js
Filip Skokan 3ef38c4bd7
crypto: use WebIDL converters in WebCryptoAPI
WebCryptoAPI functions' arguments are now coersed and validated as per
their WebIDL definitions like in other Web Crypto API implementations.
This further improves interoperability with other implementations of
Web Crypto API.

PR-URL: https://github.com/nodejs/node/pull/46067
Reviewed-By: Antoine du Hamel <duhamelantoine1995@gmail.com>
Reviewed-By: Chengzhong Wu <legendecas@gmail.com>
2023-01-17 08:57:58 +00:00

348 lines
8.4 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 {
validateInt32,
} = require('internal/validators');
const {
bigIntArrayToUnsignedInt,
getUsagesUnion,
hasAnyNotIn,
jobPromise,
normalizeHashName,
validateKeyOps,
validateMaxBufferLength,
kHandle,
kKeyObject,
} = require('internal/crypto/util');
const {
lazyDOMException,
promisify,
} = require('internal/util');
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, isPublic, usages) {
let checkSet;
switch (name) {
case 'RSA-OAEP':
checkSet = isPublic ? ['encrypt', 'wrapKey'] : ['decrypt', 'unwrapKey'];
break;
case 'RSA-PSS':
// Fall through
case 'RSASSA-PKCS1-v1_5':
checkSet = isPublic ? ['verify'] : ['sign'];
break;
default:
throw lazyDOMException(
'The algorithm is not supported', 'NotSupportedError');
}
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) {
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;
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 { __proto__: null, 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 usagesSet = new SafeSet(keyUsages);
let keyObject;
switch (format) {
case 'spki': {
verifyAcceptableRsaKeyUse(algorithm.name, true, usagesSet);
try {
keyObject = createPublicKey({
key: keyData,
format: 'der',
type: 'spki'
});
} catch (err) {
throw lazyDOMException(
'Invalid keyData', { name: 'DataError', cause: err });
}
break;
}
case 'pkcs8': {
verifyAcceptableRsaKeyUse(algorithm.name, false, usagesSet);
try {
keyObject = createPrivateKey({
key: keyData,
format: 'der',
type: 'pkcs8'
});
} catch (err) {
throw lazyDOMException(
'Invalid keyData', { name: 'DataError', cause: err });
}
break;
}
case 'jwk': {
if (!keyData.kty)
throw lazyDOMException('Invalid keyData', 'DataError');
if (keyData.kty !== 'RSA')
throw lazyDOMException('Invalid JWK "kty" Parameter', 'DataError');
verifyAcceptableRsaKeyUse(
algorithm.name,
keyData.d === undefined,
usagesSet);
if (usagesSet.size > 0 && keyData.use !== undefined) {
const checkUse = algorithm.name === 'RSA-OAEP' ? 'enc' : 'sig';
if (keyData.use !== checkUse)
throw lazyDOMException('Invalid JWK "use" Parameter', 'DataError');
}
validateKeyOps(keyData.key_ops, usagesSet);
if (keyData.ext !== undefined &&
keyData.ext === false &&
extractable === true) {
throw lazyDOMException(
'JWK "ext" Parameter and extractable mismatch',
'DataError');
}
if (keyData.alg !== undefined) {
const hash =
normalizeHashName(keyData.alg, normalizeHashName.kContextWebCrypto);
if (hash !== algorithm.hash.name)
throw lazyDOMException(
'JWK "alg" does not match the requested algorithm',
'DataError');
}
const handle = new KeyObjectHandle();
const type = handle.initJwk(keyData);
if (type === undefined)
throw lazyDOMException('Invalid JWK', '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,
};