Skip to content

Commit

Permalink
feat: allow CryptoKey instances in a regular non-webcrypto node runtime
Browse files Browse the repository at this point in the history
  • Loading branch information
panva committed Feb 4, 2021
1 parent eef442c commit e8d41a9
Show file tree
Hide file tree
Showing 22 changed files with 255 additions and 59 deletions.
3 changes: 3 additions & 0 deletions .github/workflows/test.yml
Original file line number Diff line number Diff line change
Expand Up @@ -91,6 +91,9 @@ jobs:
working-directory: ./test
- name: Test Node.js crypto
run: npm run test
- name: Test Node.js crypto w/ CryptoKey
run: npm run test-cryptokey
if: ${{ !startsWith(matrix.node-version, '14') && !startsWith(matrix.node-version, '12') }}
- name: Test Web Cryptography API
run: npm run test-webcrypto
if: ${{ !startsWith(matrix.node-version, '14') && !startsWith(matrix.node-version, '12') }}
Expand Down
2 changes: 2 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -327,6 +327,7 @@
"build:node-webcrypto-esm": "run-s runtime-node-webcrypto lint 'build -- -p ./tsconfig/node-webcrypto-esm.json' && echo '{\"type\": \"module\"}'> dist/node/webcrypto/esm/package.json",
"clear": "rm -rf dist",
"coverage": "npm run-script runtime-node && c8 npm run-script test",
"coverage-cryptokey": "npm run-script runtime-node && c8 npm run-script test-cryptokey",
"coverage-webcrypto": "npm run-script runtime-node-webcrypto && c8 npm run-script test-webcrypto",
"docs": "run-s docs:*",
"docs:generate": "typedoc --disableOutputCheck --excludeNotExported --excludePrivate --excludeProtected --gitRevision main --readme none --listInvalidSymbolLinks --plugin typedoc-plugin-markdown --out docs --includeDeclarations --excludeExternals --tsconfig ./tsconfig/browser.json --mode modules src/types.d.ts src/jwt/*.ts src/jwe/**/*.ts src/jws/**/*.ts src/jwk/*.ts src/jwks/*.ts src/util/*.ts --hideProjectName --hideGenerator --allReflectionsHaveOwnDocument --hideBreadcrumbs",
Expand All @@ -343,6 +344,7 @@
"test": "npm run-script test-cjs && ava",
"test-browser": "find test-browser -type f -name '*.js' -print0 | xargs -0 npx esbuild --outdir=dist-browser-tests --bundle && karma start",
"test-cjs": "rm -rf test/cjs && find test -type f -name '*.mjs' -print0 | xargs -0 npx esbuild --target=esnext --outdir=test/cjs --format=cjs",
"test-cryptokey": "CRYPTOKEY=true npm test",
"test-webcrypto": "WEBCRYPTO=true npm test"
},
"devDependencies": {
Expand Down
18 changes: 14 additions & 4 deletions src/runtime/node/aesgcmkw.ts
Original file line number Diff line number Diff line change
@@ -1,24 +1,29 @@
import type { KeyObject } from 'crypto'

import type { AesGcmKwWrapFunction, AesGcmKwUnwrapFunction } from '../interfaces.d'
import encrypt from './encrypt.js'
import decrypt from './decrypt.js'
import ivFactory from '../../lib/iv.js'
import random from './random.js'
import { encode as base64url } from './base64url.js'
import { isCryptoKey, getKeyObject } from './webcrypto.js'
import type { KeyLike } from '../../types.d'

const generateIv = ivFactory(random)

export const wrap: AesGcmKwWrapFunction = async (
alg: string,
key: KeyObject | Uint8Array,
key: KeyLike,
cek: Uint8Array,
iv?: Uint8Array,
) => {
const jweAlgorithm = alg.substr(0, 7)
// eslint-disable-next-line no-param-reassign
iv ||= generateIv(jweAlgorithm)

if (isCryptoKey(key)) {
// eslint-disable-next-line no-param-reassign
key = getKeyObject(key)
}

const { ciphertext: encryptedKey, tag } = await encrypt(
jweAlgorithm,
cek,
Expand All @@ -32,13 +37,18 @@ export const wrap: AesGcmKwWrapFunction = async (

export const unwrap: AesGcmKwUnwrapFunction = async (
alg: string,
key: KeyObject | Uint8Array,
key: KeyLike,
encryptedKey: Uint8Array,
iv: Uint8Array,
tag: Uint8Array,
) => {
const jweAlgorithm = alg.substr(0, 7)

if (isCryptoKey(key)) {
// eslint-disable-next-line no-param-reassign
key = getKeyObject(key)
}

return decrypt(
jweAlgorithm,
key instanceof Uint8Array ? key : key.export(),
Expand Down
28 changes: 20 additions & 8 deletions src/runtime/node/aeskw.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,34 +4,39 @@ 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 type { KeyLike } from '../../types.d'
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}`)
}
}

export const wrap: AesKwWrapFunction = async (
alg: string,
key: KeyObject | Uint8Array,
cek: Uint8Array,
) => {
export const wrap: AesKwWrapFunction = async (alg: string, key: KeyLike, cek: Uint8Array) => {
const size = parseInt(alg.substr(1, 3), 10)
const algorithm = `aes${size}-wrap`
if (!getCiphers().includes(algorithm)) {
throw new JOSENotSupported(
`alg ${alg} is unsupported either by JOSE or your javascript runtime`,
)
}
const keyObject = getSecretKey(key)
let keyObject: KeyObject
if (key instanceof Uint8Array) {
keyObject = getSecretKey(key)
} else if (isCryptoKey(key)) {
keyObject = getKeyObject(key)
} else {
keyObject = key
}
checkKeySize(keyObject, alg)
const cipher = createCipheriv(algorithm, keyObject, Buffer.alloc(8, 0xa6))
return concat(cipher.update(cek), cipher.final())
}

export const unwrap: AesKwUnwrapFunction = async (
alg: string,
key: KeyObject | Uint8Array,
key: KeyLike,
encryptedKey: Uint8Array,
) => {
const size = parseInt(alg.substr(1, 3), 10)
Expand All @@ -41,7 +46,14 @@ export const unwrap: AesKwUnwrapFunction = async (
`alg ${alg} is unsupported either by JOSE or your javascript runtime`,
)
}
const keyObject = getSecretKey(key)
let keyObject: KeyObject
if (key instanceof Uint8Array) {
keyObject = getSecretKey(key)
} else if (isCryptoKey(key)) {
keyObject = getKeyObject(key)
} else {
keyObject = key
}
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/check_cek_length.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { KeyObject } from 'crypto'
import { JWEInvalid, JOSENotSupported } from '../../util/errors.js'

const checkCekLength = (enc: string, cek: Uint8Array | KeyObject) => {
const checkCekLength = (enc: string, cek: KeyObject | Uint8Array) => {
let expected: number
switch (enc) {
case 'A128CBC-HS256':
Expand Down
13 changes: 10 additions & 3 deletions src/runtime/node/decrypt.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,10 +8,12 @@ import { concat } from '../../lib/buffer_utils.js'
import { JOSENotSupported, JWEDecryptionFailed } from '../../util/errors.js'
import timingSafeEqual from './timing_safe_equal.js'
import cbcTag from './cbc_tag.js'
import { isCryptoKey, getKeyObject } from './webcrypto.js'
import type { KeyLike } from '../../types.d'

async function cbcDecrypt(
enc: string,
cek: Uint8Array | KeyObject,
cek: KeyObject | Uint8Array,
ciphertext: Uint8Array,
iv: Uint8Array,
tag: Uint8Array,
Expand Down Expand Up @@ -58,7 +60,7 @@ async function cbcDecrypt(
}
async function gcmDecrypt(
enc: string,
cek: Uint8Array | KeyObject,
cek: KeyObject | Uint8Array,
ciphertext: Uint8Array,
iv: Uint8Array,
tag: Uint8Array,
Expand All @@ -83,12 +85,17 @@ async function gcmDecrypt(

const decrypt: DecryptFunction = async (
enc: string,
cek: Uint8Array | KeyObject,
cek: KeyLike,
ciphertext: Uint8Array,
iv: Uint8Array,
tag: Uint8Array,
aad: Uint8Array,
) => {
if (isCryptoKey(cek)) {
// eslint-disable-next-line no-param-reassign
cek = getKeyObject(cek)
}

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

Expand Down
29 changes: 24 additions & 5 deletions src/runtime/node/ecdhes.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,14 +21,15 @@ import {
import type { EpkJwk } from '../../types.i.d'
import digest from './digest.js'
import { JOSENotSupported } from '../../util/errors.js'
import { isCryptoKey, getKeyObject } from './webcrypto.js'

const generateKeyPair = promisify(generateKeyPairCb)

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

export const deriveKey: EcdhESDeriveKeyFunction = async (
publicKey: KeyObject,
privateKey: KeyObject,
publicKey: KeyObject | CryptoKey,
privateKey: KeyObject | CryptoKey,
algorithm: string,
keyLength: number,
apu: Uint8Array = new Uint8Array(0),
Expand All @@ -41,13 +42,27 @@ export const deriveKey: EcdhESDeriveKeyFunction = async (
uint32be(keyLength),
)

if (isCryptoKey(publicKey)) {
// eslint-disable-next-line no-param-reassign
publicKey = getKeyObject(publicKey)
}

if (isCryptoKey(privateKey)) {
// eslint-disable-next-line no-param-reassign
privateKey = getKeyObject(privateKey)
}

const sharedSecret = diffieHellman({ privateKey, publicKey })
return concatKdf(sharedSecret, keyLength, value)
}

export const ephemeralKeyToPublicJWK: EphemeralKeyToPublicJwkFunction = function ephemeralKeyToPublicJWK(
key: KeyObject,
key: KeyObject | CryptoKey,
) {
if (isCryptoKey(key)) {
// eslint-disable-next-line no-param-reassign
key = getKeyObject(key)
}
switch (key.asymmetricKeyType) {
case 'x25519':
case 'x448': {
Expand All @@ -72,7 +87,11 @@ export const ephemeralKeyToPublicJWK: EphemeralKeyToPublicJwkFunction = function
}
}

export const generateEpk: GenerateEpkFunction = async (key: KeyObject) => {
export const generateEpk: GenerateEpkFunction = async (key: KeyObject | CryptoKey) => {
if (isCryptoKey(key)) {
// eslint-disable-next-line no-param-reassign
key = getKeyObject(key)
}
switch (key.asymmetricKeyType) {
case 'x25519':
return (await generateKeyPair('x25519')).privateKey
Expand Down Expand Up @@ -134,5 +153,5 @@ export const publicJwkToEphemeralKey: PublicJwkToEphemeralKeyFunction = async (j
}

const curves = ['P-256', 'P-384', 'P-521', 'X25519', 'X448']
export const ecdhAllowed: EcdhAllowedFunction = (key: KeyObject) =>
export const ecdhAllowed: EcdhAllowedFunction = (key: KeyObject | CryptoKey) =>
curves.includes(getNamedCurve(key))
9 changes: 8 additions & 1 deletion src/runtime/node/encrypt.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,8 @@ import checkIvLength from '../../lib/check_iv_length.js'
import checkCekLength from './check_cek_length.js'
import { concat } from '../../lib/buffer_utils.js'
import cbcTag from './cbc_tag.js'
import type { KeyLike } from '../../types.d'
import { isCryptoKey, getKeyObject } from './webcrypto.js'

async function cbcEncrypt(
enc: string,
Expand Down Expand Up @@ -55,10 +57,15 @@ async function gcmEncrypt(
const encrypt: EncryptFunction = async (
enc: string,
plaintext: Uint8Array,
cek: KeyObject | Uint8Array,
cek: KeyLike,
iv: Uint8Array,
aad: Uint8Array,
) => {
if (isCryptoKey(cek)) {
// eslint-disable-next-line no-param-reassign
cek = getKeyObject(cek)
}

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

Expand Down
9 changes: 8 additions & 1 deletion src/runtime/node/get_named_curve.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { KeyObject, createPublicKey } from 'crypto'
import { JOSENotSupported } from '../../util/errors.js'
import { isCryptoKey, getKeyObject } from './webcrypto.js'

const p256 = Buffer.from([42, 134, 72, 206, 61, 3, 1, 7])
const p384 = Buffer.from([43, 129, 4, 0, 34])
Expand All @@ -23,10 +24,16 @@ const namedCurveToJOSE = (namedCurve: string) => {
}
}

const getNamedCurve = (key: KeyObject, raw?: boolean): string => {
const getNamedCurve = (key: KeyObject | CryptoKey, raw?: boolean): string => {
if (key.type === 'secret') {
throw new TypeError('only "private" or "public" key objects can be used for this operation')
}

if (isCryptoKey(key)) {
// eslint-disable-next-line no-param-reassign
key = getKeyObject(key)
}

switch (key.asymmetricKeyType) {
case 'ed25519':
case 'ed448':
Expand Down
8 changes: 7 additions & 1 deletion src/runtime/node/key_to_jwk.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,14 @@ import { encode as base64url } from './base64url.js'
import Asn1SequenceDecoder from './asn1_sequence_decoder.js'
import { JOSENotSupported } from '../../util/errors.js'
import getNamedCurve from './get_named_curve.js'
import { isCryptoKey, getKeyObject } from './webcrypto.js'

const keyToJWK: JWKConvertFunction = (key: KeyObject | CryptoKey): JWK => {
if (isCryptoKey(key)) {
// eslint-disable-next-line no-param-reassign
key = getKeyObject(key)
}

const keyToJWK: JWKConvertFunction = (key: KeyObject): JWK => {
if (!(key instanceof KeyObject)) {
throw new TypeError('invalid key argument type')
}
Expand Down
24 changes: 20 additions & 4 deletions src/runtime/node/pbes2kw.ts
Original file line number Diff line number Diff line change
@@ -1,12 +1,12 @@
import { promisify } from 'util'
import type { KeyObject } from 'crypto'
import { pbkdf2 as pbkdf2cb } from 'crypto'
import { KeyObject, pbkdf2 as pbkdf2cb } from 'crypto'
import type { Pbes2KWDecryptFunction, Pbes2KWEncryptFunction } from '../interfaces.d'
import random from './random.js'
import { p2s as concatSalt } from '../../lib/buffer_utils.js'
import { encode as base64url } from './base64url.js'
import { wrap, unwrap } from './aeskw.js'
import checkP2s from '../../lib/check_p2s.js'
import { isCryptoKey, getKeyObject } from './webcrypto.js'

const pbkdf2 = promisify(pbkdf2cb)

Expand All @@ -20,7 +20,15 @@ export const encrypt: Pbes2KWEncryptFunction = async (
checkP2s(p2s)
const salt = concatSalt(alg, p2s)
const keylen = parseInt(alg.substr(13, 3), 10) >> 3
const password = key instanceof Uint8Array ? key : key.export()
let password: Uint8Array

if (isCryptoKey(key)) {
password = getKeyObject(key).export()
} else if (key instanceof KeyObject) {
password = key.export()
} else {
password = key
}
const derivedKey = await pbkdf2(password, salt, p2c, keylen, `sha${alg.substr(8, 3)}`)
const encryptedKey = await wrap(alg.substr(-6), derivedKey, cek)

Expand All @@ -37,7 +45,15 @@ export const decrypt: Pbes2KWDecryptFunction = async (
checkP2s(p2s)
const salt = concatSalt(alg, p2s)
const keylen = parseInt(alg.substr(13, 3), 10) >> 3
const password = key instanceof Uint8Array ? key : key.export()
let password: Uint8Array

if (isCryptoKey(key)) {
password = getKeyObject(key).export()
} else if (key instanceof KeyObject) {
password = key.export()
} else {
password = key
}
const derivedKey = await pbkdf2(password, salt, p2c, keylen, `sha${alg.substr(8, 3)}`)

return unwrap(alg.substr(-6), derivedKey, encryptedKey)
Expand Down
Loading

0 comments on commit e8d41a9

Please sign in to comment.