@enbox/agent 0.7.5 → 0.7.7

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -48,6 +48,30 @@ export type HdIdentityVaultInitializeParams = {
48
48
  dwnEndpoints?: string[];
49
49
  };
50
50
 
51
+ export type HdIdentityVaultResetPasswordWithRecoveryPhraseParams = {
52
+ /** The BIP-39 recovery phrase originally used to initialize the vault. */
53
+ recoveryPhrase: string;
54
+
55
+ /** The new password used to unlock the existing vault from this point forward. */
56
+ password: string;
57
+ };
58
+
59
+ type HdIdentityVaultDerivedMaterial = {
60
+ recoveryPhrase: string;
61
+ contentEncryptionKey: Jwk;
62
+ contentEncryptionKeyJwe: string;
63
+ portableDid: PortableDid;
64
+ };
65
+
66
+ export class HdIdentityVaultRecoveryPhraseMismatchError extends Error {
67
+ public readonly code = 'HD_IDENTITY_VAULT_RECOVERY_PHRASE_MISMATCH';
68
+
69
+ constructor(message = 'HdIdentityVault: Recovery phrase does not match the initialized vault.') {
70
+ super(message);
71
+ this.name = 'HdIdentityVaultRecoveryPhraseMismatchError';
72
+ }
73
+ }
74
+
51
75
  /**
52
76
  * Type guard function to check if a given object is an empty string or a string containing only
53
77
  * whitespace.
@@ -311,22 +335,7 @@ export class HdIdentityVault implements IdentityVault<{ InitializeResult: string
311
335
  // vault store. This avoids the expensive LevelDB read + JWE decrypt on
312
336
  // every call while the vault is unlocked.
313
337
  if (!this._cachedPortableDid) {
314
- const didJwe = await this.getStoredDid();
315
-
316
- const { plaintext: portableDidBytes } = await CompactJwe.decrypt({
317
- jwe : didJwe,
318
- key : this._contentEncryptionKey!,
319
- crypto : this.crypto,
320
- keyManager : new LocalKeyManager(),
321
- options : { minP2cCount: 1 }, // Vault decrypts its own JWEs; no external-input floor needed.
322
- });
323
-
324
- const portableDid = Convert.uint8Array(portableDidBytes).toObject();
325
- if (!isPortableDid(portableDid)) {
326
- throw new Error('HdIdentityVault: Unable to decode malformed DID in identity vault');
327
- }
328
-
329
- this._cachedPortableDid = portableDid;
338
+ this._cachedPortableDid = await this.decryptStoredPortableDid(this._contentEncryptionKey!);
330
339
  }
331
340
 
332
341
  // Always return a fresh BearerDid from a deep copy of the cached
@@ -398,217 +407,61 @@ export class HdIdentityVault implements IdentityVault<{ InitializeResult: string
398
407
  public async initialize({ password, recoveryPhrase, dwnEndpoints }:
399
408
  HdIdentityVaultInitializeParams
400
409
  ): Promise<string> {
401
- /**
402
- * STEP 0: Validate the input parameters and verify the identity vault is not already
403
- * initialized.
404
- */
405
-
406
410
  // Verify that the identity vault was not previously initialized.
407
411
  if (await this.isInitialized()) {
408
412
  throw new Error(`HdIdentityVault: Vault has already been initialized.`);
409
413
  }
410
414
 
411
- // Verify that the password is not empty.
412
- if (isEmptyString(password)) {
413
- throw new Error(
414
- `HdIdentityVault: The password is required and cannot be blank. Please provide a ' +
415
- 'valid, non-empty password.`
416
- );
417
- }
415
+ const derivedMaterial = await this.deriveVaultMaterial({ password, recoveryPhrase, dwnEndpoints });
418
416
 
