Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 13 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,18 @@
# 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.1 - 2022-08-14

### Fixed
- Fix chacha bug.

## 5.1.0 - 2022-07-31

### Added
Expand Down
3 changes: 2 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
@@ -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

Expand Down
94 changes: 62 additions & 32 deletions lib/Cipher.js
Original file line number Diff line number Diff line change
@@ -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'];

Expand Down Expand Up @@ -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);
}

Expand All @@ -86,12 +84,14 @@ 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<TransformStream>} 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);
}

Expand All @@ -111,18 +111,19 @@ 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<object>} 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.');
}
if(data) {
data = stringToUint8Array(data);
}
const transformer = await this.createEncryptTransformer(
{recipients, keyResolver});
{recipients, keyResolver, keyAgreementKey});
return transformer.encrypt(data);
}

Expand Down Expand Up @@ -160,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<Uint8Array>} - 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);
}

Expand All @@ -178,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<object>} - 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;
Expand All @@ -209,30 +212,37 @@ 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<EncryptTransformer>} - 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;

// generate a CEK for encrypting the content
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(
{recipient, cek, ephemeralKeyPair, keyResolver})));
{recipient, cek, ephemeralKeyPair, keyResolver, keyAgreementKey})));

// create shared protected header as associated authenticated data (aad)
// ASCII(BASE64URL(UTF8(JWE Protected Header)))
Expand All @@ -249,7 +259,8 @@ export class Cipher {
cipher,
additionalData,
cek,
chunkSize
chunkSize,
keyAgreementKey
});
}

Expand All @@ -259,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<DecryptTransformer>} - Resolves to a DecryptTransformer.
*/
async createDecryptTransformer({keyAgreementKey}) {
async createDecryptTransformer({keyAgreementKey, keyResolver}) {
return new DecryptTransformer({
keyAgreement: this.keyAgreement,
keyAgreementKey
keyAgreementKey,
keyResolver
});
}

Expand All @@ -279,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<object>} 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.');
}
Expand All @@ -297,23 +313,31 @@ 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
// contains the algorithm - alg
...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,
Expand All @@ -322,3 +346,9 @@ export class Cipher {
};
}
}

/**
* See: https://streams.spec.whatwg.org/#ts-model .
*
* @typedef TransformStream
*/
19 changes: 15 additions & 4 deletions lib/DecryptTransformer.js
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand All @@ -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.');
Expand All @@ -32,6 +35,7 @@ export class DecryptTransformer {
}
this.keyAgreement = keyAgreement;
this.keyAgreementKey = keyAgreementKey;
this.keyResolver = keyResolver;
}

async transform(chunk, controller) {
Expand Down Expand Up @@ -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.');
}
Expand Down Expand Up @@ -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
Expand All @@ -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)));
}
Loading