From 67a8601b09ea879b760facab5c10949df3107a5e Mon Sep 17 00:00:00 2001 From: Filip Skokan Date: Thu, 22 Aug 2019 20:28:48 +0200 Subject: [PATCH] feat: added Node.js lts/dubnium support for runtime supported features --- .codecov.yml | 6 +- .github/workflows/test.yml | 4 + CHANGELOG.md | 4 +- README.md | 14 +- lib/errors.js | 6 +- lib/help/asn1/index.js | 5 + lib/help/key_object.js | 420 +++++++++++++++++++++++ lib/help/key_utils.js | 31 +- lib/help/node_support.js | 6 +- lib/jwa/aes_cbc_hmac_sha2.js | 4 +- lib/jwa/aes_gcm.js | 6 +- lib/jwa/aes_kw.js | 6 +- lib/jwa/ecdsa.js | 34 +- lib/jwa/hmac.js | 4 +- lib/jwa/rsaes.js | 6 +- lib/jwa/rsassa.js | 28 +- lib/jwa/rsassa_pss.js | 56 ++- lib/jwe/decrypt.js | 2 +- lib/jwe/encrypt.js | 89 +++-- lib/jwe/generate_cek.js | 3 +- lib/jwk/import.js | 2 +- lib/jwk/key/base.js | 21 +- lib/jwk/key/ec.js | 40 ++- lib/jwk/key/oct.js | 3 +- lib/jwk/key/rsa.js | 43 ++- lib/jwks/keystore.js | 38 +- lib/jws/sign.js | 35 +- package.json | 2 +- test/cookbook/jwk.test.js | 25 +- test/cookbook/rfc8037.a4.ed25519.test.js | 4 + test/help/P-256K.key_utils.test.js | 2 +- test/help/key_utils.test.js | 33 +- test/jwe/smoke.test.js | 2 + test/jwk/P-256K.import.test.js | 2 +- test/jwk/ec.test.js | 3 +- test/jwk/generate.test.js | 26 +- test/jwk/import.test.js | 17 +- test/jwk/key_ops.test.js | 24 +- test/jwk/oct.test.js | 3 +- test/jwk/okp_enc.test.js | 4 +- test/jwk/okp_sig.test.js | 5 + test/jwk/rsa.test.js | 18 +- test/jwk/x5c_thumbprints.test.js | 4 + test/jws/smoke.test.js | 2 + test/macros/generate.js | 20 ++ 45 files changed, 888 insertions(+), 224 deletions(-) create mode 100644 lib/help/key_object.js create mode 100644 test/macros/generate.js diff --git a/.codecov.yml b/.codecov.yml index b288422a65..e6779cb506 100644 --- a/.codecov.yml +++ b/.codecov.yml @@ -1,8 +1,8 @@ coverage: status: - project: no - patch: yes - changes: yes + project: off + patch: off + changes: off comment: layout: diff diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 346cb9fbae..1c0da78447 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -20,8 +20,11 @@ jobs: test: runs-on: ${{ matrix.os }} strategy: + fail-fast: false matrix: node-version: + - 10.13.0 + - 10 - 12.0.0 - 12 os: @@ -58,6 +61,7 @@ jobs: test-electron: runs-on: ${{ matrix.os }} strategy: + fail-fast: false matrix: electron-version: - 6.0.0 diff --git a/CHANGELOG.md b/CHANGELOG.md index 11be591ee8..485720b292 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -155,7 +155,7 @@ its original on-by-default form - v1.1.0 and v1.2.0 wrapKey/unwrapKey/deriveKey returns * keystore.all and keystore.get `operation` option was removed, `key_ops: string[]` supersedes it -* node.js minimal version is now v12.0.0 due to its +* Node.js minimal version is now v12.0.0 due to its added EdDSA support (crypto.sign, crypto.verify and eddsa key objects) @@ -323,7 +323,7 @@ Initial release | AES | ✓ | A128KW, A192KW, A256KW | | AES GCM | ✓ | A128GCMKW, A192GCMKW, A256GCMKW | | Direct Key Agreement | ✓ | dir | -| RSAES OAEP | ✓* | RSA-OAEP (*RSA-OAEP-256 is not supported due to its lack of support in Node.JS) | +| RSAES OAEP | ✓* | RSA-OAEP (*RSA-OAEP-256 is not supported due to its lack of support in Node.js) | | RSAES-PKCS1-v1_5 | ✓ | RSA1_5 | | PBES2 | ✓ | PBES2-HS256+A128KW, PBES2-HS384+A192KW, PBES2-HS512+A256KW | | ECDH-ES | ✓ | ECDH-ES, ECDH-ES+A128KW, ECDH-ES+A192KW, ECDH-ES+A256KW | diff --git a/README.md b/README.md index 5241cd8bcf..27615cad9b 100644 --- a/README.md +++ b/README.md @@ -80,7 +80,9 @@ Legend: | JARM - [JWT Secured Authorization Response Mode for OAuth 2.0][draft-jarm] | ◯ || Notes -- RSA-OAEP-256 is only supported when Node.js >= 12.9.0 runtime is detected +- RSA-OAEP-256 JWE algorithm is only supported when Node.js >= 12.9.0 runtime is detected +- Importing X.509 certificates and handling `x5c` is only supported when Node.js >= 12.0.0 runtime is detected +- OKP keys are only supported when Node.js >= 12.0.0 runtime is detected - See [#electron-support](#electron-support) for electron exceptions --- @@ -126,7 +128,8 @@ If you or your business use @panva/jose, please consider becoming a [sponsor][su ## Usage -For its improvements in the crypto module ⚠️ the minimal Node.js version required is **v12.0.0** ⚠️ +For the best performance Node.js version **>=12.0.0** is recommended, but **^10.13.0** lts/dubnium +is also supported. Installing @panva/jose @@ -318,7 +321,7 @@ Electron v6.x runtime is supported to the extent of the crypto engine BoringSSL standard Node.js OpenSSL. The following is disabled in Electron runtime because of its lack of [support](https://github.com/panva/jose/blob/master/test/electron/electron.test.js). -- JWE `A128KW`, `A192KW` and `A256KW` algs are not available, this also means that other JWAs +- JWE `A128KW`, `A192KW` and `A256KW` algorithms are not available, this also means that other JWAs depending on those are not working, those are `ECDH-ES+A128KW`, `ECDH-ES+A192KW`, `ECDH-ES+A256KW`, `PBES2-HS256+A128KW`, `PBES2-HS384+A192KW`, `PBES2-HS512+A256KW`) - OKP curves `Ed448`, `X25519` and `X448` are not supported @@ -335,8 +338,9 @@ private API and is subject to change between any versions. #### How do I use it outside of Node.js -It is **only built for Node.js** environment - it builds on top of the `crypto` module and requires -the KeyObject API that was added in Node.js v11.6.0 and one-shot sign/verify API added in v12.0.0 +It is **only built for ^10.13.0 || >=12.0.0 Node.js** environment - including @panva/jose in +transpiled browser-environment targetted projects is not supported and may result in unexpected +results. #### How is it different from [`jws`](https://github.com/brianloveswords/node-jws), [`jwa`](https://github.com/brianloveswords/node-jwa) or [`jsonwebtoken`](https://github.com/auth0/node-jsonwebtoken)? diff --git a/lib/errors.js b/lib/errors.js index f99508caad..b867d7daa4 100644 --- a/lib/errors.js +++ b/lib/errors.js @@ -37,19 +37,17 @@ class JOSEError extends Error { const isMulti = e => e instanceof JOSEMultiError class JOSEMultiError extends JOSEError { - #errors - constructor (errors) { super() let i while ((i = errors.findIndex(isMulti)) && i !== -1) { errors.splice(i, 1, ...errors[i]) } - this.#errors = errors + Object.defineProperty(this, 'errors', { value: errors }) } * [Symbol.iterator] () { - for (const error of this.#errors) { + for (const error of this.errors) { yield error } } diff --git a/lib/help/asn1/index.js b/lib/help/asn1/index.js index 1684c6bb55..88722281b2 100644 --- a/lib/help/asn1/index.js +++ b/lib/help/asn1/index.js @@ -26,5 +26,10 @@ types.set('RSAPrivateKey', RSAPrivateKey) const RSAPublicKey = asn1.define('RSAPublicKey', require('./rsa_public_key')) types.set('RSAPublicKey', RSAPublicKey) +const OID = asn1.define('OID', function () { + return this.objid() +}) +types.set('OID', OID) + module.exports = types module.exports.bignum = asn1.bignum diff --git a/lib/help/key_object.js b/lib/help/key_object.js new file mode 100644 index 0000000000..45b1b0e89b --- /dev/null +++ b/lib/help/key_object.js @@ -0,0 +1,420 @@ +let { createPublicKey, createPrivateKey, createSecretKey, KeyObject } = require('crypto') + +if (!createPublicKey || !createPrivateKey || !createSecretKey || !KeyObject) { + const { EOL } = require('os') + + const errors = require('../errors') + const isObject = require('./is_object') + const asn1 = require('./asn1') + + const namedCurve = Symbol('namedCurve') + const map = new WeakMap() + + const i = (ctx) => { + if (!map.has(ctx)) { + map.set(ctx, {}) + } + return map.get(ctx) + } + + const pemToDer = pem => Buffer.from(pem.replace(/(?:-----(?:BEGIN|END)(?: (?:RSA|EC))? (?:PRIVATE|PUBLIC) KEY-----|\s)/g, ''), 'base64') + const derToPem = (der, label) => `-----BEGIN ${label}-----${EOL}${der.toString('base64').match(/.{1,64}/g).join(EOL)}${EOL}-----END ${label}-----` + + KeyObject = class KeyObject { + export ({ cipher, passphrase, type, format } = {}) { + if (i(this).type === 'secret') { + return Buffer.from(i(this).buffer) + } + + if (i(this).type === 'public') { + if (format !== 'pem' && format !== 'der') { + throw new TypeError('format must be one of "pem" or "der"') + } + + if (this.asymmetricKeyType === 'rsa') { + switch (type) { + case 'pkcs1': + if (format === 'pem') { + return i(this).pem + } + + return pemToDer(i(this).pem) + case 'spki': { + const PublicKeyInfo = asn1.get('PublicKeyInfo') + const pem = PublicKeyInfo.encode({ + algorithm: { + algorithm: '1.2.840.113549.1.1.1'.split('.'), + parameters: Buffer.from('BQA=', 'base64') + }, + publicKey: { + unused: 0, + data: pemToDer(i(this).pem) + } + }, 'pem', { label: 'PUBLIC KEY' }) + + return format === 'pem' ? pem : pemToDer(pem) + } + default: + throw new TypeError('type must be one of "pkcs1" or "spki"') + } + } + + if (this.asymmetricKeyType === 'ec') { + if (type !== 'spki') { + throw new TypeError('type must be "spki"') + } + + if (format === 'pem') { + return i(this).pem + } + + return pemToDer(i(this).pem) + } + } + + if (i(this).type === 'private') { + if (format !== 'pem' && format !== 'der') { + throw new TypeError('format must be one of "pem" or "der"') + } + + if (passphrase !== undefined || cipher !== undefined) { + throw new errors.JOSENotSupported('encrypted private keys are not supported in your Node.js runtime version') + } + + if (type === 'pkcs8') { + if (i(this).pkcs8) { + if (format === 'der' && typeof i(this).pkcs8 === 'string') { + return pemToDer(i(this).pkcs8) + } + + if (format === 'pem' && Buffer.isBuffer(i(this).pkcs8)) { + return derToPem(i(this).pkcs8, 'PRIVATE KEY') + } + + return i(this).pkcs8 + } + + if (this.asymmetricKeyType === 'rsa') { + const parsed = i(this).asn1 + const RSAPrivateKey = asn1.get('RSAPrivateKey') + const privateKey = RSAPrivateKey.encode(parsed) + const PrivateKeyInfo = asn1.get('PrivateKeyInfo') + const pkcs8 = PrivateKeyInfo.encode({ + version: 0, + privateKey, + algorithm: { + algorithm: '1.2.840.113549.1.1.1'.split('.'), + parameters: Buffer.from('BQA=', 'base64') + } + }) + + i(this).pkcs8 = pkcs8 + + return this.export({ type, format }) + } + + if (this.asymmetricKeyType === 'ec') { + const parsed = i(this).asn1 + const ECPrivateKey = asn1.get('ECPrivateKey') + const privateKey = ECPrivateKey.encode({ + version: parsed.version, + privateKey: parsed.privateKey, + publicKey: parsed.publicKey + }) + const PrivateKeyInfo = asn1.get('PrivateKeyInfo') + const OID = asn1.get('OID') + const pkcs8 = PrivateKeyInfo.encode({ + version: 0, + privateKey, + algorithm: { + algorithm: '1.2.840.10045.2.1'.split('.'), + parameters: OID.encode(i(this).asn1.parameters.value) + } + }) + + i(this).pkcs8 = pkcs8 + + return this.export({ type, format }) + } + } + + if (this.asymmetricKeyType === 'rsa' && type === 'pkcs1') { + if (format === 'pem') { + return i(this).pem + } + + return pemToDer(i(this).pem) + } else if (this.asymmetricKeyType === 'ec' && type === 'sec1') { + if (format === 'pem') { + return i(this).pem + } + + return pemToDer(i(this).pem) + } else { + throw new TypeError(`type must be one of "spki" or "${this.asymmetricKeyType === 'rsa' ? 'pkcs1' : 'sec1'}"`) + } + } + } + + get type () { + return i(this).type + } + + get asymmetricKeyType () { + return i(this).asymmetricKeyType + } + + get symmetricKeySize () { + return i(this).symmetricKeySize + } + + asInput (needsPublic = false) { + switch (i(this).type) { + case 'secret': + return i(this).buffer + case 'public': + return i(this).pem + default: + if (needsPublic) { + if (!('pub' in i(this))) { + i(this).pub = createPublicKey(this) + } + + return i(this).pub.asInput() + } + + return i(this).pem + } + } + } + + createSecretKey = (buffer) => { + if (!Buffer.isBuffer(buffer) || !buffer.length) { + throw new TypeError('input must be a non-empty Buffer instance') + } + + const keyObject = new KeyObject() + i(keyObject).buffer = Buffer.from(buffer) + i(keyObject).symmetricKeySize = buffer.length + i(keyObject).type = 'secret' + + return keyObject + } + + createPublicKey = (input) => { + if (input instanceof KeyObject) { + if (input.type !== 'private') { + throw new TypeError('expected a private key') + } + + switch (input.asymmetricKeyType) { + case 'ec': { + const PublicKeyInfo = asn1.get('PublicKeyInfo') + const OID = asn1.get('OID') + const key = PublicKeyInfo.encode({ + algorithm: { + algorithm: '1.2.840.10045.2.1'.split('.'), + parameters: OID.encode(i(input).asn1.parameters.value) + }, + publicKey: i(input).asn1.publicKey + }) + + return createPublicKey({ key, format: 'der', type: 'spki' }) + } + case 'rsa': { + const RSAPublicKey = asn1.get('RSAPublicKey') + const key = RSAPublicKey.encode(i(input).asn1) + return createPublicKey({ key, format: 'der', type: 'pkcs1' }) + } + } + } + + if (typeof input === 'string' || Buffer.isBuffer(input)) { + input = { key: input, format: 'pem' } + } + + if (!isObject(input)) { + throw new TypeError('input must be a string, Buffer or an object') + } + + const { format } = input + let { key, type } = input + + if (typeof key !== 'string' && !Buffer.isBuffer(key)) { + throw new TypeError('key must be a string or Buffer') + } + + if (format !== 'pem' && format !== 'der') { + throw new TypeError('format must be one of "pem" or "der"') + } + + let label + if (format === 'pem') { + key = key.toString() + switch (key.split(/\r?\n/g)[0].toString()) { + case '-----BEGIN PUBLIC KEY-----': + type = 'spki' + label = 'PUBLIC KEY' + break + case '-----BEGIN RSA PUBLIC KEY-----': + type = 'pkcs1' + label = 'RSA PUBLIC KEY' + break + case '-----BEGIN CERTIFICATE-----': + throw new errors.JOSENotSupported('X.509 certificates are supported in your Node.js runtime version') + default: + throw new TypeError('unknown/unsupported PEM type') + } + } + + switch (type) { + case 'spki': { + const PublicKeyInfo = asn1.get('PublicKeyInfo') + const parsed = PublicKeyInfo.decode(key, format, { label }) + + let type, keyObject + switch (parsed.algorithm.algorithm.join('.')) { + case '1.2.840.10045.2.1': { + keyObject = new KeyObject() + i(keyObject).asn1 = parsed + i(keyObject).asymmetricKeyType = 'ec' + i(keyObject).type = 'public' + i(keyObject).pem = PublicKeyInfo.encode(parsed, 'pem', { label: 'PUBLIC KEY' }) + + break + } + case '1.2.840.113549.1.1.1': { + type = 'pkcs1' + keyObject = createPublicKey({ type, key: parsed.publicKey.data, format: 'der' }) + break + } + default: + throw new errors.JOSENotSupported(`OID ${parsed.algorithm.algorithm.join('.')} is not supported in your Node.js runtime version`) + } + + return keyObject + } + case 'pkcs1': { + const RSAPublicKey = asn1.get('RSAPublicKey') + const parsed = RSAPublicKey.decode(key, format, { label }) + + const keyObject = new KeyObject() + i(keyObject).asn1 = parsed + i(keyObject).asymmetricKeyType = 'rsa' + i(keyObject).type = 'public' + i(keyObject).pem = RSAPublicKey.encode(parsed, 'pem', { label: 'RSA PUBLIC KEY' }) + + return keyObject + } + default: + throw new TypeError('type must be one of "pkcs1" or "spki"') + } + } + + createPrivateKey = (input, hints) => { + if (typeof input === 'string' || Buffer.isBuffer(input)) { + input = { key: input, format: 'pem' } + } + + if (!isObject(input)) { + throw new TypeError('input must be a string, Buffer or an object') + } + + const { format, passphrase } = input + let { key, type } = input + + if (typeof key !== 'string' && !Buffer.isBuffer(key)) { + throw new TypeError('key must be a string or Buffer') + } + + if (passphrase !== undefined) { + throw new errors.JOSENotSupported('encrypted private keys are not supported in your Node.js runtime version') + } + + if (format !== 'pem' && format !== 'der') { + throw new TypeError('format must be one of "pem" or "der"') + } + + let label + if (format === 'pem') { + key = key.toString() + switch (key.split(/\r?\n/g)[0].toString()) { + case '-----BEGIN PRIVATE KEY-----': + type = 'pkcs8' + label = 'PRIVATE KEY' + break + case '-----BEGIN EC PRIVATE KEY-----': + type = 'sec1' + label = 'EC PRIVATE KEY' + break + case '-----BEGIN RSA PRIVATE KEY-----': + type = 'pkcs1' + label = 'RSA PRIVATE KEY' + break + default: + throw new TypeError('unknown/unsupported PEM type') + } + } + + switch (type) { + case 'pkcs8': { + const PrivateKeyInfo = asn1.get('PrivateKeyInfo') + const parsed = PrivateKeyInfo.decode(key, format, { label }) + + let type, keyObject + switch (parsed.algorithm.algorithm.join('.')) { + case '1.2.840.10045.2.1': { + const OID = asn1.get('OID') + type = 'sec1' + keyObject = createPrivateKey({ type, key: parsed.privateKey, format: 'der' }, { [namedCurve]: OID.decode(parsed.algorithm.parameters) }) + break + } + case '1.2.840.113549.1.1.1': { + type = 'pkcs1' + keyObject = createPrivateKey({ type, key: parsed.privateKey, format: 'der' }) + break + } + default: + throw new errors.JOSENotSupported(`OID ${parsed.algorithm.algorithm.join('.')} is not supported in your Node.js runtime version`) + } + + i(keyObject).pkcs8 = key + return keyObject + } + case 'pkcs1': { + const RSAPrivateKey = asn1.get('RSAPrivateKey') + const parsed = RSAPrivateKey.decode(key, format, { label }) + + const keyObject = new KeyObject() + i(keyObject).asn1 = parsed + i(keyObject).asymmetricKeyType = 'rsa' + i(keyObject).type = 'private' + i(keyObject).pem = RSAPrivateKey.encode(parsed, 'pem', { label: 'RSA PRIVATE KEY' }) + + return keyObject + } + case 'sec1': { + const ECPrivateKey = asn1.get('ECPrivateKey') + let parsed = ECPrivateKey.decode(key, format, { label }) + + if (!('parameters' in parsed) && !hints[namedCurve]) { + throw new Error('invalid sec1') + } else if (!('parameters' in parsed)) { + parsed = { ...parsed, parameters: { type: 'namedCurve', value: hints[namedCurve] } } + } + + const keyObject = new KeyObject() + i(keyObject).asn1 = parsed + i(keyObject).asymmetricKeyType = 'ec' + i(keyObject).type = 'private' + i(keyObject).pem = ECPrivateKey.encode(parsed, 'pem', { label: 'EC PRIVATE KEY' }) + + return keyObject + } + default: + throw new TypeError('type must be one of "pkcs8", "pkcs1" or "sec1"') + } + } +} + +module.exports = { createPublicKey, createPrivateKey, createSecretKey, KeyObject } diff --git a/lib/help/key_utils.js b/lib/help/key_utils.js index 371fc9092f..36b9578f52 100644 --- a/lib/help/key_utils.js +++ b/lib/help/key_utils.js @@ -1,8 +1,9 @@ -const { createPublicKey } = require('crypto') +const { EOL } = require('os') const { name: secp256k1 } = require('../jwk/key/secp256k1_crv') const errors = require('../errors') +const { createPublicKey } = require('./key_object') const base64url = require('./base64url') const asn1 = require('./asn1') const computePrimes = require('./rsa_primes') @@ -29,7 +30,7 @@ const crvToOidBuf = new Map([ ['P-521', Buffer.from('06052b81040023', 'hex')] ]) -const formatPem = (base64pem, descriptor) => `-----BEGIN ${descriptor} KEY-----\n${base64pem.match(/.{1,64}/g).join('\n')}\n-----END ${descriptor} KEY-----` +const formatPem = (base64pem, descriptor) => `-----BEGIN ${descriptor} KEY-----${EOL}${base64pem.match(/.{1,64}/g).join(EOL)}${EOL}-----END ${descriptor} KEY-----` const okpToJWK = { private (crv, keyObject) { @@ -192,6 +193,19 @@ const concatEcPublicKey = (x, y) => ({ ]) }) +const okpCrvToOid = (crv) => { + switch (crv) { + case 'X25519': + return '1.3.101.110'.split('.') + case 'X448': + return '1.3.101.111'.split('.') + case 'Ed25519': + return '1.3.101.112'.split('.') + case 'Ed448': + return '1.3.101.113'.split('.') + } +} + const jwkToPem = { RSA: { private (jwk, { calculateMissingRSAPrimes }) { @@ -288,19 +302,6 @@ const jwkToPem = { } } -const okpCrvToOid = (crv) => { - switch (crv) { - case 'X25519': - return '1.3.101.110'.split('.') - case 'X448': - return '1.3.101.111'.split('.') - case 'Ed25519': - return '1.3.101.112'.split('.') - case 'Ed448': - return '1.3.101.113'.split('.') - } -} - module.exports.jwkToPem = (jwk, { calculateMissingRSAPrimes = false } = {}) => { switch (jwk.kty) { case 'EC': diff --git a/lib/help/node_support.js b/lib/help/node_support.js index 25a5a9b69c..2a23491c55 100644 --- a/lib/help/node_support.js +++ b/lib/help/node_support.js @@ -1,5 +1,9 @@ +const { KeyObject, sign, verify } = require('crypto') + const [major, minor] = process.version.substr(1).split('.').map(x => parseInt(x, 10)) module.exports = { - oaepHash: major > 12 || (major === 12 && minor >= 9) + oaepHashSupported: major > 12 || (major === 12 && minor >= 9), + keyObjectSupported: !!KeyObject, + edDSASupported: !!sign && !!verify } diff --git a/lib/jwa/aes_cbc_hmac_sha2.js b/lib/jwa/aes_cbc_hmac_sha2.js index 264c8b6f6a..11607a83ad 100644 --- a/lib/jwa/aes_cbc_hmac_sha2.js +++ b/lib/jwa/aes_cbc_hmac_sha2.js @@ -23,7 +23,7 @@ const encrypt = (size, sign, { [KEYOBJECT]: keyObject }, cleartext, { iv, aad = const keySize = size / 8 const encKey = key.slice(keySize) - const cipher = createCipheriv(`AES-${size}-CBC`, encKey, iv) + const cipher = createCipheriv(`aes-${size}-cbc`, encKey, iv) const ciphertext = Buffer.concat([cipher.update(cleartext), cipher.final()]) const macData = Buffer.concat([aad, iv, ciphertext, uint64be(aad.length * 8)]) @@ -47,7 +47,7 @@ const decrypt = (size, sign, { [KEYOBJECT]: keyObject }, ciphertext, { iv, tag = let cleartext try { - const cipher = createDecipheriv(`AES-${size}-CBC`, encKey, iv) + const cipher = createDecipheriv(`aes-${size}-cbc`, encKey, iv) cleartext = Buffer.concat([cipher.update(ciphertext), cipher.final()]) } catch (err) {} diff --git a/lib/jwa/aes_gcm.js b/lib/jwa/aes_gcm.js index 221dcd6dd9..4a3269968c 100644 --- a/lib/jwa/aes_gcm.js +++ b/lib/jwa/aes_gcm.js @@ -16,9 +16,10 @@ const checkInput = function (size, iv, tag) { } const encrypt = (size, { [KEYOBJECT]: keyObject }, cleartext, { iv, aad = Buffer.alloc(0) }) => { + const key = keyObject.asInput ? keyObject.asInput() : keyObject checkInput(size, iv) - const cipher = createCipheriv(`AES-${size}-GCM`, keyObject, iv) + const cipher = createCipheriv(`aes-${size}-gcm`, key, iv) cipher.setAAD(aad) const ciphertext = Buffer.concat([cipher.update(cleartext), cipher.final()]) @@ -28,10 +29,11 @@ const encrypt = (size, { [KEYOBJECT]: keyObject }, cleartext, { iv, aad = Buffer } const decrypt = (size, { [KEYOBJECT]: keyObject }, ciphertext, { iv, tag = Buffer.alloc(0), aad = Buffer.alloc(0) }) => { + const key = keyObject.asInput ? keyObject.asInput() : keyObject checkInput(size, iv, tag) try { - const cipher = createDecipheriv(`AES-${size}-GCM`, keyObject, iv) + const cipher = createDecipheriv(`aes-${size}-gcm`, key, iv) cipher.setAuthTag(tag) cipher.setAAD(aad) diff --git a/lib/jwa/aes_kw.js b/lib/jwa/aes_kw.js index 0483b0b055..0dd6e2e6e3 100644 --- a/lib/jwa/aes_kw.js +++ b/lib/jwa/aes_kw.js @@ -32,6 +32,7 @@ const split = (input, size) => { } const wrapKey = (size, { [KEYOBJECT]: keyObject }, payload) => { + const key = keyObject.asInput ? keyObject.asInput() : keyObject const iv = Buffer.alloc(16) let R = split(payload, 8) let A @@ -41,7 +42,7 @@ const wrapKey = (size, { [KEYOBJECT]: keyObject }, payload) => { for (let jdx = 0; jdx < 6; jdx++) { for (let idx = 0; R.length > idx; idx++) { count = (R.length * jdx) + idx + 1 - const cipher = createCipheriv(`AES${size}`, keyObject, iv) + const cipher = createCipheriv(`aes${size}`, key, iv) B = Buffer.concat([A, R[idx]]) B = cipher.update(B) @@ -55,6 +56,7 @@ const wrapKey = (size, { [KEYOBJECT]: keyObject }, payload) => { } const unwrapKey = (size, { [KEYOBJECT]: keyObject }, payload) => { + const key = keyObject.asInput ? keyObject.asInput() : keyObject checkInput(payload) const iv = Buffer.alloc(16) @@ -70,7 +72,7 @@ const unwrapKey = (size, { [KEYOBJECT]: keyObject }, payload) => { count = (R.length * jdx) + idx + 1 B = xor(A, uint64be(count)) B = Buffer.concat([B, R[idx], iv]) - const cipher = createDecipheriv(`AES${size}`, keyObject, iv) + const cipher = createDecipheriv(`aes${size}`, key, iv) B = cipher.update(B) A = B.slice(0, 8) diff --git a/lib/jwa/ecdsa.js b/lib/jwa/ecdsa.js index b71ac41bdc..e75e9f3d76 100644 --- a/lib/jwa/ecdsa.js +++ b/lib/jwa/ecdsa.js @@ -1,19 +1,37 @@ const { strict: assert } = require('assert') -const { sign: signOneShot, verify: verifyOneShot } = require('crypto') +const { sign: signOneShot, verify: verifyOneShot, createSign, createVerify } = require('crypto') const { derToJose, joseToDer } = require('../help/ecdsa_signatures') const { KEYOBJECT } = require('../help/consts') const resolveNodeAlg = require('../help/node_alg') -const sign = (jwaAlg, nodeAlg, { [KEYOBJECT]: keyObject }, payload) => { - return derToJose(signOneShot(nodeAlg, payload, keyObject), jwaAlg) +let sign, verify + +if (signOneShot) { + sign = (jwaAlg, nodeAlg, { [KEYOBJECT]: keyObject }, payload) => { + return derToJose(signOneShot(nodeAlg, payload, keyObject), jwaAlg) + } +} else { + sign = (jwaAlg, nodeAlg, { [KEYOBJECT]: keyObject }, payload) => { + return derToJose(createSign(nodeAlg).update(payload).sign(keyObject.asInput()), jwaAlg) + } } -const verify = (jwaAlg, nodeAlg, { [KEYOBJECT]: keyObject }, payload, signature) => { - try { - return verifyOneShot(nodeAlg, payload, keyObject, joseToDer(signature, jwaAlg)) - } catch (err) { - return false +if (verifyOneShot) { + verify = (jwaAlg, nodeAlg, { [KEYOBJECT]: keyObject }, payload, signature) => { + try { + return verifyOneShot(nodeAlg, payload, keyObject, joseToDer(signature, jwaAlg)) + } catch (err) { + return false + } + } +} else { + verify = (jwaAlg, nodeAlg, { [KEYOBJECT]: keyObject }, payload, signature) => { + try { + return createVerify(nodeAlg).update(payload).verify(keyObject.asInput(true), joseToDer(signature, jwaAlg)) + } catch (err) { + return false + } } } diff --git a/lib/jwa/hmac.js b/lib/jwa/hmac.js index 0b090147ef..766b0b1d8b 100644 --- a/lib/jwa/hmac.js +++ b/lib/jwa/hmac.js @@ -6,13 +6,13 @@ const timingSafeEqual = require('../help/timing_safe_equal') const resolveNodeAlg = require('../help/node_alg') const sign = (jwaAlg, hmacAlg, { [KEYOBJECT]: keyObject }, payload) => { - const hmac = createHmac(hmacAlg, keyObject) + const hmac = createHmac(hmacAlg, keyObject.asInput ? keyObject.asInput() : keyObject) hmac.update(payload) return hmac.digest() } const verify = (jwaAlg, hmacAlg, { [KEYOBJECT]: keyObject }, payload, signature) => { - const hmac = createHmac(hmacAlg, keyObject) + const hmac = createHmac(hmacAlg, keyObject.asInput ? keyObject.asInput() : keyObject) hmac.update(payload) const expected = hmac.digest() const actual = signature diff --git a/lib/jwa/rsaes.js b/lib/jwa/rsaes.js index bc236acc39..4cba7e2fd4 100644 --- a/lib/jwa/rsaes.js +++ b/lib/jwa/rsaes.js @@ -25,11 +25,13 @@ const resolveOaepHash = (alg) => { } const wrapKey = (padding, oaepHash, { [KEYOBJECT]: keyObject }, payload) => { - return { wrapped: publicEncrypt({ key: keyObject, oaepHash, padding }, payload) } + const key = keyObject.asInput ? keyObject.asInput(true) : keyObject + return { wrapped: publicEncrypt({ key, oaepHash, padding }, payload) } } const unwrapKey = (padding, oaepHash, { [KEYOBJECT]: keyObject }, payload) => { - return privateDecrypt({ key: keyObject, oaepHash, padding }, payload) + const key = keyObject.asInput ? keyObject.asInput(false) : keyObject + return privateDecrypt({ key, oaepHash, padding }, payload) } module.exports = (JWA) => { diff --git a/lib/jwa/rsassa.js b/lib/jwa/rsassa.js index 9f15ce6783..a64d906092 100644 --- a/lib/jwa/rsassa.js +++ b/lib/jwa/rsassa.js @@ -1,15 +1,33 @@ const { strict: assert } = require('assert') -const { sign: signOneShot, verify: verifyOneShot } = require('crypto') +const { sign: signOneShot, verify: verifyOneShot, createSign, createVerify } = require('crypto') const { KEYOBJECT } = require('../help/consts') const resolveNodeAlg = require('../help/node_alg') -const sign = (nodeAlg, { [KEYOBJECT]: keyObject }, payload) => { - return signOneShot(nodeAlg, payload, keyObject) +let sign, verify + +if (signOneShot) { + sign = (nodeAlg, { [KEYOBJECT]: keyObject }, payload) => { + return signOneShot(nodeAlg, payload, keyObject) + } +} else { + sign = (nodeAlg, { [KEYOBJECT]: keyObject }, payload) => { + return createSign(nodeAlg).update(payload).sign(keyObject.asInput()) + } } -const verify = (nodeAlg, { [KEYOBJECT]: keyObject }, payload, signature) => { - return verifyOneShot(nodeAlg, payload, keyObject, signature) +if (verifyOneShot) { + verify = (nodeAlg, { [KEYOBJECT]: keyObject }, payload, signature) => { + return verifyOneShot(nodeAlg, payload, keyObject, signature) + } +} else { + verify = (nodeAlg, { [KEYOBJECT]: keyObject }, payload, signature) => { + try { + return createVerify(nodeAlg).update(payload).verify(keyObject.asInput(true), signature) + } catch (err) { + return false + } + } } module.exports = (JWA) => { diff --git a/lib/jwa/rsassa_pss.js b/lib/jwa/rsassa_pss.js index c62d6bd45d..b96ce345ee 100644 --- a/lib/jwa/rsassa_pss.js +++ b/lib/jwa/rsassa_pss.js @@ -1,23 +1,53 @@ const { strict: assert } = require('assert') -const { sign: signOneShot, verify: verifyOneShot, constants } = require('crypto') +const { + sign: signOneShot, + verify: verifyOneShot, + createSign, + createVerify, + constants +} = require('crypto') const { KEYOBJECT } = require('../help/consts') const resolveNodeAlg = require('../help/node_alg') -const sign = (nodeAlg, { [KEYOBJECT]: keyObject }, payload) => { - return signOneShot(nodeAlg, payload, { - key: keyObject, - padding: constants.RSA_PKCS1_PSS_PADDING, - saltLength: constants.RSA_PSS_SALTLEN_DIGEST - }) +let sign, verify + +if (signOneShot) { + sign = (nodeAlg, { [KEYOBJECT]: keyObject }, payload) => { + return signOneShot(nodeAlg, payload, { + key: keyObject, + padding: constants.RSA_PKCS1_PSS_PADDING, + saltLength: constants.RSA_PSS_SALTLEN_DIGEST + }) + } +} else { + sign = (nodeAlg, { [KEYOBJECT]: keyObject }, payload) => { + const key = keyObject.asInput() + return createSign(nodeAlg).update(payload).sign({ + key, + padding: constants.RSA_PKCS1_PSS_PADDING, + saltLength: constants.RSA_PSS_SALTLEN_DIGEST + }) + } } -const verify = (nodeAlg, { [KEYOBJECT]: keyObject }, payload, signature) => { - return verifyOneShot(nodeAlg, payload, { - key: keyObject, - padding: constants.RSA_PKCS1_PSS_PADDING, - saltLength: constants.RSA_PSS_SALTLEN_DIGEST - }, signature) +if (verifyOneShot) { + verify = (nodeAlg, { [KEYOBJECT]: keyObject }, payload, signature) => { + return verifyOneShot(nodeAlg, payload, { + key: keyObject, + padding: constants.RSA_PKCS1_PSS_PADDING, + saltLength: constants.RSA_PSS_SALTLEN_DIGEST + }, signature) + } +} else { + verify = (nodeAlg, { [KEYOBJECT]: keyObject }, payload, signature) => { + const key = keyObject.asInput(true) + return createVerify(nodeAlg).update(payload).verify({ + key, + padding: constants.RSA_PKCS1_PSS_PADDING, + saltLength: constants.RSA_PSS_SALTLEN_DIGEST + }, signature) + } } module.exports = (JWA) => { diff --git a/lib/jwe/decrypt.js b/lib/jwe/decrypt.js index 8490d3236c..214a00f7a0 100644 --- a/lib/jwe/decrypt.js +++ b/lib/jwe/decrypt.js @@ -1,4 +1,3 @@ -const { createSecretKey } = require('crypto') const { inflateRawSync } = require('zlib') const base64url = require('../help/base64url') @@ -8,6 +7,7 @@ const errors = require('../errors') const { check, decrypt, keyManagementDecrypt } = require('../jwa') const JWK = require('../jwk') +const { createSecretKey } = require('../help/key_object') const generateCEK = require('./generate_cek') const validateHeaders = require('./validate_headers') const { detect: resolveSerialization } = require('./serializers') diff --git a/lib/jwe/encrypt.js b/lib/jwe/encrypt.js index 017ea7e9ae..cdc6adae13 100644 --- a/lib/jwe/encrypt.js +++ b/lib/jwe/encrypt.js @@ -1,10 +1,10 @@ -const { createSecretKey } = require('crypto') const { deflateRawSync } = require('zlib') const { KEYOBJECT } = require('../help/consts') const generateIV = require('../help/generate_iv') const base64url = require('../help/base64url') const isObject = require('../help/is_object') +const { createSecretKey } = require('../help/key_object') const deepClone = require('../help/deep_clone') const Key = require('../jwk/key/base') const importKey = require('../jwk/import') @@ -17,19 +17,16 @@ const validateHeaders = require('./validate_headers') const PROCESS_RECIPIENT = Symbol('PROCESS_RECIPIENT') -class Encrypt { - #aad - - #cek - - #unprotected - - #protected +const map = new WeakMap() - #cleartext - - #recipients +const i = (ctx) => { + if (!map.has(ctx)) { + map.set(ctx, {}) + } + return map.get(ctx) +} +class Encrypt { constructor (cleartext, protectedHeader, unprotectedHeader, aad) { if (!Buffer.isBuffer(cleartext) && typeof cleartext !== 'string') { throw new TypeError('cleartext argument must be a Buffer or a string') @@ -49,11 +46,11 @@ class Encrypt { throw new TypeError('unprotectedHeader argument must be a plain object when provided') } - this.#recipients = [] - this.#cleartext = cleartext - this.#aad = aad - this.#unprotected = unprotectedHeader ? deepClone(unprotectedHeader) : undefined - this.#protected = protectedHeader ? deepClone(protectedHeader) : undefined + i(this).recipients = [] + i(this).cleartext = cleartext + i(this).aad = aad + i(this).unprotected = unprotectedHeader ? deepClone(unprotectedHeader) : undefined + i(this).protected = protectedHeader ? deepClone(protectedHeader) : undefined } /* @@ -68,7 +65,7 @@ class Encrypt { throw new TypeError('header argument must be a plain object when provided') } - this.#recipients.push({ + i(this).recipients.push({ key, header: header ? deepClone(header) : undefined }) @@ -80,9 +77,9 @@ class Encrypt { * @private */ [PROCESS_RECIPIENT] (recipient) { - const unprotectedHeader = this.#unprotected - const protectedHeader = this.#protected - const { length: recipientCount } = this.#recipients + const unprotectedHeader = i(this).unprotected + const protectedHeader = i(this).protected + const { length: recipientCount } = i(this).recipients const jweHeader = { ...protectedHeader, @@ -114,7 +111,7 @@ class Encrypt { if (protectedHeader) { protectedHeader.alg = alg } else { - this.#protected = { alg } + i(this).protected = { alg } } } else { if (recipient.header) { @@ -129,11 +126,11 @@ class Encrypt { let generatedHeader if (key.kty === 'oct' && alg === 'dir') { - this.#cek = importKey(key[KEYOBJECT], { use: 'enc', alg: enc }) + i(this).cek = importKey(key[KEYOBJECT], { use: 'enc', alg: enc }) } else { - ({ wrapped, header: generatedHeader } = keyManagementEncrypt(alg, key, this.#cek[KEYOBJECT].export(), { enc, alg })) + ({ wrapped, header: generatedHeader } = keyManagementEncrypt(alg, key, i(this).cek[KEYOBJECT].export(), { enc, alg })) if (alg === 'ECDH-ES') { - this.#cek = importKey(createSecretKey(wrapped), { use: 'enc', alg: enc }) + i(this).cek = importKey(createSecretKey(wrapped), { use: 'enc', alg: enc }) } } @@ -157,58 +154,58 @@ class Encrypt { throw new TypeError('serialization must be one of "compact", "flattened", "general"') } - if (!this.#recipients.length) { + if (!i(this).recipients.length) { throw new JWEInvalid('missing recipients') } - serializer.validate(this.#protected, this.#unprotected, this.#aad, this.#recipients) + serializer.validate(i(this).protected, i(this).unprotected, i(this).aad, i(this).recipients) - let enc = validateHeaders(this.#protected, this.#unprotected, this.#recipients, false, this.#protected ? this.#protected.crit : undefined) + let enc = validateHeaders(i(this).protected, i(this).unprotected, i(this).recipients, false, i(this).protected ? i(this).protected.crit : undefined) if (!enc) { enc = 'A128CBC-HS256' - if (this.#protected) { - this.#protected.enc = enc + if (i(this).protected) { + i(this).protected.enc = enc } else { - this.#protected = { enc } + i(this).protected = { enc } } } const final = {} - this.#cek = generateCEK(enc) + i(this).cek = generateCEK(enc) - this.#recipients.forEach(this[PROCESS_RECIPIENT].bind(this)) + i(this).recipients.forEach(this[PROCESS_RECIPIENT].bind(this)) const iv = generateIV(enc) final.iv = base64url.encodeBuffer(iv) - if (this.#recipients.length === 1 && this.#recipients[0].generatedHeader) { - const [{ generatedHeader }] = this.#recipients - delete this.#recipients[0].generatedHeader - this.#protected = Object.assign({}, this.#protected, generatedHeader) + if (i(this).recipients.length === 1 && i(this).recipients[0].generatedHeader) { + const [{ generatedHeader }] = i(this).recipients + delete i(this).recipients[0].generatedHeader + i(this).protected = Object.assign({}, i(this).protected, generatedHeader) } - if (this.#protected) { - final.protected = base64url.JSON.encode(this.#protected) + if (i(this).protected) { + final.protected = base64url.JSON.encode(i(this).protected) } - final.unprotected = this.#unprotected + final.unprotected = i(this).unprotected let aad - if (this.#aad) { - final.aad = base64url.encode(this.#aad) + if (i(this).aad) { + final.aad = base64url.encode(i(this).aad) aad = Buffer.concat([Buffer.from(final.protected || ''), Buffer.from('.'), Buffer.from(final.aad)]) } else { aad = Buffer.from(final.protected || '') } - let cleartext = this.#cleartext - if (this.#protected && 'zip' in this.#protected) { + let cleartext = i(this).cleartext + if (i(this).protected && 'zip' in i(this).protected) { cleartext = deflateRawSync(cleartext) } - const { ciphertext, tag } = encrypt(enc, this.#cek, cleartext, { iv, aad }) + const { ciphertext, tag } = encrypt(enc, i(this).cek, cleartext, { iv, aad }) final.tag = base64url.encodeBuffer(tag) final.ciphertext = base64url.encodeBuffer(ciphertext) - return serializer(final, this.#recipients) + return serializer(final, i(this).recipients) } } diff --git a/lib/jwe/generate_cek.js b/lib/jwe/generate_cek.js index 49aa304bbe..d87d4efb62 100644 --- a/lib/jwe/generate_cek.js +++ b/lib/jwe/generate_cek.js @@ -1,5 +1,6 @@ -const { randomBytes, createSecretKey } = require('crypto') +const { randomBytes } = require('crypto') +const { createSecretKey } = require('../help/key_object') const { KEYLENGTHS } = require('../help/consts') const importKey = require('../jwk/import') diff --git a/lib/jwk/import.js b/lib/jwk/import.js index 6bf31393e8..e78c121191 100644 --- a/lib/jwk/import.js +++ b/lib/jwk/import.js @@ -1,6 +1,6 @@ -const { createPublicKey, createPrivateKey, createSecretKey, KeyObject } = require('crypto') const { deprecate } = require('util') +const { createPublicKey, createPrivateKey, createSecretKey, KeyObject } = require('../help/key_object') const base64url = require('../help/base64url') const isObject = require('../help/is_object') const { jwkToPem } = require('../help/key_utils') diff --git a/lib/jwk/key/base.js b/lib/jwk/key/base.js index ea4ea7c01c..d1dbcb755d 100644 --- a/lib/jwk/key/base.js +++ b/lib/jwk/key/base.js @@ -1,7 +1,9 @@ const { strict: assert } = require('assert') -const { createPublicKey } = require('crypto') const { inspect } = require('util') +const { EOL } = require('os') +const { keyObjectSupported } = require('../../help/node_support') +const { createPublicKey } = require('../../help/key_object') const { keyObjectToJWK } = require('../../help/key_utils') const { THUMBPRINT_MATERIAL, PUBLIC_MEMBERS, PRIVATE_MEMBERS, JWK_MEMBERS, KEYOBJECT, @@ -47,7 +49,7 @@ class Key { } } - if (x5c !== undefined) { + if (keyObjectSupported && x5c !== undefined) { if (!Array.isArray(x5c) || !x5c.length || x5c.some(c => typeof c !== 'string')) { throw new TypeError('`x5c` must be an array of one or more PKIX certificates when provided') } @@ -56,7 +58,7 @@ class Key { let publicKey try { publicKey = createPublicKey({ - key: `-----BEGIN CERTIFICATE-----\n${cert.match(/.{1,64}/g).join('\n')}\n-----END CERTIFICATE-----`, format: 'pem' + key: `-----BEGIN CERTIFICATE-----${EOL}${cert.match(/.{1,64}/g).join(EOL)}${EOL}-----END CERTIFICATE-----`, format: 'pem' }) } catch (err) { throw new errors.JWKInvalid(`\`x5c\` member at index ${i} is not a valid base64-encoded DER PKIX certificate`) @@ -165,9 +167,16 @@ class Key { throw new TypeError('public key cannot be exported as private') } - const result = Object.fromEntries( - [...this.constructor[priv ? PRIVATE_MEMBERS : PUBLIC_MEMBERS]].map(k => [k, this[k]]) - ) + const components = [...this.constructor[priv ? PRIVATE_MEMBERS : PUBLIC_MEMBERS]] + .map(k => [k, this[k]]) + + const result = {} + + Object.keys(components).forEach((key) => { + const [k, v] = components[key] + + result[k] = v + }) result.kty = this.kty result.kid = this.kid diff --git a/lib/jwk/key/ec.js b/lib/jwk/key/ec.js index 65d15610fe..d23f9d5986 100644 --- a/lib/jwk/key/ec.js +++ b/lib/jwk/key/ec.js @@ -5,6 +5,8 @@ const { THUMBPRINT_MATERIAL, JWK_MEMBERS, PUBLIC_MEMBERS, EC_CURVES, PRIVATE_MEMBERS, KEY_MANAGEMENT_DECRYPT, KEY_MANAGEMENT_ENCRYPT, ECDH_ALGS } = require('../../help/consts') +const { keyObjectSupported } = require('../../help/node_support') +const { createPublicKey, createPrivateKey } = require('../../help/key_object') const errors = require('../../errors') const { name: secp256k1 } = require('./secp256k1_crv') @@ -122,9 +124,24 @@ class ECKey extends Key { crv = 'secp256k1' } - const { privateKey, publicKey } = await generateKeyPair('ec', { namedCurve: crv }) + let privateKey, publicKey - return privat ? privateKey : publicKey + if (keyObjectSupported) { + ({ privateKey, publicKey } = await generateKeyPair('ec', { namedCurve: crv })) + return privat ? privateKey : publicKey + } + + ({ privateKey, publicKey } = await generateKeyPair('ec', { + namedCurve: crv, + publicKeyEncoding: { type: 'spki', format: 'pem' }, + privateKeyEncoding: { type: 'pkcs8', format: 'pem' } + })) + + if (privat) { + return createPrivateKey(privateKey) + } else { + return createPublicKey(publicKey) + } } static generateSync (crv = 'P-256', privat = true) { @@ -136,9 +153,24 @@ class ECKey extends Key { crv = 'secp256k1' } - const { privateKey, publicKey } = generateKeyPairSync('ec', { namedCurve: crv }) + let privateKey, publicKey - return privat ? privateKey : publicKey + if (keyObjectSupported) { + ({ privateKey, publicKey } = generateKeyPairSync('ec', { namedCurve: crv })) + return privat ? privateKey : publicKey + } + + ({ privateKey, publicKey } = generateKeyPairSync('ec', { + namedCurve: crv, + publicKeyEncoding: { type: 'spki', format: 'pem' }, + privateKeyEncoding: { type: 'pkcs8', format: 'pem' } + })) + + if (privat) { + return createPrivateKey(privateKey) + } else { + return createPublicKey(publicKey) + } } } diff --git a/lib/jwk/key/oct.js b/lib/jwk/key/oct.js index 3eaa54762d..b6340e2f57 100644 --- a/lib/jwk/key/oct.js +++ b/lib/jwk/key/oct.js @@ -1,5 +1,6 @@ -const { randomBytes, createSecretKey } = require('crypto') +const { randomBytes } = require('crypto') +const { createSecretKey } = require('../../help/key_object') const base64url = require('../../help/base64url') const { KEYOBJECT } = require('../../help/consts') const { diff --git a/lib/jwk/key/rsa.js b/lib/jwk/key/rsa.js index a5f7d4322d..cebab3e76f 100644 --- a/lib/jwk/key/rsa.js +++ b/lib/jwk/key/rsa.js @@ -5,7 +5,8 @@ const { THUMBPRINT_MATERIAL, JWK_MEMBERS, PUBLIC_MEMBERS, PRIVATE_MEMBERS, KEY_MANAGEMENT_DECRYPT, KEY_MANAGEMENT_ENCRYPT } = require('../../help/consts') -const { oaepHash } = require('../../help/node_support') +const { oaepHashSupported, keyObjectSupported } = require('../../help/node_support') +const { createPublicKey, createPrivateKey } = require('../../help/key_object') const Key = require('./base') @@ -14,7 +15,7 @@ const generateKeyPair = promisify(async) const SIG_ALGS = ['PS256', 'RS256', 'PS384', 'RS384', 'PS512', 'RS512'] const WRAP_ALGS = ['RSA-OAEP', 'RSA1_5'] -if (oaepHash) { +if (oaepHashSupported) { WRAP_ALGS.splice(1, 0, 'RSA-OAEP-256') } @@ -151,9 +152,24 @@ class RSAKey extends Key { throw new TypeError('invalid bit length') } - const { privateKey, publicKey } = await generateKeyPair('rsa', { modulusLength: len }) + let privateKey, publicKey - return privat ? privateKey : publicKey + if (keyObjectSupported) { + ({ privateKey, publicKey } = await generateKeyPair('rsa', { modulusLength: len })) + return privat ? privateKey : publicKey + } + + ({ privateKey, publicKey } = await generateKeyPair('rsa', { + modulusLength: len, + publicKeyEncoding: { type: 'spki', format: 'pem' }, + privateKeyEncoding: { type: 'pkcs8', format: 'pem' } + })) + + if (privat) { + return createPrivateKey(privateKey) + } else { + return createPublicKey(publicKey) + } } static generateSync (len = 2048, privat = true) { @@ -161,9 +177,24 @@ class RSAKey extends Key { throw new TypeError('invalid bit length') } - const { privateKey, publicKey } = generateKeyPairSync('rsa', { modulusLength: len }) + let privateKey, publicKey - return privat ? privateKey : publicKey + if (keyObjectSupported) { + ({ privateKey, publicKey } = generateKeyPairSync('rsa', { modulusLength: len })) + return privat ? privateKey : publicKey + } + + ({ privateKey, publicKey } = generateKeyPairSync('rsa', { + modulusLength: len, + publicKeyEncoding: { type: 'spki', format: 'pem' }, + privateKeyEncoding: { type: 'pkcs8', format: 'pem' } + })) + + if (privat) { + return createPrivateKey(privateKey) + } else { + return createPublicKey(publicKey) + } } } diff --git a/lib/jwks/keystore.js b/lib/jwks/keystore.js index 1eb425f533..7b03f16f5c 100644 --- a/lib/jwks/keystore.js +++ b/lib/jwks/keystore.js @@ -36,18 +36,32 @@ const keyscore = (key, { alg, kid, use, ops, x5t, x5t256 }) => { return score } -class KeyStore { - #keys +const map = new WeakMap() + +const i = (ctx) => { + if (!map.has(ctx)) { + map.set(ctx, {}) + } + return map.get(ctx) +} +class KeyStore { constructor (...keys) { while (keys.some(Array.isArray)) { - keys = keys.flat() + keys = keys.flat ? keys.flat() : keys.reduce((acc, val) => { + if (Array.isArray(val)) { + return [...acc, ...val] + } + + acc.push(val) + return acc + }, []) } if (keys.some(k => !(k instanceof Key))) { throw new TypeError('all keys must be an instances of a key instantiated by JWK.asKey') } - this.#keys = new Set(keys) + i(this).keys = new Set(keys) } all ({ alg, kid, use, kty, key_ops: ops, x5t, 'x5t#S256': x5t256 } = {}) { @@ -56,7 +70,7 @@ class KeyStore { } const search = { alg, kid, use, ops, x5t, x5t256 } - return [...this.#keys] + return [...i(this).keys] .filter((key) => { let candidate = true @@ -110,7 +124,7 @@ class KeyStore { throw new TypeError('key must be an instance of a key instantiated by JWK.asKey') } - this.#keys.add(key) + i(this).keys.add(key) } remove (key) { @@ -118,23 +132,23 @@ class KeyStore { throw new TypeError('key must be an instance of a key instantiated by JWK.asKey') } - this.#keys.delete(key) + i(this).keys.delete(key) } toJWKS (priv = false) { - return { keys: [...this.#keys.values()].map(key => key.toJWK(priv)) } + return { keys: [...i(this).keys.values()].map(key => key.toJWK(priv)) } } async generate (...args) { - this.#keys.add(await generate(...args)) + i(this).keys.add(await generate(...args)) } generateSync (...args) { - this.#keys.add(generateSync(...args)) + i(this).keys.add(generateSync(...args)) } get size () { - return this.#keys.size + return i(this).keys.size } /* c8 ignore next 8 */ @@ -148,7 +162,7 @@ class KeyStore { } * [Symbol.iterator] () { - for (const key of this.#keys) { + for (const key of i(this).keys) { yield key } } diff --git a/lib/jws/sign.js b/lib/jws/sign.js index 110ba37c66..d7c8adf834 100644 --- a/lib/jws/sign.js +++ b/lib/jws/sign.js @@ -10,13 +10,16 @@ const serializers = require('./serializers') const PROCESS_RECIPIENT = Symbol('PROCESS_RECIPIENT') -class Sign { - #b64 - - #payload +const map = new WeakMap() - #recipients +const i = (ctx) => { + if (!map.has(ctx)) { + map.set(ctx, {}) + } + return map.get(ctx) +} +class Sign { constructor (payload) { if (typeof payload === 'string') { payload = base64url.encode(payload) @@ -28,8 +31,8 @@ class Sign { throw new TypeError('payload argument must be a Buffer, string or an object') } - this.#payload = payload - this.#recipients = [] + i(this).payload = payload + i(this).recipients = [] } /* @@ -52,7 +55,7 @@ class Sign { throw new JWSInvalid('JWS Protected and JWS Unprotected Header Parameter names must be disjoint') } - this.#recipients.push({ + i(this).recipients.push({ key, protectedHeader: protectedHeader ? deepClone(protectedHeader) : undefined, unprotectedHeader: unprotectedHeader ? deepClone(unprotectedHeader) : undefined @@ -90,19 +93,19 @@ class Sign { } if (joseHeader.protected.crit && joseHeader.protected.crit.includes('b64')) { - if (this.#b64 !== undefined && this.#b64 !== joseHeader.protected.b64) { + if (i(this).b64 !== undefined && i(this).b64 !== joseHeader.protected.b64) { throw new JWSInvalid('the "b64" Header Parameter value MUST be the same for all recipients') } else { - this.#b64 = joseHeader.protected.b64 + i(this).b64 = joseHeader.protected.b64 } if (!joseHeader.protected.b64) { - this.#payload = base64url.decode(this.#payload) + i(this).payload = base64url.decode(i(this).payload) } } recipient.header = unprotectedHeader recipient.protected = Object.keys(joseHeader.protected).length ? base64url.JSON.encode(joseHeader.protected) : '' - recipient.signature = base64url.encodeBuffer(sign(alg, key, Buffer.from(`${recipient.protected}.${this.#payload}`))) + recipient.signature = base64url.encodeBuffer(sign(alg, key, Buffer.from(`${recipient.protected}.${i(this).payload}`))) } /* @@ -114,15 +117,15 @@ class Sign { throw new TypeError('serialization must be one of "compact", "flattened", "general"') } - if (!this.#recipients.length) { + if (!i(this).recipients.length) { throw new JWSInvalid('missing recipients') } - serializer.validate(this, this.#recipients) + serializer.validate(this, i(this).recipients) - this.#recipients.forEach(this[PROCESS_RECIPIENT].bind(this)) + i(this).recipients.forEach(this[PROCESS_RECIPIENT].bind(this)) - return serializer(this.#payload, this.#recipients) + return serializer(i(this).payload, i(this).recipients) } } diff --git a/package.json b/package.json index 5818ede3b0..453c48e590 100644 --- a/package.json +++ b/package.json @@ -67,7 +67,7 @@ "standard": "^13.1.0" }, "engines": { - "node": ">=12.0.0" + "node": "^10.13.0 || >=12.0.0" }, "ava": { "babel": false, diff --git a/test/cookbook/jwk.test.js b/test/cookbook/jwk.test.js index 0f4000e33a..ee0f602791 100644 --- a/test/cookbook/jwk.test.js +++ b/test/cookbook/jwk.test.js @@ -1,8 +1,11 @@ const test = require('ava') +const { keyObjectSupported } = require('../../lib/help/node_support') + const recipes = require('./recipes') const { JWK: { asKey }, JWKS: { KeyStore } } = require('../..') +const errors = require('../../lib/errors') test('public EC', t => { const jwk = recipes.get('3.1') @@ -27,9 +30,15 @@ test('public EC', t => { test('private EC', t => { const jwk = recipes.get('3.2') const key = asKey(jwk) - t.true(key.toPEM(true, { cipher: 'aes-256-cbc', passphrase: 'top secret' }).includes('BEGIN ENCRYPTED PRIVATE KEY')) t.true(key.toPEM(true, { type: 'sec1' }).includes('BEGIN EC PRIVATE KEY')) - t.true(key.toPEM(true, { type: 'sec1', cipher: 'aes-256-cbc', passphrase: 'top secret' }).includes('ENCRYPTED')) + if (keyObjectSupported) { + t.true(key.toPEM(true, { cipher: 'aes-256-cbc', passphrase: 'top secret' }).includes('BEGIN ENCRYPTED PRIVATE KEY')) + t.true(key.toPEM(true, { type: 'sec1', cipher: 'aes-256-cbc', passphrase: 'top secret' }).includes('ENCRYPTED')) + } else { + t.throws(() => { + key.toPEM(true, { cipher: 'aes-256-cbc', passphrase: 'top secret' }).includes('BEGIN ENCRYPTED PRIVATE KEY') + }, { instanceOf: errors.JOSENotSupported, code: 'ERR_JOSE_NOT_SUPPORTED', message: 'encrypted private keys are not supported in your Node.js runtime version' }) + } t.true(key.toPEM(true).includes('BEGIN PRIVATE KEY')) t.true(key.toPEM().includes('BEGIN PUBLIC KEY')) t.deepEqual(key.toJWK(true), jwk) @@ -62,9 +71,15 @@ test('private RSA', t => { const jwk = recipes.get('3.4') const key = asKey(jwk) t.true(key.toPEM(true, { type: 'pkcs1' }).includes('BEGIN RSA PRIVATE KEY')) - t.true(key.toPEM(true, { cipher: 'aes-256-cbc', passphrase: 'top secret', type: 'pkcs1' }).includes('ENCRYPTED')) - t.true(key.toPEM(true, { type: 'pkcs1', cipher: 'aes-256-cbc', passphrase: 'top secret' }).includes('BEGIN RSA PRIVATE KEY')) - t.true(key.toPEM(true, { cipher: 'aes-256-cbc', passphrase: 'top secret' }).includes('BEGIN ENCRYPTED PRIVATE KEY')) + if (keyObjectSupported) { + t.true(key.toPEM(true, { cipher: 'aes-256-cbc', passphrase: 'top secret', type: 'pkcs1' }).includes('ENCRYPTED')) + t.true(key.toPEM(true, { type: 'pkcs1', cipher: 'aes-256-cbc', passphrase: 'top secret' }).includes('BEGIN RSA PRIVATE KEY')) + t.true(key.toPEM(true, { cipher: 'aes-256-cbc', passphrase: 'top secret' }).includes('BEGIN ENCRYPTED PRIVATE KEY')) + } else { + t.throws(() => { + key.toPEM(true, { cipher: 'aes-256-cbc', passphrase: 'top secret' }).includes('BEGIN ENCRYPTED PRIVATE KEY') + }, { instanceOf: errors.JOSENotSupported, code: 'ERR_JOSE_NOT_SUPPORTED', message: 'encrypted private keys are not supported in your Node.js runtime version' }) + } t.true(key.toPEM(true).includes('BEGIN PRIVATE KEY')) t.true(key.toPEM().includes('BEGIN PUBLIC KEY')) t.deepEqual(key.toJWK(true), jwk) diff --git a/test/cookbook/rfc8037.a4.ed25519.test.js b/test/cookbook/rfc8037.a4.ed25519.test.js index 667d7ebaec..b82c7d82d0 100644 --- a/test/cookbook/rfc8037.a4.ed25519.test.js +++ b/test/cookbook/rfc8037.a4.ed25519.test.js @@ -1,5 +1,9 @@ const test = require('ava') +const { edDSASupported } = require('../../lib/help/node_support') + +if (!edDSASupported) return + const recipe = require('./recipes').get('A.4 rfc8037') const { JWS, JWK: { asKey, generateSync }, JWKS: { KeyStore }, errors } = require('../..') diff --git a/test/help/P-256K.key_utils.test.js b/test/help/P-256K.key_utils.test.js index f6e5a27f8a..b4db798472 100644 --- a/test/help/P-256K.key_utils.test.js +++ b/test/help/P-256K.key_utils.test.js @@ -5,7 +5,7 @@ const test = require('ava') if ('electron' in process.versions) return -const { createPublicKey, createPrivateKey } = require('crypto') +const { createPublicKey, createPrivateKey } = require('../../lib/help/key_object') const { keyObjectToJWK, jwkToPem } = require('../../lib/help/key_utils') const { JWK: fixtures } = require('../fixtures') diff --git a/test/help/key_utils.test.js b/test/help/key_utils.test.js index f448b238c7..cc9a94fff8 100644 --- a/test/help/key_utils.test.js +++ b/test/help/key_utils.test.js @@ -1,6 +1,7 @@ const test = require('ava') -const { createPublicKey, createPrivateKey } = require('crypto') +const { edDSASupported } = require('../../lib/help/node_support') +const { createPublicKey, createPrivateKey } = require('../../lib/help/key_object') const { errors } = require('../..') const { keyObjectToJWK, jwkToPem } = require('../../lib/help/key_utils') const { JWK: fixtures } = require('../fixtures') @@ -40,24 +41,26 @@ test('RSA Private key', t => { t.deepEqual(actual, expected) }) -test('Ed25519 Public key', t => { - const expected = clone(fixtures.Ed25519) - delete expected.d - const pem = createPublicKey(jwkToPem(expected)) - const actual = keyObjectToJWK(pem) +if (edDSASupported) { + test('Ed25519 Public key', t => { + const expected = clone(fixtures.Ed25519) + delete expected.d + const pem = createPublicKey(jwkToPem(expected)) + const actual = keyObjectToJWK(pem) - t.deepEqual(actual, expected) -}) + t.deepEqual(actual, expected) + }) -test('Ed25519 Private key', t => { - const expected = fixtures.Ed25519 - const pem = createPrivateKey(jwkToPem(expected)) - const actual = keyObjectToJWK(pem) + test('Ed25519 Private key', t => { + const expected = fixtures.Ed25519 + const pem = createPrivateKey(jwkToPem(expected)) + const actual = keyObjectToJWK(pem) - t.deepEqual(actual, expected) -}) + t.deepEqual(actual, expected) + }) +} -if (!('electron' in process.versions)) { +if (!('electron' in process.versions) && edDSASupported) { test('Ed448 Public key', t => { const expected = clone(fixtures.Ed448) delete expected.d diff --git a/test/jwe/smoke.test.js b/test/jwe/smoke.test.js index 10ac13320a..90aef26253 100644 --- a/test/jwe/smoke.test.js +++ b/test/jwe/smoke.test.js @@ -2,6 +2,7 @@ const test = require('ava') const { randomBytes } = require('crypto') +const { edDSASupported } = require('../../lib/help/node_support') const { JWK: { asKey, generateSync } } = require('../..') const ENCS = [ @@ -20,6 +21,7 @@ const { JWE: { success, failure } } = require('../macros') Object.entries(fixtures.PEM).forEach(([type, { private: key, public: pub }]) => { if (type === 'P-256K') return if ('electron' in process.versions && (type.startsWith('X') || type === 'Ed448' || type === 'secp256k1')) return + if (!edDSASupported && (type.startsWith('Ed') || type.startsWith('X'))) return const eKey = asKey(pub) const dKey = asKey(key) diff --git a/test/jwk/P-256K.import.test.js b/test/jwk/P-256K.import.test.js index 0e85647d5f..f0e8a14ba0 100644 --- a/test/jwk/P-256K.import.test.js +++ b/test/jwk/P-256K.import.test.js @@ -5,7 +5,7 @@ const test = require('ava') if ('electron' in process.versions) return -const { createPrivateKey, createPublicKey } = require('crypto') +const { createPrivateKey, createPublicKey } = require('../../lib/help/key_object') const { hasProperty, hasNoProperties, hasProperties } = require('../macros') const fixtures = require('../fixtures') diff --git a/test/jwk/ec.test.js b/test/jwk/ec.test.js index a0b9233071..7499d008a5 100644 --- a/test/jwk/ec.test.js +++ b/test/jwk/ec.test.js @@ -1,6 +1,7 @@ const test = require('ava') -const { createPrivateKey, createPublicKey, generateKeyPairSync } = require('crypto') +const { createPrivateKey, createPublicKey } = require('../../lib/help/key_object') const { hasProperty, hasNoProperties, hasProperties } = require('../macros') +const { generateKeyPairSync } = require('../macros/generate') const fixtures = require('../fixtures') const errors = require('../../lib/errors') diff --git a/test/jwk/generate.test.js b/test/jwk/generate.test.js index 2576886311..5c044f1fce 100644 --- a/test/jwk/generate.test.js +++ b/test/jwk/generate.test.js @@ -1,5 +1,7 @@ const test = require('ava') +const { edDSASupported } = require('../../lib/help/node_support') + const { JWK: { generate, generateSync }, errors } = require('../..') ;[ @@ -13,23 +15,23 @@ const { JWK: { generate, generateSync }, errors } = require('../..') ['RSA', 2048, { use: 'enc', alg: 'RSA-OAEP' }], ['RSA', 2048, { alg: 'PS256' }], ['RSA', 2048, { alg: 'RSA-OAEP' }], - ['OKP'], - ['OKP', undefined, undefined, true], - ['OKP', undefined, undefined, false], - ['OKP', 'Ed25519'], - ['OKP', 'Ed25519', { use: 'sig' }], + edDSASupported ? ['OKP'] : undefined, + edDSASupported ? ['OKP', undefined, undefined, true] : undefined, + edDSASupported ? ['OKP', undefined, undefined, false] : undefined, + edDSASupported ? ['OKP', 'Ed25519'] : undefined, + edDSASupported ? ['OKP', 'Ed25519', { use: 'sig' }] : undefined, // ['OKP', 'Ed25519', { use: 'sig', alg: 'EdDSA' }], // ['OKP', 'Ed25519', { alg: 'EdDSA' }], - ['OKP', 'Ed448'], - ['OKP', 'Ed448', { use: 'sig' }], + edDSASupported ? ['OKP', 'Ed448'] : undefined, + edDSASupported ? ['OKP', 'Ed448', { use: 'sig' }] : undefined, // ['OKP', 'Ed448', { use: 'sig', alg: 'EdDSA' }], // ['OKP', 'Ed448', { alg: 'EdDSA' }], - ['OKP', 'X25519'], - ['OKP', 'X25519', { use: 'enc' }], + edDSASupported ? ['OKP', 'X25519'] : undefined, + edDSASupported ? ['OKP', 'X25519', { use: 'enc' }] : undefined, // ['OKP', 'X25519', { use: 'enc', alg: 'ECDH-ES' }], // ['OKP', 'X25519', { alg: 'ECDH-ES' }], - ['OKP', 'X448'], - ['OKP', 'X448', { use: 'enc' }], + edDSASupported ? ['OKP', 'X448'] : undefined, + edDSASupported ? ['OKP', 'X448', { use: 'enc' }] : undefined, // ['OKP', 'X448', { use: 'enc', alg: 'ECDH-ES' }], // ['OKP', 'X448', { alg: 'ECDH-ES' }], ['EC'], @@ -71,7 +73,7 @@ const { JWK: { generate, generateSync }, errors } = require('../..') ['oct', 192, { use: 'enc', alg: 'A192GCM' }], ['oct', 192, { alg: 'HS256' }], ['oct', 192, { alg: 'A192GCM' }] -].forEach((args) => { +].filter(Boolean).forEach((args) => { if ('electron' in process.versions) { const [, crv] = args if (crv === 'secp256k1' || String(crv).startsWith('X') || crv === 'Ed448') return diff --git a/test/jwk/import.test.js b/test/jwk/import.test.js index fe1d9cbfa1..df6bbb7d41 100644 --- a/test/jwk/import.test.js +++ b/test/jwk/import.test.js @@ -1,22 +1,24 @@ const test = require('ava') -const crypto = require('crypto') const { JWS, JWE, JWK: { asKey, importKey, generate }, errors } = require('../..') +const { edDSASupported, keyObjectSupported } = require('../../lib/help/node_support') +const { createSecretKey } = require('../../lib/help/key_object') +const { generateKeyPairSync } = require('../macros/generate') const fixtures = require('../fixtures') test('imports PrivateKeyObject and then its Key instance', t => { - const k = asKey(crypto.generateKeyPairSync('ec', { namedCurve: 'P-256' }).privateKey) + const k = asKey(generateKeyPairSync('ec', { namedCurve: 'P-256' }).privateKey) t.deepEqual(asKey(k).toJWK(), k.toJWK()) }) test('imports PublicKeyObject and then its Key instance', t => { - const k = asKey(crypto.generateKeyPairSync('ec', { namedCurve: 'P-256' }).publicKey) + const k = asKey(generateKeyPairSync('ec', { namedCurve: 'P-256' }).publicKey) t.deepEqual(asKey(k).toJWK(), k.toJWK()) }) test('imports SecretKeyObject and then its Key instance', t => { - const k = asKey(crypto.createSecretKey(Buffer.from('foo'))) + const k = asKey(createSecretKey(Buffer.from('foo'))) t.deepEqual(asKey(k).toJWK(), k.toJWK()) }) @@ -39,6 +41,7 @@ test('parameters must be a plain object', t => { Object.entries(fixtures.PEM).forEach(([type, { private: priv, public: pub }]) => { if (type === 'P-256K') return if ('electron' in process.versions && (type.startsWith('X') || type === 'Ed448' || type === 'secp256k1')) return + if (!edDSASupported && (type.startsWith('Ed') || type.startsWith('X'))) return test(`fails to import ${type} as invalid string`, t => { t.throws(() => { @@ -77,11 +80,11 @@ test('failed to import throws an error', t => { }, { instanceOf: errors.JWKImportFailed, code: 'ERR_JWK_IMPORT_FAILED' }) }) -if (!('electron' in process.versions)) { +if (!('electron' in process.versions) && keyObjectSupported) { ;[ `-----BEGIN PUBLIC KEY-----\nMIIBtjCCASsGByqGSM44BAEwggEeAoGBANuHjLdqQcKozzWf9fUfe/mw4i5NLT8k\nCIA75k+GNYNbBaGZ2lGNeKsrjHzM8w7mE5k6qx5hDB4n88qFoauqCsUZ4knbTybn\nYV08gfWS375l/EGSpt3c/1dezVZuT/FmEeXbMhOIDORf/9f/6PpEMFN3eghszLvN\ng+L/19HVpWAXAhUAnOFG9vvOiZIz/ZxdpR+EVv8o4T8CgYBDk/ChY3fo4DrxzLZT\n7AjsAiJOzO8QnsV07Gh8gSzUCBsb+Hb4GvMs2U6rB5mxOMib3S2HGbs791uBva2a\nA6pzNzRmgV/w6CyOcxhCkZdVL7MwO9y5iq6V65R4GgfkCrIAYi/BW6XdXOyw/7J0\nt/4wB0/wKtsXf541NLfmUprJ+QOBhAACgYBGbXflbrGGg02+w8Xo6RO+tHoekREZ\nlJA0KKBN4jT0S3/OsLQeHtO7k/gkdMMbXD1J1fae9tIxy1SwYVTR6csgydGuvuyG\nB4A/ZtXEb+dumCBbtw8dyred4Okhl44Fdrs79F1rjSWEcwKqJghxS+GsbA0vcTaf\nAHDL6OblN04uzg==\n-----END PUBLIC KEY-----`, - crypto.generateKeyPairSync('dsa', { modulusLength: 1024 }).publicKey, - crypto.generateKeyPairSync('dsa', { modulusLength: 1024 }).privateKey + generateKeyPairSync('dsa', { modulusLength: 1024 }).publicKey, + generateKeyPairSync('dsa', { modulusLength: 1024 }).privateKey ].forEach((unsupported, i) => { test(`fails to import unsupported PEM ${i + 1}/4`, t => { t.throws(() => { diff --git a/test/jwk/key_ops.test.js b/test/jwk/key_ops.test.js index 5c15935e83..49279cbc6b 100644 --- a/test/jwk/key_ops.test.js +++ b/test/jwk/key_ops.test.js @@ -1,9 +1,11 @@ const test = require('ava') -const crypto = require('crypto') const errors = require('../../lib/errors') const asKey = require('../../lib/jwk/import') const { generateSync } = require('../../lib/jwk/generate') +const { generateKeyPairSync } = require('../macros/generate') + +const { edDSASupported } = require('../../lib/help/node_support') const jwk = asKey('foo').toJWK(true) @@ -53,7 +55,7 @@ test('JWK asKey with invalid use / key_ops throws', t => { }) test('keyObject asKey with invalid use / key_ops throws 1/2', t => { - const { publicKey } = crypto.generateKeyPairSync('ed25519') + const { publicKey } = generateKeyPairSync('ec', { namedCurve: 'P-256' }) t.throws(() => { asKey(publicKey, { use: 'sig', key_ops: ['wrapKey'] }) @@ -61,7 +63,7 @@ test('keyObject asKey with invalid use / key_ops throws 1/2', t => { }) test('keyObject asKey with invalid use / key_ops throws 2/2', t => { - const { publicKey } = crypto.generateKeyPairSync('ed25519') + const { publicKey } = generateKeyPairSync('ec', { namedCurve: 'P-256' }) t.throws(() => { asKey(publicKey, { use: 'enc', key_ops: ['sign'] }) @@ -69,7 +71,7 @@ test('keyObject asKey with invalid use / key_ops throws 2/2', t => { }) test('PEM asKey with invalid use / key_ops throws', t => { - const { publicKey } = crypto.generateKeyPairSync('ed25519') + const { publicKey } = generateKeyPairSync('ec', { namedCurve: 'P-256' }) t.throws(() => { asKey(publicKey.export({ type: 'spki', format: 'pem' }), { use: 'sig', key_ops: ['wrapKey'] }) @@ -97,9 +99,11 @@ test('oct key key_ops', t => { t.deepEqual([...k.algorithms('sign')], []) }) -test('OKP key key_ops', t => { - const k = generateSync('OKP', 'Ed25519', { key_ops: ['verify'] }) - t.deepEqual([...k.algorithms()], ['EdDSA']) - t.deepEqual([...k.algorithms('verify')], ['EdDSA']) - t.deepEqual([...k.algorithms('sign')], []) -}) +if (edDSASupported) { + test('OKP key key_ops', t => { + const k = generateSync('OKP', 'Ed25519', { key_ops: ['verify'] }) + t.deepEqual([...k.algorithms()], ['EdDSA']) + t.deepEqual([...k.algorithms('verify')], ['EdDSA']) + t.deepEqual([...k.algorithms('sign')], []) + }) +} diff --git a/test/jwk/oct.test.js b/test/jwk/oct.test.js index 6c105db024..bfc240944c 100644 --- a/test/jwk/oct.test.js +++ b/test/jwk/oct.test.js @@ -1,5 +1,6 @@ const test = require('ava') -const { createSecretKey } = require('crypto') + +const { createSecretKey } = require('../../lib/help/key_object') const { hasProperty, hasNoProperties } = require('../macros') const errors = require('../../lib/errors') diff --git a/test/jwk/okp_enc.test.js b/test/jwk/okp_enc.test.js index bac3ddfc3c..2fc29c7ee9 100644 --- a/test/jwk/okp_enc.test.js +++ b/test/jwk/okp_enc.test.js @@ -1,6 +1,8 @@ const test = require('ava') -if ('electron' in process.versions) return +const { keyObjectSupported } = require('../../lib/help/node_support') + +if ('electron' in process.versions || !keyObjectSupported) return const { createPrivateKey, createPublicKey } = require('crypto') const { hasProperty, hasNoProperties, hasProperties } = require('../macros') diff --git a/test/jwk/okp_sig.test.js b/test/jwk/okp_sig.test.js index 7133d441e0..e26ab5f5af 100644 --- a/test/jwk/okp_sig.test.js +++ b/test/jwk/okp_sig.test.js @@ -1,4 +1,9 @@ const test = require('ava') + +const { keyObjectSupported } = require('../../lib/help/node_support') + +if (!keyObjectSupported) return + const { createPrivateKey, createPublicKey } = require('crypto') const { hasProperty, hasNoProperties, hasProperties } = require('../macros') const fixtures = require('../fixtures') diff --git a/test/jwk/rsa.test.js b/test/jwk/rsa.test.js index f87f96ddf4..8b1a934c86 100644 --- a/test/jwk/rsa.test.js +++ b/test/jwk/rsa.test.js @@ -1,9 +1,9 @@ const test = require('ava') -const { createPrivateKey, createPublicKey } = require('crypto') +const { createPrivateKey, createPublicKey } = require('../../lib/help/key_object') const { hasProperty, hasNoProperties, hasProperties } = require('../macros') const fixtures = require('../fixtures') -const { oaepHash } = require('../../lib/help/node_support') +const { oaepHashSupported } = require('../../lib/help/node_support') const { generateSync } = require('../../lib/jwk/generate') const RSAKey = require('../../lib/jwk/key/rsa') @@ -32,7 +32,7 @@ test(`RSA key .algorithms invalid operation`, t => { test(`RSA Private key`, hasProperty, key, 'type', 'private') test(`RSA Private key`, hasProperty, key, 'use', undefined) - if (oaepHash) { + if (oaepHashSupported) { test('RSA Private key algorithms (no operation)', t => { const result = key.algorithms() t.is(result.constructor, Set) @@ -125,7 +125,7 @@ test(`RSA key .algorithms invalid operation`, t => { t.deepEqual([...result], []) }) - if (oaepHash) { + if (oaepHashSupported) { test('RSA Private key .algorithms("wrapKey")', t => { const result = key.algorithms('wrapKey') t.is(result.constructor, Set) @@ -146,7 +146,7 @@ test(`RSA key .algorithms invalid operation`, t => { t.deepEqual([...result], []) }) - if (oaepHash) { + if (oaepHashSupported) { test('RSA Private key .algorithms("unwrapKey")', t => { const result = key.algorithms('unwrapKey') t.is(result.constructor, Set) @@ -188,7 +188,7 @@ test(`RSA key .algorithms invalid operation`, t => { test(`RSA Public key`, hasProperty, key, 'type', 'public') test(`RSA Public key`, hasProperty, key, 'use', undefined) - if (oaepHash) { + if (oaepHashSupported) { test('RSA EC Public key algorithms (no operation)', t => { const result = key.algorithms() t.is(result.constructor, Set) @@ -281,7 +281,7 @@ test(`RSA key .algorithms invalid operation`, t => { t.deepEqual([...result], []) }) - if (oaepHash) { + if (oaepHashSupported) { test('RSA Public key .algorithms("wrapKey")', t => { const result = key.algorithms('wrapKey') t.is(result.constructor, Set) @@ -336,7 +336,7 @@ test(`RSA key .algorithms invalid operation`, t => { t.true(k.algorithms().has('RS512')) }) - if (oaepHash) { + if (oaepHashSupported) { test('RSA key >= 784 bits can do RSA-OAEP-256', t => { const k = generateSync('RSA', 784) t.true(k.algorithms().has('RSA-OAEP-256')) @@ -370,7 +370,7 @@ test(`RSA key .algorithms invalid operation`, t => { t.true(k.algorithms().has('PS384')) }) - if (oaepHash) { + if (oaepHashSupported) { test('RSA key >= 896 bits can do RSA-OAEP-256', t => { const k = generateSync('RSA', 896) t.true(k.algorithms().has('RSA-OAEP-256')) diff --git a/test/jwk/x5c_thumbprints.test.js b/test/jwk/x5c_thumbprints.test.js index dd58bb0371..6d2a6e86bb 100644 --- a/test/jwk/x5c_thumbprints.test.js +++ b/test/jwk/x5c_thumbprints.test.js @@ -1,5 +1,9 @@ const test = require('ava') +const { keyObjectSupported } = require('../../lib/help/node_support') + +if (!keyObjectSupported) return + const errors = require('../../lib/errors') const { JWK: { asKey } } = require('../..') diff --git a/test/jws/smoke.test.js b/test/jws/smoke.test.js index 944735896e..35b8bedac5 100644 --- a/test/jws/smoke.test.js +++ b/test/jws/smoke.test.js @@ -1,5 +1,6 @@ const test = require('ava') +const { edDSASupported } = require('../../lib/help/node_support') const { JWK: { asKey, generateSync } } = require('../..') const fixtures = require('../fixtures') @@ -9,6 +10,7 @@ const { JWS: { success, failure } } = require('../macros') Object.entries(fixtures.PEM).forEach(([type, { private: key, public: pub }]) => { if (type === 'P-256K') return if ('electron' in process.versions && (type.startsWith('X') || type === 'Ed448' || type === 'secp256k1')) return + if (!edDSASupported && (type.startsWith('Ed') || type.startsWith('X'))) return const sKey = asKey(key) const vKey = asKey(pub) diff --git a/test/macros/generate.js b/test/macros/generate.js new file mode 100644 index 0000000000..e790314149 --- /dev/null +++ b/test/macros/generate.js @@ -0,0 +1,20 @@ +const { generateKeyPairSync } = require('crypto') + +const { keyObjectSupported } = require('../../lib/help/node_support') +const { createPublicKey, createPrivateKey } = require('../../lib/help/key_object') + +module.exports = { + generateKeyPairSync (type, options) { + if (keyObjectSupported) { + return generateKeyPairSync(type, options) + } + + const { privateKey, publicKey } = generateKeyPairSync(type, { + publicKeyEncoding: { type: 'spki', format: 'pem' }, + privateKeyEncoding: { type: 'pkcs8', format: 'pem' }, + ...options + }) + + return { privateKey: createPrivateKey(privateKey), publicKey: createPublicKey(publicKey) } + } +}