Skip to content

Commit

Permalink
feat: add JWK key_ops support, fix .algorithms() op returns
Browse files Browse the repository at this point in the history
BREAKING CHANGE: key.algorithms(op) un+wrapKey was split into correct
wrapKey/unwrapKey/deriveKey returns

BREAKING CHANGE: keystore.all and keystore.get `operation` option was
removed, `key_ops: string[]` supersedes it
  • Loading branch information
panva committed Apr 23, 2019
1 parent 4ace4be commit 23b874c
Show file tree
Hide file tree
Showing 40 changed files with 556 additions and 173 deletions.
1 change: 0 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -81,7 +81,6 @@ Won't implement:
- no crypto, no use

Not Planned / PR | Use-Case | Discussion Welcome:
- ◯ automatically adding `kid` reference to JWS / JWE Headers
-`x5c`, `x5t`, `x5t#S256`, `x5u` etc `JWK.Key` fields

</details>
Expand Down
62 changes: 48 additions & 14 deletions docs/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ I can continue maintaining it and adding new features carefree. You may also don
- [key.alg](#keyalg)
- [key.use](#keyuse)
- [key.kid](#keykid)
- [key.key_ops](#keykey_ops)
- [key.thumbprint](#keythumbprint)
- [key.type](#keytype)
- [key.public](#keypublic)
Expand Down Expand Up @@ -116,6 +117,16 @@ defined in [RFC7638][spec-thumbprint].

---

#### `key.key_ops`

Returns the key's JWK Key Operations Parameter if set. If set the key can only be used for the
specified operations. Supported values are 'sign', 'verify', 'encrypt', 'decrypt', 'wrapKey',
'unwrapKey' and 'deriveKey'.

- `string[]`

---

#### `key.thumbprint`

Returns the key's JWK Key thumbprint calculated using the method defined in [RFC7638][spec-thumbprint].
Expand Down Expand Up @@ -415,11 +426,15 @@ Securely generates a new RSA, EC, OKP or oct key.
- `crvOrSize`: `<number>` &vert; `<string>` key's bit size or in case of OKP and EC keys the curve
**Default:** 2048 for RSA, 'P-256' for EC, 'Ed25519' for OKP and 256 for oct.
- `options`: `<Object>`
- `alg`: `<string>` option identifies the algorithm intended for use with the key.
- `alg`: `<string>` Key Algorithm Parameter. It identifies the algorithm intended for use with the
key.
- `kid`: `<string>` Key ID Parameter. When not provided is computed using the method defined in
[RFC7638][spec-thumbprint]
- `use`: `<string>` option indicates whether the key is to be used for encrypting & decrypting
data or signing & verifying data. Must be 'sig' or 'enc'.
[RFC7638][spec-thumbprint].
- `use`: `<string>` Public Key Use Parameter. Indicates whether the key is to be used for
encrypting & decrypting data or signing & verifying data. Must be 'sig' or 'enc'.
- `key_ops`: `string[]` Key Operations Parameter. If set, the key can only be used for the
specified operations. Supported values are 'sign', 'verify', 'encrypt', 'decrypt', 'wrapKey',
'unwrapKey' and 'deriveKey'.
- `private`: `<boolean>` **Default** 'true'. Is the resulting key private or public (when
asymmetrical)
- Returns: `Promise<JWK.RSAKey>` &vert; `Promise<JWK.ECKey>` &vert; `Promise<JWK.OKPKey>` &vert; `Promise<JWK.OctKey>`
Expand Down Expand Up @@ -454,11 +469,15 @@ Synchronous version of `JWK.generate()`
- `crvOrSize`: `<number>` &vert; `<string>` key's bit size or in case of OKP and EC keys the curve.
**Default:** 2048 for RSA, 'P-256' for EC, 'Ed25519' for OKP and 256 for oct.
- `options`: `<Object>`
- `alg`: `<string>` option identifies the algorithm intended for use with the key.
- `use`: `<string>` option indicates whether the key is to be used for encrypting & decrypting
data or signing & verifying data. Must be 'sig' or 'enc'.
- `alg`: `<string>` Key Algorithm Parameter. It identifies the algorithm intended for use with the
key.
- `kid`: `<string>` Key ID Parameter. When not provided is computed using the method defined in
[RFC7638][spec-thumbprint]
[RFC7638][spec-thumbprint].
- `use`: `<string>` Public Key Use Parameter. Indicates whether the key is to be used for
encrypting & decrypting data or signing & verifying data. Must be 'sig' or 'enc'.
- `key_ops`: `string[]` Key Operations Parameter. If set, the key can only be used for the
specified operations. Supported values are 'sign', 'verify', 'encrypt', 'decrypt', 'wrapKey',
'unwrapKey' and 'deriveKey'.
- `private`: `<boolean>` **Default** 'true'. Is the resulting key private or public (when
asymmetrical)
- Returns: `<JWK.RSAKey>` &vert; `<JWK.ECKey>` &vert; `<JWK.OKPKey>` &vert; `<JWK.OctKey>`
Expand Down Expand Up @@ -551,10 +570,12 @@ specified by the parameters are first.
- `parameters`: `<Object>`
- `kty`: `<string>` Key Type to filter for.
- `alg`: `<string>` Key supported algorithm to filter for.
- `use`: `<string>` Key use to filter for.
- `kid`: `<string>` Key ID to filter for.
- `operation`: `<string>` Further specify the operation a given alg must be valid for. Must be one
of 'encrypt', 'decrypt', 'sign', 'verify', 'wrapKey', 'unwrapKey'
- `use`: `<string>` Filter keys with the specified use defined. Keys missing "use" parameter will
be matched but rank lower then ones with an exact match.
- `key_ops`: `string[]` Filter keys with specified key_ops defined (if key_ops is defined on the
key). Keys missing "key_ops" parameter will be matched but rank lower then ones with matching
entries.
- Returns: `<Key[]>` Array of key instances or an empty array when none are matching the parameters.

---
Expand All @@ -567,10 +588,12 @@ parameters is returned.
- `parameters`: `<Object>`
- `kty`: `<string>` Key Type to filter for.
- `alg`: `<string>` Key supported algorithm to filter for.
- `use`: `<string>` Key use to filter for.
- `kid`: `<string>` Key ID to filter for.
- `operation`: `<string>` Further specify the operation a given alg must be valid for. Must be one
of 'encrypt', 'decrypt', 'sign', 'verify', 'wrapKey', 'unwrapKey'
- `use`: `<string>` Filter keys with the specified use defined. Keys missing "use" parameter will
be matched but rank lower then ones with an exact match.
- `key_ops`: `string[]` Filter keys with specified key_ops defined (if key_ops is defined on the
key). Keys missing "key_ops" parameter will be matched but rank lower then ones with matching
entries.
- Returns: `<JWK.RSAKey>` &vert; `<JWK.ECKey>` &vert; `<JWK.OKPKey>` &vert; `<JWK.OctKey>` &vert; `<undefined>`

---
Expand Down Expand Up @@ -1206,6 +1229,7 @@ Verifies the provided JWE in either serialization with a given `<JWK.Key>` or `<
- [Class: &lt;JWEDecryptionFailed&gt;](#class-jwedecryptionfailed)
- [Class: &lt;JWEInvalid&gt;](#class-jweinvalid)
- [Class: &lt;JWKImportFailed&gt;](#class-jwkimportfailed)
- [Class: &lt;JWKKeyInvalid&gt;](#class-jwkkeyinvalid)
- [Class: &lt;JWKKeySupport&gt;](#class-jwkkeysupport)
- [Class: &lt;JWKSNoMatchingKey&gt;](#class-jwksnomatchingkey)
- [Class: &lt;JWSInvalid&gt;](#class-jwsinvalid)
Expand Down Expand Up @@ -1311,6 +1335,16 @@ if (err.code === 'ERR_JWK_IMPORT_FAILED') {
}
```

#### Class: `JWKKeyInvalid`

Thrown when key's parameters are invalid, e.g. key_ops and use values are inconsistent.

```js
if (err.code === 'ERR_JWK_INVALID') {
// ...
}
```

#### Class: `JWKKeySupport`

Thrown when a key does not support the request algorithm.
Expand Down
2 changes: 2 additions & 0 deletions lib/errors.js
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ const CODES = {
JWEDecryptionFailed: 'ERR_JWE_DECRYPTION_FAILED',
JWEInvalid: 'ERR_JWE_INVALID',
JWKImportFailed: 'ERR_JWK_IMPORT_FAILED',
JWKInvalid: 'ERR_JWK_INVALID',
JWKKeySupport: 'ERR_JWK_KEY_SUPPORT',
JWKSNoMatchingKey: 'ERR_JWKS_NO_MATCHING_KEY',
JWSInvalid: 'ERR_JWS_INVALID',
Expand Down Expand Up @@ -63,6 +64,7 @@ module.exports.JWEDecryptionFailed = class JWEDecryptionFailed extends JOSEError
module.exports.JWEInvalid = class JWEInvalid extends JOSEError {}

module.exports.JWKImportFailed = class JWKImportFailed extends JOSEError {}
module.exports.JWKInvalid = class JWKInvalid extends JOSEError {}
module.exports.JWKKeySupport = class JWKKeySupport extends JOSEError {}

module.exports.JWKSNoMatchingKey = class JWKSNoMatchingKey extends JOSEError {}
Expand Down
31 changes: 31 additions & 0 deletions lib/help/consts.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
module.exports.KEYOBJECT = Symbol('KEYOBJECT')
module.exports.PRIVATE_MEMBERS = Symbol('PRIVATE_MEMBERS')
module.exports.PUBLIC_MEMBERS = Symbol('PUBLIC_MEMBERS')
module.exports.THUMBPRINT_MATERIAL = Symbol('THUMBPRINT_MATERIAL')
module.exports.JWK_MEMBERS = Symbol('JWK_MEMBERS')
module.exports.KEY_MANAGEMENT_ENCRYPT = Symbol('KEY_MANAGEMENT_ENCRYPT')
module.exports.KEY_MANAGEMENT_DECRYPT = Symbol('KEY_MANAGEMENT_DECRYPT')

const USES_MAPPING = {
sig: new Set(['sign', 'verify']),
enc: new Set(['encrypt', 'decrypt', 'wrapKey', 'unwrapKey', 'deriveKey'])
}
const OPS = new Set([...USES_MAPPING.sig, ...USES_MAPPING.enc])
const USES = new Set(Object.keys(USES_MAPPING))

module.exports.USES_MAPPING = USES_MAPPING
module.exports.OPS = OPS
module.exports.USES = USES

module.exports.OKP_CURVES = new Set(['Ed25519', 'Ed448', 'X25519', 'X448'])
module.exports.EC_CURVES = new Set(['P-256', 'P-384', 'P-521'])
module.exports.ECDH_ALGS = ['ECDH-ES', 'ECDH-ES+A128KW', 'ECDH-ES+A192KW', 'ECDH-ES+A256KW']

module.exports.KEYLENGTHS = {
'A128CBC-HS256': 256,
'A192CBC-HS384': 384,
'A256CBC-HS512': 512,
'A128GCM': 128,
'A192GCM': 192,
'A256GCM': 256
}
10 changes: 0 additions & 10 deletions lib/help/key_lengths.js

This file was deleted.

4 changes: 1 addition & 3 deletions lib/help/key_utils.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,9 +3,7 @@ const { createPublicKey } = require('crypto')
const base64url = require('./base64url')
const errors = require('../errors')
const asn1 = require('./asn1')

const EC_CURVES = new Set(['P-256', 'P-384', 'P-521'])
const OKP_CURVES = new Set(['Ed25519', 'Ed448', 'X25519', 'X448'])
const { OKP_CURVES, EC_CURVES } = require('./consts')

const oidHexToCurve = new Map([
['06082a8648ce3d030107', 'P-256'],
Expand Down
5 changes: 0 additions & 5 deletions lib/help/symbols.js

This file was deleted.

6 changes: 4 additions & 2 deletions lib/index.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,15 +3,16 @@
import { KeyObject, PrivateKeyInput, PublicKeyInput } from 'crypto'

type use = 'sig' | 'enc'
type keyOperation = 'sign' | 'verify' | 'encrypt' | 'decrypt' | 'wrapKey' | 'unwrapKey' | 'deriveKey'
interface KeyParameters {
alg?: string
use?: use
kid?: string
key_ops?: keyOperation[]
}
type ECCurve = 'P-256' | 'P-384' | 'P-521'
type OKPCurve = 'Ed25519' | 'Ed448' | 'X25519' | 'X448'
type keyType = 'RSA' | 'EC' | 'OKP' | 'oct'
type keyOperation = 'encrypt' | 'decrypt' | 'sign' | 'verify' | 'wrapKey' | 'unwrapKey'
type asymmetricKeyObjectTypes = 'private' | 'public'
type keyObjectTypes = asymmetricKeyObjectTypes | 'secret'

Expand All @@ -31,6 +32,7 @@ export namespace JWK {
secret: boolean
alg?: string
use?: use
key_ops?: keyOperation[]
kid: string
thumbprint: string

Expand Down Expand Up @@ -144,7 +146,6 @@ export namespace JWK {
export namespace JWKS {
interface KeyQuery extends KeyParameters {
kty: keyType
operation: keyOperation
}

class KeyStore {
Expand Down Expand Up @@ -341,6 +342,7 @@ export namespace errors {
export class JWEInvalid extends JOSEError {}

export class JWKImportFailed extends JOSEError {}
export class JWKInvalid extends JOSEError {}
export class JWKKeySupport extends JOSEError {}

export class JWKSNoMatchingKey extends JOSEError {}
Expand Down
2 changes: 1 addition & 1 deletion lib/jwa/aes_cbc_hmac_sha2.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ const { createCipheriv, createDecipheriv } = require('crypto')

const uint64be = require('../help/uint64be')
const timingSafeEqual = require('../help/timing_safe_equal')
const { KEYOBJECT } = require('../help/symbols')
const { KEYOBJECT } = require('../help/consts')
const { JWEInvalid, JWEDecryptionFailed } = require('../errors')

const checkInput = function (size, iv, tag) {
Expand Down
2 changes: 1 addition & 1 deletion lib/jwa/aes_gcm.js
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
const { strict: assert } = require('assert')
const { createCipheriv, createDecipheriv } = require('crypto')

const { KEYOBJECT } = require('../help/symbols')
const { KEYOBJECT } = require('../help/consts')
const { JWEInvalid, JWEDecryptionFailed } = require('../errors')

const checkInput = function (size, iv, tag) {
Expand Down
8 changes: 4 additions & 4 deletions lib/jwa/aes_gcm_kw.js
Original file line number Diff line number Diff line change
Expand Up @@ -5,21 +5,21 @@ const base64url = require('../help/base64url')

module.exports = (JWA) => {
['A128GCMKW', 'A192GCMKW', 'A256GCMKW'].forEach((jwaAlg) => {
assert(!JWA.wrapKey.has(jwaAlg), `wrapKey alg ${jwaAlg} already registered`)
assert(!JWA.unwrapKey.has(jwaAlg), `unwrapKey alg ${jwaAlg} already registered`)
assert(!JWA.keyManagementEncrypt.has(jwaAlg), `keyManagementEncrypt alg ${jwaAlg} already registered`)
assert(!JWA.keyManagementDecrypt.has(jwaAlg), `keyManagementDecrypt alg ${jwaAlg} already registered`)

const encAlg = jwaAlg.substr(0, 7)
const encrypt = JWA.encrypt.get(encAlg)
const decrypt = JWA.decrypt.get(encAlg)

JWA.wrapKey.set(jwaAlg, (key, payload) => {
JWA.keyManagementEncrypt.set(jwaAlg, (key, payload) => {
const iv = generateIV(jwaAlg)
const { ciphertext, tag } = encrypt(key, payload, { iv })
return {
wrapped: ciphertext,
header: { tag: base64url.encodeBuffer(tag), iv: base64url.encodeBuffer(iv) }
}
})
JWA.unwrapKey.set(jwaAlg, decrypt)
JWA.keyManagementDecrypt.set(jwaAlg, decrypt)
})
}
10 changes: 5 additions & 5 deletions lib/jwa/aes_kw.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ const { createCipheriv, createDecipheriv } = require('crypto')

const uint64be = require('../help/uint64be')
const timingSafeEqual = require('../help/timing_safe_equal')
const { KEYOBJECT } = require('../help/symbols')
const { KEYOBJECT } = require('../help/consts')

const checkInput = (data) => {
if (data !== undefined && data.length % 8 !== 0) {
Expand Down Expand Up @@ -89,10 +89,10 @@ module.exports = (JWA) => {
['A128KW', 'A192KW', 'A256KW'].forEach((jwaAlg) => {
const size = parseInt(jwaAlg.substr(1, 3), 10)

assert(!JWA.wrapKey.has(jwaAlg), `wrapKey alg ${jwaAlg} already registered`)
assert(!JWA.unwrapKey.has(jwaAlg), `unwrapKey alg ${jwaAlg} already registered`)
assert(!JWA.keyManagementEncrypt.has(jwaAlg), `keyManagementEncrypt alg ${jwaAlg} already registered`)
assert(!JWA.keyManagementDecrypt.has(jwaAlg), `keyManagementDecrypt alg ${jwaAlg} already registered`)

JWA.wrapKey.set(jwaAlg, wrapKey.bind(undefined, size))
JWA.unwrapKey.set(jwaAlg, unwrapKey.bind(undefined, size))
JWA.keyManagementEncrypt.set(jwaAlg, wrapKey.bind(undefined, size))
JWA.keyManagementDecrypt.set(jwaAlg, unwrapKey.bind(undefined, size))
})
}
10 changes: 5 additions & 5 deletions lib/jwa/ecdh/dir.js
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
const { strict: assert } = require('assert')

const KEYLENGTHS = require('../../help/key_lengths')
const { KEYLENGTHS } = require('../../help/consts')
const { generateSync } = require('../../jwk/generate')

const derive = require('./derive')
Expand All @@ -23,9 +23,9 @@ const unwrapKey = (key, payload, { apu, apv, epk, enc }) => {
const ALG = 'ECDH-ES'

module.exports = (JWA) => {
assert(!JWA.wrapKey.has(ALG), `wrapKey alg ${ALG} already registered`)
assert(!JWA.unwrapKey.has(ALG), `unwrapKey alg ${ALG} already registered`)
assert(!JWA.keyManagementEncrypt.has(ALG), `keyManagementEncrypt alg ${ALG} already registered`)
assert(!JWA.keyManagementDecrypt.has(ALG), `keyManagementDecrypt alg ${ALG} already registered`)

JWA.wrapKey.set(ALG, wrapKey)
JWA.unwrapKey.set(ALG, unwrapKey)
JWA.keyManagementEncrypt.set(ALG, wrapKey)
JWA.keyManagementDecrypt.set(ALG, unwrapKey)
}
14 changes: 7 additions & 7 deletions lib/jwa/ecdh/kw.js
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
const { strict: assert } = require('assert')

const { KEYOBJECT } = require('../../help/symbols')
const { KEYOBJECT } = require('../../help/consts')
const { generateSync } = require('../../jwk/generate')

const derive = require('./derive')
Expand All @@ -24,15 +24,15 @@ const unwrapKey = (unwrap, derive, key, payload, { apu, apv, epk }) => {

module.exports = (JWA) => {
['ECDH-ES+A128KW', 'ECDH-ES+A192KW', 'ECDH-ES+A256KW'].forEach((jwaAlg) => {
assert(!JWA.wrapKey.has(jwaAlg), `wrapKey alg ${jwaAlg} already registered`)
assert(!JWA.unwrapKey.has(jwaAlg), `unwrapKey alg ${jwaAlg} already registered`)
assert(!JWA.keyManagementEncrypt.has(jwaAlg), `keyManagementEncrypt alg ${jwaAlg} already registered`)
assert(!JWA.keyManagementDecrypt.has(jwaAlg), `keyManagementDecrypt alg ${jwaAlg} already registered`)

const kw = jwaAlg.substr(-6)
const kwWrap = JWA.wrapKey.get(kw)
const kwUnwrap = JWA.unwrapKey.get(kw)
const kwWrap = JWA.keyManagementEncrypt.get(kw)
const kwUnwrap = JWA.keyManagementDecrypt.get(kw)
const keylen = parseInt(jwaAlg.substr(9, 3), 10)

JWA.wrapKey.set(jwaAlg, wrapKey.bind(undefined, kwWrap, derive.bind(undefined, jwaAlg, keylen)))
JWA.unwrapKey.set(jwaAlg, unwrapKey.bind(undefined, kwUnwrap, derive.bind(undefined, jwaAlg, keylen)))
JWA.keyManagementEncrypt.set(jwaAlg, wrapKey.bind(undefined, kwWrap, derive.bind(undefined, jwaAlg, keylen)))
JWA.keyManagementDecrypt.set(jwaAlg, unwrapKey.bind(undefined, kwUnwrap, derive.bind(undefined, jwaAlg, keylen)))
})
}
2 changes: 1 addition & 1 deletion lib/jwa/ecdsa.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ const { strict: assert } = require('assert')
const { sign: signOneShot, verify: verifyOneShot } = require('crypto')

const { derToJose, joseToDer } = require('../help/ecdsa_signatures')
const { KEYOBJECT } = require('../help/symbols')
const { KEYOBJECT } = require('../help/consts')
const resolveNodeAlg = require('../help/node_alg')

const sign = (jwaAlg, nodeAlg, { [KEYOBJECT]: keyObject }, payload) => {
Expand Down
2 changes: 1 addition & 1 deletion lib/jwa/eddsa.js
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
const { strict: assert } = require('assert')
const { sign: signOneShot, verify: verifyOneShot } = require('crypto')

const { KEYOBJECT } = require('../help/symbols')
const { KEYOBJECT } = require('../help/consts')

const sign = ({ [KEYOBJECT]: keyObject }, payload) => {
return signOneShot(undefined, payload, keyObject)
Expand Down
2 changes: 1 addition & 1 deletion lib/jwa/hmac.js
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
const { strict: assert } = require('assert')
const { createHmac } = require('crypto')

const { KEYOBJECT } = require('../help/symbols')
const { KEYOBJECT } = require('../help/consts')
const timingSafeEqual = require('../help/timing_safe_equal')
const resolveNodeAlg = require('../help/node_alg')

Expand Down
Loading

0 comments on commit 23b874c

Please sign in to comment.