From 16aa8ef804b423692e486dfdce8a80923f39ec37 Mon Sep 17 00:00:00 2001 From: Greg Linklater Date: Sun, 20 Aug 2017 15:33:47 +0200 Subject: [PATCH 01/17] doc(JWK): updated jsdoc fields and comments --- src/JWK.js | 129 +++++++++++++++++++++++++++++++++++++---------------- 1 file changed, 90 insertions(+), 39 deletions(-) diff --git a/src/JWK.js b/src/JWK.js index 80f6838..f5c2d67 100644 --- a/src/JWK.js +++ b/src/JWK.js @@ -21,8 +21,13 @@ class JWK { /** * constructor * - * @class + * @class JWK + * + * @description * JSON Web Key + * + * @param {Object} data + * @param {Object} [options={}] - Additional JWK metadata. */ constructor (data, options = {}) { if (!data) { @@ -38,40 +43,52 @@ class JWK { } } - // Handle object input - if (data.alg && options.alg && data.alg !== options.alg) { - throw new DataError('Conflicting algorithm option') + // Handle options input + let metadata = Object.keys(options) + .filter(key => { + let dataValue = data[key] + let optionsValue = options[key] - } else if (!data.alg && options.alg) { - data.alg = options.alg + if (dataValue && optionsValue && dataValue !== optionsValue) { + throw new DataError(`Conflicting '${key}' option`) + } - } else if (!data.alg && !options.alg) { - throw new DataError('Valid JWA algorithm required for JWK') - } - - // Handle kid transferral - if (data.kid && options.kid && data.kid !== options.kid) { - throw new DataError('Conflicting key identifier option') + return !dataValue && optionsValue + }) + .reduce((state, key) => { + state[key] = options[key] + return state + }, {}) - } else if (!data.kid && options.kid) { - data.kid = options.kid + // Assign input + Object.assign(this, data, metadata) - } else if (!data.kid && !options.kid) { - throw new DataError('Valid JWA key identifier required for JWK') + // Enforce required properties + if (!this.alg) { + throw new DataError('Valid \'alg\' required for JWK') } - Object.assign(this, data) + if (!this.kid) { + throw new DataError('Valid \'kid\' required for JWK') + } } /** * importKey * + * @description + * Import a JWK from JSON String or a JS Object. + * * @param {(String|Object)} data - * @return {Promise} + * @param {Object} [options] - Additional JWK metadata. + * @return {Promise.} A promise that resolves the JWK instance. */ - static importKey (data, options = {}) { + static importKey (data, options) { return Promise.resolve() + // Create instance .then(() => new JWK(data, options)) + + // Import CryptoKey and assign to JWK .then(jwk => { return JWA.importKey(jwk) .then(({ cryptoKey }) => { @@ -89,12 +106,18 @@ class JWK { /** * fromCryptoKey * - * @param {CryptoKey} key - * @param {Object} [options] - * @return {Promise} + * @description + * Import a JWK from a [WebCrypto CryptoKey](https://github.com/anvilresearch/webcrypto). + * + * @param {CryptoKey} key - [WebCrypto CryptoKey]{@link https://github.com/anvilresearch/webcrypto}. + * @param {Object} [options] - Additional JWK metadata. + * @return {Promise.} A promise that resolves the JWK instance. */ static fromCryptoKey (key, options) { + // Export JWK return JWA.exportKey('jwk', key) + + // Create JWK instance and assign CryptoKey .then(data => { let jwk = new JWK(data, options) Object.defineProperty(jwk, 'cryptoKey', { value: key, enumerable: false, configurable: false }) @@ -105,8 +128,15 @@ class JWK { /** * sign * - * @param {(String|Buffer)} data - * @return {Promise} + * @description + * Sign arbitrary data using the JWK. + * + * @example + * privateJwk.sign('test').then(console.log) + * // => "MEUCIQCHwnGM8IsOJgfQsoPgs3hMd8ahfWHM9ZNvj1K6i2yhKQIgWGOuXX43lSTo-U8Pa8sURR53lv6Osjw-dtoLselftqQ" + * + * @param {(String|Buffer)} data - The data to sign. + * @return {Promise.} A promise that resolves the base64url encoded signature string. */ sign (data) { let { alg, cryptoKey } = this @@ -116,9 +146,16 @@ class JWK { /** * verify * - * @param {(String|Buffer)} data - * @param {String} signature - * @return {Promise} + * @description + * Verify a signature using the JWK. + * + * @example + * publicJwk.verify('test', 'MEUCIQCHwnGM8IsOJgfQsoPgs3hMd8ahfWHM9ZNvj1K6i2yhKQIgWGOuXX43lSTo-U8Pa8sURR53lv6Osjw-dtoLselftqQ').then(console.log) + * // => true + * + * @param {(String|Buffer)} data - The data to verify. + * @param {String} signature - A base64url signature string. + * @return {Promise.} A promise that resolves the boolean result of the signature verification. */ verify (data, signature) { let { alg, cryptoKey } = this @@ -128,11 +165,18 @@ class JWK { /** * encrypt * - * @param {(String|Object)} data - * @param {(String|Buffer)} aad - integrity protected data - * @return {Promise} + * @description + * Encrypt arbitrary data using the JWK. + * + * @example + * secretJwk.encrypt('data').then(console.log) + * // => { iv: 'u0l3ttqUFDQ8mcRboHv5Vw', ciphertext: 'yq3K4w', tag: 'fHlZ__uuUnHn0ac-Lnrr-A' } + * + * @param {(String|Object)} data - The data to encrypt. + * @param {(String|Buffer)} [aad] - Additional non-encrypted integrity protected data (AES-GCM). + * @return {Promise.} A promise that resolves an object containing the base64url encoded `iv`, `ciphertext` and `tag` (AES-GCM). */ - encrypt (data, aad) { + encrypt (data, aad = Buffer.from('')) { let { alg, cryptoKey } = this return JWA.encrypt(alg, cryptoKey, data, aad) } @@ -140,15 +184,22 @@ class JWK { /** * decrypt * - * @param {(String|Object)} data - * @param {(String|Buffer)} iv - * @param {(String|Buffer)} tag - * @param {(String|Buffer)} aad - * @return {Promise} + * @description + * Decrypt data using the JWK. + * + * @example + * secretJwk.decrypt('yq3K4w', 'u0l3ttqUFDQ8mcRboHv5Vw', 'fHlZ__uuUnHn0ac-Lnrr-A').then(console.log) + * // => "data" + * + * @param {(String|Buffer)} ciphertext - The encrypted data to decrypt. + * @param {(String|Buffer)} iv - The initialization vector. + * @param {(String|Buffer)} [tag] - The authorization tag (AES-GCM). + * @param {(String|Buffer)} [aad] - Additional non-encrypted integrity protected data (AES-GCM). + * @return {Promise.} A promise that resolves the plaintext data. */ - decrypt (data, iv, tag, aad) { + decrypt (ciphertext, iv, tag, aad = Buffer.from('')) { let { alg, cryptoKey } = this - return JWA.decrypt(alg, cryptoKey, data, iv, tag, aad) + return JWA.decrypt(alg, cryptoKey, ciphertext, iv, tag, aad) } } From 07bf376a88f01117105762774642c12d1c068bde Mon Sep 17 00:00:00 2001 From: Greg Linklater Date: Sun, 20 Aug 2017 15:34:02 +0200 Subject: [PATCH 02/17] doc(errors): ignore --- src/errors/DataError.js | 1 + src/errors/OperationError.js | 1 + 2 files changed, 2 insertions(+) diff --git a/src/errors/DataError.js b/src/errors/DataError.js index 0bc564e..6838819 100644 --- a/src/errors/DataError.js +++ b/src/errors/DataError.js @@ -2,6 +2,7 @@ /** * DataError + * @ignore */ class DataError extends Error { diff --git a/src/errors/OperationError.js b/src/errors/OperationError.js index ef80910..ad18674 100644 --- a/src/errors/OperationError.js +++ b/src/errors/OperationError.js @@ -2,6 +2,7 @@ /** * OperationError + * @ignore */ class OperationError extends Error { From 7033a71725001abe1971ec09c9dba893b13eb006 Mon Sep 17 00:00:00 2001 From: Greg Linklater Date: Sun, 20 Aug 2017 15:34:51 +0200 Subject: [PATCH 03/17] test(JWK): updated error messages --- test/JWKSpec.js | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/test/JWKSpec.js b/test/JWKSpec.js index ddf38fc..7397387 100644 --- a/test/JWKSpec.js +++ b/test/JWKSpec.js @@ -69,7 +69,7 @@ describe('JWK', () => { }) it('should throw if conflicting alg options are passed in', () => { - expect(() => new JWK(ECPublicJWK, { alg: 'KS256' })).to.throw('Conflicting algorithm option') + expect(() => new JWK(ECPublicJWK, { alg: 'KS256' })).to.throw('Conflicting \'alg\' option') }) it('should not throw if alg passed in twice but not conflicting', () => { @@ -82,11 +82,11 @@ describe('JWK', () => { }) it('should throw if alg is not present on data or options', () => { - expect(new JWK(ECPublicJWKNoAlg, { alg: 'ES256' })).to.throw + expect(() => new JWK(ECPublicJWKNoAlg, {})).to.throw('Valid \'alg\' required for JWK') }) it('should throw if conflicting kid options are passed in', () => { - expect(() => new JWK(ECPublicJWK, { kid: '1' })).to.throw('Conflicting key identifier option') + expect(() => new JWK(ECPublicJWK, { kid: '1' })).to.throw('Conflicting \'kid\' option') }) it('should not throw if kid passed in twice but not conflicting', () => { @@ -99,7 +99,7 @@ describe('JWK', () => { }) it('should throw if kid is not present on data or options', () => { - expect(() => new JWK(ECPublicJWKNoKid, {})).to.throw('Valid JWA key identifier required for JWK') + expect(() => new JWK(ECPublicJWKNoKid, {})).to.throw('Valid \'kid\' required for JWK') }) it('should copy non-standard key metadata', () => { From 75778baf2d2ed1199177a06e920616b9033c8ce1 Mon Sep 17 00:00:00 2001 From: Greg Linklater Date: Sun, 20 Aug 2017 16:41:56 +0200 Subject: [PATCH 04/17] doc(JWK): add example captions --- src/JWK.js | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/src/JWK.js b/src/JWK.js index f5c2d67..62dc704 100644 --- a/src/JWK.js +++ b/src/JWK.js @@ -24,7 +24,7 @@ class JWK { * @class JWK * * @description - * JSON Web Key + * JSON Web Key ([IETF RFC7517](https://tools.ietf.org/html/rfc7517)) * * @param {Object} data * @param {Object} [options={}] - Additional JWK metadata. @@ -131,7 +131,7 @@ class JWK { * @description * Sign arbitrary data using the JWK. * - * @example + * @example Signing the string "test" * privateJwk.sign('test').then(console.log) * // => "MEUCIQCHwnGM8IsOJgfQsoPgs3hMd8ahfWHM9ZNvj1K6i2yhKQIgWGOuXX43lSTo-U8Pa8sURR53lv6Osjw-dtoLselftqQ" * @@ -149,7 +149,7 @@ class JWK { * @description * Verify a signature using the JWK. * - * @example + * @example Verify a signature of the string "test" * publicJwk.verify('test', 'MEUCIQCHwnGM8IsOJgfQsoPgs3hMd8ahfWHM9ZNvj1K6i2yhKQIgWGOuXX43lSTo-U8Pa8sURR53lv6Osjw-dtoLselftqQ').then(console.log) * // => true * @@ -168,7 +168,7 @@ class JWK { * @description * Encrypt arbitrary data using the JWK. * - * @example + * @example Encrypt the string "data" * secretJwk.encrypt('data').then(console.log) * // => { iv: 'u0l3ttqUFDQ8mcRboHv5Vw', ciphertext: 'yq3K4w', tag: 'fHlZ__uuUnHn0ac-Lnrr-A' } * @@ -187,7 +187,7 @@ class JWK { * @description * Decrypt data using the JWK. * - * @example + * @example Decrypt encrypted string "test" * secretJwk.decrypt('yq3K4w', 'u0l3ttqUFDQ8mcRboHv5Vw', 'fHlZ__uuUnHn0ac-Lnrr-A').then(console.log) * // => "data" * From 7e453fa70b736fc6ac43dbb61418b70a901d6eb2 Mon Sep 17 00:00:00 2001 From: Greg Linklater Date: Sun, 20 Aug 2017 16:43:28 +0200 Subject: [PATCH 05/17] doc(JWKSet): updated jsdoc fields and comments --- src/JWKSet.js | 177 +++++++++++++++++++++++++++++++++++++++++++++----- 1 file changed, 162 insertions(+), 15 deletions(-) diff --git a/src/JWKSet.js b/src/JWKSet.js index 640917a..77a7f03 100644 --- a/src/JWKSet.js +++ b/src/JWKSet.js @@ -19,8 +19,8 @@ const { DataError, OperationError } = require('./errors') /** * Random KID Generator + * @ignore */ -/* istanbul ignore */ function random (byteLen) { let value = crypto.getRandomValues(new Uint8Array(byteLen)) return Buffer.from(value).toString('hex') @@ -35,10 +35,12 @@ class JWKSet { /** * constructor * - * @class - * JWKSet + * @class JWKSet + * + * @description + * JSON Web Key Set ([IETF RFC7517 Section 5.](https://tools.ietf.org/html/rfc7517#section-5)) * - * @param {(Object|Array)} data + * @param {(Object|Array)} [data] */ constructor (data = {}) { if (Array.isArray(data)) { @@ -55,8 +57,30 @@ class JWKSet { /** * generateKeys * + * @description + * Instantiate a new JWKSet and generate one or many JWK keypairs and secret keys. + * + * @example Simple RSA keypair + * JWKSet.generateKeys('RS256').then(console.log) + * // => { keys: [{ ..., d: '...', kty: 'RSA', alg: 'RS256', kid: 'abcd' }, + * // { ..., kty: 'RSA', alg: 'RS256', kid: 'abcd' }] } + * + * @example Multiple keypairs + * JWKSet.generateKeys(['RS256', 'ES256']).then(console.log) + * // => { keys: [{ ..., kty: 'RSA', alg: 'RS256' }, { ..., kty: 'RSA', alg: 'RS256' }, + * // { ..., kty: 'EC', alg: 'ES256' }, { ..., kty: 'EC', alg: 'ES256' }] } + * + * @example Object descriptor RSA keypair + * JWKSet.generateKeys({ alg: 'RS256', kid: 'custom', modulusLength: 1024 }).then(console.log) + * // => { keys: [{ ..., alg: 'RS256', kid: 'custom' }, { ..., alg: 'RS256', kid: 'custom' }] } + * + * @example Mixed input, multiple keypairs + * JWKSet.generateKeys([{ alg: 'RS512', modulusLength: 1024 }, 'ES256']).then(console.log) + * // => { keys: [{ ..., kty: 'RSA', alg: 'RS512' }, { ..., kty: 'RSA', alg: 'RS512' }, + * // { ..., kty: 'EC', alg: 'ES256' }, { ..., kty: 'EC', alg: 'ES256' }] } + * * @param {(String|Object|Array)} data - * @return {Promise} + * @return {Promise.} A promise that resolves a new JWKSet containing the generated key pairs. */ static generateKeys (data) { return Promise.resolve(new JWKSet()) @@ -66,8 +90,45 @@ class JWKSet { /** * importKeys * + * @description + * Instantiate a new JWKSet and import keys from JSON string, JS object, remote URL or file path. + * + * @example Import keys from JSON string + * JWKSet.importKeys('{"meta":"abcd","keys":[...]}').then(console.log) + * // => { meta: 'abcd', keys: [...] } + * + * @example Import keys from object + * JWKSet.importKeys({ meta: 'abcd', keys: [...] }).then(console.log) + * // => { meta: 'abcd', keys: [...] } + * + * @example Import keys from URL + * JWKSet.importKeys('https://idp.example.com/jwks').then(console.log) + * // + * // HTTP/1.1 200 OK + * // Content-Type: application/json + * // + * // {"meta":"abcd","keys":[...]} + * // + * // => { meta: 'abcd', keys: [...] } + * + * @example Import keys from file path + * JWKSet.importKeys('./path/to/my/file.json').then(console.log) + * // + * // Contents of ./path/to/my/file.json - + * // {"meta":"abcd","keys":[...]} + * // + * // => { meta: 'abcd', keys: [...] } + * + * @example Mixed input, multiple sources + * JWKSet.importKeys([{ meta: 'abcd', keys: [...] }, './path/to/my/file.json']).then(console.log) + * // + * // Contents of ./path/to/my/file.json - + * // {"other":"efgh","keys":[...]} + * // + * // => { meta: 'abcd', other: 'efgh', keys: [...] } + * * @param {(String|Object|Array)} data - * @return {Promise} + * @return {Promise.} A promise that resolves a new JWKSet containing the generated key pairs. */ static importKeys (data) { return Promise.resolve(new JWKSet()) @@ -77,8 +138,27 @@ class JWKSet { /** * generateKeys * + * @description + * Generate additional keys and include them in the JWKSet. + * + * @example Simple RSA keypair + * jwks.generateKeys('RS256').then(console.log) + * // => [{ kty: 'RSA' }, { kty: 'RSA' }] + * + * @example Multiple keypairs + * jwks.generateKeys(['RS256', 'ES256']).then(console.log) + * // => [[{ kty: 'RSA' }, { kty: 'RSA' }], [{ kty: 'EC' }, { kty: 'EC' }]] + * + * @example Object descriptor RSA keypair + * jwks.generateKeys({ alg: 'RS256', kid: 'custom', modulusLength: 1024 }).then(console.log) + * // => [{ kty: 'RSA', kid: 'custom' }, { kty: 'RSA', kid: 'custom' }] + * + * @example Mixed input, multiple keypairs + * jwks.generateKeys([{ alg: 'RS512', modulusLength: 1024 }, 'ES256']).then(console.log) + * // => [[{ kty: 'RSA' }, { kty: 'RSA' }], [{ kty: 'EC' }, { kty: 'EC' }]] + * * @param {(String|Object|Array)} data - * @return {Promise} + * @return {Promise., Array.>>} A promise that resolves the newly generated key pairs after they are added to the JWKSet instance. */ generateKeys (data) { let cryptoKeyPromise, alg @@ -129,10 +209,48 @@ class JWKSet { /** * importKeys * + * @description + * Import additional keys and include them in the JWKSet. + * + * @example Import keys from JSON string + * jwks.importKeys('{"meta":"abcd","keys":[...]}').then(console.log) + * // => [{...}, {...}] + * + * @example Import keys from object + * jwks.importKeys({ meta: 'abcd', keys: [...] }).then(console.log) + * // => [{...}, {...}] + * + * @example Import keys from URL + * jwks.importKeys('https://idp.example.com/jwks').then(console.log) + * // + * // HTTP/1.1 200 OK + * // Content-Type: application/json + * // + * // {"meta":"abcd","keys":[...]} + * // + * // => [{...}, {...}] + * + * @example Import keys from file path + * jwks.importKeys('./path/to/my/file.json').then(console.log) + * // + * // Contents of ./path/to/my/file.json - + * // {"meta":"abcd","keys":[...]} + * // + * // => [{...}, {...}] + * + * @example Mixed input, multiple sources + * jwks.importKeys([{ meta: 'abcd', keys: [...] }, './path/to/my/file.json']).then(console.log) + * // + * // Contents of ./path/to/my/file.json - + * // {"other":"efgh","keys":[...]} + * // + * // => [{...}, {...}, {...}, {...}] + * * @param {(String|Object|Array)} data - * @return {Promise} + * @param {JWK} [kek] - Key encryption key. + * @return {Promise.>} A promise that resolves the newly imported key pairs after they are added to the JWKSet instance. * - * @todo import encrypted JWKSet + * @todo Import encrypted JWKSet */ importKeys (data) { if (!data) { @@ -192,8 +310,19 @@ class JWKSet { /** * filter * + * @description + * Execute a filter query on the JWKSet keys. + * + * @example Function predicate + * let filtered = jwks.filter(key => key.key_ops.includes('sign')) + * // => [{ ..., key_ops: ['sign'] }] + * + * @example MongoDB-like object predicate (see [Sift]{@link https://github.com/crcn/sift.js}) + * let filtered = jwks.filter({ key_ops: { $in: ['sign', 'verify'] } }) + * // => [{ ..., key_ops: ['sign'] }, { ..., key_ops: ['verify'] }] + * * @param {(Function|Object)} predicate - Filter function or predicate object - * @return {Promise} + * @return {Array.} An array of JWKs matching the filter predicate. */ filter (predicate) { let { keys } = this @@ -215,8 +344,19 @@ class JWKSet { /** * find * + * @description + * Execute a find query on the JWKSet keys. + * + * @example Function predicate + * let filtered = jwks.find(key => key.key_ops.includes('sign')) + * // => { ..., key_ops: ['sign'] } + * + * @example MongoDB-like object predicate (see [Sift]{@link https://github.com/crcn/sift.js}) + * let filtered = jwks.find({ key_ops: { $in: ['sign', 'verify'] } }) + * // => { ..., key_ops: ['sign'] } + * * @param {(Function|Object)} predicate - Find function or predicate object - * @return {Promise} + * @return {JWK} The _first_ JWK matching the find predicate. */ find (predicate) { let { keys } = this @@ -238,6 +378,7 @@ class JWKSet { /** * rotate + * @ignore * * @param {(JWK|Array|Object|Function)} keys - jwk, array of jwks, filter predicate object or function. * @return {Promise} @@ -251,10 +392,13 @@ class JWKSet { /** * exportKeys * + * @description + * Serialize the JWKSet for storage or transmission. + * * @param {JWK} [kek] - optional encryption key - * @return {String} JSON String + * @return {String} The JSON serialized string of the JWKSet. * - * @todo encryption + * @todo Encryption */ exportKeys (kek) { return JSON.stringify(this) @@ -263,9 +407,12 @@ class JWKSet { /** * publicJwks * - * @return {String} Publishable JSON JWKSet + * @type {String} + * + * @description + * The publishable JSON serialized string of the JWKSet. Returns _only public keys_. * - * @todo memoise this + * @todo Memoization */ get publicJwks () { let keys = this.filter(key => key.cryptoKey.type === 'public') From 88b501283d83b1fb1f15c3972a4583b3ca182941 Mon Sep 17 00:00:00 2001 From: Greg Linklater Date: Sun, 20 Aug 2017 17:00:16 +0200 Subject: [PATCH 06/17] doc(README): add basic method signatures and link to full API documentation --- README.md | 31 ++++++++++++++++++------------- 1 file changed, 18 insertions(+), 13 deletions(-) diff --git a/README.md b/README.md index c36a78e..dee7f4d 100644 --- a/README.md +++ b/README.md @@ -71,24 +71,29 @@ $ npm run karma // Karma (browser) ## API +Full documentation available [here](https://anvilresearch.github.io/jwk). + ### JWK -#### new JWK() -#### (static) importKey() -#### sign() -#### verify() -#### encrypt() -#### decrypt() +#### new JWK(data, [options]) +#### (static) importKey(data, [options]) => Promise. +#### (static) fromCryptoKey(data, [options]) => Promise. +#### sign(data) => Promise. +#### verify(data, signature) => Promise. +#### encrypt(data, aad) => Promise. +#### decrypt(ciphertext, iv, tag, aad) => Promise. ### JWKSet -#### new JWKSet() -#### (static) generateKeys() -#### (static) importKeys() -#### generateKeys() -#### importKeys() -#### filter() -#### find() +#### new JWKSet([data]) +#### (static) generateKeys(data) => Promise. +#### (static) importKeys(data) => Promise. +#### get publicJwks => String +#### generateKeys(data) => Promise.>|> +#### importKeys(data) => Promise.> +#### filter(predicate) => Array. +#### find(predicate) => JWK +#### exportKeys(kek) => Promise. ## Contribute From 9e7d50bf67ebcbdee92b8eb5ecc5df4ed0ec7f3f Mon Sep 17 00:00:00 2001 From: Greg Linklater Date: Sun, 20 Aug 2017 17:04:12 +0200 Subject: [PATCH 07/17] doc(README): removed README docs in favour of examples section --- README.md | 29 ++++++----------------------- 1 file changed, 6 insertions(+), 23 deletions(-) diff --git a/README.md b/README.md index dee7f4d..fde01dc 100644 --- a/README.md +++ b/README.md @@ -18,6 +18,7 @@ available natively in browsers and [via npm][node-webcrypto] in Node.js. [jwkset]: https://tools.ietf.org/html/rfc7517#section-5 [w3c-webcrypto]: https://www.w3.org/TR/WebCryptoAPI/ [node-webcrypto]: https://www.npmjs.com/package/@trust/webcrypto +[api-docs]: https://anvilresearch.github.io/jwk ## Table of Contents @@ -71,29 +72,11 @@ $ npm run karma // Karma (browser) ## API -Full documentation available [here](https://anvilresearch.github.io/jwk). - -### JWK - -#### new JWK(data, [options]) -#### (static) importKey(data, [options]) => Promise. -#### (static) fromCryptoKey(data, [options]) => Promise. -#### sign(data) => Promise. -#### verify(data, signature) => Promise. -#### encrypt(data, aad) => Promise. -#### decrypt(ciphertext, iv, tag, aad) => Promise. - -### JWKSet - -#### new JWKSet([data]) -#### (static) generateKeys(data) => Promise. -#### (static) importKeys(data) => Promise. -#### get publicJwks => String -#### generateKeys(data) => Promise.>|> -#### importKeys(data) => Promise.> -#### filter(predicate) => Array. -#### find(predicate) => JWK -#### exportKeys(kek) => Promise. +Full documentation available [here][api-docs]. + +### Examples + +TBD ## Contribute From 2c80e294d9d5d8ec9895509a9eff5493b8939038 Mon Sep 17 00:00:00 2001 From: Greg Linklater Date: Sun, 20 Aug 2017 22:53:18 +0200 Subject: [PATCH 08/17] style+doc(JWK): shorten example line length --- src/JWK.js | 25 +++++++++++++++++++++---- 1 file changed, 21 insertions(+), 4 deletions(-) diff --git a/src/JWK.js b/src/JWK.js index 62dc704..523095f 100644 --- a/src/JWK.js +++ b/src/JWK.js @@ -133,7 +133,13 @@ class JWK { * * @example Signing the string "test" * privateJwk.sign('test').then(console.log) - * // => "MEUCIQCHwnGM8IsOJgfQsoPgs3hMd8ahfWHM9ZNvj1K6i2yhKQIgWGOuXX43lSTo-U8Pa8sURR53lv6Osjw-dtoLselftqQ" + * // + * // (line breaks for display only) + * // + * // => "MEUCIQCHwnGM8IsOJgfQsoPgs3hMd8 + * // ahfWHM9ZNvj1K6i2yhKQIgWGOuXX43 + * // lSTo-U8Pa8sURR53lv6Osjw-dtoLse + * // lftqQ" * * @param {(String|Buffer)} data - The data to sign. * @return {Promise.} A promise that resolves the base64url encoded signature string. @@ -150,7 +156,11 @@ class JWK { * Verify a signature using the JWK. * * @example Verify a signature of the string "test" - * publicJwk.verify('test', 'MEUCIQCHwnGM8IsOJgfQsoPgs3hMd8ahfWHM9ZNvj1K6i2yhKQIgWGOuXX43lSTo-U8Pa8sURR53lv6Osjw-dtoLselftqQ').then(console.log) + * // base64url encoded signature string + * let signature = `MEUCIQCHwnGM8IsOJgfQsoPgs3hMd8ahfWHM9ZN + * vj1K6i2yhKQIgWGOuXX43lSTo-U8Pa8sURR53lv6Osjw-dtoLselftqQ` + * + * publicJwk.verify('test', signature).then(console.log) * // => true * * @param {(String|Buffer)} data - The data to verify. @@ -170,7 +180,9 @@ class JWK { * * @example Encrypt the string "data" * secretJwk.encrypt('data').then(console.log) - * // => { iv: 'u0l3ttqUFDQ8mcRboHv5Vw', ciphertext: 'yq3K4w', tag: 'fHlZ__uuUnHn0ac-Lnrr-A' } + * // => { iv: 'u0l3ttqUFDQ8mcRboHv5Vw', + * // ciphertext: 'yq3K4w', + * // tag: 'fHlZ__uuUnHn0ac-Lnrr-A' } * * @param {(String|Object)} data - The data to encrypt. * @param {(String|Buffer)} [aad] - Additional non-encrypted integrity protected data (AES-GCM). @@ -188,7 +200,12 @@ class JWK { * Decrypt data using the JWK. * * @example Decrypt encrypted string "test" - * secretJwk.decrypt('yq3K4w', 'u0l3ttqUFDQ8mcRboHv5Vw', 'fHlZ__uuUnHn0ac-Lnrr-A').then(console.log) + * // base64url encoded data + * let ciphertext = 'yq3K4w' + * let iv = 'u0l3ttqUFDQ8mcRboHv5Vw' + * let tag = 'fHlZ__uuUnHn0ac-Lnrr-A' + * + * secretJwk.decrypt(ciphertext, iv, tag).then(console.log) * // => "data" * * @param {(String|Buffer)} ciphertext - The encrypted data to decrypt. From 30883ee0d6ce98499740bddfe57c5af76a9b352a Mon Sep 17 00:00:00 2001 From: Greg Linklater Date: Sun, 20 Aug 2017 22:53:28 +0200 Subject: [PATCH 09/17] style+doc(JWKSet): shorten example line length --- src/JWKSet.js | 190 +++++++++++++++++++++++++++++++++++++++----------- 1 file changed, 149 insertions(+), 41 deletions(-) diff --git a/src/JWKSet.js b/src/JWKSet.js index 77a7f03..08a4994 100644 --- a/src/JWKSet.js +++ b/src/JWKSet.js @@ -61,23 +61,47 @@ class JWKSet { * Instantiate a new JWKSet and generate one or many JWK keypairs and secret keys. * * @example Simple RSA keypair - * JWKSet.generateKeys('RS256').then(console.log) - * // => { keys: [{ ..., d: '...', kty: 'RSA', alg: 'RS256', kid: 'abcd' }, + * JWKSet.generateKeys('RS256') + * .then(console.log) + * // => { keys: [ + * // { ..., d: '...', kty: 'RSA', alg: 'RS256', kid: 'abcd' }, * // { ..., kty: 'RSA', alg: 'RS256', kid: 'abcd' }] } * * @example Multiple keypairs - * JWKSet.generateKeys(['RS256', 'ES256']).then(console.log) - * // => { keys: [{ ..., kty: 'RSA', alg: 'RS256' }, { ..., kty: 'RSA', alg: 'RS256' }, - * // { ..., kty: 'EC', alg: 'ES256' }, { ..., kty: 'EC', alg: 'ES256' }] } + * JWKSet.generateKeys(['RS256', 'ES256']) + * .then(console.log) + * // => { keys: [ + * // { ..., kty: 'RSA', alg: 'RS256' }, + * // { ..., kty: 'RSA', alg: 'RS256' }, + * // { ..., kty: 'EC', alg: 'ES256' }, + * // { ..., kty: 'EC', alg: 'ES256' }] } * * @example Object descriptor RSA keypair - * JWKSet.generateKeys({ alg: 'RS256', kid: 'custom', modulusLength: 1024 }).then(console.log) - * // => { keys: [{ ..., alg: 'RS256', kid: 'custom' }, { ..., alg: 'RS256', kid: 'custom' }] } + * let keyDescriptor = { + * alg: 'RS256', + * kid: 'custom', + * modulusLength: 1024 + * } + * + * JWKSet.generateKeys(keyDescriptor) + * .then(console.log) + * // => { keys: [ + * // { ..., alg: 'RS256', kid: 'custom' }, + * // { ..., alg: 'RS256', kid: 'custom' }] } * * @example Mixed input, multiple keypairs - * JWKSet.generateKeys([{ alg: 'RS512', modulusLength: 1024 }, 'ES256']).then(console.log) - * // => { keys: [{ ..., kty: 'RSA', alg: 'RS512' }, { ..., kty: 'RSA', alg: 'RS512' }, - * // { ..., kty: 'EC', alg: 'ES256' }, { ..., kty: 'EC', alg: 'ES256' }] } + * let keyDescriptor = { + * alg: 'RS512', + * modulusLength: 1024 + * } + * + * JWKSet.generateKeys([keyDescriptor, 'ES256']) + * .then(console.log) + * // => { keys: [ + * // { ..., kty: 'RSA', alg: 'RS512' }, + * // { ..., kty: 'RSA', alg: 'RS512' }, + * // { ..., kty: 'EC', alg: 'ES256' }, + * // { ..., kty: 'EC', alg: 'ES256' }] } * * @param {(String|Object|Array)} data * @return {Promise.} A promise that resolves a new JWKSet containing the generated key pairs. @@ -94,38 +118,60 @@ class JWKSet { * Instantiate a new JWKSet and import keys from JSON string, JS object, remote URL or file path. * * @example Import keys from JSON string - * JWKSet.importKeys('{"meta":"abcd","keys":[...]}').then(console.log) + * let jsonJwkSet = '{"meta":"abcd","keys":[...]}' + * + * JWKSet.importKeys(jsonJwkSet).then(console.log) * // => { meta: 'abcd', keys: [...] } * * @example Import keys from object - * JWKSet.importKeys({ meta: 'abcd', keys: [...] }).then(console.log) + * let jwkSet = { + * meta: 'abcd', + * keys: [...] + * } + * + * JWKSet.importKeys(jwkSet) + * .then(console.log) * // => { meta: 'abcd', keys: [...] } * * @example Import keys from URL - * JWKSet.importKeys('https://idp.example.com/jwks').then(console.log) + * let jwkSetUrl = 'https://idp.example.com/jwks' + * + * JWKSet.importKeys(jwkSetUrl) + * .then(console.log) * // * // HTTP/1.1 200 OK * // Content-Type: application/json * // * // {"meta":"abcd","keys":[...]} * // - * // => { meta: 'abcd', keys: [...] } + * // => { meta: 'abcd', + * // keys: [...] } * * @example Import keys from file path - * JWKSet.importKeys('./path/to/my/file.json').then(console.log) + * let jwkSetPath = './path/to/my/file.json' + * + * JWKSet.importKeys(jwkSetPath) + * .then(console.log) * // * // Contents of ./path/to/my/file.json - * // {"meta":"abcd","keys":[...]} * // - * // => { meta: 'abcd', keys: [...] } + * // => { meta: 'abcd', + * // keys: [...] } * * @example Mixed input, multiple sources - * JWKSet.importKeys([{ meta: 'abcd', keys: [...] }, './path/to/my/file.json']).then(console.log) + * let jwkSetPath = './path/to/my/file.json' + * let jwkSet = { meta: 'abcd', keys: [...] } + * + * JWKSet.importKeys([jwkSet, jwkSetPath]) + * .then(console.log) * // * // Contents of ./path/to/my/file.json - * // {"other":"efgh","keys":[...]} * // - * // => { meta: 'abcd', other: 'efgh', keys: [...] } + * // => { meta: 'abcd', + * // other: 'efgh', + * // keys: [...] } * * @param {(String|Object|Array)} data * @return {Promise.} A promise that resolves a new JWKSet containing the generated key pairs. @@ -142,20 +188,47 @@ class JWKSet { * Generate additional keys and include them in the JWKSet. * * @example Simple RSA keypair - * jwks.generateKeys('RS256').then(console.log) - * // => [{ kty: 'RSA' }, { kty: 'RSA' }] + * jwks.generateKeys('RS256') + * .then(console.log) + * // => [ + * // { kty: 'RSA' }, + * // { kty: 'RSA' } + * // ] * * @example Multiple keypairs * jwks.generateKeys(['RS256', 'ES256']).then(console.log) - * // => [[{ kty: 'RSA' }, { kty: 'RSA' }], [{ kty: 'EC' }, { kty: 'EC' }]] + * // => [ + * // [ { kty: 'RSA' }, + * // { kty: 'RSA' } ], + * // [ { kty: 'EC' }, + * // { kty: 'EC' } ] ] * * @example Object descriptor RSA keypair - * jwks.generateKeys({ alg: 'RS256', kid: 'custom', modulusLength: 1024 }).then(console.log) - * // => [{ kty: 'RSA', kid: 'custom' }, { kty: 'RSA', kid: 'custom' }] + * let keyDescriptor = { + * alg: 'RS256', + * kid: 'custom', + * modulusLength: 1024 + * } + * + * jwks.generateKeys(keyDescriptor) + * .then(console.log) + * // => [ { kty: 'RSA', kid: 'custom' }, + * // { kty: 'RSA', kid: 'custom' } ] * * @example Mixed input, multiple keypairs - * jwks.generateKeys([{ alg: 'RS512', modulusLength: 1024 }, 'ES256']).then(console.log) - * // => [[{ kty: 'RSA' }, { kty: 'RSA' }], [{ kty: 'EC' }, { kty: 'EC' }]] + * let keyDescriptor = { + * alg: 'RS512', + * modulusLength: 1024 + * } + * + * jwks.generateKeys([keyDescriptor, 'ES256']) + * .then(console.log) + * // => [ + * // [ { kty: 'RSA' }, + * // { kty: 'RSA' } ], + * // [ { kty: 'EC' }, + * // { kty: 'EC' } ] + * // ] * * @param {(String|Object|Array)} data * @return {Promise., Array.>>} A promise that resolves the newly generated key pairs after they are added to the JWKSet instance. @@ -213,38 +286,64 @@ class JWKSet { * Import additional keys and include them in the JWKSet. * * @example Import keys from JSON string - * jwks.importKeys('{"meta":"abcd","keys":[...]}').then(console.log) - * // => [{...}, {...}] + * let jsonJwkSet = '{"meta":"abcd","keys":[...]}' + * + * jwks.importKeys(jsonJwkSet) + * .then(console.log) + * // => [ {...}, + * // {...} ] * * @example Import keys from object - * jwks.importKeys({ meta: 'abcd', keys: [...] }).then(console.log) - * // => [{...}, {...}] + * let jwkSet = { + * meta: 'abcd', + * keys: [...] + * } + * + * jwks.importKeys(jwkSet) + * .then(console.log) + * // => [ {...}, + * // {...} ] * * @example Import keys from URL - * jwks.importKeys('https://idp.example.com/jwks').then(console.log) + * let jwkSetUrl = 'https://idp.example.com/jwks' + * + * jwks.importKeys(jwkSetUrl) + * .then(console.log) * // * // HTTP/1.1 200 OK * // Content-Type: application/json * // * // {"meta":"abcd","keys":[...]} * // - * // => [{...}, {...}] + * // => [ {...}, + * // {...} ] * * @example Import keys from file path - * jwks.importKeys('./path/to/my/file.json').then(console.log) + * let jwkSetPath = './path/to/my/file.json' + * + * jwks.importKeys(jwkSetPath) + * .then(console.log) * // * // Contents of ./path/to/my/file.json - * // {"meta":"abcd","keys":[...]} * // - * // => [{...}, {...}] + * // => [ {...}, + * // {...} ] * * @example Mixed input, multiple sources - * jwks.importKeys([{ meta: 'abcd', keys: [...] }, './path/to/my/file.json']).then(console.log) + * let jwkSetPath = './path/to/my/file.json' + * let jwkSet = { meta: 'abcd', keys: [...] } + * + * jwks.importKeys([jwkSet, jwkSetPath]) + * .then(console.log) * // * // Contents of ./path/to/my/file.json - * // {"other":"efgh","keys":[...]} * // - * // => [{...}, {...}, {...}, {...}] + * // => [ {...}, + * // {...}, + * // {...}, + * // {...} ] * * @param {(String|Object|Array)} data * @param {JWK} [kek] - Key encryption key. @@ -314,12 +413,17 @@ class JWKSet { * Execute a filter query on the JWKSet keys. * * @example Function predicate - * let filtered = jwks.filter(key => key.key_ops.includes('sign')) - * // => [{ ..., key_ops: ['sign'] }] + * let predicate = key => key.key_ops.includes('sign') + * + * let filtered = jwks.filter(predicate) + * // => [ { ..., key_ops: ['sign'] } ] * * @example MongoDB-like object predicate (see [Sift]{@link https://github.com/crcn/sift.js}) - * let filtered = jwks.filter({ key_ops: { $in: ['sign', 'verify'] } }) - * // => [{ ..., key_ops: ['sign'] }, { ..., key_ops: ['verify'] }] + * let predicate = { key_ops: { $in: ['sign', 'verify'] } } + * + * let filtered = jwks.filter(predicate) + * // => [ { ..., key_ops: ['sign'] }, + * // { ..., key_ops: ['verify'] } ] * * @param {(Function|Object)} predicate - Filter function or predicate object * @return {Array.} An array of JWKs matching the filter predicate. @@ -348,11 +452,15 @@ class JWKSet { * Execute a find query on the JWKSet keys. * * @example Function predicate - * let filtered = jwks.find(key => key.key_ops.includes('sign')) + * let predicate = key => key.key_ops.includes('sign') + * + * let filtered = jwks.find(predicate) * // => { ..., key_ops: ['sign'] } * * @example MongoDB-like object predicate (see [Sift]{@link https://github.com/crcn/sift.js}) - * let filtered = jwks.find({ key_ops: { $in: ['sign', 'verify'] } }) + * let predicate = { key_ops: { $in: ['sign', 'verify'] } } + * + * let filtered = jwks.find() * // => { ..., key_ops: ['sign'] } * * @param {(Function|Object)} predicate - Find function or predicate object From 579263d9eaae8e2affd8c37bf4ed88059036faac Mon Sep 17 00:00:00 2001 From: Greg Linklater Date: Mon, 21 Aug 2017 10:52:58 +0200 Subject: [PATCH 10/17] style+doc(JWK): remain consistent across examples --- src/JWK.js | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/src/JWK.js b/src/JWK.js index 523095f..0e17d29 100644 --- a/src/JWK.js +++ b/src/JWK.js @@ -132,7 +132,8 @@ class JWK { * Sign arbitrary data using the JWK. * * @example Signing the string "test" - * privateJwk.sign('test').then(console.log) + * privateJwk.sign('test') + * .then(console.log) * // * // (line breaks for display only) * // @@ -160,7 +161,8 @@ class JWK { * let signature = `MEUCIQCHwnGM8IsOJgfQsoPgs3hMd8ahfWHM9ZN * vj1K6i2yhKQIgWGOuXX43lSTo-U8Pa8sURR53lv6Osjw-dtoLselftqQ` * - * publicJwk.verify('test', signature).then(console.log) + * publicJwk.verify('test', signature) + * .then(console.log) * // => true * * @param {(String|Buffer)} data - The data to verify. @@ -179,7 +181,8 @@ class JWK { * Encrypt arbitrary data using the JWK. * * @example Encrypt the string "data" - * secretJwk.encrypt('data').then(console.log) + * secretJwk.encrypt('data') + * .then(console.log) * // => { iv: 'u0l3ttqUFDQ8mcRboHv5Vw', * // ciphertext: 'yq3K4w', * // tag: 'fHlZ__uuUnHn0ac-Lnrr-A' } @@ -205,7 +208,8 @@ class JWK { * let iv = 'u0l3ttqUFDQ8mcRboHv5Vw' * let tag = 'fHlZ__uuUnHn0ac-Lnrr-A' * - * secretJwk.decrypt(ciphertext, iv, tag).then(console.log) + * secretJwk.decrypt(ciphertext, iv, tag) + * .then(console.log) * // => "data" * * @param {(String|Buffer)} ciphertext - The encrypted data to decrypt. From d0039f531327ecfad2d60c464ae791deb4ae66ca Mon Sep 17 00:00:00 2001 From: Greg Linklater Date: Mon, 21 Aug 2017 10:53:08 +0200 Subject: [PATCH 11/17] style+doc(JWKSet): remain consistent across examples --- src/JWKSet.js | 18 ++++++++++++++---- 1 file changed, 14 insertions(+), 4 deletions(-) diff --git a/src/JWKSet.js b/src/JWKSet.js index 08a4994..e95f3d4 100644 --- a/src/JWKSet.js +++ b/src/JWKSet.js @@ -64,8 +64,16 @@ class JWKSet { * JWKSet.generateKeys('RS256') * .then(console.log) * // => { keys: [ - * // { ..., d: '...', kty: 'RSA', alg: 'RS256', kid: 'abcd' }, - * // { ..., kty: 'RSA', alg: 'RS256', kid: 'abcd' }] } + * // { d: '...', + * // kty: 'RSA', + * // alg: 'RS256', + * // kid: 'abcd', + * // ... }, + * // { kty: 'RSA', + * // alg: 'RS256', + * // kid: 'abcd', + * // ... } + * // ] } * * @example Multiple keypairs * JWKSet.generateKeys(['RS256', 'ES256']) @@ -120,7 +128,8 @@ class JWKSet { * @example Import keys from JSON string * let jsonJwkSet = '{"meta":"abcd","keys":[...]}' * - * JWKSet.importKeys(jsonJwkSet).then(console.log) + * JWKSet.importKeys(jsonJwkSet) + * .then(console.log) * // => { meta: 'abcd', keys: [...] } * * @example Import keys from object @@ -196,7 +205,8 @@ class JWKSet { * // ] * * @example Multiple keypairs - * jwks.generateKeys(['RS256', 'ES256']).then(console.log) + * jwks.generateKeys(['RS256', 'ES256']) + * .then(console.log) * // => [ * // [ { kty: 'RSA' }, * // { kty: 'RSA' } ], From 793b32233e9131a82355f287f6cc88763f98f011 Mon Sep 17 00:00:00 2001 From: Greg Linklater Date: Mon, 21 Aug 2017 13:54:49 +0200 Subject: [PATCH 12/17] refactor(JWK): remove default values for aad params --- src/JWK.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/JWK.js b/src/JWK.js index 0e17d29..fd1969b 100644 --- a/src/JWK.js +++ b/src/JWK.js @@ -191,7 +191,7 @@ class JWK { * @param {(String|Buffer)} [aad] - Additional non-encrypted integrity protected data (AES-GCM). * @return {Promise.} A promise that resolves an object containing the base64url encoded `iv`, `ciphertext` and `tag` (AES-GCM). */ - encrypt (data, aad = Buffer.from('')) { + encrypt (data, aad) { let { alg, cryptoKey } = this return JWA.encrypt(alg, cryptoKey, data, aad) } @@ -218,7 +218,7 @@ class JWK { * @param {(String|Buffer)} [aad] - Additional non-encrypted integrity protected data (AES-GCM). * @return {Promise.} A promise that resolves the plaintext data. */ - decrypt (ciphertext, iv, tag, aad = Buffer.from('')) { + decrypt (ciphertext, iv, tag, aad) { let { alg, cryptoKey } = this return JWA.decrypt(alg, cryptoKey, ciphertext, iv, tag, aad) } From 68dd2dd1b5390761f7a66443e78671dc3bacfc6d Mon Sep 17 00:00:00 2001 From: Greg Linklater Date: Mon, 21 Aug 2017 13:55:13 +0200 Subject: [PATCH 13/17] test(JWK): added tests for authenticated additionalData (aad) --- test/JWKSpec.js | 32 ++++++++++++++++++++++++++++++-- 1 file changed, 30 insertions(+), 2 deletions(-) diff --git a/test/JWKSpec.js b/test/JWKSpec.js index 7397387..6d6fff2 100644 --- a/test/JWKSpec.js +++ b/test/JWKSpec.js @@ -44,7 +44,9 @@ const ECPublicJWKNoKid = Object.assign({}, ECPublicJWK) delete ECPublicJWKNoKid.kid const plainTextData = 'data' +const plainTextAad = 'meta' const encryptedData = {"iv":"RH9i_J861XN7qvgHYZ86ag","ciphertext":"qkrkiw","tag":"kqfdLgy8qopnzeKmC5JwQA"} +const encryptedDataWithAad = {"iv":"zrXJWOthT2tnFErPhWCrfw","ciphertext":"JWwKBg","tag":"txl7BQK4fxEP5cie2OQEZA","aad":"bWV0YQ"} const signedData = 'MEQCIAIqNr8-7Pozi1D-cigvEKbkP5SpKezzEEDSqM9McIV1AiBd4gioW8njOpr29Ymrvjp46q7hA7lSOjAJpdi5TjHWsg' /** @@ -169,6 +171,19 @@ describe('JWK', () => { data.should.haveOwnProperty('ciphertext') data.should.haveOwnProperty('iv') data.should.haveOwnProperty('tag') + data.should.not.haveOwnProperty('aad') + }) + }) + + describe('with aad', () => { + + it('should resolve encrypted data', () => { + return jwk.encrypt(plainTextData, plainTextAad).then(data => { + data.should.haveOwnProperty('ciphertext') + data.should.haveOwnProperty('iv') + data.should.haveOwnProperty('tag') + data.should.haveOwnProperty('aad') + }) }) }) }) @@ -182,12 +197,25 @@ describe('JWK', () => { it('should return a promise', () => { let { ciphertext, iv, tag } = encryptedData - return jwk.decrypt(ciphertext, iv, tag, Buffer.from('')).should.be.fulfilled + return jwk.decrypt(ciphertext, iv, tag).should.be.fulfilled }) it('should resolve plain text data', () => { let { ciphertext, iv, tag } = encryptedData - return jwk.decrypt(ciphertext, iv, tag, Buffer.from('')).should.eventually.equal(plainTextData) + return jwk.decrypt(ciphertext, iv, tag).should.eventually.equal(plainTextData) + }) + + describe('with aad', () => { + + it('should reject if aad is omitted', () => { + let { ciphertext, iv, tag } = encryptedDataWithAad + return jwk.decrypt(ciphertext, iv, tag).should.be.rejected + }) + + it('should resolve plain text data', () => { + let { ciphertext, iv, tag, aad } = encryptedDataWithAad + return jwk.decrypt(ciphertext, iv, tag, aad).should.eventually.equal(plainTextData) + }) }) }) }) From 9ca21d558f4a94b6a9057fc0b7295654eab48dd1 Mon Sep 17 00:00:00 2001 From: Greg Linklater Date: Mon, 21 Aug 2017 17:01:09 +0200 Subject: [PATCH 14/17] chore(npm): bump jwa to 0.4.4 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index e858682..a0794c1 100644 --- a/package.json +++ b/package.json @@ -50,7 +50,7 @@ "sinon-chai": "^2.12.0" }, "dependencies": { - "@trust/jwa": "^0.4.3", + "@trust/jwa": "^0.4.4", "@trust/webcrypto": "^0.4.0", "node-fetch": "^1.7.2", "sift": "^5.0.0" From e81b11ba79e0afd469f92242a1f8dacc53ba455d Mon Sep 17 00:00:00 2001 From: Greg Linklater Date: Wed, 23 Aug 2017 16:47:40 +0200 Subject: [PATCH 15/17] chore(npm): bump jwa to 0.4.5 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index a0794c1..c38070c 100644 --- a/package.json +++ b/package.json @@ -50,7 +50,7 @@ "sinon-chai": "^2.12.0" }, "dependencies": { - "@trust/jwa": "^0.4.4", + "@trust/jwa": "^0.4.5", "@trust/webcrypto": "^0.4.0", "node-fetch": "^1.7.2", "sift": "^5.0.0" From c9239957dabee6da21e0662bedd246cdd4684fb9 Mon Sep 17 00:00:00 2001 From: Greg Linklater Date: Thu, 24 Aug 2017 17:49:00 +0200 Subject: [PATCH 16/17] style(JWK): additional whitespace around importKey and fromCryptoKey --- src/JWK.js | 29 ++++++++++++++++------------- 1 file changed, 16 insertions(+), 13 deletions(-) diff --git a/src/JWK.js b/src/JWK.js index fd1969b..1d61f6b 100644 --- a/src/JWK.js +++ b/src/JWK.js @@ -89,18 +89,15 @@ class JWK { .then(() => new JWK(data, options)) // Import CryptoKey and assign to JWK - .then(jwk => { - return JWA.importKey(jwk) - .then(({ cryptoKey }) => { - Object.defineProperty(jwk, 'cryptoKey', { - value: cryptoKey, - enumerable: false, - configurable: false - }) - - return jwk - }) - }) + .then(jwk => JWA.importKey(jwk).then(({ cryptoKey }) => { + Object.defineProperty(jwk, 'cryptoKey', { + value: cryptoKey, + enumerable: false, + configurable: false + }) + + return jwk + })) } /** @@ -120,7 +117,13 @@ class JWK { // Create JWK instance and assign CryptoKey .then(data => { let jwk = new JWK(data, options) - Object.defineProperty(jwk, 'cryptoKey', { value: key, enumerable: false, configurable: false }) + + Object.defineProperty(jwk, 'cryptoKey', { + value: key, + enumerable: false, + configurable: false + }) + return jwk }) } From 5b760d560d6e05b5a94519a6f2e236f48a297ed8 Mon Sep 17 00:00:00 2001 From: Greg Linklater Date: Thu, 24 Aug 2017 17:49:13 +0200 Subject: [PATCH 17/17] test(JWK): test importKey and fromCryptoKey --- test/JWKSpec.js | 33 +++++++++++++++++++++++++++++++++ 1 file changed, 33 insertions(+) diff --git a/test/JWKSpec.js b/test/JWKSpec.js index 6d6fff2..55f6c43 100644 --- a/test/JWKSpec.js +++ b/test/JWKSpec.js @@ -113,10 +113,43 @@ describe('JWK', () => { describe('importKey', () => { + it('should return a promise', () => { + return JWK.importKey(ECPrivateJWK).should.be.fulfilled + }) + + it('should resolve an instance of JWK', () => { + return JWK.importKey(ECPrivateJWK).should.eventually.be.an.instanceOf(JWK) + }) + + it('should have a cryptoKey property', () => { + return JWK.importKey(ECPrivateJWK).then(jwk => { + jwk.should.haveOwnProperty('cryptoKey') + }) + }) }) describe('fromCryptoKey', () => { + let cryptoKey + + before(() => { + return JWK.importKey(ECPrivateJWK).then(jwk => { + cryptoKey = jwk.cryptoKey + }) + }) + + it('should return a promise', () => { + return JWK.fromCryptoKey(cryptoKey, { alg: 'EC256', kid: 'abcd123$' }).should.eventually.be.an.instanceOf(JWK) + }) + it('should resolve an instance of JWK', () => { + return JWK.fromCryptoKey(cryptoKey, { alg: 'EC256', kid: 'abcd123$' }).should.be.fulfilled + }) + + it('should have a cryptoKey property', () => { + return JWK.fromCryptoKey(cryptoKey, { alg: 'EC256', kid: 'abcd123$' }).then(jwk => { + jwk.should.haveOwnProperty('cryptoKey') + }) + }) }) describe('sign', () => {