419
- // If provided, verify that the recovery phrase is not empty.
420
- if (recoveryPhrase && isEmptyString(recoveryPhrase)) {
421
- throw new Error(
422
- `HdIdentityVault: The password is required and cannot be blank. Please provide a ' +
423
- 'valid, non-empty password.`
424
- );
425
- }
417
+ await this._store.set('contentEncryptionKey', derivedMaterial.contentEncryptionKeyJwe);
418
+ await this._store.set(
419
+ 'did',
420
+ await this.encryptPortableDid(derivedMaterial.portableDid, derivedMaterial.contentEncryptionKey)
421
+ );
426
422
 
427
- /**
428
- * STEP 1: Derive a Hierarchical Deterministic (HD) key pair from the given (or generated)
429
- * recoveryPhrase.
430
- */
423
+ this._contentEncryptionKey = derivedMaterial.contentEncryptionKey;
424
+ this._cachedPortableDid = derivedMaterial.portableDid;
425
+ await this.setStatus({ initialized: true });
431
426
 
432
- // Generate a 12-word (128-bit) mnemonic, if one was not provided.
433
- recoveryPhrase ??= generateMnemonic(wordlist, 128);
427
+ // Return the recovery phrase in case it was generated so that it can be displayed to the user
428
+ // for safekeeping.
429
+ return derivedMaterial.recoveryPhrase;
430
+ }
434
431
 
