Skip to content

Commit

Permalink
feat: add verbose key type error messages
Browse files Browse the repository at this point in the history
  • Loading branch information
panva committed Jul 1, 2021
1 parent b83821b commit df56b94
Show file tree
Hide file tree
Showing 36 changed files with 295 additions and 475 deletions.
8 changes: 0 additions & 8 deletions src/jwk/from_key_like.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
import type { JWK, KeyLike } from '../types.d'
import { encode as base64url } from '../runtime/base64url.js'
import asJWK from '../runtime/key_to_jwk.js'

/**
Expand Down Expand Up @@ -27,13 +26,6 @@ import asJWK from '../runtime/key_to_jwk.js'
* ```
*/
async function fromKeyLike(key: KeyLike): Promise<JWK> {
if (key instanceof Uint8Array) {
return {
kty: 'oct',
k: base64url(key),
}
}

return asJWK(key)
}

Expand Down
2 changes: 1 addition & 1 deletion src/jws/flattened/sign.ts
Original file line number Diff line number Diff line change
Expand Up @@ -117,7 +117,7 @@ class FlattenedSign {
throw new JWSInvalid('JWS "alg" (Algorithm) Header Parameter missing or invalid')
}

checkKeyType(alg, key)
checkKeyType(alg, key, 'sign')

