@fgv/ts-extras 5.1.0-15 → 5.1.0-16

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.
@@ -584,49 +584,29 @@ export class KeyStore {
584
584
  if (keyResult.isFailure()) {
585
585
  return fail(`Key derivation failed: ${keyResult.message}`);
586
586
  }
587
- // Build vault contents
588
- const secrets = {};
589
- for (const [name, entry] of this._secrets) {
590
- secrets[name] = {
591
- name: entry.name,
592
- type: entry.type,
593
- key: this._cryptoProvider.toBase64(entry.key),
594
- description: entry.description,
595
- createdAt: entry.createdAt
596
- };
597
- }
598
- const vaultContents = {
599
- version: KEYSTORE_FORMAT,
600
- secrets
601
- };
602
- // Serialize and encrypt
603
- const jsonResult = captureResult(() => JSON.stringify(vaultContents));
604
- /* c8 ignore next 3 - error path tested but coverage intermittently missed */
605
- if (jsonResult.isFailure()) {
606
- return fail(`Failed to serialize vault: ${jsonResult.message}`);
587
+ return this._encryptVault(keyResult.value);
588
+ }
589
+ /**
590
+ * Saves the key store using a pre-derived key, bypassing PBKDF2 key
591
+ * derivation. Use this when the derived key has been stored externally
592
+ * (e.g., in another key store) and the original password is no longer
593
+ * available.
594
+ *
595
+ * The supplied key must be the same key that was (or would be) derived
596
+ * from the master password using the key store's PBKDF2 parameters.
597
+ *
598
+ * @param derivedKey - The pre-derived master key (32 bytes for AES-256)
599
+ * @returns Success with IKeyStoreFile, Failure if locked or key invalid
600
+ * @public
601
+ */
602
+ async saveWithKey(derivedKey) {
603
+ if (!this._secrets || !this._salt) {
604
+ return fail('Key store is locked');
607
605
  }
608
- const encryptResult = await this._cryptoProvider.encrypt(jsonResult.value, keyResult.value);
609
- /* c8 ignore next 3 - crypto provider errors tested but coverage intermittently missed */
610
- if (encryptResult.isFailure()) {
611
- return fail(`Encryption failed: ${encryptResult.message}`);
606
+ if (derivedKey.length !== Constants.AES_256_KEY_SIZE) {
607
+ return fail(`Key must be ${Constants.AES_256_KEY_SIZE} bytes, got ${derivedKey.length}`);
612
608
  }
613
- const { iv, authTag, encryptedData } = encryptResult.value;
614
- const keystoreFileData = {
615
- format: KEYSTORE_FORMAT,
616
- algorithm: Constants.DEFAULT_ALGORITHM,
617
- iv: this._cryptoProvider.toBase64(iv),
618
- authTag: this._cryptoProvider.toBase64(authTag),
619
- encryptedData: this._cryptoProvider.toBase64(encryptedData),
620
- keyDerivation: {
621
- kdf: 'pbkdf2',
622
- salt: this._cryptoProvider.toBase64(this._salt),
623
- iterations: this._iterations
624
- }
625
- };
626
- this._keystoreFile = keystoreFileData;
627
- this._dirty = false;
628
- this._isNew = false;
629
- return succeed(keystoreFileData);
609
+ return this._encryptVault(derivedKey);
630
610
  }
631
611
  /**
632
612
  * Changes the master password.
@@ -741,8 +721,60 @@ export class KeyStore {
741
721
  });
742
722
  }
743
723
  // ============================================================================
744
- // Private: Vault Decryption
724
+ // Private: Vault Encryption / Decryption
745
725
  // ============================================================================
726
+ /**
727
+ * Encrypts the vault with a derived key and returns the key store file.
728
+ * Shared by `save()` and `saveWithKey()`.
729
+ */
730
+ async _encryptVault(derivedKey) {
731
+ // _secrets and _salt are guaranteed non-undefined by callers
732
+ const secrets = this._secrets;
733
+ const salt = this._salt;
734
+ // Build vault contents
735
+ const secretEntries = {};
736
+ for (const [name, entry] of secrets) {
737
+ secretEntries[name] = {
738
+ name: entry.name,
739
+ type: entry.type,
740
+ key: this._cryptoProvider.toBase64(entry.key),
741
+ description: entry.description,
742
+ createdAt: entry.createdAt
743
+ };
744
+ }
745
+ const vaultContents = {
746
+ version: KEYSTORE_FORMAT,
747
+ secrets: secretEntries
748
+ };
749
+ // Serialize and encrypt
750
+ const jsonResult = captureResult(() => JSON.stringify(vaultContents));
751
+ /* c8 ignore next 3 - error path tested but coverage intermittently missed */
752
+ if (jsonResult.isFailure()) {
753
+ return fail(`Failed to serialize vault: ${jsonResult.message}`);
754
+ }
755
+ const encryptResult = await this._cryptoProvider.encrypt(jsonResult.value, derivedKey);
756
+ /* c8 ignore next 3 - crypto provider errors tested but coverage intermittently missed */
757
+ if (encryptResult.isFailure()) {
758
+ return fail(`Encryption failed: ${encryptResult.message}`);
759
+ }
760
+ const { iv, authTag, encryptedData } = encryptResult.value;
761
+ const keystoreFileData = {
762
+ format: KEYSTORE_FORMAT,
763
+ algorithm: Constants.DEFAULT_ALGORITHM,
764
+ iv: this._cryptoProvider.toBase64(iv),
765
+ authTag: this._cryptoProvider.toBase64(authTag),
766
+ encryptedData: this._cryptoProvider.toBase64(encryptedData),
767
+ keyDerivation: {
768
+ kdf: 'pbkdf2',
769
+ salt: this._cryptoProvider.toBase64(salt),
770
+ iterations: this._iterations
771
+ }
772
+ };
773
+ this._keystoreFile = keystoreFileData;
774
+ this._dirty = false;
775
+ this._isNew = false;
776
+ return succeed(keystoreFileData);
777
+ }
746
778
  /**
747
779
  * Decrypts the vault with a derived key and loads secrets into memory.
748
780
  * Shared by `unlock()` and `unlockWithKey()`.
@@ -1792,6 +1792,20 @@ declare class KeyStore_2 implements IEncryptionProvider {
1792
1792
  * @public
1793
1793
  */
1794
1794
  save(password: string): Promise<Result<IKeyStoreFile>>;
1795
+ /**
1796
+ * Saves the key store using a pre-derived key, bypassing PBKDF2 key
1797
+ * derivation. Use this when the derived key has been stored externally
1798
+ * (e.g., in another key store) and the original password is no longer
1799
+ * available.
1800
+ *
1801
+ * The supplied key must be the same key that was (or would be) derived
1802
+ * from the master password using the key store's PBKDF2 parameters.
1803
+ *
1804
+ * @param derivedKey - The pre-derived master key (32 bytes for AES-256)
1805
+ * @returns Success with IKeyStoreFile, Failure if locked or key invalid
1806
+ * @public
1807
+ */
1808
+ saveWithKey(derivedKey: Uint8Array): Promise<Result<IKeyStoreFile>>;
1795
1809
  /**
1796
1810
  * Changes the master password.
1797
1811
  * Re-encrypts the vault with the new password-derived key.
@@ -1816,6 +1830,11 @@ declare class KeyStore_2 implements IEncryptionProvider {
1816
1830
  * @public
1817
1831
  */
1818
1832
  getEncryptionConfig(): Result<Pick<IEncryptionConfig, 'secretProvider' | 'cryptoProvider'>>;
1833
+ /**
1834
+ * Encrypts the vault with a derived key and returns the key store file.
1835
+ * Shared by `save()` and `saveWithKey()`.
1836
+ */
1837
+ private _encryptVault;
1819
1838
  /**
1820
1839
  * Decrypts the vault with a derived key and loads secrets into memory.
1821
1840
  * Shared by `unlock()` and `unlockWithKey()`.
@@ -232,6 +232,20 @@ export declare class KeyStore implements IEncryptionProvider {
232
232
  * @public
233
233
  */
234
234
  save(password: string): Promise<Result<IKeyStoreFile>>;
235
+ /**
236
+ * Saves the key store using a pre-derived key, bypassing PBKDF2 key
237
+ * derivation. Use this when the derived key has been stored externally
238
+ * (e.g., in another key store) and the original password is no longer
239
+ * available.
240
+ *
241
+ * The supplied key must be the same key that was (or would be) derived
242
+ * from the master password using the key store's PBKDF2 parameters.
243
+ *
244
+ * @param derivedKey - The pre-derived master key (32 bytes for AES-256)
245
+ * @returns Success with IKeyStoreFile, Failure if locked or key invalid
246
+ * @public
247
+ */
248
+ saveWithKey(derivedKey: Uint8Array): Promise<Result<IKeyStoreFile>>;
235
249
  /**
236
250
  * Changes the master password.
237
251
  * Re-encrypts the vault with the new password-derived key.
@@ -256,6 +270,11 @@ export declare class KeyStore implements IEncryptionProvider {
256
270
  * @public
257
271
  */
258
272
  getEncryptionConfig(): Result<Pick<IEncryptionConfig, 'secretProvider' | 'cryptoProvider'>>;
273
+ /**
274
+ * Encrypts the vault with a derived key and returns the key store file.
275
+ * Shared by `save()` and `saveWithKey()`.
276
+ */
277
+ private _encryptVault;
259
278
  /**
260
279
  * Decrypts the vault with a derived key and loads secrets into memory.
261
280
  * Shared by `unlock()` and `unlockWithKey()`.
@@ -620,49 +620,29 @@ class KeyStore {
620
620
  if (keyResult.isFailure()) {
621
621
  return (0, ts_utils_1.fail)(`Key derivation failed: ${keyResult.message}`);
622
622
  }
623
- // Build vault contents
624
- const secrets = {};
625
- for (const [name, entry] of this._secrets) {
626
- secrets[name] = {
627
- name: entry.name,
628
- type: entry.type,
629
- key: this._cryptoProvider.toBase64(entry.key),
630
- description: entry.description,
631
- createdAt: entry.createdAt
632
- };
633
- }
634
- const vaultContents = {
635
- version: model_1.KEYSTORE_FORMAT,
636
- secrets
637
- };
638
- // Serialize and encrypt
639
- const jsonResult = (0, ts_utils_1.captureResult)(() => JSON.stringify(vaultContents));
640
- /* c8 ignore next 3 - error path tested but coverage intermittently missed */
641
- if (jsonResult.isFailure()) {
642
- return (0, ts_utils_1.fail)(`Failed to serialize vault: ${jsonResult.message}`);
623
+ return this._encryptVault(keyResult.value);
624
+ }
625
+ /**
626
+ * Saves the key store using a pre-derived key, bypassing PBKDF2 key
627
+ * derivation. Use this when the derived key has been stored externally
628
+ * (e.g., in another key store) and the original password is no longer
629
+ * available.
630
+ *
631
+ * The supplied key must be the same key that was (or would be) derived
632
+ * from the master password using the key store's PBKDF2 parameters.
633
+ *
634
+ * @param derivedKey - The pre-derived master key (32 bytes for AES-256)
635
+ * @returns Success with IKeyStoreFile, Failure if locked or key invalid
636
+ * @public
637
+ */
638
+ async saveWithKey(derivedKey) {
639
+ if (!this._secrets || !this._salt) {
640
+ return (0, ts_utils_1.fail)('Key store is locked');
643
641
  }
644
- const encryptResult = await this._cryptoProvider.encrypt(jsonResult.value, keyResult.value);
645
- /* c8 ignore next 3 - crypto provider errors tested but coverage intermittently missed */
646
- if (encryptResult.isFailure()) {
647
- return (0, ts_utils_1.fail)(`Encryption failed: ${encryptResult.message}`);
642
+ if (derivedKey.length !== Constants.AES_256_KEY_SIZE) {
643
+ return (0, ts_utils_1.fail)(`Key must be ${Constants.AES_256_KEY_SIZE} bytes, got ${derivedKey.length}`);
648
644
  }
649
- const { iv, authTag, encryptedData } = encryptResult.value;
650
- const keystoreFileData = {
651
- format: model_1.KEYSTORE_FORMAT,
652
- algorithm: Constants.DEFAULT_ALGORITHM,
653
- iv: this._cryptoProvider.toBase64(iv),
654
- authTag: this._cryptoProvider.toBase64(authTag),
655
- encryptedData: this._cryptoProvider.toBase64(encryptedData),
656
- keyDerivation: {
657
- kdf: 'pbkdf2',
658
- salt: this._cryptoProvider.toBase64(this._salt),
659
- iterations: this._iterations
660
- }
661
- };
662
- this._keystoreFile = keystoreFileData;
663
- this._dirty = false;
664
- this._isNew = false;
665
- return (0, ts_utils_1.succeed)(keystoreFileData);
645
+ return this._encryptVault(derivedKey);
666
646
  }
667
647
  /**
668
648
  * Changes the master password.
@@ -777,8 +757,60 @@ class KeyStore {
777
757
  });
778
758
  }
779
759
  // ============================================================================
780
- // Private: Vault Decryption
760
+ // Private: Vault Encryption / Decryption
781
761
  // ============================================================================
762
+ /**
763
+ * Encrypts the vault with a derived key and returns the key store file.
764
+ * Shared by `save()` and `saveWithKey()`.
765
+ */
766
+ async _encryptVault(derivedKey) {
767
+ // _secrets and _salt are guaranteed non-undefined by callers
768
+ const secrets = this._secrets;
769
+ const salt = this._salt;
770
+ // Build vault contents
771
+ const secretEntries = {};
772
+ for (const [name, entry] of secrets) {
773
+ secretEntries[name] = {
774
+ name: entry.name,
775
+ type: entry.type,
776
+ key: this._cryptoProvider.toBase64(entry.key),
777
+ description: entry.description,
778
+ createdAt: entry.createdAt
779
+ };
780
+ }
781
+ const vaultContents = {
782
+ version: model_1.KEYSTORE_FORMAT,
783
+ secrets: secretEntries
784
+ };
785
+ // Serialize and encrypt
786
+ const jsonResult = (0, ts_utils_1.captureResult)(() => JSON.stringify(vaultContents));
787
+ /* c8 ignore next 3 - error path tested but coverage intermittently missed */
788
+ if (jsonResult.isFailure()) {
789
+ return (0, ts_utils_1.fail)(`Failed to serialize vault: ${jsonResult.message}`);
790
+ }
791
+ const encryptResult = await this._cryptoProvider.encrypt(jsonResult.value, derivedKey);
792
+ /* c8 ignore next 3 - crypto provider errors tested but coverage intermittently missed */
793
+ if (encryptResult.isFailure()) {
794
+ return (0, ts_utils_1.fail)(`Encryption failed: ${encryptResult.message}`);
795
+ }
796
+ const { iv, authTag, encryptedData } = encryptResult.value;
797
+ const keystoreFileData = {
798
+ format: model_1.KEYSTORE_FORMAT,
799
+ algorithm: Constants.DEFAULT_ALGORITHM,
800
+ iv: this._cryptoProvider.toBase64(iv),
801
+ authTag: this._cryptoProvider.toBase64(authTag),
802
+ encryptedData: this._cryptoProvider.toBase64(encryptedData),
803
+ keyDerivation: {
804
+ kdf: 'pbkdf2',
805
+ salt: this._cryptoProvider.toBase64(salt),
806
+ iterations: this._iterations
807
+ }
808
+ };
809
+ this._keystoreFile = keystoreFileData;
810
+ this._dirty = false;
811
+ this._isNew = false;
812
+ return (0, ts_utils_1.succeed)(keystoreFileData);
813
+ }
782
814
  /**
783
815
  * Decrypts the vault with a derived key and loads secrets into memory.
784
816
  * Shared by `unlock()` and `unlockWithKey()`.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@fgv/ts-extras",
3
- "version": "5.1.0-15",
3
+ "version": "5.1.0-16",
4
4
  "description": "Assorted Typescript Utilities",
5
5
  "main": "lib/index.js",
6
6
  "types": "dist/ts-extras.d.ts",
@@ -86,10 +86,10 @@
86
86
  "@types/js-yaml": "~4.0.9",
87
87
  "typedoc": "~0.28.16",
88
88
  "typedoc-plugin-markdown": "~4.9.0",
89
- "@fgv/heft-dual-rig": "5.1.0-15",
90
- "@fgv/typedoc-compact-theme": "5.1.0-15",
91
- "@fgv/ts-utils-jest": "5.1.0-15",
92
- "@fgv/ts-utils": "5.1.0-15"
89
+ "@fgv/typedoc-compact-theme": "5.1.0-16",
90
+ "@fgv/heft-dual-rig": "5.1.0-16",
91
+ "@fgv/ts-utils-jest": "5.1.0-16",
92
+ "@fgv/ts-utils": "5.1.0-16"
93
93
  },
94
94
  "dependencies": {
95
95
  "luxon": "^3.7.2",
@@ -97,10 +97,10 @@
97
97
  "papaparse": "^5.4.1",
98
98
  "fflate": "~0.8.2",
99
99
  "js-yaml": "~4.1.1",
100
- "@fgv/ts-json-base": "5.1.0-15"
100
+ "@fgv/ts-json-base": "5.1.0-16"
101
101
  },
102
102
  "peerDependencies": {
103
- "@fgv/ts-utils": "5.1.0-15"
103
+ "@fgv/ts-utils": "5.1.0-16"
104
104
  },
105
105
  "repository": {
106
106
  "type": "git",