mirror of
https://github.com/danbulant/jose
synced 2026-05-22 05:49:01 +00:00
fix(node): check CryptoKey algorithm & usage before exporting KeyObject
This commit is contained in:
parent
0f990a46c1
commit
dab4b2f03e
11 changed files with 142 additions and 25 deletions
|
|
@ -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())
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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')
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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')
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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)}`)
|
||||
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in a new issue