Skip to content

Commit

Permalink
fix(web): check Uint8Array CEK lengths, refactor for better tree-shaking
Browse files Browse the repository at this point in the history
  • Loading branch information
panva committed Oct 17, 2021
1 parent 244e2f9 commit e8299f2
Show file tree
Hide file tree
Showing 27 changed files with 346 additions and 478 deletions.
35 changes: 17 additions & 18 deletions src/lib/cek.ts
Original file line number Diff line number Diff line change
@@ -1,23 +1,22 @@
import { JOSENotSupported } from '../util/errors.js'
import random from '../runtime/random.js'

const bitLengths = new Map<string, number>([
['A128CBC-HS256', 256],
['A128GCM', 128],
['A192CBC-HS384', 384],
['A192GCM', 192],
['A256CBC-HS512', 512],
['A256GCM', 256],
])

const generateCek = (alg: string): Uint8Array => {
const bitLength = bitLengths.get(alg)
if (!bitLength) {
throw new JOSENotSupported(`Unsupported JWE Algorithm: ${alg}`)
export function bitLength(alg: string) {
switch (alg) {
case 'A128CBC-HS256':
return 256
case 'A192CBC-HS384':
return 384
case 'A256CBC-HS512':
return 512
case 'A128GCM':
return 128
case 'A192GCM':
return 192
case 'A256GCM':
return 256
default:
throw new JOSENotSupported(`Unsupported JWE Algorithm: ${alg}`)
}

return random(new Uint8Array(bitLength >> 3))
}

export default generateCek
export { bitLengths }
export default (alg: string): Uint8Array => random(new Uint8Array(bitLength(alg) >> 3))
4 changes: 2 additions & 2 deletions src/lib/check_iv_length.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
import { JWEInvalid } from '../util/errors.js'
import { bitLengths } from './iv.js'
import { bitLength } from './iv.js'

const checkIvLength = (enc: string, iv: Uint8Array) => {
if (iv.length << 3 !== bitLengths.get(enc)) {
if (iv.length << 3 !== bitLength(enc)) {
throw new JWEInvalid('Invalid Initialization Vector length')
}
}
Expand Down
209 changes: 209 additions & 0 deletions src/lib/crypto_key.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,209 @@
import { isCloudflareWorkers, isNodeJs } from '../runtime/global.js'

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

function checkUsage(key: CryptoKey, usages: KeyUsage[]) {
if (usages.length && !usages.some((expected) => key.usages.includes(expected))) {
let msg = 'CryptoKey does not support this operation, its usages must include '
if (usages.length > 2) {
const last = usages.pop()
msg += `one of ${usages.join(', ')}, or ${last}.`
} else if (usages.length === 2) {
msg += `one of ${usages[0]} or ${usages[1]}.`
} else {
msg += `${usages[0]}.`
}

throw new TypeError(msg)
}
}

export function checkSigCryptoKey(key: CryptoKey, alg: string, ...usages: KeyUsage[]) {
switch (alg) {
case 'HS256':
case 'HS384':
case 'HS512': {
if (key.algorithm.name !== 'HMAC') {
throw new TypeError(
`CryptoKey does not support this operation, its algorithm.name must be HMAC.`,
)
}

const expected = parseInt(alg.substr(2), 10)
const actual = getHashLength((<HmacKeyAlgorithm>key.algorithm).hash)
if (actual !== expected) {
throw new TypeError(
`CryptoKey does not support this operation, its algorithm.hash must be SHA-${expected}.`,
)
}
break
}
case 'RS256':
case 'RS384':
case 'RS512': {
if (key.algorithm.name !== 'RSASSA-PKCS1-v1_5') {
throw new TypeError(
`CryptoKey does not support this operation, its algorithm.name must be RSASSA-PKCS1-v1_5.`,
)
}

const expected = parseInt(alg.substr(2), 10)
const actual = getHashLength((<RsaHashedKeyAlgorithm>key.algorithm).hash)
if (actual !== expected) {
throw new TypeError(
`CryptoKey does not support this operation, its algorithm.hash must be SHA-${expected}.`,
)
}
break
}
case 'PS256':
case 'PS384':
case 'PS512': {
if (key.algorithm.name !== 'RSA-PSS') {
throw new TypeError(
`CryptoKey does not support this operation, its algorithm.name must be RSA-PSS.`,
)
}

const expected = parseInt(alg.substr(2), 10)
const actual = getHashLength((<RsaHashedKeyAlgorithm>key.algorithm).hash)
if (actual !== expected) {
throw new TypeError(
`CryptoKey does not support this operation, its algorithm.hash must be SHA-${expected}.`,
)
}
break
}
case isNodeJs() && 'EdDSA': {
if (key.algorithm.name !== 'NODE-ED25519' && key.algorithm.name !== 'NODE-ED448') {
throw new TypeError(
`CryptoKey does not support this operation, its algorithm.name must be NODE-ED25519 or NODE-ED448.`,
)
}
break
}
case isCloudflareWorkers() && 'EdDSA': {
if (key.algorithm.name !== 'NODE-ED25519') {
throw new TypeError(
`CryptoKey does not support this operation, its algorithm.name must be NODE-ED25519.`,
)
}
break
}
case 'ES256':
case 'ES384':
case 'ES512': {
if (key.algorithm.name !== 'ECDSA') {
throw new TypeError(
`CryptoKey does not support this operation, its algorithm.name must be ECDSA.`,
)
}

const expected = getNamedCurve(alg)
const actual = (<EcKeyAlgorithm>key.algorithm).namedCurve
if (actual !== expected) {
throw new TypeError(
`CryptoKey does not support this operation, its algorithm.namedCurve must be ${expected}.`,
)
}
break
}
default:
throw new TypeError('CryptoKey does not support this operation')
}

checkUsage(key, usages)
}

export function checkEncCryptoKey(key: CryptoKey, alg: string, ...usages: KeyUsage[]) {
switch (alg) {
case 'A128GCM':
case 'A192GCM':
case 'A256GCM': {
if (key.algorithm.name !== 'AES-GCM') {
throw new TypeError(
`CryptoKey does not support this operation, its algorithm.name must be AES-GCM.`,
)
}

const expected = parseInt(alg.substr(1, 3), 10)
const actual = (<AesKeyAlgorithm>key.algorithm).length
if (actual !== expected) {
throw new TypeError(
`CryptoKey does not support this operation, its algorithm.length must be ${expected}.`,
)
}
break
}
case 'A128KW':
case 'A192KW':
case 'A256KW': {
if (key.algorithm.name !== 'AES-KW') {
throw new TypeError(
`CryptoKey does not support this operation, its algorithm.name must be AES-KW.`,
)
}

const expected = parseInt(alg.substr(1, 3), 10)
const actual = (<AesKeyAlgorithm>key.algorithm).length
if (actual !== expected) {
throw new TypeError(
`CryptoKey does not support this operation, its algorithm.length must be ${expected}.`,
)
}
break
}
case 'ECDH-ES':
if (key.algorithm.name !== 'ECDH') {
throw new TypeError(
`CryptoKey does not support this operation, its algorithm.name must be ECDH.`,
)
}
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, its algorithm.name must be PBKDF2.`,
)
}
break
case 'RSA-OAEP':
case 'RSA-OAEP-256':
case 'RSA-OAEP-384':
case 'RSA-OAEP-512': {
if (key.algorithm.name !== 'RSA-OAEP') {
throw new TypeError(
`CryptoKey does not support this operation, its algorithm.name must be RSA-OAEP.`,
)
}

const expected = parseInt(alg.substr(9), 10) || 1
const actual = getHashLength((<RsaHashedKeyAlgorithm>key.algorithm).hash)
if (actual !== expected) {
throw new TypeError(
`CryptoKey does not support this operation, its algorithm.hash must be SHA-${expected}.`,
)
}
break
}
default:
throw new TypeError('CryptoKey does not support this operation')
}

checkUsage(key, usages)
}
4 changes: 2 additions & 2 deletions src/lib/decrypt_key_management.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ import { decode as base64url } from '../runtime/base64url.js'

import type { JWEHeaderParameters, KeyLike, JWK } from '../types.d'
import { JOSENotSupported, JWEInvalid } from '../util/errors.js'
import { bitLengths as cekLengths } from '../lib/cek.js'
import { bitLength as cekLength } from '../lib/cek.js'
import { importJWK } from '../key/import.js'
import checkKeyType from './check_key_type.js'
import isObject from './is_object.js'
Expand Down Expand Up @@ -65,7 +65,7 @@ async function decryptKeyManagement(
epk,
key,
alg === 'ECDH-ES' ? joseHeader.enc! : alg,
parseInt(alg.substr(-5, 3), 10) || <number>cekLengths.get(joseHeader.enc!),
parseInt(alg.substr(-5, 3), 10) || cekLength(joseHeader.enc!),
partyUInfo,
partyVInfo,
)
Expand Down
4 changes: 2 additions & 2 deletions src/lib/encrypt_key_management.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ import { wrap as aesGcmKw } from '../runtime/aesgcmkw.js'
import { encode as base64url } from '../runtime/base64url.js'

import type { KeyLike, JWEKeyManagementHeaderParameters, JWEHeaderParameters } from '../types.d'
import generateCek, { bitLengths as cekLengths } from '../lib/cek.js'
import generateCek, { bitLength as cekLength } from '../lib/cek.js'
import { JOSENotSupported } from '../util/errors.js'
import { exportJWK } from '../key/export.js'
import checkKeyType from './check_key_type.js'
Expand Down Expand Up @@ -52,7 +52,7 @@ async function encryptKeyManagement(
key,
ephemeralKey,
alg === 'ECDH-ES' ? enc : alg,
parseInt(alg.substr(-5, 3), 10) || <number>cekLengths.get(enc),
parseInt(alg.substr(-5, 3), 10) || cekLength(enc),
apu,
apv,
)
Expand Down
44 changes: 23 additions & 21 deletions src/lib/iv.ts
Original file line number Diff line number Diff line change
@@ -1,26 +1,28 @@
import { JOSENotSupported } from '../util/errors.js'
import random from '../runtime/random.js'

const bitLengths = new Map<string, number>([
['A128CBC-HS256', 128],
['A128GCM', 96],
['A128GCMKW', 96],
['A192CBC-HS384', 128],
['A192GCM', 96],
['A192GCMKW', 96],
['A256CBC-HS512', 128],
['A256GCM', 96],
['A256GCMKW', 96],
])

const generateIv = (alg: string): Uint8Array => {
const bitLength = bitLengths.get(alg)
if (!bitLength) {
throw new JOSENotSupported(`Unsupported JWE Algorithm: ${alg}`)
export function bitLength(alg: string) {
switch (alg) {
case 'A128CBC-HS256':
return 128
case 'A128GCM':
return 96
case 'A128GCMKW':
return 96
case 'A192CBC-HS384':
return 128
case 'A192GCM':
return 96
case 'A192GCMKW':
return 96
case 'A256CBC-HS512':
return 128
case 'A256GCM':
return 96
case 'A256GCMKW':
return 96
default:
throw new JOSENotSupported(`Unsupported JWE Algorithm: ${alg}`)
}

return random(new Uint8Array(bitLength >> 3))
}

export default generateIv
export { bitLengths }
export default (alg: string): Uint8Array => random(new Uint8Array(bitLength(alg) >> 3))
5 changes: 3 additions & 2 deletions 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, { checkCryptoKey, isCryptoKey } from './webcrypto.js'
import crypto, { isCryptoKey } from './webcrypto.js'
import { checkEncCryptoKey } from '../../lib/crypto_key.js'
import invalidKeyInput from './invalid_key_input.js'

function checkKeySize(key: CryptoKey, alg: string) {
Expand All @@ -11,7 +12,7 @@ function checkKeySize(key: CryptoKey, alg: string) {

function getCryptoKey(key: unknown, alg: string, usage: KeyUsage) {
if (isCryptoKey(key)) {
checkCryptoKey(key, alg, usage)
checkEncCryptoKey(key, alg, usage)
return key
}

Expand Down
9 changes: 9 additions & 0 deletions src/runtime/browser/check_cek_length.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
import { JWEInvalid } from '../../util/errors.js'

const checkCekLength = (cek: Uint8Array, expected: number) => {
if (cek.length << 3 !== expected) {
throw new JWEInvalid('Invalid Content Encryption Key length')
}
}

export default checkCekLength
Loading

0 comments on commit e8299f2

Please sign in to comment.