UNPKG

@dcxp/root

Version:

DCX: Decentralized Credential Exchange. DWN protocol for verifiable credential exchange.

398 lines (334 loc) 14.1 kB
import { generateMnemonic, mnemonicToSeed, validateMnemonic } from '@scure/bip39'; import { wordlist } from '@scure/bip39/wordlists/english'; import { AgentCryptoApi, IdentityVault, IdentityVaultBackup, IdentityVaultBackupData, IdentityVaultParams, IdentityVaultStatus, LocalKeyManager, } from '@web5/agent'; import { Convert, KeyValueStore, MemoryStore } from '@web5/common'; import { Jwk } from '@web5/crypto'; import { BearerDid, DidDht, DidDhtCreateOptions } from '@web5/dids'; import { HDKey } from 'ed25519-keygen/hdkey'; import { CompactJwe } from './prototyping/crypto/jose/jwe-compact.js'; import { DeterministicKeyGenerator } from './prototyping/crypto/utils.js'; import { isEmptyString, isIdentityVaultBackup, isIdentityVaultStatus, isPortableDid, Logger } from './index.js'; export type DcxIdentityVaultInitializeParams = { /** * The password used to secure the vault. * * The password selected should be strong and securely managed to prevent unauthorized access. */ password: string; /** * An optional recovery phrase used to derive the cryptographic keys for the vault. * * Providing a recovery phrase can be used to recover the vault's content or establish a * deterministic key generation scheme. If not provided, a new recovery phrase will be generated * during the initialization process. */ recoveryPhrase?: string; /** * An array of Decentralized Web Node (DWN) endpoints associated with the DID. * * The DWN endpoints are used to resolve the DID and interact with the DID Document. */ dwnEndpoints: string[]; }; export class DcxIdentityVault implements IdentityVault<{ InitializeResult: string }> { /** Provides cryptographic functions needed for secure storage and management of the vault. */ public crypto = new AgentCryptoApi(); /** Determines the computational intensity of the key derivation process. */ private _keyDerivationWorkFactor: number; /** The underlying key-value store for the vault's encrypted content. */ private _store: KeyValueStore<string, string>; /** The cryptographic key used to encrypt and decrypt the vault's content securely. */ private _contentEncryptionKey: Jwk | undefined; constructor({ keyDerivationWorkFactor, store }: IdentityVaultParams = {}) { this._keyDerivationWorkFactor = keyDerivationWorkFactor ?? 210_000; this._store = store ?? new MemoryStore<string, string>(); } public static create(): DcxIdentityVault { return new DcxIdentityVault(); } public changePassword(params: { oldPassword: string; newPassword: string; }): Promise<void> { Logger.debug('DcxIdentityVault: Changing password...', params); throw new Error('Method not implemented.'); } public async getStatus(): Promise<IdentityVaultStatus> { const storedStatus = await this._store.get('vaultStatus'); if (!storedStatus) { return { initialized : false, lastBackup : null, lastRestore : null, }; } const vaultStatus = Convert.string(storedStatus).toObject(); if (!isIdentityVaultStatus(vaultStatus)) { throw new Error('DcxIdentityVault: Invalid IdentityVaultStatus object in store'); } return vaultStatus; } private async getStoredDid(): Promise<string> { const didJwe = await this._store.get('did'); if (!didJwe) { throw new Error( 'DcxIdentityVault: Unable to retrieve the DID record from the vault. Please check the ' + 'vault status and if the problem persists consider re-initializing the vault and ' + 'restoring the contents from a previous backup.', ); } return didJwe; } public async getDid(): Promise<BearerDid> { if (this.isLocked()) { throw new Error(`DcxIdentityVault: Vault has not been initialized and unlocked.`); } const didJwe = await this.getStoredDid(); const { plaintext: portableDidBytes } = await CompactJwe.decrypt({ jwe : didJwe, key : this._contentEncryptionKey!, crypto : this.crypto, keyManager : new LocalKeyManager(), }); const portableDid = Convert.uint8Array(portableDidBytes).toObject(); if (!isPortableDid(portableDid)) { throw new Error('DcxIdentityVault: Unable to decode malformed DID in identity vault'); } return await BearerDid.import({ portableDid }); } public async isInitialized(): Promise<boolean> { return this.getStatus().then(({ initialized }) => initialized); } public isLocked(): boolean { return !this._contentEncryptionKey; } public async lock(): Promise<void> { if ((await this.isInitialized()) === false) { throw new Error(`DcxIdentityVault: Lock operation failed. Vault has not been initialized.`); } if (this._contentEncryptionKey) this._contentEncryptionKey.k = ''; this._contentEncryptionKey = undefined; } public async restore({ backup, password, }: { backup: IdentityVaultBackup; password: string; }): Promise<void> { // Validate the backup object. if (!isIdentityVaultBackup(backup)) { throw new Error(`DcxIdentityVault: Restore operation failed due to invalid backup object.`); } // Temporarily save the status and contents of the data store while attempting to restore the // backup so that they are not lost in case the restore operation fails. let previousStatus: IdentityVaultStatus; let previousContentEncryptionKey: string; let previousDid: string; try { previousDid = await this.getStoredDid(); previousContentEncryptionKey = await this.getStoredContentEncryptionKey(); previousStatus = await this.getStatus(); } catch { throw new Error( 'DcxIdentityVault: The restore operation cannot proceed because the existing vault ' + 'contents are missing or inaccessible. If the problem persists consider re-initializing ' + 'the vault and retrying the restore.', ); } try { // Convert the backup data to a JSON object. const backupData = Convert.base64Url(backup.data).toObject() as IdentityVaultBackupData; // Restore the backup to the data store. await this._store.set('did', backupData.did); await this._store.set('contentEncryptionKey', backupData.contentEncryptionKey); await this.setStatus(backupData.status); // Attempt to unlock the vault with the given `password`. await this.unlock({ password }); } catch (error: any) { // If the restore operation fails, revert the data store to the status and contents that were // saved before the restore operation was attempted. await this.setStatus(previousStatus); await this._store.set('contentEncryptionKey', previousContentEncryptionKey); await this._store.set('did', previousDid); throw new Error( 'DcxIdentityVault: Restore operation failed due to invalid backup data or an incorrect ' + 'password. Please verify the password is correct for the provided backup and try again.', ); } // Update the last restore timestamp in the data store. await this.setStatus({ lastRestore: new Date().toISOString() }); } public async unlock({ password }: { password: string }): Promise<void> { // Lock the vault. await this.lock(); // Retrieve the content encryption key (CEK) record as a compact JWE from the data store. const cekJwe = await this.getStoredContentEncryptionKey(); // Decrypt the compact JWE. try { const { plaintext: contentEncryptionKeyBytes } = await CompactJwe.decrypt({ jwe : cekJwe, key : Convert.string(password).toUint8Array(), crypto : this.crypto, keyManager : new LocalKeyManager(), }); const contentEncryptionKey = Convert.uint8Array(contentEncryptionKeyBytes).toObject() as Jwk; // Save the content encryption key in memory, thereby unlocking the vault. this._contentEncryptionKey = contentEncryptionKey; } catch (error: any) { throw new Error(`DcxIdentityVault: Unable to unlock the vault due to an incorrect password.`); } } async backup(): Promise<IdentityVaultBackup> { throw new Error('Method not implemented.'); } public async initialize({ password, recoveryPhrase, dwnEndpoints, }: DcxIdentityVaultInitializeParams): Promise<string> { if (await this.isInitialized()) { throw new Error(`DcxIdentityVault: Vault has already been initialized.`); } if (isEmptyString(password)) { throw new Error( `DcxIdentityVault: The password is required and cannot be blank. Please provide a ' + 'valid, non-empty password.`, ); } if (recoveryPhrase && isEmptyString(recoveryPhrase)) { throw new Error( `DcxIdentityVault: The password is required and cannot be blank. Please provide a ' + 'valid, non-empty password.`, ); } recoveryPhrase ??= generateMnemonic(wordlist, 128); if (!validateMnemonic(recoveryPhrase, wordlist)) { throw new Error( 'DcxIdentityVault: The provided recovery phrase is invalid. Please ensure that the ' + 'recovery phrase is a correctly formatted series of 12 words.', ); } const rootSeed = await mnemonicToSeed(recoveryPhrase); const rootHdKey = HDKey.fromMasterSeed(rootSeed); const vaultHdKey = rootHdKey.derive(`m/44'/0'/0'/0'/0'`); const contentEncryptionKey = await this.crypto.deriveKey({ algorithm : 'HKDF-512', // key derivation function baseKeyBytes : vaultHdKey.privateKey, // input keying material salt : '', // empty salt because private key is sufficiently random info : 'vault_cek', // non-secret application specific information derivedKeyAlgorithm : 'A256GCM', // derived key algorithm }); const saltInput = await this.crypto.deriveKeyBytes({ algorithm : 'HKDF-512', // key derivation function baseKeyBytes : vaultHdKey.publicKey, // input keying material salt : '', // empty salt because public key is sufficiently random info : 'vault_unlock_salt', // non-secret application specific information length : 256, // derived key length, in bits }); const cekJwe = await CompactJwe.encrypt({ key : Convert.string(password).toUint8Array(), protectedHeader : { alg : 'PBES2-HS512+A256KW', enc : 'A256GCM', cty : 'text/plain', p2c : this._keyDerivationWorkFactor, p2s : Convert.uint8Array(saltInput).toBase64Url(), }, plaintext : Convert.object(contentEncryptionKey).toUint8Array(), crypto : this.crypto, keyManager : new LocalKeyManager(), }); await this._store.set('contentEncryptionKey', cekJwe); const identityHdKey = rootHdKey.derive(`m/44'/0'/1708523827'/0'/0'`); const identityPrivateKey = await this.crypto.bytesToPrivateKey({ algorithm : 'Ed25519', privateKeyBytes : identityHdKey.privateKey, }); const signingHdKey = rootHdKey.derive(`m/44'/0'/1708523827'/0'/1'`); const signingPrivateKey = await this.crypto.bytesToPrivateKey({ algorithm : 'Ed25519', privateKeyBytes : signingHdKey.privateKey, }); const deterministicKeyGenerator = new DeterministicKeyGenerator(); await deterministicKeyGenerator.addPredefinedKeys({ privateKeys: [identityPrivateKey, signingPrivateKey], }); const options = { verificationMethods: [ { algorithm : 'Ed25519', id : 'sig', purposes : ['assertionMethod', 'authentication'] }, ] } as DidDhtCreateOptions<DeterministicKeyGenerator>; if(dwnEndpoints && !!dwnEndpoints.length) { options.services = [ { id : 'dwn', type : 'DecentralizedWebNode', serviceEndpoint : dwnEndpoints, enc : '#enc', sig : '#sig', } ]; } const did = await DidDht.create({ keyManager: deterministicKeyGenerator, options }); const portableDid = await did.export(); const didJwe = await CompactJwe.encrypt({ key : contentEncryptionKey, plaintext : Convert.object(portableDid).toUint8Array(), protectedHeader : { alg : 'dir', enc : 'A256GCM', cty : 'json', }, crypto : this.crypto, keyManager : new LocalKeyManager(), }); // Store the compact JWE in the data store. await this._store.set('did', didJwe); this._contentEncryptionKey = contentEncryptionKey; await this.setStatus({ initialized: true }); return recoveryPhrase; } private async setStatus({ initialized, lastBackup, lastRestore, }: Partial<IdentityVaultStatus>): Promise<boolean> { // Get the current status values from the store, if any. const vaultStatus = await this.getStatus(); // Update the status properties with new values specified, if any. vaultStatus.initialized = initialized ?? vaultStatus.initialized; vaultStatus.lastBackup = lastBackup ?? vaultStatus.lastBackup; vaultStatus.lastRestore = lastRestore ?? vaultStatus.lastRestore; // Write the changes to the store. await this._store.set('vaultStatus', JSON.stringify(vaultStatus)); return true; } private async getStoredContentEncryptionKey(): Promise<string> { // Retrieve the content encryption key (CEK) record as a compact JWE from the data store. const cekJwe = await this._store.get('contentEncryptionKey'); if (!cekJwe) { throw new Error( 'DcxIdentityVault: Unable to retrieve the Content Encryption Key record from the vault. ' + 'Please check the vault status and if the problem persists consider re-initializing the ' + 'vault and restoring the contents from a previous backup.', ); } return cekJwe; } }