@digitalcredentials/minimal-cipher
Version:
Minimal encryption/decryption JWE library.
85 lines (77 loc) • 3.48 kB
JavaScript
/*!
* Copyright (c) 2019-2020 Digital Bazaar, Inc. All rights reserved.
*/
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'};
// derived keys are always 256-bits
const KEY_LENGTH = 256;
/**
* Derives a 256-bit AES-KW key encryption key from a shared secret that
* was derived from an ephemeral and static pair
* of Elliptic Curve Diffie-Hellman keys.
*
* The KDF used is described in RFC 7518. This KDF is referenced by RFC 8037,
* which defines how to perform Curve25519 (X25519) ECDH key agreement.
*
* @param {object} options - The options to use.
* @param {Uint8Array} options.secret - The shared secret (i.e., `Z`) to use.
* @param {Uint8Array} options.producerInfo - An array of application-specific
* bytes describing the consumer (aka the "encrypter" or "sender").
* @param {Uint8Array} options.consumerInfo - An array of application-specific
* bytes describing the producer (aka the "decrypter" or
* "receiver"/"recipient").
*
* @returns {Promise<Uint8Array>} - Resolves to the generated key.
*/
export async function deriveKey({secret, producerInfo, consumerInfo}) {
if(!(secret instanceof Uint8Array && secret.length > 0)) {
throw new TypeError('"secret" must be a non-empty Uint8Array.');
}
if(!(producerInfo instanceof Uint8Array && producerInfo.length > 0)) {
throw new TypeError('"producerInfo" must be a non-empty Uint8Array.');
}
if(!(consumerInfo instanceof Uint8Array && consumerInfo.length > 0)) {
throw new TypeError('"consumerInfo" must be a non-empty Uint8Array.');
}
// 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
// where OtherInfo is:
// AlgorithmID || PartyUInfo || PartyVInfo || SuppPubInfo
// where SuppPubInfo is the key length in bits, big endian encoded as a
// 32-bit number, i.e., 256 === [0, 0, 1, 0]
const input = new Uint8Array(
4 + // round number
secret.length + // `Z`
ALGORITHM_ID.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(producerInfo, offset += 4);
dv.setUint32(offset += producerInfo.length, consumerInfo.length);
input.set(consumerInfo, offset += 4);
dv.setUint32(offset += consumerInfo.length, KEY_LENGTH);
// hash input and return result as derived key
return new Uint8Array(
await crypto.subtle.digest(HASH_ALGORITHM, input));
}