Skip to content

Commit

Permalink
fix(node): check CryptoKey algorithm & usage before exporting KeyObject
Browse files Browse the repository at this point in the history
  • Loading branch information
panva committed Apr 1, 2021
1 parent 0f990a4 commit dab4b2f
Show file tree
Hide file tree
Showing 11 changed files with 142 additions and 25 deletions.
10 changes: 5 additions & 5 deletions src/runtime/node/aeskw.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,23 +3,23 @@ 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)) {
throw new TypeError(`invalid key size for alg: ${alg}`)
}
}

function getKeyObject(key: unknown) {
function ensureKeyObject(key: unknown, alg: string, usage: KeyUsage) {
if (key instanceof KeyObject) {
return key
}
if (key instanceof Uint8Array) {
return getSecretKey(key)
}
if (isCryptoKey(key)) {
return exportCryptoKey(key)
return getKeyObject(key, alg, new Set([usage]))
}

throw new TypeError('invalid key input')
Expand All @@ -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())
Expand All @@ -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())
Expand Down
2 changes: 1 addition & 1 deletion src/runtime/node/decrypt.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down
4 changes: 2 additions & 2 deletions src/runtime/node/ecdhes.ts
Original file line number Diff line number Diff line change
Expand Up @@ -31,15 +31,15 @@ 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')
}

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')
Expand Down
2 changes: 1 addition & 1 deletion src/runtime/node/encrypt.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down
6 changes: 3 additions & 3 deletions src/runtime/node/get_sign_verify_key.ts
Original file line number Diff line number Diff line change
@@ -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
}
Expand All @@ -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')
}
3 changes: 3 additions & 0 deletions src/runtime/node/key_to_jwk.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
8 changes: 4 additions & 4 deletions src/runtime/node/pbes2kw.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,15 +10,15 @@ 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()
}
if (key instanceof Uint8Array) {
return key
}
if (isCryptoKey(key)) {
return getKeyObject(key).export()
return getKeyObject(key, alg, new Set(['deriveBits', 'deriveKey'])).export()
}
throw new TypeError('invalid key input')
}
Expand All @@ -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)
Expand All @@ -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)}`)

Expand Down
10 changes: 5 additions & 5 deletions src/runtime/node/rsaes.ts
Original file line number Diff line number Diff line change
@@ -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') {
Expand Down Expand Up @@ -39,20 +39,20 @@ 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')
}

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)
Expand All @@ -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)
Expand Down
2 changes: 1 addition & 1 deletion src/runtime/node/sign.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
4 changes: 2 additions & 2 deletions src/runtime/node/verify.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand All @@ -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)
Expand Down
116 changes: 115 additions & 1 deletion src/runtime/node/webcrypto.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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)
}

0 comments on commit dab4b2f

Please sign in to comment.