435
- // Validate the mnemonic for being 12-24 words contained in `wordlist`.
436
- if (!validateMnemonic(recoveryPhrase, wordlist)) {
432
+ /**
433
+ * Resets the vault password using the original recovery phrase.
434
+ *
435
+ * The recovery phrase must derive the same vault CEK and agent DID that are already stored in
436
+ * this vault. If it does not, no stored state is changed. On success, only the password-wrapped
437
+ * CEK is replaced; the encrypted DID and all local vault data are preserved.
438
+ */
439
+ public async resetPasswordWithRecoveryPhrase({
440
+ recoveryPhrase,
441
+ password,
442
+ }: HdIdentityVaultResetPasswordWithRecoveryPhraseParams): Promise<void> {
443
+ if (await this.isInitialized() === false) {
437
444
  throw new Error(
438
- 'HdIdentityVault: The provided recovery phrase is invalid. Please ensure that the ' +
439
- 'recovery phrase is a correctly formatted series of 12 words.'
445
+ 'HdIdentityVault: Unable to reset the vault password because the identity vault has not ' +
446
+ 'been initialized.'
440
447
  );
441
448
  }
442
449
 
443
- // Derive a root seed from the mnemonic.
444
- const rootSeed = await mnemonicToSeed(recoveryPhrase);
445
-
446
- // Derive a root key for the DID from the root seed.
447
- const rootHdKey = HDKey.fromMasterSeed(rootSeed);
448
-
449
- /**
450
- * STEP 2: Derive the vault HD key pair from the root key.
451
- */
452
-
453
- // The vault HD key is derived using account 0 and index 0 so that it can be
454
- // deterministically re-derived. The vault key pair serves as input keying material for:
455
- // - deriving the vault content encryption key (CEK)
456
- // - deriving the salt that serves as input to derive the key that encrypts the vault CEK
457
- const vaultHdKey = rootHdKey.derive(`m/44'/0'/0'/0'/0'`);
458
-
459
- /**
460
- * STEP 3: Derive the vault Content Encryption Key (CEK) from the vault private
461
- * key and a non-secret static info value.
462
- */
463
-
464
- // A non-secret static info value is combined with the vault private key as input to HKDF
465
- // (Hash-based Key Derivation Function) to derive a 32-byte content encryption key (CEK).
466
- const contentEncryptionKey = await this.crypto.deriveKey({
467
- algorithm : 'HKDF-512', // key derivation function
468
- baseKeyBytes : vaultHdKey.privateKey, // input keying material
469
- salt : '', // empty salt because private key is sufficiently random
470
- info : 'vault_cek', // non-secret application specific information
471
- derivedKeyAlgorithm : 'A256GCM' // derived key algorithm
472
- });
473
-
474
- /**
475
- * STEP 4: Using the given `password` and a `salt` derived from the vault public key, encrypt
476
- * the vault CEK and store it in the data store as a compact JWE.
477
- */
478
-
479
- // A non-secret static info value is combined with the vault public key as input to HKDF
480
- // (Hash-based Key Derivation Function) to derive a new 32-byte salt.
481
- const saltInput = await this.crypto.deriveKeyBytes({
482
- algorithm : 'HKDF-512', // key derivation function
483
- baseKeyBytes : vaultHdKey.publicKey, // input keying material
484
- salt : '', // empty salt because public key is sufficiently random
485
- info : 'vault_unlock_salt', // non-secret application specific information
486
- length : 256, // derived key length, in bits
487
- });
488
-
489
- // Construct the JWE header.
490
- const cekJweProtectedHeader: JweHeaderParams = {
491
- alg : 'PBES2-HS512+A256KW',
492
- enc : 'A256GCM',
493
- cty : 'text/plain',
494
- p2c : this._keyDerivationWorkFactor,
495
- p2s : Convert.uint8Array(saltInput).toBase64Url()
496
- };
497
-
498
- // Encrypt the vault content encryption key (CEK) to compact JWE format.
499
- const cekJwe = await CompactJwe.encrypt({
500
- key : Convert.string(password).toUint8Array(),
501
- protectedHeader : cekJweProtectedHeader,
502
- plaintext : Convert.object(contentEncryptionKey).toUint8Array(),
503
- crypto : this.crypto,
504
- keyManager : new LocalKeyManager()
505
- });
506
-
507
- // Store the compact JWE in the data store.
508
- await this._store.set('contentEncryptionKey', cekJwe);
509
-
510
- /**
511
- * STEP 5: Create a DID using identity, signing, and encryption keys derived from the root key.
512
- */
513
-
514
- // Derive the identity key pair using index 0 and convert to JWK format.
515
- // Note: The account is set to Unix epoch time so that in the future, the keys for a DID DHT
516
- // document can be deterministically derived based on the versionId returned in a DID
517
- // resolution result.
518
- const identityHdKey = rootHdKey.derive(`m/44'/0'/1708523827'/0'/0'`);
519
- const identityPrivateKey = await this.crypto.bytesToPrivateKey({
520
- algorithm : 'Ed25519',
521
- privateKeyBytes : identityHdKey.privateKey
522
- });
523
-
524
- // Derive the signing key using index 1 and convert to JWK format.
525
- const signingHdKey = rootHdKey.derive(`m/44'/0'/1708523827'/0'/1'`);
526
- const signingPrivateKey = await this.crypto.bytesToPrivateKey({
527
- algorithm : 'Ed25519',
528
- privateKeyBytes : signingHdKey.privateKey
529
- });
530
-
531
- // Derive the encryption key using index 2 (X25519 for ECDH-ES JWE encryption).
532
- const encryptionHdKey = rootHdKey.derive(`m/44'/0'/1708523827'/0'/2'`);
533
- const encryptionPrivateKey = await this.crypto.bytesToPrivateKey({
534
- algorithm : 'X25519',
535
- privateKeyBytes : encryptionHdKey.privateKey
536
- });
537
-
538
- // Add the identity, signing, and encryption keys to the deterministic key generator so that
539
- // when the DID is created it will use the derived keys.
540
- const deterministicKeyGenerator = new DeterministicKeyGenerator();
541
- await deterministicKeyGenerator.addPredefinedKeys({
542
- privateKeys: [identityPrivateKey, signingPrivateKey, encryptionPrivateKey]
543
- });
544
-
545
- // Create the DID using the derived identity, signing, and encryption keys.
546
- const options = {
547
- verificationMethods: [
548
- {
549
- algorithm : 'Ed25519',
550
- id : 'sig',
551
- purposes : ['assertionMethod', 'authentication']
552
- },
553
- {
554
- algorithm : 'X25519',
555
- id : 'enc',
556
- purposes : ['keyAgreement']
557
- }
558
- ]
559
- } as DidDhtCreateOptions<DeterministicKeyGenerator>;
560
-
561
- if (dwnEndpoints && !!dwnEndpoints.length) {
562
- options.services = [
563
- {
564
- id : 'dwn',
565
- type : 'DecentralizedWebNode',
566
- serviceEndpoint : dwnEndpoints,
567
- }
568
- ];
450
+ const derivedMaterial = await this.deriveVaultMaterial({ password, recoveryPhrase });
451
+ let storedPortableDid: PortableDid;
452
+ try {
453
+ storedPortableDid = await this.decryptStoredPortableDid(derivedMaterial.contentEncryptionKey);
454
+ } catch {
455
+ throw new HdIdentityVaultRecoveryPhraseMismatchError();
569
456
  }
570
457
 
571
- const did = await DidDht.create({ keyManager: deterministicKeyGenerator, options });
572
-
573
- /**
574
- * STEP 6: Convert the DID to portable format and store it in the data store as a
575
- * compact JWE.
576
- */
577
-
578
- // Convert the DID to a portable format.
579
- const portableDid = await did.export();
580
-
581
- // Construct the JWE header.
582
- const didJweProtectedHeader: JweHeaderParams = {
583
- alg : 'dir',
584
- enc : 'A256GCM',
585
- cty : 'json'
586
- };
587
-
588
- // Encrypt the DID to compact JWE format.
589
- const didJwe = await CompactJwe.encrypt({
590
- key : contentEncryptionKey,
591
- plaintext : Convert.object(portableDid).toUint8Array(),
592
- protectedHeader : didJweProtectedHeader,
593
- crypto : this.crypto,
594
- keyManager : new LocalKeyManager()
595
- });
596
-
597
- // Store the compact JWE in the data store.
598
- await this._store.set('did', didJwe);
599
-
600
- /**
601
- * STEP 7: Set the vault CEK (effectively unlocking the vault), set the status to initialized,
602
- * and return the mnemonic used to generate the vault key.
603
- */
604
-
605
- this._contentEncryptionKey = contentEncryptionKey;
606
-
607
- await this.setStatus({ initialized: true });
458
+ if (storedPortableDid.uri !== derivedMaterial.portableDid.uri) {
459
+ throw new HdIdentityVaultRecoveryPhraseMismatchError();
460
+ }
608
461
 
609
- // Return the recovery phrase in case it was generated so that it can be displayed to the user
610
- // for safekeeping.
611
- return recoveryPhrase;
462
+ await this._store.set('contentEncryptionKey', derivedMaterial.contentEncryptionKeyJwe);
463
+ this._contentEncryptionKey = derivedMaterial.contentEncryptionKey;
464
+ this._cachedPortableDid = storedPortableDid;
612
465
  }
613
466
 
614
467
  /**
@@ -868,6 +721,185 @@ export class HdIdentityVault implements IdentityVault<{ InitializeResult: string
868
721
  return plaintext;
869
722
  }
870
723
 
724
+ private async deriveVaultMaterial({
725
+ password,
726
+ recoveryPhrase,
727
+ dwnEndpoints,
728
+ }: HdIdentityVaultInitializeParams): Promise<HdIdentityVaultDerivedMaterial> {
729
+ this.validatePassword(password);
730
+ const resolvedRecoveryPhrase = this.resolveRecoveryPhrase(recoveryPhrase);
731
+
732
+ const rootSeed = await mnemonicToSeed(resolvedRecoveryPhrase);
733
+ const rootHdKey = HDKey.fromMasterSeed(rootSeed);
734
+
735
+ // The vault key is deterministic so the same phrase can re-derive both the CEK and unlock salt.
736
+ const vaultHdKey = rootHdKey.derive(`m/44'/0'/0'/0'/0'`);
737
+ const contentEncryptionKey = await this.crypto.deriveKey({
738
+ algorithm : 'HKDF-512',
739
+ baseKeyBytes : vaultHdKey.privateKey,
740
+ salt : '',
741
+ info : 'vault_cek',
742
+ derivedKeyAlgorithm : 'A256GCM',
743
+ });
744
+ const saltInput = await this.crypto.deriveKeyBytes({
745
+ algorithm : 'HKDF-512',
746
+ baseKeyBytes : vaultHdKey.publicKey,
747
+ salt : '',
748
+ info : 'vault_unlock_salt',
749
+ length : 256,
750
+ });
751
+
752
+ return {
753
+ recoveryPhrase : resolvedRecoveryPhrase,
754
+ contentEncryptionKey,
755
+ contentEncryptionKeyJwe : await this.encryptContentEncryptionKey({
756
+ password,
757
+ contentEncryptionKey,
758
+ saltInput,
759
+ }),
760
+ portableDid: await this.derivePortableDid({ rootHdKey, dwnEndpoints }),
761
+ };
762
+ }
763
+
764
+ private validatePassword(password: string): void {
765
+ if (isEmptyString(password)) {
766
+ throw new Error(
767
+ 'HdIdentityVault: The password is required and cannot be blank. Please provide a ' +
768
+ 'valid, non-empty password.'
769
+ );
770
+ }
771
+ }
772
+
773
+ private resolveRecoveryPhrase(recoveryPhrase?: string): string {
774
+ if (recoveryPhrase !== undefined && isEmptyString(recoveryPhrase)) {
775
+ throw new Error(
776
+ 'HdIdentityVault: The recovery phrase is required and cannot be blank. Please provide a ' +
777
+ 'valid BIP-39 recovery phrase.'
778
+ );
779
+ }
780
+
781
+ const resolvedRecoveryPhrase = recoveryPhrase ?? generateMnemonic(wordlist, 128);
782
+ if (!validateMnemonic(resolvedRecoveryPhrase, wordlist)) {
783
+ throw new Error(
784
+ 'HdIdentityVault: The provided recovery phrase is invalid. Please ensure that the ' +
785
+ 'recovery phrase is a correctly formatted series of 12 words.'
786
+ );
787
+ }
788
+
789
+ return resolvedRecoveryPhrase;
790
+ }
791
+
792
+ private async encryptContentEncryptionKey({
793
+ password,
794
+ contentEncryptionKey,
795
+ saltInput,
796
+ }: {
797
+ password: string;
798
+ contentEncryptionKey: Jwk;
799
+ saltInput: Uint8Array;
800
+ }): Promise<string> {
801
+ const protectedHeader: JweHeaderParams = {
802
+ alg : 'PBES2-HS512+A256KW',
803
+ enc : 'A256GCM',
804
+ cty : 'text/plain',
805
+ p2c : this._keyDerivationWorkFactor,
806
+ p2s : Convert.uint8Array(saltInput).toBase64Url(),
807
+ };
808
+
809
+ return CompactJwe.encrypt({
810
+ key : Convert.string(password).toUint8Array(),
811
+ protectedHeader,
812
+ plaintext : Convert.object(contentEncryptionKey).toUint8Array(),
813
+ crypto : this.crypto,
814
+ keyManager : new LocalKeyManager(),
815
+ });
816
+ }
817
+
818
+ private async derivePortableDid({
819
+ rootHdKey,
820
+ dwnEndpoints,
821
+ }: {
822
+ rootHdKey: HDKey;
823
+ dwnEndpoints?: string[];
824
+ }): Promise<PortableDid> {
825
+ const identityHdKey = rootHdKey.derive(`m/44'/0'/1708523827'/0'/0'`);
826
+ const identityPrivateKey = await this.crypto.bytesToPrivateKey({
827
+ algorithm : 'Ed25519',
828
+ privateKeyBytes : identityHdKey.privateKey,
829
+ });
830
+
831
+ const signingHdKey = rootHdKey.derive(`m/44'/0'/1708523827'/0'/1'`);
832
+ const signingPrivateKey = await this.crypto.bytesToPrivateKey({
833
+ algorithm : 'Ed25519',
834
+ privateKeyBytes : signingHdKey.privateKey,
835
+ });
836
+
837
+ const encryptionHdKey = rootHdKey.derive(`m/44'/0'/1708523827'/0'/2'`);
838
+ const encryptionPrivateKey = await this.crypto.bytesToPrivateKey({
839
+ algorithm : 'X25519',
840
+ privateKeyBytes : encryptionHdKey.privateKey,
841
+ });
842
+
843
+ const deterministicKeyGenerator = new DeterministicKeyGenerator();
844
+ await deterministicKeyGenerator.addPredefinedKeys({
845
+ privateKeys: [identityPrivateKey, signingPrivateKey, encryptionPrivateKey],
846
+ });
847
+
848
+ const options = {
849
+ verificationMethods: [
850
+ {
851
+ algorithm : 'Ed25519',
852
+ id : 'sig',
853
+ purposes : ['assertionMethod', 'authentication'],
854
+ },
855
+ {
856
+ algorithm : 'X25519',
857
+ id : 'enc',
858
+ purposes : ['keyAgreement'],
859
+ },
860
+ ],
861
+ } as DidDhtCreateOptions<DeterministicKeyGenerator>;
862
+
863
+ if (dwnEndpoints && dwnEndpoints.length > 0) {
864
+ options.services = [
865
+ {
866
+ id : 'dwn',
867
+ type : 'DecentralizedWebNode',
868
+ serviceEndpoint : dwnEndpoints,
869
+ },
870
+ ];
871
+ }
872
+
873
+ return (await DidDht.create({ keyManager: deterministicKeyGenerator, options })).export();
874
+ }
875
+
876
+ private async encryptPortableDid(portableDid: PortableDid, contentEncryptionKey: Jwk): Promise<string> {
877
+ return CompactJwe.encrypt({
878
+ key : contentEncryptionKey,
879
+ plaintext : Convert.object(portableDid).toUint8Array(),
880
+ protectedHeader : { alg: 'dir', enc: 'A256GCM', cty: 'json' },
881
+ crypto : this.crypto,
882
+ keyManager : new LocalKeyManager(),
883
+ });
884
+ }
885
+
886
+ private async decryptStoredPortableDid(contentEncryptionKey: Jwk): Promise<PortableDid> {
887
+ const { plaintext: portableDidBytes } = await CompactJwe.decrypt({
888
+ jwe : await this.getStoredDid(),
889
+ key : contentEncryptionKey,
890
+ crypto : this.crypto,
891
+ keyManager : new LocalKeyManager(),
892
+ options : { minP2cCount: 1 },
893
+ });
894
+
895
+ const portableDid = Convert.uint8Array(portableDidBytes).toObject();
896
+ if (!isPortableDid(portableDid)) {
897
+ throw new Error('HdIdentityVault: Unable to decode malformed DID in identity vault');
898
+ }
899
+
900
+ return portableDid;
901
+ }
902
+
871
903
  /**
872
904
  * Retrieves the Decentralized Identifier (DID) associated with the identity vault from the vault
873
905
  * store.
@@ -950,4 +982,4 @@ export class HdIdentityVault implements IdentityVault<{ InitializeResult: string
950
982
 
951
983
  return true;
952
984
  }
953
- }
985
+ }
@@ -88,6 +88,13 @@ export interface IdentityVault<T extends Record<string, any> = { InitializeResul
88
88
  */
89
89
  initialize(params: { password: string }): Promise<T['InitializeResult']>;
90
90
 
91
+ /**
92
+ * Resets the vault password by proving knowledge of the original recovery phrase.
93
+ *
94
+ * Implementations must leave existing vault contents unchanged when the phrase does not match.
95
+ */
96
+ resetPasswordWithRecoveryPhrase(params: { recoveryPhrase: string, password: string }): Promise<void>;
97
+
91
98
  /**
92
99
  * Returns a boolean indicating whether the IdentityVault has been initialized.
93
100
  */
@@ -152,4 +159,4 @@ export type IdentityVaultStatus = {
152
159
  * The timestamp of the last restore.
153
160
  */
154
161
  lastRestore: string | null;
155
- };
162
+ };