From 6ac9090c095ac75d45c5b154d6eb3ae2762f9a2c Mon Sep 17 00:00:00 2001 From: Dave Longley Date: Sun, 31 Jul 2022 11:33:11 -0400 Subject: [PATCH 1/9] Remove mention of CWE; CWE is not a straight translation of JWE. --- README.md | 3 ++- package.json | 2 +- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 41737c4..8b2893f 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,7 @@ # Minimal Cipher _(@digitalbazaar/minimal-cipher)_ -> Minimal encryption/decryption [JWE](https://tools.ietf.org/html/rfc7516)/[CWE](https://tools.ietf.org/html/rfc8152) library, secure algs only, browser-compatible +Minimal encryption/decryption [JWE](https://tools.ietf.org/html/rfc7516) +library, secure algs only, browser-compatible. ## Table of Contents diff --git a/package.json b/package.json index 0f97c88..795bde5 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "@digitalbazaar/minimal-cipher", "version": "5.1.1-0", - "description": "Minimal encryption/decryption JWE/CWE library.", + "description": "Minimal encryption/decryption JWE library.", "license": "BSD-3-Clause", "type": "module", "exports": "./lib/index.js", From aa0363b40dd8fc67e9aae9d653c4b8951a97d3e3 Mon Sep 17 00:00:00 2001 From: Dave Longley Date: Sun, 31 Jul 2022 11:37:15 -0400 Subject: [PATCH 2/9] Rename internal function (`derive...` => `generateEphemeralKeyPair`). - Addresses #25. --- lib/Cipher.js | 2 +- lib/algorithms/x25519-helper-browser.js | 2 +- lib/algorithms/x25519-helper.js | 4 ++-- lib/algorithms/x25519.js | 4 ++-- 4 files changed, 6 insertions(+), 6 deletions(-) diff --git a/lib/Cipher.js b/lib/Cipher.js index d6c251f..e5256aa 100644 --- a/lib/Cipher.js +++ b/lib/Cipher.js @@ -228,7 +228,7 @@ export class Cipher { const cek = await cipher.generateKey(); // derive ephemeral ECDH key pair to use with all recipients - const ephemeralKeyPair = await keyAgreement.deriveEphemeralKeyPair(); + const ephemeralKeyPair = await keyAgreement.generateEphemeralKeyPair(); recipients = await Promise.all(recipients.map( recipient => this._createRecipient( diff --git a/lib/algorithms/x25519-helper-browser.js b/lib/algorithms/x25519-helper-browser.js index ba840db..a169952 100644 --- a/lib/algorithms/x25519-helper-browser.js +++ b/lib/algorithms/x25519-helper-browser.js @@ -4,7 +4,7 @@ import * as base64url from 'base64url-universal'; import nacl from 'tweetnacl'; -export async function deriveEphemeralKeyPair() { +export async function generateEphemeralKeyPair() { // generate X25519 ephemeral public key const keyPair = nacl.box.keyPair(); const {secretKey: privateKey, publicKey} = keyPair; diff --git a/lib/algorithms/x25519-helper.js b/lib/algorithms/x25519-helper.js index 45ecfb3..02f0980 100644 --- a/lib/algorithms/x25519-helper.js +++ b/lib/algorithms/x25519-helper.js @@ -1,5 +1,5 @@ /*! - * Copyright (c) 2019-2020 Digital Bazaar, Inc. All rights reserved. + * Copyright (c) 2019-2022 Digital Bazaar, Inc. All rights reserved. */ import * as base64url from 'base64url-universal'; import nacl from 'tweetnacl'; @@ -17,7 +17,7 @@ const PRIVATE_KEY_DER_PREFIX = new Uint8Array([ 48, 46, 2, 1, 0, 48, 5, 6, 3, 43, 101, 110, 4, 34, 4, 32 ]); -export async function deriveEphemeralKeyPair() { +export async function generateEphemeralKeyPair() { // generate X25519 ephemeral public key let keyPair; if(await _hasNodeDiffieHellman()) { diff --git a/lib/algorithms/x25519.js b/lib/algorithms/x25519.js index d2c875e..9959b02 100644 --- a/lib/algorithms/x25519.js +++ b/lib/algorithms/x25519.js @@ -5,7 +5,7 @@ import * as base64url from 'base64url-universal'; import {createKek} from './aeskw.js'; import * as base58btc from 'base58-universal'; import {deriveKey} from './ecdhkdf.js'; -import {deriveSecret, deriveEphemeralKeyPair} from './x25519-helper.js'; +import {deriveSecret, generateEphemeralKeyPair} from './x25519-helper.js'; const KEY_TYPE = 'X25519KeyAgreementKey2020'; // multibase base58-btc header @@ -13,7 +13,7 @@ const MULTIBASE_BASE58BTC_HEADER = 'z'; // multicodec X25519-pub header as varint export const MULTICODEC_X25519_PUB_HEADER = new Uint8Array([0xec, 0x01]); export const JWE_ALG = 'ECDH-ES+A256KW'; -export {deriveEphemeralKeyPair, deriveSecret}; +export {generateEphemeralKeyPair, deriveSecret}; // Decryption case: get Kek from a private key agreement key and a // peer's public ephemeral DH key encoded as an `epk` From 75e093b55d6b126c32bc17c7dff60f380e3ddc14 Mon Sep 17 00:00:00 2001 From: Dave Longley Date: Sun, 31 Jul 2022 11:44:12 -0400 Subject: [PATCH 3/9] Fix style and address TODO. --- lib/Cipher.js | 17 +++++++++++------ lib/algorithms/x25519.js | 3 +++ 2 files changed, 14 insertions(+), 6 deletions(-) diff --git a/lib/Cipher.js b/lib/Cipher.js index e5256aa..3d7036d 100644 --- a/lib/Cipher.js +++ b/lib/Cipher.js @@ -1,15 +1,12 @@ /*! * Copyright (c) 2019-2022 Digital Bazaar, Inc. All rights reserved. */ -// TODO: Remove when TransformStream is recognized properly. -/* eslint-disable no-undef, jsdoc/no-undefined-types */ - import * as base64url from 'base64url-universal'; -import {stringToUint8Array} from './util.js'; -import {DecryptTransformer} from './DecryptTransformer.js'; -import {EncryptTransformer} from './EncryptTransformer.js'; import * as fipsAlgorithm from './algorithms/fips.js'; import * as recAlgorithm from './algorithms/recommended.js'; +import {DecryptTransformer} from './DecryptTransformer.js'; +import {EncryptTransformer} from './EncryptTransformer.js'; +import {stringToUint8Array} from './util.js'; const VERSIONS = ['recommended', 'fips']; @@ -67,6 +64,7 @@ export class Cipher { async createEncryptStream({recipients, keyResolver, chunkSize}) { const transformer = await this.createEncryptTransformer( {recipients, keyResolver, chunkSize}); + // eslint-disable-next-line no-undef return new TransformStream(transformer); } @@ -92,6 +90,7 @@ export class Cipher { async createDecryptStream({keyAgreementKey}) { const transformer = await this.createDecryptTransformer( {keyAgreementKey}); + // eslint-disable-next-line no-undef return new TransformStream(transformer); } @@ -322,3 +321,9 @@ export class Cipher { }; } } + +/** + * See: https://streams.spec.whatwg.org/#ts-model . + * + * @typedef TransformStream + */ diff --git a/lib/algorithms/x25519.js b/lib/algorithms/x25519.js index 9959b02..087d6be 100644 --- a/lib/algorithms/x25519.js +++ b/lib/algorithms/x25519.js @@ -62,6 +62,7 @@ export async function kekFromEphemeralPeer({keyAgreementKey, epk}) { * @typedef {{ * kek: (object), epk: *, apv: (*|string), apu: (*|string), ephemeralPublicKey * }} kekObject + * * @returns {Promise} - Resolves with kek object derived from static * peer. */ @@ -100,6 +101,7 @@ export async function kekFromStaticPeer({ephemeralKeyPair, staticPublicKey}) { * * @param {Uint8Array} header - Multicodec x25519 pub or pri key header, varint. * @param {Uint8Array} bytes - Byte array representing a public or private key. + * * @returns {string} Base58-btc encoded key (with multicodec prefix). */ export function multibaseEncode(header, bytes) { @@ -114,6 +116,7 @@ export function multibaseEncode(header, bytes) { * * @param {Uint8Array} header - Expected header bytes for the multicodec value. * @param {string} text - Multibase encoded string to decode. + * * @returns {Uint8Array} Decoded bytes. */ export function multibaseDecode(header, text) { From 3ed29ad03c31aadc849cf9eab99cc5695db57142 Mon Sep 17 00:00:00 2001 From: Dave Longley Date: Sun, 31 Jul 2022 11:50:07 -0400 Subject: [PATCH 4/9] Remove `nacl` from node implementation. - Now that node 14+ is required, the required DH + X25519 APIs are always available. --- lib/algorithms/x25519-helper.js | 62 ++++++++++++--------------------- 1 file changed, 22 insertions(+), 40 deletions(-) diff --git a/lib/algorithms/x25519-helper.js b/lib/algorithms/x25519-helper.js index 02f0980..2e0fedb 100644 --- a/lib/algorithms/x25519-helper.js +++ b/lib/algorithms/x25519-helper.js @@ -2,7 +2,6 @@ * Copyright (c) 2019-2022 Digital Bazaar, Inc. All rights reserved. */ import * as base64url from 'base64url-universal'; -import nacl from 'tweetnacl'; import * as crypto from 'node:crypto'; import * as util from 'node:util'; const {promisify} = util; @@ -19,21 +18,14 @@ const PRIVATE_KEY_DER_PREFIX = new Uint8Array([ export async function generateEphemeralKeyPair() { // generate X25519 ephemeral public key - let keyPair; - if(await _hasNodeDiffieHellman()) { - const publicKeyEncoding = {format: 'der', type: 'spki'}; - const privateKeyEncoding = {format: 'der', type: 'pkcs8'}; - const {publicKey: publicDerBytes, privateKey: privateDerBytes} = - await generateKeyPairAsync('x25519', { - publicKeyEncoding, privateKeyEncoding - }); - const publicKey = publicDerBytes.slice(12, 12 + 32); - const secretKey = privateDerBytes.slice(16, 16 + 32); - keyPair = {secretKey, publicKey}; - } else { - keyPair = nacl.box.keyPair(); - } - const {secretKey: privateKey, publicKey} = keyPair; + const publicKeyEncoding = {format: 'der', type: 'spki'}; + const privateKeyEncoding = {format: 'der', type: 'pkcs8'}; + const {publicKey: publicDerBytes, privateKey: privateDerBytes} = + await generateKeyPairAsync('x25519', { + publicKeyEncoding, privateKeyEncoding + }); + const publicKey = publicDerBytes.slice(12, 12 + 32); + const privateKey = privateDerBytes.slice(16, 16 + 32); return { privateKey, publicKey, @@ -46,28 +38,18 @@ export async function generateEphemeralKeyPair() { } export async function deriveSecret({privateKey, remotePublicKey}) { - if(await _hasNodeDiffieHellman()) { - const nodePrivateKey = crypto.createPrivateKey({ - key: Buffer.concat([PRIVATE_KEY_DER_PREFIX, privateKey]), - format: 'der', - type: 'pkcs8' - }); - const nodePublicKey = crypto.createPublicKey({ - key: Buffer.concat([PUBLIC_KEY_DER_PREFIX, remotePublicKey]), - format: 'der', - type: 'spki' - }); - return crypto.diffieHellman({ - privateKey: nodePrivateKey, - publicKey: nodePublicKey, - }); - } - - // `scalarMult` takes secret key as param 1, public key as param 2 - return nacl.scalarMult(privateKey, remotePublicKey); -} - -async function _hasNodeDiffieHellman() { - // crypto.diffieHellman was added in Node.js v13.9.0 - return !!crypto.diffieHellman; + const nodePrivateKey = crypto.createPrivateKey({ + key: Buffer.concat([PRIVATE_KEY_DER_PREFIX, privateKey]), + format: 'der', + type: 'pkcs8' + }); + const nodePublicKey = crypto.createPublicKey({ + key: Buffer.concat([PUBLIC_KEY_DER_PREFIX, remotePublicKey]), + format: 'der', + type: 'spki' + }); + return crypto.diffieHellman({ + privateKey: nodePrivateKey, + publicKey: nodePublicKey, + }); } From 27322729f622f4e30f1ad43565908b76adacb241 Mon Sep 17 00:00:00 2001 From: Dave Longley Date: Sun, 31 Jul 2022 12:07:28 -0400 Subject: [PATCH 5/9] Use `@noble/ed25519` to provide `X25519` implementation. --- lib/algorithms/x25519-helper-browser.js | 9 +++++---- package.json | 5 +++-- test/KaK.js | 3 ++- 3 files changed, 10 insertions(+), 7 deletions(-) diff --git a/lib/algorithms/x25519-helper-browser.js b/lib/algorithms/x25519-helper-browser.js index a169952..fb9d6a1 100644 --- a/lib/algorithms/x25519-helper-browser.js +++ b/lib/algorithms/x25519-helper-browser.js @@ -2,12 +2,13 @@ * Copyright (c) 2019-2022 Digital Bazaar, Inc. All rights reserved. */ import * as base64url from 'base64url-universal'; -import nacl from 'tweetnacl'; +import crypto from '../crypto.js'; +import {curve25519} from '@noble/ed25519'; export async function generateEphemeralKeyPair() { // generate X25519 ephemeral public key - const keyPair = nacl.box.keyPair(); - const {secretKey: privateKey, publicKey} = keyPair; + const privateKey = await crypto.getRandomValues(new Uint8Array(32)); + const publicKey = curve25519.scalarMultBase(privateKey); return { privateKey, publicKey, @@ -21,5 +22,5 @@ export async function generateEphemeralKeyPair() { export async function deriveSecret({privateKey, remotePublicKey}) { // `scalarMult` takes secret key as param 1, public key as param 2 - return nacl.scalarMult(privateKey, remotePublicKey); + return curve25519.scalarMult(privateKey, remotePublicKey); } diff --git a/package.json b/package.json index 795bde5..39b472c 100644 --- a/package.json +++ b/package.json @@ -23,11 +23,11 @@ "lint": "eslint ." }, "dependencies": { + "@noble/ed25519": "^1.6.1", "@stablelib/chacha": "^1.0.1", "@stablelib/chacha20poly1305": "^1.0.1", "base58-universal": "^2.0.0", - "base64url-universal": "^2.0.0", - "tweetnacl": "^1.0.3" + "base64url-universal": "^2.0.0" }, "devDependencies": { "@digitalbazaar/did-io": "^2.0.0", @@ -50,6 +50,7 @@ "karma-webpack": "^5.0.0", "mocha": "^10.0.0", "mocha-lcov-reporter": "^1.3.0", + "tweetnacl": "^1.0.3", "web-streams-polyfill": "^3.2.1", "webpack": "^5.73.0" }, diff --git a/test/KaK.js b/test/KaK.js index e533e9a..d58510b 100644 --- a/test/KaK.js +++ b/test/KaK.js @@ -1,5 +1,5 @@ /*! - * Copyright (c) 2019-2020 Digital Bazaar, Inc. All rights reserved. + * Copyright (c) 2019-2022 Digital Bazaar, Inc. All rights reserved. */ import * as base58 from 'base58-universal'; import nacl from 'tweetnacl'; @@ -16,6 +16,7 @@ export class KaK { this.id = id; this.type = 'X25519KeyAgreementKey2020'; if(!keyPair) { + // use `tweetnacl` lib to cross-compare X25519 implementations keyPair = nacl.box.keyPair(); this.privateKey = keyPair.secretKey; this.publicKey = keyPair.publicKey; From 4b13e202882ebbf35c14dbe830ff41550225e5c1 Mon Sep 17 00:00:00 2001 From: Dave Longley Date: Sun, 31 Jul 2022 12:11:52 -0400 Subject: [PATCH 6/9] Update changelog. --- CHANGELOG.md | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index f245136..736b65e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,13 @@ # minimal-cipher ChangeLog +## 5.2.0 - 2022-xx-xx + +### Changed +- Use `@noble/ed25519` to provide X25519 implementation. This lib + is often used in other libs that are combined with this one and + it has been through a comprehensive security audit. Additional + benefits include speed and tree-shaking capabilities. + ## 5.1.0 - 2022-07-31 ### Added From 563c8c6931b5c7bd2d3b958a3d25121ad23d67f3 Mon Sep 17 00:00:00 2001 From: Dave Longley Date: Sun, 14 Aug 2022 15:28:58 -0400 Subject: [PATCH 7/9] Fix chacha bug. --- lib/algorithms/xc20p.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/lib/algorithms/xc20p.js b/lib/algorithms/xc20p.js index 299c4ef..de38451 100644 --- a/lib/algorithms/xc20p.js +++ b/lib/algorithms/xc20p.js @@ -159,11 +159,11 @@ async function _hchacha20({key, nonce}) { const dvOut = new DataView(out.buffer, out.byteOffset, out.length); const dvDst = new DataView(dst.buffer, dst.byteOffset, dst.length); for(let i = 0; i < 4; ++i) { - dvOut.setUint32(i * 4, (state[i] - dvDst.getUint32(i * 4, LE)) | 0); + dvOut.setUint32(i * 4, (dvDst.getUint32(i * 4, LE) - state[i]) | 0, LE); } for(let i = 0; i < 4; ++i) { dvOut.setUint32( - i * 4 + 16, (state[i + 12] - dvDst.getUint32(i * 4 + 48, LE)) | 0); + i * 4 + 16, (dvDst.getUint32(i * 4 + 48, LE) - state[i + 12]) | 0, LE); } return out; From d27d8974d86d485074b41dbfab3403e33293da0f Mon Sep 17 00:00:00 2001 From: Dave Longley Date: Sun, 14 Aug 2022 15:37:19 -0400 Subject: [PATCH 8/9] Update changelog. --- CHANGELOG.md | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 736b65e..f54df91 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,6 +8,11 @@ it has been through a comprehensive security audit. Additional benefits include speed and tree-shaking capabilities. +## 5.1.1 - 2022-08-14 + +### Fixed +- Fix chacha bug. + ## 5.1.0 - 2022-07-31 ### Added From f8086ebdd7082ead19497d2712802abb6a8d7eb7 Mon Sep 17 00:00:00 2001 From: Samuel Hellawell Date: Tue, 21 Feb 2023 06:49:31 +0000 Subject: [PATCH 9/9] ECDH-1PU+A256KW initial implementation Signed-off-by: Samuel Hellawell --- lib/Cipher.js | 75 ++++++++++++++++++++++++------------- lib/DecryptTransformer.js | 19 ++++++++-- lib/algorithms/ecdhkdf.js | 43 ++++++++++++--------- lib/algorithms/x25519.js | 79 ++++++++++++++++++++++++++++++++++----- test/KaK.js | 17 ++++++++- test/unit/index.js | 42 +++++++++++++++++++++ 6 files changed, 217 insertions(+), 58 deletions(-) diff --git a/lib/Cipher.js b/lib/Cipher.js index 3d7036d..300e3a8 100644 --- a/lib/Cipher.js +++ b/lib/Cipher.js @@ -84,12 +84,13 @@ export class Cipher { * @param {object} options - Options for createDecryptStream. * @param {object} options.keyAgreementKey - A key agreement key API with * `id` and deriveSecret`. - * + * @param {Function} options.keyResolver - A function that returns a Promise + * that resolves a key ID to a DH public key. * @returns {Promise} Resolves to the TransformStream. */ - async createDecryptStream({keyAgreementKey}) { + async createDecryptStream({keyAgreementKey, keyResolver}) { const transformer = await this.createDecryptTransformer( - {keyAgreementKey}); + {keyAgreementKey, keyResolver}); // eslint-disable-next-line no-undef return new TransformStream(transformer); } @@ -110,10 +111,11 @@ export class Cipher { * encrypted content. * @param {Function} options.keyResolver - A function that returns a Promise * that resolves a key ID to a DH public key. - * + * @param {object} options.keyAgreementKey - A key agreement key API with + * `id` and `deriveSecret`. * @returns {Promise} Resolves to a JWE. */ - async encrypt({data, recipients, keyResolver}) { + async encrypt({data, recipients, keyResolver, keyAgreementKey}) { if(!(data instanceof Uint8Array) && typeof data !== 'string') { throw new TypeError('"data" must be a Uint8Array or a string.'); } @@ -121,7 +123,7 @@ export class Cipher { data = stringToUint8Array(data); } const transformer = await this.createEncryptTransformer( - {recipients, keyResolver}); + {recipients, keyResolver, keyAgreementKey}); return transformer.encrypt(data); } @@ -159,13 +161,14 @@ export class Cipher { * @param {object} options.jwe - The JWE to decrypt. * @param {object} options.keyAgreementKey - A key agreement key API with * `id` and `deriveSecret`. - * + * @param {Function} options.keyResolver - A function that returns a Promise + * that resolves a key ID to a DH public key. * @returns {Promise} - Resolves to the decrypted data * or `null` if the decryption failed. */ - async decrypt({jwe, keyAgreementKey}) { + async decrypt({jwe, keyAgreementKey, keyResolver}) { const transformer = await this.createDecryptTransformer( - {keyAgreementKey}); + {keyAgreementKey, keyResolver}); return transformer.decrypt(jwe); } @@ -177,12 +180,13 @@ export class Cipher { * @param {object} options.jwe - The JWE to decrypt. * @param {object} options.keyAgreementKey - A key agreement key API with * `id` and `deriveSecret`. - * + * @param {Function} options.keyResolver - A function that returns a Promise + * that resolves a key ID to a DH public key. * @returns {Promise} - Resolves to the decrypted object or `null` * if the decryption failed. */ - async decryptObject({jwe, keyAgreementKey}) { - const data = await this.decrypt({jwe, keyAgreementKey}); + async decryptObject({jwe, keyAgreementKey, keyResolver}) { + const data = await this.decrypt({jwe, keyAgreementKey, keyResolver}); if(!data) { // decryption failed return null; @@ -208,18 +212,25 @@ export class Cipher { * @param {number} [options.chunkSize=1048576] - The size, in bytes, of the * chunks to break the incoming data into (only applies if returning a * stream). + * @param {object} options.keyAgreementKey - A key agreement key API with + * `id` and `deriveSecret`. * * @returns {Promise} - Resolves to an EncryptTransformer. */ - async createEncryptTransformer({recipients, keyResolver, chunkSize}) { + async createEncryptTransformer({ + recipients, keyResolver, chunkSize, keyAgreementKey + }) { if(!(Array.isArray(recipients) && recipients.length > 0)) { throw new TypeError('"recipients" must be a non-empty array.'); } // ensure all recipients use the supported key agreement algorithm const {keyAgreement} = this; - const {JWE_ALG: alg} = keyAgreement; - if(!recipients.every(e => e.header && e.header.alg === alg)) { - throw new Error(`All recipients must use the algorithm "${alg}".`); + const {JWE_ALG: alg, JWE_ALG_SENDER_AUTH: algSenderAuth} = keyAgreement; + if(!recipients.every(e => e.header && + (e.header.alg === alg || e.header.alg === algSenderAuth))) { + throw new Error( + `All recipients must use the algorithm "${alg}" or "${algSenderAuth}".` + ); } const {cipher} = this; @@ -231,7 +242,7 @@ export class Cipher { recipients = await Promise.all(recipients.map( recipient => this._createRecipient( - {recipient, cek, ephemeralKeyPair, keyResolver}))); + {recipient, cek, ephemeralKeyPair, keyResolver, keyAgreementKey}))); // create shared protected header as associated authenticated data (aad) // ASCII(BASE64URL(UTF8(JWE Protected Header))) @@ -248,7 +259,8 @@ export class Cipher { cipher, additionalData, cek, - chunkSize + chunkSize, + keyAgreementKey }); } @@ -258,13 +270,15 @@ export class Cipher { * @param {object} options - Options to use. * @param {object} options.keyAgreementKey - A key agreement key API with * `id` and `deriveSecret`. - * + * @param {Function} options.keyResolver - A function that returns a Promise + * that resolves a key ID to a DH public key. * @returns {Promise} - Resolves to a DecryptTransformer. */ - async createDecryptTransformer({keyAgreementKey}) { + async createDecryptTransformer({keyAgreementKey, keyResolver}) { return new DecryptTransformer({ keyAgreement: this.keyAgreement, - keyAgreementKey + keyAgreementKey, + keyResolver }); } @@ -278,11 +292,14 @@ export class Cipher { * kid and alg. * @param {object} options.ephemeralKeyPair - An ephemeral key pair. * @param {object} options.cek - A content encryption key. + * @param {object} options.keyAgreementKey - A key agreement key API with. * @param {Function} options.keyResolver - A function that can resolve keys. * * @returns {Promise} A JWE recipient object. */ - async _createRecipient({recipient, ephemeralKeyPair, cek, keyResolver}) { + async _createRecipient({ + recipient, ephemeralKeyPair, cek, keyResolver, keyAgreementKey + }) { if(!recipient) { throw new TypeError('"options.recipient" is required.'); } @@ -296,11 +313,13 @@ export class Cipher { throw new TypeError('"options.keyResolver" is required.'); } // resolve public DH key for recipient + const {alg} = recipient.header; const {keyAgreement} = this; const staticPublicKey = await keyResolver({id: recipient.header.kid}); // derive KEKs for each recipient - const derivedResult = await keyAgreement.kekFromStaticPeer( - {ephemeralKeyPair, staticPublicKey}); + const derivedResult = await keyAgreement.kekFromStaticPeer({ + ephemeralKeyPair, staticPublicKey, keyAgreementKey, alg + }); const {kek, epk, apu, apv} = derivedResult; const header = { // contains the key id - kid @@ -308,11 +327,17 @@ export class Cipher { ...recipient.header, // the ephemeralKeyPair epk, - // base64 encoded ephemeralKeyPair's publicKey + // base64 encoded ephemeralKeyPair's publicKey or sender key ID apu, // base64 encoded staticPublicKey's id apv, }; + + // If sender key id is provided, set skid property + if(keyAgreementKey) { + header.skid = keyAgreementKey.id; + } + return { ...recipient, header, diff --git a/lib/DecryptTransformer.js b/lib/DecryptTransformer.js index 34142f2..bb40119 100644 --- a/lib/DecryptTransformer.js +++ b/lib/DecryptTransformer.js @@ -2,6 +2,7 @@ * Copyright (c) 2019-2022 Digital Bazaar, Inc. All rights reserved. */ import * as base64url from 'base64url-universal'; + import * as fipsAlgorithm from './algorithms/fips.js'; import * as recAlgorithm from './algorithms/recommended.js'; import {stringToUint8Array} from './util.js'; @@ -18,11 +19,13 @@ const CIPHER_ALGORITHMS = { // only supported key algorithm const KEY_ALGORITHM = 'ECDH-ES+A256KW'; +const KEY_ALGORITHM_1PU = 'ECDH-1PU+A256KW'; export class DecryptTransformer { constructor({ keyAgreement, - keyAgreementKey + keyAgreementKey, + keyResolver, } = {}) { if(!keyAgreement) { throw new TypeError('"keyAgreement" is a required parameter.'); @@ -32,6 +35,7 @@ export class DecryptTransformer { } this.keyAgreement = keyAgreement; this.keyAgreementKey = keyAgreementKey; + this.keyResolver = keyResolver; } async transform(chunk, controller) { @@ -80,6 +84,10 @@ export class DecryptTransformer { } catch(e) { throw new Error('Invalid JWE "protected" header.'); } + // support older JWEs with alg set where enc would be + if(!header.enc && typeof header.alg === 'string') { + header = {enc: header.alg}; + } if(!(header.enc && typeof header.enc === 'string')) { throw new Error('Invalid JWE "enc" header.'); } @@ -107,10 +115,12 @@ export class DecryptTransformer { // calls which may even need to hit the network (e.g., Web KMS) // derive KEK and unwrap CEK - const {epk} = recipient.header; + const {epk, skid, alg} = recipient.header; const {keyAgreement} = this; + + // i think we can modify only this to work and get it right? const {kek} = await keyAgreement.kekFromEphemeralPeer( - {keyAgreementKey, epk}); + {keyAgreementKey, epk, skid, alg, keyResolver: this.keyResolver}); const cek = await kek.unwrapKey({wrappedKey}); if(!cek) { // failed to unwrap key @@ -132,5 +142,6 @@ export class DecryptTransformer { function _findRecipient(recipients, key) { return recipients.find( e => e.header && e.header.kid === key.id && - (!key.algorithm && e.header.alg === KEY_ALGORITHM)); + (!key.algorithm && + (e.header.alg === KEY_ALGORITHM_1PU || e.header.alg === KEY_ALGORITHM))); } diff --git a/lib/algorithms/ecdhkdf.js b/lib/algorithms/ecdhkdf.js index d9f958c..af05980 100644 --- a/lib/algorithms/ecdhkdf.js +++ b/lib/algorithms/ecdhkdf.js @@ -3,20 +3,6 @@ */ import crypto from '../crypto.js'; -// only supported algorithm -const KEY_ALGORITHM = 'ECDH-ES+A256KW'; - -// create static ALGORITHM_ID -const ALGORITHM_CONTENT = new TextEncoder().encode(KEY_ALGORITHM); -const ALGORITHM_ID = new Uint8Array(4 + ALGORITHM_CONTENT.length); -// write length of content as 32-bit big endian integer, then write content -const dv = new DataView( - ALGORITHM_ID.buffer, - ALGORITHM_ID.byteOffset, - ALGORITHM_ID.byteLength); -dv.setUint32(0, ALGORITHM_CONTENT.length); -ALGORITHM_ID.set(ALGORITHM_CONTENT, 4); - // RFC 7518 Section 4.6.2 specifies using SHA-256 for ECDH-ES KDF // https://tools.ietf.org/html/rfc7518#section-4.6.2 const HASH_ALGORITHM = {name: 'SHA-256'}; @@ -39,13 +25,22 @@ const KEY_LENGTH = 256; * @param {Uint8Array} options.consumerInfo - An array of application-specific * bytes describing the producer (aka the "decrypter" or * "receiver"/"recipient"). + * @param {string} options.alg - The algorithm name, such as ECDH-ES+256KW. * * @returns {Promise} - Resolves to the generated key. */ -export async function deriveKey({secret, producerInfo, consumerInfo}) { +export async function deriveKey({secret, producerInfo, consumerInfo, alg}) { if(!(secret instanceof Uint8Array && secret.length > 0)) { throw new TypeError('"secret" must be a non-empty Uint8Array.'); } + + // no extra info supplied, just hash the secret + if(!producerInfo && !consumerInfo) { + // hash input and return result as derived key + return new Uint8Array( + await crypto.subtle.digest(HASH_ALGORITHM, secret)); + } + if(!(producerInfo instanceof Uint8Array && producerInfo.length > 0)) { throw new TypeError('"producerInfo" must be a non-empty Uint8Array.'); } @@ -53,6 +48,17 @@ export async function deriveKey({secret, producerInfo, consumerInfo}) { throw new TypeError('"consumerInfo" must be a non-empty Uint8Array.'); } + // create algorithmID encoded buffer + const algorithmContent = new TextEncoder().encode(alg); + const algorithmID = new Uint8Array(4 + algorithmContent.length); + // write length of content as 32-bit big endian integer, then write content + const algoDV = new DataView( + algorithmID.buffer, + algorithmID.byteOffset, + algorithmID.byteLength); + algoDV.setUint32(0, algorithmContent.length); + algorithmID.set(algorithmContent, 4); + // the output of Concat KDF is hash(roundNumber || Z || OtherInfo) // where roundNumber is always 1 because the hash length is presumed to // ...match the key length, encoded as a big endian 32-bit integer @@ -63,16 +69,17 @@ export async function deriveKey({secret, producerInfo, consumerInfo}) { const input = new Uint8Array( 4 + // round number secret.length + // `Z` - ALGORITHM_ID.length + // AlgorithmID + algorithmID.length + // AlgorithmID 4 + producerInfo.length + // PartyUInfo 4 + consumerInfo.length + // PartyVInfo 4); // SuppPubInfo (key data length in bits) + let offset = 0; const dv = new DataView(input.buffer, input.byteOffset, input.byteLength); dv.setUint32(offset, 1); input.set(secret, offset += 4); - input.set(ALGORITHM_ID, offset += secret.length); - dv.setUint32(offset += ALGORITHM_ID.length, producerInfo.length); + input.set(algorithmID, offset += secret.length); + dv.setUint32(offset += algorithmID.length, producerInfo.length); input.set(producerInfo, offset += 4); dv.setUint32(offset += producerInfo.length, consumerInfo.length); input.set(consumerInfo, offset += 4); diff --git a/lib/algorithms/x25519.js b/lib/algorithms/x25519.js index 087d6be..d82aeb8 100644 --- a/lib/algorithms/x25519.js +++ b/lib/algorithms/x25519.js @@ -10,14 +10,20 @@ import {deriveSecret, generateEphemeralKeyPair} from './x25519-helper.js'; const KEY_TYPE = 'X25519KeyAgreementKey2020'; // multibase base58-btc header const MULTIBASE_BASE58BTC_HEADER = 'z'; -// multicodec X25519-pub header as varint +// multicodec x25519-pub header as varint export const MULTICODEC_X25519_PUB_HEADER = new Uint8Array([0xec, 0x01]); +// multicodec x25519-priv header as varint +export const MULTICODEC_X25519_PRIV_HEADER = new Uint8Array([0x82, 0x26]); export const JWE_ALG = 'ECDH-ES+A256KW'; +export const JWE_ALG_SENDER_AUTH = 'ECDH-1PU+A256KW'; export {generateEphemeralKeyPair, deriveSecret}; // Decryption case: get Kek from a private key agreement key and a // peer's public ephemeral DH key encoded as an `epk` -export async function kekFromEphemeralPeer({keyAgreementKey, epk}) { +export async function kekFromEphemeralPeer({ + keyAgreementKey, epk, skid, alg, keyResolver +}) { + const isSenderAuthAlg = alg === JWE_ALG_SENDER_AUTH; if(!(epk && typeof epk === 'object')) { throw new TypeError('"epk" must be an object.'); } @@ -27,6 +33,9 @@ export async function kekFromEphemeralPeer({keyAgreementKey, epk}) { if(epk.crv !== 'X25519') { throw new Error('"epk.crv" must be the string "X25519".'); } + if(isSenderAuthAlg && !keyResolver) { + throw new Error(`${alg} requires keyResolver argument for sender key.`); + } // decode public key material const publicKey = base64url.decode(epk.x); @@ -41,12 +50,35 @@ export async function kekFromEphemeralPeer({keyAgreementKey, epk}) { // https://tools.ietf.org/html/rfc7748#section-7 pose any issues? const encoder = new TextEncoder(); // "Party U Info" - const producerInfo = publicKey; + const producerInfo = isSenderAuthAlg && skid ? + encoder.encode(skid) : publicKey; // "Party V Info" const consumerInfo = encoder.encode(keyAgreementKey.id); - const secret = await keyAgreementKey.deriveSecret( - {publicKey: ephemeralPublicKey}); - const keyData = await deriveKey({secret, producerInfo, consumerInfo}); + + let secret; + if(isSenderAuthAlg) { + // resolve the sender did and convert to LD key for Web KMS + const senderKeyInfo = await keyResolver({id: skid}); + const senderPublicKey = { + type: KEY_TYPE, + publicKeyMultibase: senderKeyInfo.publicKeyMultibase + }; + + const Ze = await keyAgreementKey.deriveSecret( + {publicKey: ephemeralPublicKey}); + + const Zs = await keyAgreementKey.deriveSecret( + {publicKey: senderPublicKey}); + + const ZeHashed = await deriveKey({secret: Ze}); + const ZsHashed = await deriveKey({secret: Zs}); + secret = Buffer.concat([ZeHashed, ZsHashed]); + } else { + secret = await keyAgreementKey.deriveSecret( + {publicKey: ephemeralPublicKey}); + } + + const keyData = await deriveKey({alg, secret, producerInfo, consumerInfo}); return { kek: await createKek({keyData}) }; @@ -59,6 +91,8 @@ export async function kekFromEphemeralPeer({keyAgreementKey, epk}) { * @param {object} options - Options hashmap. * @param {object} options.ephemeralKeyPair - Ephemeral key pair. * @param {object} options.staticPublicKey - Static public key. + * @param {object} options.keyAgreementKey - A key agreement key API with. + * @param {string} options.alg - The key wrapping algorithm. * @typedef {{ * kek: (object), epk: *, apv: (*|string), apu: (*|string), ephemeralPublicKey * }} kekObject @@ -66,7 +100,9 @@ export async function kekFromEphemeralPeer({keyAgreementKey, epk}) { * @returns {Promise} - Resolves with kek object derived from static * peer. */ -export async function kekFromStaticPeer({ephemeralKeyPair, staticPublicKey}) { +export async function kekFromStaticPeer({ + ephemeralKeyPair, staticPublicKey, keyAgreementKey, alg +}) { if(!staticPublicKey) { throw new Error('"staticPublicKey" is required.'); } @@ -76,16 +112,39 @@ export async function kekFromStaticPeer({ephemeralKeyPair, staticPublicKey}) { throw new Error( `"staticPublicKey.type" must be "${KEY_TYPE}".`); } + + const isSenderAuthAlg = alg === JWE_ALG_SENDER_AUTH; + if(isSenderAuthAlg) { + if(!keyAgreementKey) { + throw new Error( + `${alg} requires keyAgreementKey for sender authentication` + ); + } + } + const remotePublicKey = multibaseDecode( MULTICODEC_X25519_PUB_HEADER, staticPublicKey.publicKeyMultibase); const encoder = new TextEncoder(); // "Party U Info" - const producerInfo = ephemeralKeyPair.publicKey; + const producerInfo = isSenderAuthAlg && keyAgreementKey ? + encoder.encode(keyAgreementKey.id) : ephemeralKeyPair.publicKey; // "Party V Info" const consumerInfo = encoder.encode(staticPublicKey.id); - const secret = await deriveSecret({privateKey, remotePublicKey}); - const keyData = await deriveKey({secret, producerInfo, consumerInfo}); + + let secret; + if(isSenderAuthAlg) { + const Ze = await deriveSecret({privateKey, remotePublicKey}); + const Zs = await keyAgreementKey.deriveSecret( + {publicKey: staticPublicKey}); + const ZeHashed = await deriveKey({secret: Ze}); + const ZsHashed = await deriveKey({secret: Zs}); + secret = Buffer.concat([ZeHashed, ZsHashed]); + } else { + secret = await deriveSecret({privateKey, remotePublicKey}); + } + + const keyData = await deriveKey({alg, secret, producerInfo, consumerInfo}); return { kek: await createKek({keyData}), epk: ephemeralKeyPair.epk, diff --git a/test/KaK.js b/test/KaK.js index d58510b..57c4b7f 100644 --- a/test/KaK.js +++ b/test/KaK.js @@ -7,7 +7,8 @@ import { deriveSecret as dhDeriveSecret, multibaseEncode, multibaseDecode, - MULTICODEC_X25519_PUB_HEADER + MULTICODEC_X25519_PUB_HEADER, + MULTICODEC_X25519_PRIV_HEADER } from '../lib/algorithms/x25519.js'; import {store} from './store.js'; @@ -27,6 +28,9 @@ export class KaK { this.publicKeyMultibase = multibaseEncode( MULTICODEC_X25519_PUB_HEADER, this.publicKey ); + this.privateKeyMultibase = multibaseEncode( + MULTICODEC_X25519_PRIV_HEADER, this.privateKey + ); store.set(id, this.publicKeyNode); } @@ -55,6 +59,17 @@ export class KaK { header: {kid: this.id, alg: 'ECDH-ES+A256KW'} }; } + /** + * Formats this Kak into a partially complete JOSE Header + * that can be used as a recipient of a JWE with sender auth. + * + * @returns {object} A partial JOSE header. + */ + get recipientSenderAuth() { + return { + header: {kid: this.id, alg: 'ECDH-1PU+A256KW'} + }; + } async deriveSecret({publicKey}) { const remotePublicKey = multibaseDecode( MULTICODEC_X25519_PUB_HEADER, publicKey.publicKeyMultibase); diff --git a/test/unit/index.js b/test/unit/index.js index 744d5b3..2c9b850 100644 --- a/test/unit/index.js +++ b/test/unit/index.js @@ -493,6 +493,48 @@ describe('minimal-cipher', function() { }); decryptResult2.should.eql(obj); }); + + it('should decrypt a simple object with sender key auth', + async function() { + const recipients = [testKaK.recipientSenderAuth]; + const obj = {simple: true}; + const jwe = await cipher.encryptObject( + {obj, recipients, keyResolver, keyAgreementKey: testKaK}); + jwe.should.be.a.JWE; + jwe.recipients.length.should.equal(1); + isRecipient({recipients: jwe.recipients, kak: testKaK}); + const result = await cipher.decryptObject( + {keyResolver, jwe, keyAgreementKey: testKaK}); + result.should.eql(obj); + }); + + it('should encrypt and decrypt an object using didKeyResolver ECDH-1PU', + async function() { + const key1 = new X25519KeyAgreementKey2020({...key1Data}); + const key2 = new X25519KeyAgreementKey2020({...key2Data}); + const recipients = [ + {header: {kid: key1.id, alg: 'ECDH-1PU+A256KW'}}, + {header: {kid: key2.id, alg: 'ECDH-1PU+A256KW'}} + ]; + const keyResolver2 = createKeyResolver(); + const obj = {simple: true}; + const result = await cipher.encryptObject({ + obj, recipients, keyResolver: keyResolver2, keyAgreementKey: key1 + }); + result.should.be.a.JWE; + + // decrypt using key1 + const decryptResult1 = await cipher.decryptObject({ + jwe: result, keyAgreementKey: key1, keyResolver: keyResolver2 + }); + decryptResult1.should.eql(obj); + + // decrypt using key2 + const decryptResult2 = await cipher.decryptObject({ + jwe: result, keyAgreementKey: key2, keyResolver: keyResolver2 + }); + decryptResult2.should.eql(obj); + }); }); }); });