UNPKG

@bedrock/web-pouch-edv

Version:
280 lines (247 loc) 8.44 kB
/*! * Copyright (c) 2021-2023 Digital Bazaar, Inc. All rights reserved. */ import * as EcdsaMultikey from '@digitalbazaar/ecdsa-multikey'; import {multihashDecode, multihashEncode, uuid} from './helpers.js'; import {assert} from './assert.js'; import {ConfigStorage} from './ConfigStorage.js'; import {deriveBits} from './pbkdf2.js'; import {Hmac} from './Hmac.js'; import {Kek} from './Kek.js'; import {P256Kak} from './P256Kak.js'; import {X25519Kak} from './X25519Kak.js'; // P-256 unwrapped key is 32 secret key bytes + 33 public key bytes + 7 padding const UNWRAPPED_KAK_SIZE = 72; // wrapped KAK + 8 byte AES-KW overhead const WRAPPED_KAK_SIZE = 80; const VERSIONS = new Map([ ['1', { // parameters for version 1 iterations: 100000, // AES-KW is used on a 32 byte key w/8 byte overhead wrappedKeySize: 40, // salt size in bytes saltSize: 16 }] ]); let _storage; /** * Initializes the encrypted secrets database. * * @returns {Promise} Settles once the operation completes. */ export async function initialize() { if(_storage) { // already initialized return; } _storage = new ConfigStorage({ assertConfig: assert.secretConfig, collectionName: 'edv-storage-secret' }); await _storage.initialize(); // queries by hmac ID await _storage.client.createIndex({ index: { fields: ['config.hmacId'] } }); // queries by keyAgreementKeyId (key agreement key ID) await _storage.client.createIndex({ index: { fields: ['config.keyAgreementKeyId'] } }); } /** * Decrypts a secret so its derived keys can be used. * * @param {object} options - The options to use. * @param {object} options.config - The secret configuration. * @param {object} options.password - The password to use to decrypt. * * @returns {Promise<object>} Returns {hmac, keyAgreementKey, cipherVersion} on * success and `null` if the password was invalid. */ export async function decrypt({config, password} = {}) { assert.secretConfig(config); assert.string(password, 'password'); // use password and version parameters to derive key encryption const {secret} = config; const {version, salt, wrappedKey} = secret; const {wrappedKeySize, saltSize} = VERSIONS.get(version); const {kek} = await _deriveKek({ password, salt: await multihashDecode({ expectedSize: saltSize, encoded: salt }), version }); // unwrap key derivation key const unwrappedKey = await kek.unwrapKey({ wrappedKey: await multihashDecode({ expectedSize: wrappedKeySize, encoded: wrappedKey }) }); if(!unwrappedKey) { // invalid password return null; } // import KDK and clear secret value from memory const kdk = await Hmac.import({secret: unwrappedKey}); unwrappedKey.fill(0); // derive HMAC and key agreement keys const { hmac, keyAgreementKey, cipherVersion } = await _deriveKeys({kdk, kek, secret}); hmac.id = config.hmacId; keyAgreementKey.id = config.keyAgreementKeyId; return {hmac, keyAgreementKey, cipherVersion}; } /** * Generates a new encrypted secret configuration. * * @param {object} options - The options to use. * @param {string} options.id - The ID for the secret. * @param {string} options.password - The password to encrypt the secret. * @param {string} options.version - The secret version. * @param {string} [options.cipherVersion='recommended'] - Sets the cipher * version to either "recommended" or "fips". * * @returns {Promise<object>} Resolves to `{hmac, keyAgreementKey, config}`. */ export async function generate({ id, version = '1', password, cipherVersion = 'recommended' } = {}) { if(version !== '1') { throw new Error('"version" must be "1".'); } assert.string(id, 'id'); assert.string(password, 'password'); // generate an HMAC key for deriving other keys const kdk = await Hmac.generate(); // create encrypted secret from key derivation key const {kek, secret} = await _createSecret( {kdk, password, version, cipherVersion}); // derive blinded index key (HMAC) and key agreement key const {hmac, keyAgreementKey} = await _deriveKeys({kdk, kek, secret}); const config = { id, hmacId: `urn:uuid:${uuid()}`, keyAgreementKeyId: `urn:uuid:${uuid()}`, secret, sequence: 0 }; hmac.id = config.hmacId; keyAgreementKey.id = config.keyAgreementKeyId; return {hmac, keyAgreementKey, config}; } /** * Establishes a new secret by inserting its config into storage. * * @param {object} options - The options to use. * @param {object} options.config - The secret configuration. * * @returns {Promise<object>} Resolves to the database record. */ export async function insert({config} = {}) { return _storage.insert({config}); } /** * Updates a secret config if its sequence number is next. * * @param {object} options - The options to use. * @param {object} options.config - The secret configuration. * * @returns {Promise<object>} Resolves to the database record. */ export async function update({config} = {}) { return _storage.update({config}); } /** * Gets a secret configuration. * * @param {object} options - The options to use. * @param {object} options.id - The ID of the EDV. * * @returns {Promise<object>} Resolves to the database record. */ export async function get({id} = {}) { return _storage.get({id}); } async function _createSecret({kdk, password, version, cipherVersion} = {}) { // use password and version parameters to derive key encryption const {kek, algorithm} = await _deriveKek({password, version}); // wrap key derivation key for storage and reuse later const wrappedKey = await kek.wrapKey({unwrappedKey: kdk.key}); const secret = { version, salt: multihashEncode({data: algorithm.salt}), wrappedKey: multihashEncode({data: wrappedKey}) }; // if `fips` cipher version is used, generate random key agreement key // instead of deriving it from a secret; this is a requirement for creating // fips-compliant P-* curve keys if(cipherVersion === 'fips') { const kak = await EcdsaMultikey.generate({curve: 'P-256'}); // export key for wrapping (secret key + public key) const {secretKey, publicKey} = await kak.export( {publicKey: true, secretKey: true, raw: true}); const unwrappedKey = new Uint8Array(UNWRAPPED_KAK_SIZE); unwrappedKey.set(secretKey); unwrappedKey.set(publicKey, secretKey.length); const wrappedKeyAgreementKey = await kek.wrapKey({unwrappedKey}); secretKey.fill(0); unwrappedKey.fill(0); secret.wrappedKeyAgreementKey = multihashEncode( {data: wrappedKeyAgreementKey}); wrappedKeyAgreementKey.fill(0); } return {kek, secret}; } async function _deriveKek({password, salt, version} = {}) { const {iterations, saltSize} = VERSIONS.get(version); const { derivedBits: kekSecret, algorithm } = await deriveBits({ bitLength: 256, iterations, password, salt, saltSize }); const kek = await Kek.import({secret: kekSecret}); kekSecret.fill(0); return {kek, algorithm}; } async function _deriveKeys({kdk, kek, secret} = {}) { const encoder = new TextEncoder(); // generate secret and derive HMAC key const hmacSecret = await kdk.sign({data: encoder.encode('hmac')}); const hmac = await Hmac.import({secret: hmacSecret}); hmacSecret.fill(0); // unwrap or generate secret and derive key agreement key let keyAgreementKey; let cipherVersion; if(secret.wrappedKeyAgreementKey) { cipherVersion = 'fips'; // unwrap key derivation key (P-256 ) const unwrappedKey = await kek.unwrapKey({ wrappedKey: await multihashDecode({ expectedSize: WRAPPED_KAK_SIZE, encoded: secret.wrappedKeyAgreementKey }) }); if(!unwrappedKey) { // invalid wrapped key agreement key throw new Error('Invalid stored key agreement key.'); } const secretKey = unwrappedKey.subarray(0, 32); const publicKey = unwrappedKey.subarray(32, 65); keyAgreementKey = await P256Kak.import({secretKey, publicKey}); unwrappedKey.fill(0); } else { cipherVersion = 'recommended'; // generate secrets for an HMAC and a key agreement key const kakSecret = await kdk.sign({data: encoder.encode('keyAgreementKey')}); keyAgreementKey = await X25519Kak.import({secret: kakSecret}); kakSecret.fill(0); } return {hmac, keyAgreementKey, cipherVersion}; }