From dab4b2f03efc5772773e66fdb757db5571deee4d Mon Sep 17 00:00:00 2001 From: Filip Skokan Date: Thu, 1 Apr 2021 23:13:16 +0200 Subject: [PATCH] fix(node): check CryptoKey algorithm & usage before exporting KeyObject --- src/runtime/node/aeskw.ts | 10 +- src/runtime/node/decrypt.ts | 2 +- src/runtime/node/ecdhes.ts | 4 +- src/runtime/node/encrypt.ts | 2 +- src/runtime/node/get_sign_verify_key.ts | 6 +- src/runtime/node/key_to_jwk.ts | 3 + src/runtime/node/pbes2kw.ts | 8 +- src/runtime/node/rsaes.ts | 10 +- src/runtime/node/sign.ts | 2 +- src/runtime/node/verify.ts | 4 +- src/runtime/node/webcrypto.ts | 116 +++++++++++++++++++++++- 11 files changed, 142 insertions(+), 25 deletions(-) diff --git a/src/runtime/node/aeskw.ts b/src/runtime/node/aeskw.ts index 1761161f..2922a966 100644 --- a/src/runtime/node/aeskw.ts +++ b/src/runtime/node/aeskw.ts @@ -3,7 +3,7 @@ import { JOSENotSupported } from '../../util/errors.js' import type { AesKwUnwrapFunction, AesKwWrapFunction } from '../interfaces.d' import { concat } from '../../lib/buffer_utils.js' import getSecretKey from './secret_key.js' -import { isCryptoKey, getKeyObject as exportCryptoKey } from './webcrypto.js' +import { isCryptoKey, getKeyObject } from './webcrypto.js' function checkKeySize(key: KeyObject, alg: string) { if (key.symmetricKeySize! << 3 !== parseInt(alg.substr(1, 3), 10)) { @@ -11,7 +11,7 @@ function checkKeySize(key: KeyObject, alg: string) { } } -function getKeyObject(key: unknown) { +function ensureKeyObject(key: unknown, alg: string, usage: KeyUsage) { if (key instanceof KeyObject) { return key } @@ -19,7 +19,7 @@ function getKeyObject(key: unknown) { return getSecretKey(key) } if (isCryptoKey(key)) { - return exportCryptoKey(key) + return getKeyObject(key, alg, new Set([usage])) } throw new TypeError('invalid key input') @@ -33,7 +33,7 @@ export const wrap: AesKwWrapFunction = async (alg: string, key: unknown, cek: Ui `alg ${alg} is unsupported either by JOSE or your javascript runtime`, ) } - const keyObject = getKeyObject(key) + const keyObject = ensureKeyObject(key, alg, 'wrapKey') checkKeySize(keyObject, alg) const cipher = createCipheriv(algorithm, keyObject, Buffer.alloc(8, 0xa6)) return concat(cipher.update(cek), cipher.final()) @@ -51,7 +51,7 @@ export const unwrap: AesKwUnwrapFunction = async ( `alg ${alg} is unsupported either by JOSE or your javascript runtime`, ) } - const keyObject = getKeyObject(key) + const keyObject = ensureKeyObject(key, alg, 'unwrapKey') checkKeySize(keyObject, alg) const cipher = createDecipheriv(algorithm, keyObject, Buffer.alloc(8, 0xa6)) return concat(cipher.update(encryptedKey), cipher.final()) diff --git a/src/runtime/node/decrypt.ts b/src/runtime/node/decrypt.ts index a1a1e2b2..fc8cb471 100644 --- a/src/runtime/node/decrypt.ts +++ b/src/runtime/node/decrypt.ts @@ -96,7 +96,7 @@ const decrypt: DecryptFunction = async ( let key: KeyLike if (isCryptoKey(cek)) { // eslint-disable-next-line no-param-reassign - key = getKeyObject(cek) + key = getKeyObject(cek, enc, new Set(['decrypt'])) } else if (cek instanceof Uint8Array || cek instanceof KeyObject) { key = cek } else { diff --git a/src/runtime/node/ecdhes.ts b/src/runtime/node/ecdhes.ts index 762d78bc..c05c6ea5 100644 --- a/src/runtime/node/ecdhes.ts +++ b/src/runtime/node/ecdhes.ts @@ -31,7 +31,7 @@ export const deriveKey: EcdhESDeriveKeyFunction = async ( if (isCryptoKey(publicKey)) { // eslint-disable-next-line no-param-reassign - publicKey = getKeyObject(publicKey) + publicKey = getKeyObject(publicKey, 'ECDH-ES') } if (!(publicKey instanceof KeyObject)) { throw new TypeError('invalid key input') @@ -39,7 +39,7 @@ export const deriveKey: EcdhESDeriveKeyFunction = async ( if (isCryptoKey(privateKey)) { // eslint-disable-next-line no-param-reassign - privateKey = getKeyObject(privateKey) + privateKey = getKeyObject(privateKey, 'ECDH-ES', new Set(['deriveBits', 'deriveKey'])) } if (!(privateKey instanceof KeyObject)) { throw new TypeError('invalid key input') diff --git a/src/runtime/node/encrypt.ts b/src/runtime/node/encrypt.ts index 6476badb..f2a81218 100644 --- a/src/runtime/node/encrypt.ts +++ b/src/runtime/node/encrypt.ts @@ -66,7 +66,7 @@ const encrypt: EncryptFunction = async ( let key: KeyLike if (isCryptoKey(cek)) { // eslint-disable-next-line no-param-reassign - key = getKeyObject(cek) + key = getKeyObject(cek, enc, new Set(['encrypt'])) } else if (cek instanceof Uint8Array || cek instanceof KeyObject) { key = cek } else { diff --git a/src/runtime/node/get_sign_verify_key.ts b/src/runtime/node/get_sign_verify_key.ts index ecf8b962..9bf49c1b 100644 --- a/src/runtime/node/get_sign_verify_key.ts +++ b/src/runtime/node/get_sign_verify_key.ts @@ -1,8 +1,8 @@ import * as crypto from 'crypto' -import { isCryptoKey, getKeyObject as exportCryptoKey } from './webcrypto.js' +import { isCryptoKey, getKeyObject } from './webcrypto.js' import getSecretKey from './secret_key.js' -export default function getKeyObject(alg: string, key: unknown) { +export default function getSignVerifyKey(alg: string, key: unknown, usage: KeyUsage) { if (key instanceof crypto.KeyObject) { return key } @@ -13,7 +13,7 @@ export default function getKeyObject(alg: string, key: unknown) { return getSecretKey(key) } if (isCryptoKey(key)) { - return exportCryptoKey(key) + return getKeyObject(key, alg, new Set([usage])) } throw new TypeError('invalid key input') } diff --git a/src/runtime/node/key_to_jwk.ts b/src/runtime/node/key_to_jwk.ts index e695923d..f34a3db3 100644 --- a/src/runtime/node/key_to_jwk.ts +++ b/src/runtime/node/key_to_jwk.ts @@ -17,6 +17,9 @@ const jwkExportSupported = major >= 16 || (major === 15 && minor >= 9) const keyToJWK: JWKConvertFunction = (key: unknown): JWK => { let keyObject: KeyObject if (isCryptoKey(key)) { + if (!key.extractable) { + throw new TypeError('CryptoKey is not extractable') + } keyObject = getKeyObject(key) } else if (key instanceof KeyObject) { keyObject = key diff --git a/src/runtime/node/pbes2kw.ts b/src/runtime/node/pbes2kw.ts index f414c456..a6bab7d0 100644 --- a/src/runtime/node/pbes2kw.ts +++ b/src/runtime/node/pbes2kw.ts @@ -10,7 +10,7 @@ import { isCryptoKey, getKeyObject } from './webcrypto.js' const pbkdf2 = promisify(pbkdf2cb) -function getPassword(key: unknown) { +function getPassword(key: unknown, alg: string) { if (key instanceof KeyObject) { return key.export() } @@ -18,7 +18,7 @@ function getPassword(key: unknown) { return key } if (isCryptoKey(key)) { - return getKeyObject(key).export() + return getKeyObject(key, alg, new Set(['deriveBits', 'deriveKey'])).export() } throw new TypeError('invalid key input') } @@ -33,7 +33,7 @@ export const encrypt: Pbes2KWEncryptFunction = async ( checkP2s(p2s) const salt = concatSalt(alg, p2s) const keylen = parseInt(alg.substr(13, 3), 10) >> 3 - const password = getPassword(key) + const password = getPassword(key, alg) const derivedKey = await pbkdf2(password, salt, p2c, keylen, `sha${alg.substr(8, 3)}`) const encryptedKey = await wrap(alg.substr(-6), derivedKey, cek) @@ -51,7 +51,7 @@ export const decrypt: Pbes2KWDecryptFunction = async ( checkP2s(p2s) const salt = concatSalt(alg, p2s) const keylen = parseInt(alg.substr(13, 3), 10) >> 3 - const password = getPassword(key) + const password = getPassword(key, alg) const derivedKey = await pbkdf2(password, salt, p2c, keylen, `sha${alg.substr(8, 3)}`) diff --git a/src/runtime/node/rsaes.ts b/src/runtime/node/rsaes.ts index c0cf963f..8f1795ab 100644 --- a/src/runtime/node/rsaes.ts +++ b/src/runtime/node/rsaes.ts @@ -1,7 +1,7 @@ import { KeyObject, publicEncrypt, constants, privateDecrypt } from 'crypto' import type { RsaEsDecryptFunction, RsaEsEncryptFunction } from '../interfaces.d' import checkModulusLength from './check_modulus_length.js' -import { isCryptoKey, getKeyObject as exportCryptoKey } from './webcrypto.js' +import { isCryptoKey, getKeyObject } from './webcrypto.js' const checkKey = (key: KeyObject, alg: string) => { if (key.type === 'secret' || key.asymmetricKeyType !== 'rsa') { @@ -39,12 +39,12 @@ const resolveOaepHash = (alg: string) => { } } -function getKeyObject(key: unknown) { +function ensureKeyObject(key: unknown, alg: string, ...usages: KeyUsage[]) { if (key instanceof KeyObject) { return key } if (isCryptoKey(key)) { - return exportCryptoKey(key) + return getKeyObject(key, alg, new Set(usages)) } throw new TypeError('invalid key input') } @@ -52,7 +52,7 @@ function getKeyObject(key: unknown) { export const encrypt: RsaEsEncryptFunction = async (alg: string, key: unknown, cek: Uint8Array) => { const padding = resolvePadding(alg) const oaepHash = resolveOaepHash(alg) - const keyObject = getKeyObject(key) + const keyObject = ensureKeyObject(key, alg, 'wrapKey', 'encrypt') checkKey(keyObject, alg) return publicEncrypt({ key: keyObject, oaepHash, padding }, cek) @@ -65,7 +65,7 @@ export const decrypt: RsaEsDecryptFunction = async ( ) => { const padding = resolvePadding(alg) const oaepHash = resolveOaepHash(alg) - const keyObject = getKeyObject(key) + const keyObject = ensureKeyObject(key, alg, 'unwrapKey', 'decrypt') checkKey(keyObject, alg) return privateDecrypt({ key: keyObject, oaepHash, padding }, encryptedKey) diff --git a/src/runtime/node/sign.ts b/src/runtime/node/sign.ts index 26b31af8..f0812ef7 100644 --- a/src/runtime/node/sign.ts +++ b/src/runtime/node/sign.ts @@ -14,7 +14,7 @@ if (oneShotSign.length > 3) { } const sign: SignFunction = async (alg, key: unknown, data) => { - const keyObject = getSignKey(alg, key) + const keyObject = getSignKey(alg, key, 'sign') if (alg.startsWith('HS')) { const bitlen = parseInt(alg.substr(-3), 10) diff --git a/src/runtime/node/verify.ts b/src/runtime/node/verify.ts index f42de1e7..885171a4 100644 --- a/src/runtime/node/verify.ts +++ b/src/runtime/node/verify.ts @@ -22,7 +22,7 @@ if (oneShotVerify.length > 4 && oneShotCallbackSupported) { const verify: VerifyFunction = async (alg, key: unknown, signature, data) => { if (alg.startsWith('HS')) { - const expected = await sign(alg, key, data) + const expected = await sign(alg, getVerifyKey(alg, key, 'verify'), data) const actual = signature try { return crypto.timingSafeEqual(actual, expected) @@ -33,7 +33,7 @@ const verify: VerifyFunction = async (alg, key: unknown, signature, data) => { } const algorithm = nodeDigest(alg) - const keyObject = getVerifyKey(alg, key) + const keyObject = getVerifyKey(alg, key, 'verify') const keyInput = nodeKey(alg, keyObject) try { return oneShotVerify(algorithm, data, keyInput, signature) diff --git a/src/runtime/node/webcrypto.ts b/src/runtime/node/webcrypto.ts index b5c9d2b6..3e3b2009 100644 --- a/src/runtime/node/webcrypto.ts +++ b/src/runtime/node/webcrypto.ts @@ -12,7 +12,121 @@ export function isCryptoKey(key: unknown): key is CryptoKey { return false } -export function getKeyObject(key: CryptoKey) { + +function getHashLength(hash: KeyAlgorithm) { + return parseInt(hash?.name.substr(4), 10) +} + +function getNamedCurve(alg: string) { + switch (alg) { + case 'ES256': + return 'P-256' + case 'ES384': + return 'P-384' + case 'ES512': + return 'P-521' + } +} + +export function getKeyObject(key: CryptoKey, alg?: string, usage?: Set) { + if (!alg) { + // @ts-expect-error + return crypto.KeyObject.from(key) + } + + if (usage && !key.usages.find(Set.prototype.has.bind(usage))) { + throw new TypeError('CryptoKey does not support this operation') + } + + switch (alg) { + case 'HS256': + case 'HS384': + case 'HS512': + if ( + key.algorithm.name !== 'HMAC' || + getHashLength((key.algorithm).hash) !== parseInt(alg.substr(2), 10) + ) { + throw new TypeError('CryptoKey does not support this operation') + } + break + case 'RS256': + case 'RS384': + case 'RS512': + if ( + key.algorithm.name.toUpperCase() !== 'RSASSA-PKCS1-V1_5' || // https://github.com/nodejs/node/pull/38029 + getHashLength((key.algorithm).hash) !== parseInt(alg.substr(2), 10) + ) { + throw new TypeError('CryptoKey does not support this operation') + } + break + case 'PS256': + case 'PS384': + case 'PS512': + if ( + key.algorithm.name !== 'RSA-PSS' || + getHashLength((key.algorithm).hash) !== parseInt(alg.substr(2), 10) + ) { + throw new TypeError('CryptoKey does not support this operation') + } + break + case 'ES256': + case 'ES384': + case 'ES512': + if ( + key.algorithm.name !== 'ECDSA' || + (key.algorithm).namedCurve !== getNamedCurve(alg) + ) { + throw new TypeError('CryptoKey does not support this operation') + } + break + case 'A128GCM': + case 'A192GCM': + case 'A256GCM': + if ( + key.algorithm.name !== 'AES-GCM' || + (key.algorithm).length !== parseInt(alg.substr(1, 3), 10) + ) { + throw new TypeError('CryptoKey does not support this operation') + } + break + case 'A128KW': + case 'A192KW': + case 'A256KW': + if ( + key.algorithm.name !== 'AES-KW' || + (key.algorithm).length !== parseInt(alg.substr(1, 3), 10) + ) { + throw new TypeError('CryptoKey does not support this operation') + } + break + case 'ECDH-ES': + if (key.algorithm.name !== 'ECDH') { + throw new TypeError('CryptoKey does not support this operation') + } + break + case 'PBES2-HS256+A128KW': + case 'PBES2-HS384+A192KW': + case 'PBES2-HS512+A256KW': + if (key.algorithm.name !== 'PBKDF2') { + throw new TypeError('CryptoKey does not support this operation') + } + break + case 'RSA-OAEP': + case 'RSA-OAEP-256': + case 'RSA-OAEP-384': + case 'RSA-OAEP-512': + if ( + key.algorithm.name !== 'RSA-OAEP' || + getHashLength((key.algorithm).hash) !== + (parseInt(alg.substr(9), 10) || 1) + ) { + throw new TypeError('CryptoKey does not support this operation') + } + break + default: + throw new TypeError('CryptoKey does not support this operation') + } + // @ts-expect-error return crypto.KeyObject.from(key) }