feat: added Node.js lts/dubnium support for runtime supported features

This commit is contained in:
Filip Skokan 2019-08-22 20:28:48 +02:00
parent 1d6d55ff91
commit 67a8601b09
45 changed files with 890 additions and 226 deletions

View file

@ -1,8 +1,8 @@
coverage:
status:
project: no
patch: yes
changes: yes
project: off
patch: off
changes: off
comment:
layout: diff

View file

@ -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

View file

@ -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 | ✓<sup>*</sup> | RSA-OAEP <sub>(<sup>*</sup>RSA-OAEP-256 is not supported due to its lack of support in Node.JS)</sub> |
| RSAES OAEP | ✓<sup>*</sup> | RSA-OAEP <sub>(<sup>*</sup>RSA-OAEP-256 is not supported due to its lack of support in Node.js)</sub> |
| 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 |

View file

@ -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)?

View file

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

View file

@ -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

420
lib/help/key_object.js Normal file
View file

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

View file

@ -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':

View file

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

View file

@ -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) {}

View file

@ -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)

View file

@ -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)

View file

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

View file

@ -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

View file

@ -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) => {

View file

@ -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) => {

View file

@ -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) => {

View file

@ -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')

View file

@ -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')
const map = new WeakMap()
const i = (ctx) => {
if (!map.has(ctx)) {
map.set(ctx, {})
}
return map.get(ctx)
}
class Encrypt {
#aad
#cek
#unprotected
#protected
#cleartext
#recipients
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)
}
}

View file

@ -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')

View file

@ -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')

View file

@ -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

View file

@ -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)
}
}
}

View file

@ -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 {

View file

@ -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)
}
}
}

View file

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

View file

@ -10,13 +10,16 @@ const serializers = require('./serializers')
const PROCESS_RECIPIENT = Symbol('PROCESS_RECIPIENT')
const map = new WeakMap()
const i = (ctx) => {
if (!map.has(ctx)) {
map.set(ctx, {})
}
return map.get(ctx)
}
class Sign {
#b64
#payload
#recipients
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)
}
}

View file

@ -67,7 +67,7 @@
"standard": "^13.1.0"
},
"engines": {
"node": ">=12.0.0"
"node": "^10.13.0 || >=12.0.0"
},
"ava": {
"babel": false,

View file

@ -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)

View file

@ -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('../..')

View file

@ -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')

View file

@ -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

View file

@ -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)

View file

@ -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')

View file

@ -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')

View file

@ -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

View file

@ -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(() => {

View file

@ -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')], [])
})
}

View file

@ -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')

View file

@ -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')

View file

@ -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')

View file

@ -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'))

View file

@ -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('../..')

View file

@ -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)

20
test/macros/generate.js Normal file
View file

@ -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) }
}
}