fix(node): check CryptoKey algorithm & usage before exporting KeyObject

This commit is contained in:
Filip Skokan 2021-04-01 23:13:16 +02:00
parent 0f990a46c1
commit dab4b2f03e
11 changed files with 142 additions and 25 deletions

View file

@ -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())

View file

@ -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 {

View file

@ -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')

View file

@ -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 {

View file

@ -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')
}

View file

@ -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

View file

@ -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)}`)

View file

@ -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)

View file

@ -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)

View file

@ -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)

View file

@ -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<KeyUsage>) {
if (!alg) {
// @ts-expect-error
return <crypto.KeyObject>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((<HmacKeyAlgorithm>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((<RsaHashedKeyAlgorithm>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((<RsaHashedKeyAlgorithm>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' ||
(<EcKeyAlgorithm>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' ||
(<AesKeyAlgorithm>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' ||
(<AesKeyAlgorithm>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((<RsaHashedKeyAlgorithm>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>crypto.KeyObject.from(key)
}