let payload = this._payload
if (b64) {
Expand Down
2 changes: 1 addition & 1 deletion src/jws/flattened/verify.ts
Original file line number Diff line number Diff line change
Expand Up @@ -145,7 +145,7 @@ async function flattenedVerify(
key = await key(parsedProt, jws)
}

checkKeyType(alg, key)
checkKeyType(alg, key, 'verify')

const data = concat(
encoder.encode(jws.protected ?? ''),
Expand Down
25 changes: 23 additions & 2 deletions src/lib/check_key_type.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,15 @@
import type { KeyLike } from '../types.d'
import invalidKeyInput from '../runtime/invalid_key_input.js'

const checkKeyType = (
alg: string,
key: KeyLike,
usage: 'sign' | 'verify' | 'encrypt' | 'decrypt',
): void => {
if (!(key instanceof Uint8Array) && !key?.type) {
throw new TypeError(invalidKeyInput(key, 'KeyObject', 'CryptoKey', 'Uint8Array'))
}

const checkKeyType = (alg: string, key: KeyLike): void => {
if (
alg.startsWith('HS') ||
alg === 'dir' ||
Expand All @@ -17,14 +26,26 @@ const checkKeyType = (alg: string, key: KeyLike): void => {
}

if (key instanceof Uint8Array) {
throw new TypeError('CryptoKey or KeyObject instances must be used for asymmetric algorithms')
throw new TypeError(invalidKeyInput(key, 'KeyObject', 'CryptoKey'))
}

if (key.type === 'secret') {
throw new TypeError(
'CryptoKey or KeyObject instances for asymmetric algorithms must not be of type "secret"',
)
}

if (usage === 'sign' && key.type === 'public') {
throw new TypeError(
'CryptoKey or KeyObject instances for asymmetric algorithm signing must be of type "private"',
)
}

if (usage === 'decrypt' && key.type === 'public') {
throw new TypeError(
'CryptoKey or KeyObject instances for asymmetric algorithm decryption must be of type "private"',
)
}
}

export default checkKeyType
3 changes: 3 additions & 0 deletions src/lib/decrypt_key_management.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import { unwrap as aesGcmKw } from '../runtime/aesgcmkw.js'
import { decode as base64url } from '../runtime/base64url.js'
import { bitLengths as cekLengths } from '../lib/cek.js'
import { parseJwk } from '../jwk/parse.js'
import checkKeyType from './check_key_type.js'

function assertEnryptedKey(encryptedKey: unknown) {
if (!encryptedKey) {
Expand All @@ -32,6 +33,8 @@ async function decryptKeyManagement(
encryptedKey: Uint8Array | undefined,
joseHeader: JWEKeyManagementHeaderResults & JWEHeaderParameters,
): Promise<KeyLike> {
checkKeyType(alg, key, 'decrypt')

switch (alg) {
case 'dir': {
// Direct Encryption
Expand Down
3 changes: 3 additions & 0 deletions src/lib/encrypt_key_management.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import { encrypt as rsaEs } from '../runtime/rsaes.js'
import { wrap as aesGcmKw } from '../runtime/aesgcmkw.js'
import { encode as base64url } from '../runtime/base64url.js'
import { fromKeyLike } from '../jwk/from_key_like.js'
import checkKeyType from './check_key_type.js'

const generateCek = cekFactory(random)

Expand All @@ -28,6 +29,8 @@ async function encryptKeyManagement(
let parameters: JWEKeyManagementHeaderResults | undefined
let cek: KeyLike

checkKeyType(alg, key, 'encrypt')

switch (alg) {
case 'dir': {
// Direct Encryption
Expand Down
2 changes: 1 addition & 1 deletion src/lib/jwt_claims_set.ts
Original file line number Diff line number Diff line change
Expand Up @@ -81,7 +81,7 @@ export default (
tolerance = 0
break
default:
throw new TypeError('invalid clockTolerance option type')
throw new TypeError('Invalid clockTolerance option type')
}

const { currentDate } = options
Expand Down
2 changes: 1 addition & 1 deletion src/lib/secs.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ export default (str: string): number => {
const matched = REGEX.exec(str)

if (!matched) {
throw new TypeError('invalid time period format')
throw new TypeError('Invalid time period format')
}

const value = parseFloat(matched[1])
Expand Down
4 changes: 3 additions & 1 deletion src/runtime/browser/aeskw.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import type { AesKwUnwrapFunction, AesKwWrapFunction } from '../interfaces.d'
import bogusWebCrypto from './bogus.js'
import crypto, { isCryptoKey } from './webcrypto.js'
import invalidKeyInput from './invalid_key_input.js'

function checkKeySize(key: CryptoKey, alg: string) {
if ((<AesKeyAlgorithm>key.algorithm).length !== parseInt(alg.substr(1, 3), 10)) {
Expand All @@ -12,11 +13,12 @@ function getCryptoKey(key: unknown, usage: KeyUsage) {
if (isCryptoKey(key)) {
return key
}

if (key instanceof Uint8Array) {
return crypto.subtle.importKey('raw', key, 'AES-KW', true, [usage])
}

throw new TypeError('invalid key input')
throw new TypeError(invalidKeyInput(key, 'CryptoKey', 'Uint8Array'))
}

export const wrap: AesKwWrapFunction = async (alg: string, key: unknown, cek: Uint8Array) => {
Expand Down
3 changes: 2 additions & 1 deletion src/runtime/browser/decrypt.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import checkCekLength from './check_cek_length.js'
import timingSafeEqual from './timing_safe_equal.js'
import { JWEDecryptionFailed } from '../../util/errors.js'
import crypto, { isCryptoKey } from './webcrypto.js'
import invalidKeyInput from './invalid_key_input.js'

async function cbcDecrypt(
enc: string,
Expand Down Expand Up @@ -103,7 +104,7 @@ const decrypt: DecryptFunction = async (
aad: Uint8Array,
) => {
if (!isCryptoKey(cek) && !(cek instanceof Uint8Array)) {
throw new TypeError('invalid key input')
throw new TypeError(invalidKeyInput(cek, 'CryptoKey', 'Uint8Array'))
}

checkCekLength(enc, cek)
Expand Down
9 changes: 5 additions & 4 deletions src/runtime/browser/ecdhes.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import type {
import { encoder, concat, uint32be, lengthAndInput, concatKdf } from '../../lib/buffer_utils.js'
import crypto, { isCryptoKey } from './webcrypto.js'
import digest from './digest.js'
import invalidKeyInput from './invalid_key_input.js'

export const deriveKey: EcdhESDeriveKeyFunction = async (
publicKey: unknown,
Expand All @@ -16,10 +17,10 @@ export const deriveKey: EcdhESDeriveKeyFunction = async (
apv: Uint8Array = new Uint8Array(0),
) => {
if (!isCryptoKey(publicKey)) {
throw new TypeError('invalid key input')
throw new TypeError(invalidKeyInput(publicKey, 'CryptoKey'))
}
if (!isCryptoKey(privateKey)) {
throw new TypeError('invalid key input')
throw new TypeError(invalidKeyInput(privateKey, 'CryptoKey'))
}

const value = concat(
Expand Down Expand Up @@ -50,7 +51,7 @@ export const deriveKey: EcdhESDeriveKeyFunction = async (

export const generateEpk: GenerateEpkFunction = async (key: unknown) => {
if (!isCryptoKey(key)) {
throw new TypeError('invalid key input')
throw new TypeError(invalidKeyInput(key, 'CryptoKey'))
}

return (
Expand All @@ -64,7 +65,7 @@ export const generateEpk: GenerateEpkFunction = async (key: unknown) => {

export const ecdhAllowed: EcdhAllowedFunction = (key: unknown) => {
if (!isCryptoKey(key)) {
throw new TypeError('invalid key input')
throw new TypeError(invalidKeyInput(key, 'CryptoKey'))
}
return ['P-256', 'P-384', 'P-521'].includes((<EcKeyAlgorithm>key.algorithm).namedCurve)
}
3 changes: 2 additions & 1 deletion src/runtime/browser/encrypt.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import type { EncryptFunction } from '../interfaces.d'
import checkIvLength from '../../lib/check_iv_length.js'
import checkCekLength from './check_cek_length.js'
import crypto, { isCryptoKey } from './webcrypto.js'
import invalidKeyInput from './invalid_key_input.js'

async function cbcEncrypt(
enc: string,
Expand Down Expand Up @@ -87,7 +88,7 @@ const encrypt: EncryptFunction = async (
aad: Uint8Array,
) => {
if (!isCryptoKey(cek) && !(cek instanceof Uint8Array)) {
throw new TypeError('invalid key input')
throw new TypeError(invalidKeyInput(cek, 'CryptoKey', 'Uint8Array'))
}

checkCekLength(enc, cek)
Expand Down
2 changes: 1 addition & 1 deletion src/runtime/browser/generate.ts
Original file line number Diff line number Diff line change
Expand Up @@ -51,7 +51,7 @@ function getModulusLengthOption(options?: GenerateKeyPairOptions) {
const modulusLength = options?.modulusLength ?? 2048
if (typeof modulusLength !== 'number' || modulusLength < 2048) {
throw new JOSENotSupported(
'invalid or unsupported modulusLength option provided, 2048 bits or larger keys must be used',
'Invalid or unsupported modulusLength option provided, 2048 bits or larger keys must be used',
)
}
return modulusLength
Expand Down
5 changes: 3 additions & 2 deletions src/runtime/browser/get_sign_verify_key.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import crypto, { isCryptoKey } from './webcrypto.js'
import invalidKeyInput from './invalid_key_input.js'

export default function getCryptoKey(alg: string, key: unknown, usage: KeyUsage) {
if (isCryptoKey(key)) {
Expand All @@ -7,7 +8,7 @@ export default function getCryptoKey(alg: string, key: unknown, usage: KeyUsage)

if (key instanceof Uint8Array) {
if (!alg.startsWith('HS')) {
throw new TypeError('symmetric keys are only applicable for HMAC-based algorithms')
throw new TypeError(invalidKeyInput(key, 'CryptoKey'))
}
return crypto.subtle.importKey(
'raw',
Expand All @@ -18,5 +19,5 @@ export default function getCryptoKey(alg: string, key: unknown, usage: KeyUsage)
)
}

throw new TypeError('invalid key input')
throw new TypeError(invalidKeyInput(key, 'CryptoKey', 'Uint8Array'))
}
24 changes: 24 additions & 0 deletions src/runtime/browser/invalid_key_input.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
export default (actual: unknown, ...types: string[]) => {
let msg = 'Key must be '

if (types.length > 2) {
const last = types.pop()
msg += `one of type ${types.join(', ')}, or ${last}.`
} else if (types.length === 2) {
msg += `one of type ${types[0]} or ${types[1]}.`
} else {
msg += `of type ${types[0]}.`
}

if (actual == null) {
msg += ` Received ${actual}`
} else if (typeof actual === 'function' && actual.name) {
msg += ` Received function ${actual.name}`
} else if (typeof actual === 'object' && actual != null) {
if (actual.constructor && actual.constructor.name) {
msg += ` Received an instance of ${actual.constructor.name}`
}
}

return msg
}
10 changes: 9 additions & 1 deletion src/runtime/browser/key_to_jwk.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,18 @@
import crypto, { isCryptoKey } from './webcrypto.js'
import type { JWKConvertFunction } from '../interfaces.d'
import type { JWK } from '../../types.d'
import invalidKeyInput from './invalid_key_input.js'
import { encode as base64url } from './base64url.js'

const keyToJWK: JWKConvertFunction = async (key: unknown): Promise<JWK> => {
if (key instanceof Uint8Array) {
return {
kty: 'oct',
k: base64url(key),
}
}
if (!isCryptoKey(key)) {
throw new TypeError('invalid key input')
throw new TypeError(invalidKeyInput(key, 'CryptoKey', 'Uint8Array'))
}
if (!key.extractable) {
throw new TypeError('non-extractable CryptoKey cannot be exported as a JWK')
Expand Down
3 changes: 2 additions & 1 deletion src/runtime/browser/pbes2kw.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import { encode as base64url } from './base64url.js'
import { wrap, unwrap } from './aeskw.js'
import checkP2s from '../../lib/check_p2s.js'
import crypto, { isCryptoKey } from './webcrypto.js'
import invalidKeyInput from './invalid_key_input.js'

function getCryptoKey(key: unknown) {
if (key instanceof Uint8Array) {
Expand All @@ -15,7 +16,7 @@ function getCryptoKey(key: unknown) {
return key
}

throw new TypeError('invalid key input')
throw new TypeError(invalidKeyInput(key, 'CryptoKey', 'Uint8Array'))
}

export const encrypt: Pbes2KWEncryptFunction = async (
Expand Down
5 changes: 3 additions & 2 deletions src/runtime/browser/rsaes.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,10 +3,11 @@ import subtleAlgorithm from './subtle_rsaes.js'
import bogusWebCrypto from './bogus.js'
import crypto, { isCryptoKey } from './webcrypto.js'
import checkKeyLength from './check_key_length.js'
import invalidKeyInput from './invalid_key_input.js'

export const encrypt: RsaEsEncryptFunction = async (alg: string, key: unknown, cek: Uint8Array) => {
if (!isCryptoKey(key)) {
throw new TypeError('invalid key input')
throw new TypeError(invalidKeyInput(key, 'CryptoKey'))
}
checkKeyLength(alg, key)

Expand All @@ -33,7 +34,7 @@ export const decrypt: RsaEsDecryptFunction = async (
encryptedKey: Uint8Array,
) => {
if (!isCryptoKey(key)) {
throw new TypeError('invalid key input')
throw new TypeError(invalidKeyInput(key, 'CryptoKey'))
}
checkKeyLength(alg, key)

Expand Down
3 changes: 2 additions & 1 deletion src/runtime/node/aeskw.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import { concat } from '../../lib/buffer_utils.js'
import getSecretKey from './secret_key.js'
import { isCryptoKey, getKeyObject } from './webcrypto.js'
import isKeyObject from './is_key_object.js'
import invalidKeyInput from './invalid_key_input.js'

function checkKeySize(key: KeyObject, alg: string) {
if (key.symmetricKeySize! << 3 !== parseInt(alg.substr(1, 3), 10)) {
Expand All @@ -23,7 +24,7 @@ function ensureKeyObject(key: unknown, alg: string, usage: KeyUsage) {
return getKeyObject(key, alg, new Set([usage]))
}

throw new TypeError('invalid key input')
throw new TypeError(invalidKeyInput(key, 'KeyObject', 'CryptoKey', 'Uint8Array'))
}

export const wrap: AesKwWrapFunction = async (alg: string, key: unknown, cek: Uint8Array) => {
Expand Down
3 changes: 2 additions & 1 deletion src/runtime/node/decrypt.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import cbcTag from './cbc_tag.js'
import { isCryptoKey, getKeyObject } from './webcrypto.js'
import type { KeyLike } from '../../types.d'
import isKeyObject from './is_key_object.js'
import invalidKeyInput from './invalid_key_input.js'

async function cbcDecrypt(
enc: string,
Expand Down Expand Up @@ -103,7 +104,7 @@ const decrypt: DecryptFunction = async (
} else if (cek instanceof Uint8Array || isKeyObject(cek)) {
key = cek
} else {
throw new TypeError('invalid key input')
throw new TypeError(invalidKeyInput(cek, 'KeyObject', 'CryptoKey', 'Uint8Array'))
}

checkCekLength(enc, key)
Expand Down
Loading

0 comments on commit df56b94

Please sign in to comment.