Skip to content

Commit

Permalink
fix: assert KeyLike input types, change "any" types to "unknown"
Browse files Browse the repository at this point in the history
  • Loading branch information
panva committed Mar 30, 2021
1 parent 1d49c6b commit edb83a8
Show file tree
Hide file tree
Showing 35 changed files with 355 additions and 320 deletions.
2 changes: 1 addition & 1 deletion src/jwk/thumbprint.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ import { encoder } from '../lib/buffer_utils.js'
import type { JWK } from '../types.d'
import isObject from '../lib/is_object.js'

const check = (value: any, description: string) => {
const check = (value: unknown, description: string) => {
if (typeof value !== 'string' || !value) {
throw new JWKInvalid(`${description} missing or invalid`)
}
Expand Down
13 changes: 9 additions & 4 deletions src/jwks/remote.ts
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,10 @@ export interface RemoteJWKSetOptions {
agent?: https.Agent | http.Agent
}

function isJWKLike(key: object): key is JWK {
return key && typeof key === 'object'
}

class RemoteJWKSet {
private _url: URL

Expand All @@ -68,7 +72,7 @@ class RemoteJWKSet {

private _cached: WeakMap<JWK, Cache> = new WeakMap()

private _pendingFetch?: ReturnType<typeof fetchJson>
private _pendingFetch?: Promise<unknown>

private _options: Pick<RemoteJWKSetOptions, 'agent'>

Expand Down Expand Up @@ -183,16 +187,17 @@ class RemoteJWKSet {
async reload() {
if (!this._pendingFetch) {
this._pendingFetch = fetchJson(this._url, this._timeoutDuration, this._options)
.then((json: { keys: object[] }) => {
.then((json) => {
if (
typeof json !== 'object' ||
!json ||
!Array.isArray(json.keys) ||
json.keys.some((key: object) => typeof key !== 'object' || !key)
!json.keys.every(isJWKLike)
) {
throw new JWKSInvalid('JSON Web Key Set malformed')
}
this._jwks = json

this._jwks = { keys: json.keys }
this._cooldownStarted = Date.now()
this._pendingFetch = undefined
})
Expand Down
13 changes: 10 additions & 3 deletions src/lib/buffer_utils.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
/* eslint-disable no-await-in-loop */

import type { DigestFunction } from '../runtime/interfaces.d'

export const encoder = new TextEncoder()
export const decoder = new TextDecoder()

Expand Down Expand Up @@ -46,7 +48,12 @@ export function lengthAndInput(input: Uint8Array) {
return concat(uint32be(input.length), input)
}

export async function concatKdf(digest: any, secret: Uint8Array, bits: number, value: Uint8Array) {
export async function concatKdf(
digest: DigestFunction,
secret: Uint8Array,
bits: number,
value: Uint8Array,
) {
const iterations = Math.ceil((bits >> 3) / 32)
let res!: Uint8Array

Expand All @@ -56,9 +63,9 @@ export async function concatKdf(digest: any, secret: Uint8Array, bits: number, v
buf.set(secret, 4)
buf.set(value, 4 + secret.length)
if (!res) {
res = await digest(buf)
res = await digest('sha256', buf)
} else {
res = concat(res, await digest(buf))
res = concat(res, await digest('sha256', buf))
}
}
res = res.slice(0, bits >> 3)
Expand Down
4 changes: 2 additions & 2 deletions src/lib/decrypt_key_management.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,14 +9,14 @@ import { unwrap as aesGcmKw } from '../runtime/aesgcmkw.js'
import { decode as base64url } from '../runtime/base64url.js'
import { bitLengths as cekLengths } from '../lib/cek.js'

function assertEnryptedKey(encryptedKey: any) {
function assertEnryptedKey(encryptedKey: unknown) {
if (!encryptedKey) {
throw new JWEInvalid('JWE Encrypted Key missing')
}
}

function assertHeaderParameter(
joseHeader: { [propName: string]: any },
joseHeader: { [propName: string]: unknown },
parameter: string,
name: string,
) {
Expand Down
4 changes: 2 additions & 2 deletions src/lib/is_object.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,3 @@
export default function isObject(input: any): boolean {
return !!input && input.constructor === Object
export default function isObject(input: unknown): boolean {
return typeof input === 'object' && !!input && input.constructor === Object
}
18 changes: 11 additions & 7 deletions src/lib/jwt_claims_set.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,14 +11,18 @@ import secs from './secs.js'

const normalizeTyp = (value: string) => value.toLowerCase().replace(/^application\//, '')

const checkAudiencePresence = (audPayload: any, audOption: string[]) => {
const checkAudiencePresence = (audPayload: unknown, audOption: unknown[]) => {
if (typeof audPayload === 'string') {
return audOption.includes(audPayload)
}

// Each principal intended to process the JWT MUST
// identify itself with a value in the audience claim
return audOption.some(Set.prototype.has.bind(new Set(audPayload)))
if (Array.isArray(audPayload)) {
// Each principal intended to process the JWT MUST
// identify itself with a value in the audience claim
return audOption.some(Set.prototype.has.bind(new Set(audPayload)))
}

return false
}

export default (
Expand All @@ -35,7 +39,7 @@ export default (
throw new JWTClaimValidationFailed('unexpected "typ" JWT header value', 'typ', 'check_failed')
}

let payload!: JWTPayload
let payload!: { [propName: string]: unknown }
try {
payload = JSON.parse(decoder.decode(encodedPayload))
} catch {
Expand All @@ -47,7 +51,7 @@ export default (
}

const { issuer } = options
if (issuer && !(typeof issuer === 'string' ? [issuer] : issuer).includes(payload.iss!)) {
if (issuer && !(<unknown[]>(Array.isArray(issuer) ? issuer : [issuer])).includes(payload.iss!)) {
throw new JWTClaimValidationFailed('unexpected "iss" claim value', 'iss', 'check_failed')
}

Expand Down Expand Up @@ -139,5 +143,5 @@ export default (
}
}

return payload
return <JWTPayload>payload
}
2 changes: 1 addition & 1 deletion src/lib/validate_crit.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import { JOSENotSupported, JWEInvalid, JWSInvalid } from '../util/errors.js'
interface CritCheckHeader {
b64?: boolean
crit?: string[]
[propName: string]: any
[propName: string]: unknown
}

function validateCrit(
Expand Down
4 changes: 2 additions & 2 deletions src/runtime/browser/aesgcmkw.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ const generateIv = ivFactory(random)

export const wrap: AesGcmKwWrapFunction = async (
alg: string,
key: CryptoKey | Uint8Array,
key: unknown,
cek: Uint8Array,
iv?: Uint8Array,
) => {
Expand All @@ -30,7 +30,7 @@ export const wrap: AesGcmKwWrapFunction = async (

export const unwrap: AesGcmKwUnwrapFunction = async (
alg: string,
key: CryptoKey | Uint8Array,
key: unknown,
encryptedKey: Uint8Array,
iv: Uint8Array,
tag: Uint8Array,
Expand Down
33 changes: 14 additions & 19 deletions src/runtime/browser/aeskw.ts
Original file line number Diff line number Diff line change
@@ -1,26 +1,27 @@
import type { AesKwUnwrapFunction, AesKwWrapFunction } from '../interfaces.d'
import bogusWebCrypto from './bogus.js'
import crypto from './webcrypto.js'
import crypto, { isCryptoKey } from './webcrypto.js'

function checkKeySize(key: CryptoKey, alg: string) {
if ((<AesKeyAlgorithm>key.algorithm).length !== parseInt(alg.substr(1, 3), 10)) {
throw new TypeError(`invalid key size for alg: ${alg}`)
}
}

export const wrap: AesKwWrapFunction = async (
alg: string,
key: CryptoKey | Uint8Array,
cek: Uint8Array,
) => {
let cryptoKey: CryptoKey

function getCryptoKey(key: unknown, usage: KeyUsage) {
if (isCryptoKey(key)) {
return key
}
if (key instanceof Uint8Array) {
cryptoKey = await crypto.subtle.importKey('raw', key, 'AES-KW', true, ['wrapKey'])
} else {
cryptoKey = key
return crypto.subtle.importKey('raw', key, 'AES-KW', true, [usage])
}

throw new TypeError('invalid key input')
}

export const wrap: AesKwWrapFunction = async (alg: string, key: unknown, cek: Uint8Array) => {
const cryptoKey = await getCryptoKey(key, 'wrapKey')

checkKeySize(cryptoKey, alg)

// we're importing the cek to end up with CryptoKey instance that can be wrapped, the algorithm used is irrelevant
Expand All @@ -31,16 +32,10 @@ export const wrap: AesKwWrapFunction = async (

export const unwrap: AesKwUnwrapFunction = async (
alg: string,
key: CryptoKey | Uint8Array,
key: unknown,
encryptedKey: Uint8Array,
) => {
let cryptoKey: CryptoKey

if (key instanceof Uint8Array) {
cryptoKey = await crypto.subtle.importKey('raw', key, 'AES-KW', true, ['unwrapKey'])
} else {
cryptoKey = key
}
const cryptoKey = await getCryptoKey(key, 'unwrapKey')

checkKeySize(cryptoKey, alg)

Expand Down
8 changes: 6 additions & 2 deletions src/runtime/browser/decrypt.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ import checkIvLength from '../../lib/check_iv_length.js'
import checkCekLength from './check_cek_length.js'
import timingSafeEqual from './timing_safe_equal.js'
import { JWEDecryptionFailed } from '../../util/errors.js'
import crypto from './webcrypto.js'
import crypto, { isCryptoKey } from './webcrypto.js'

async function cbcDecrypt(
enc: string,
Expand Down Expand Up @@ -94,12 +94,16 @@ async function gcmDecrypt(

const decrypt: DecryptFunction = async (
enc: string,
cek: CryptoKey | Uint8Array,
cek: unknown,
ciphertext: Uint8Array,
iv: Uint8Array,
tag: Uint8Array,
aad: Uint8Array,
) => {
if (!isCryptoKey(cek) && !(cek instanceof Uint8Array)) {
throw new TypeError('invalid key input')
}

checkCekLength(enc, cek)
checkIvLength(enc, iv)

Expand Down
47 changes: 29 additions & 18 deletions src/runtime/browser/ecdhes.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,27 +5,26 @@ import type {
GenerateEpkFunction,
PublicJwkToEphemeralKeyFunction,
} from '../interfaces.d'
import {
encoder,
concat,
uint32be,
lengthAndInput,
concatKdf as KDF,
} from '../../lib/buffer_utils.js'
import { encoder, concat, uint32be, lengthAndInput, concatKdf } from '../../lib/buffer_utils.js'
import type { EpkJwk } from '../../types.i.d'
import crypto from './webcrypto.js'
import crypto, { isCryptoKey } from './webcrypto.js'
import digest from './digest.js'

const concatKdf = KDF.bind(undefined, digest.bind(undefined, 'sha256'))

export const deriveKey: EcdhESDeriveKeyFunction = async (
publicKey: CryptoKey,
privateKey: CryptoKey,
publicKey: unknown,
privateKey: unknown,
algorithm: string,
keyLength: number,
apu: Uint8Array = new Uint8Array(0),
apv: Uint8Array = new Uint8Array(0),
) => {
if (!isCryptoKey(publicKey)) {
throw new TypeError('invalid key input')
}
if (!isCryptoKey(privateKey)) {
throw new TypeError('invalid key input')
}

const value = concat(
lengthAndInput(encoder.encode(algorithm)),
lengthAndInput(apu),
Expand All @@ -49,29 +48,41 @@ export const deriveKey: EcdhESDeriveKeyFunction = async (
),
)

return concatKdf(sharedSecret, keyLength, value)
return concatKdf(digest, sharedSecret, keyLength, value)
}

export const ephemeralKeyToPublicJWK: EphemeralKeyToPublicJwkFunction = async function ephemeralKeyToPublicJWK(
key: CryptoKey,
key: unknown,
) {
if (!isCryptoKey(key)) {
throw new TypeError('invalid key input')
}
// eslint-disable-next-line @typescript-eslint/keyword-spacing
const { crv, kty, x, y } = <EpkJwk>await crypto.subtle.exportKey('jwk', key)
return { crv, kty, x, y }
}

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

return (
await crypto.subtle.generateKey(
{ name: 'ECDH', namedCurve: (<EcKeyAlgorithm>key.algorithm).namedCurve },
true,
['deriveBits'],
)
).privateKey
}

export const publicJwkToEphemeralKey: PublicJwkToEphemeralKeyFunction = (jwk: EpkJwk) =>
crypto.subtle.importKey('jwk', jwk, { name: 'ECDH', namedCurve: jwk.crv }, true, [])

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

async function cbcEncrypt(
enc: string,
Expand Down Expand Up @@ -82,10 +82,14 @@ async function gcmEncrypt(
const encrypt: EncryptFunction = async (
enc: string,
plaintext: Uint8Array,
cek: CryptoKey | Uint8Array,
cek: unknown,
iv: Uint8Array,
aad: Uint8Array,
) => {
if (!isCryptoKey(cek) && !(cek instanceof Uint8Array)) {
throw new TypeError('invalid key input')
}

checkCekLength(enc, cek)
checkIvLength(enc, iv)

Expand Down
Loading

0 comments on commit edb83a8

Please sign in to comment.