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

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.
@@ -146,7 +146,6 @@ export class KeyStore {
146
146
  * @public
147
147
  */
148
148
  async unlock(password) {
149
- var _a;
150
149
  if (this._isNew) {
151
150
  return fail('Cannot unlock a new key store - use initialize() instead');
152
151
  }
@@ -170,57 +169,37 @@ export class KeyStore {
170
169
  if (keyResult.isFailure()) {
171
170
  return fail(`Key derivation failed: ${keyResult.message}`);
172
171
  }
173
- // Decrypt the vault
174
- const ivResult = this._cryptoProvider.fromBase64(this._keystoreFile.iv);
175
- const authTagResult = this._cryptoProvider.fromBase64(this._keystoreFile.authTag);
176
- const encryptedDataResult = this._cryptoProvider.fromBase64(this._keystoreFile.encryptedData);
177
- /* c8 ignore next 9 - base64 decode errors tested but coverage intermittently missed */
178
- if (ivResult.isFailure()) {
179
- return fail(`Invalid IV in key store file: ${ivResult.message}`);
180
- }
181
- if (authTagResult.isFailure()) {
182
- return fail(`Invalid auth tag in key store file: ${authTagResult.message}`);
183
- }
184
- if (encryptedDataResult.isFailure()) {
185
- return fail(`Invalid encrypted data in key store file: ${encryptedDataResult.message}`);
186
- }
187
- const decryptResult = await this._cryptoProvider.decrypt(encryptedDataResult.value, keyResult.value, ivResult.value, authTagResult.value);
188
- if (decryptResult.isFailure()) {
189
- return fail('Incorrect password or corrupted key store');
172
+ return this._decryptVault(keyResult.value);
173
+ }
174
+ /**
175
+ * Unlocks an existing key store with a pre-derived key, bypassing
176
+ * PBKDF2 key derivation. Use this when the derived key has been
177
+ * stored externally (e.g., in another key store) and the original
178
+ * password is no longer available.
179
+ *
180
+ * The supplied key must have been derived from the correct password
181
+ * using the key store file's own PBKDF2 parameters (salt and
182
+ * iteration count).
183
+ *
184
+ * @param derivedKey - The pre-derived master key (32 bytes for AES-256)
185
+ * @returns Success with this instance when unlocked, Failure if key is incorrect
186
+ * @public
187
+ */
188
+ async unlockWithKey(derivedKey) {
189
+ if (this._isNew) {
190
+ return fail('Cannot unlock a new key store - use initialize() instead');
190
191
  }
191
- // Parse the vault contents
192
- const parseResult = captureResult(() => JSON.parse(decryptResult.value));
193
- /* c8 ignore next 3 - error path tested but coverage intermittently missed */
194
- if (parseResult.isFailure()) {
195
- return fail(`Failed to parse vault contents: ${parseResult.message}`);
192
+ if (this._state === 'unlocked') {
193
+ return fail('Key store is already unlocked');
196
194
  }
197
- const vaultResult = keystoreVaultContents.convert(parseResult.value);
198
- /* c8 ignore next 3 - error path tested but coverage intermittently missed */
199
- if (vaultResult.isFailure()) {
200
- return fail(`Invalid vault format: ${vaultResult.message}`);
195
+ if (derivedKey.length !== Constants.AES_256_KEY_SIZE) {
196
+ return fail(`Key must be ${Constants.AES_256_KEY_SIZE} bytes, got ${derivedKey.length}`);
201
197
  }
202
- // Load secrets into memory
203
- this._salt = salt;
204
- this._secrets = new Map();
205
- for (const [name, jsonEntry] of Object.entries(vaultResult.value.secrets)) {
206
- const keyBytesResult = this._cryptoProvider.fromBase64(jsonEntry.key);
207
- /* c8 ignore next 3 - error path tested but coverage intermittently missed */
208
- if (keyBytesResult.isFailure()) {
209
- return fail(`Invalid key for secret '${name}': ${keyBytesResult.message}`);
210
- }
211
- const entry = {
212
- name,
213
- /* c8 ignore next 1 - backwards compatibility: old vaults may lack type field */
214
- type: (_a = jsonEntry.type) !== null && _a !== void 0 ? _a : 'encryption-key',
215
- key: keyBytesResult.value,
216
- description: jsonEntry.description,
217
- createdAt: jsonEntry.createdAt
218
- };
219
- this._secrets.set(name, entry);
198
+ /* c8 ignore next 3 - defensive coding: unreachable via public API (open sets file, create sets isNew) */
199
+ if (!this._keystoreFile) {
200
+ return fail('No key store file to unlock');
220
201
  }
221
- this._state = 'unlocked';
222
- this._dirty = false;
223
- return succeed(this);
202
+ return this._decryptVault(derivedKey);
224
203
  }
225
204
  /**
226
205
  * Locks the key store, clearing all secrets from memory.
@@ -761,5 +740,75 @@ export class KeyStore {
761
740
  cryptoProvider: this._cryptoProvider
762
741
  });
763
742
  }
743
+ // ============================================================================
744
+ // Private: Vault Decryption
745
+ // ============================================================================
746
+ /**
747
+ * Decrypts the vault with a derived key and loads secrets into memory.
748
+ * Shared by `unlock()` and `unlockWithKey()`.
749
+ */
750
+ async _decryptVault(derivedKey) {
751
+ var _a;
752
+ const keystoreFile = this._keystoreFile;
753
+ if (keystoreFile === undefined) {
754
+ return fail('No key store file loaded');
755
+ }
756
+ const ivResult = this._cryptoProvider.fromBase64(keystoreFile.iv);
757
+ const authTagResult = this._cryptoProvider.fromBase64(keystoreFile.authTag);
758
+ const encryptedDataResult = this._cryptoProvider.fromBase64(keystoreFile.encryptedData);
759
+ /* c8 ignore next 9 - base64 decode errors tested but coverage intermittently missed */
760
+ if (ivResult.isFailure()) {
761
+ return fail(`Invalid IV in key store file: ${ivResult.message}`);
762
+ }
763
+ if (authTagResult.isFailure()) {
764
+ return fail(`Invalid auth tag in key store file: ${authTagResult.message}`);
765
+ }
766
+ if (encryptedDataResult.isFailure()) {
767
+ return fail(`Invalid encrypted data in key store file: ${encryptedDataResult.message}`);
768
+ }
769
+ const decryptResult = await this._cryptoProvider.decrypt(encryptedDataResult.value, derivedKey, ivResult.value, authTagResult.value);
770
+ if (decryptResult.isFailure()) {
771
+ return fail('Incorrect password or corrupted key store');
772
+ }
773
+ // Parse the vault contents
774
+ const parseResult = captureResult(() => JSON.parse(decryptResult.value));
775
+ /* c8 ignore next 3 - error path tested but coverage intermittently missed */
776
+ if (parseResult.isFailure()) {
777
+ return fail(`Failed to parse vault contents: ${parseResult.message}`);
778
+ }
779
+ const vaultResult = keystoreVaultContents.convert(parseResult.value);
780
+ /* c8 ignore next 3 - error path tested but coverage intermittently missed */
781
+ if (vaultResult.isFailure()) {
782
+ return fail(`Invalid vault format: ${vaultResult.message}`);
783
+ }
784
+ // Build secrets into local variables to avoid partial state on failure
785
+ const saltResult = this._cryptoProvider.fromBase64(keystoreFile.keyDerivation.salt);
786
+ if (saltResult.isFailure()) {
787
+ return fail(`Invalid salt in key store file: ${saltResult.message}`);
788
+ }
789
+ const secrets = new Map();
790
+ for (const [name, jsonEntry] of Object.entries(vaultResult.value.secrets)) {
791
+ const keyBytesResult = this._cryptoProvider.fromBase64(jsonEntry.key);
792
+ /* c8 ignore next 3 - error path tested but coverage intermittently missed */
793
+ if (keyBytesResult.isFailure()) {
794
+ return fail(`Invalid key for secret '${name}': ${keyBytesResult.message}`);
795
+ }
796
+ const entry = {
797
+ name,
798
+ /* c8 ignore next 1 - backwards compatibility: old vaults may lack type field */
799
+ type: (_a = jsonEntry.type) !== null && _a !== void 0 ? _a : 'encryption-key',
800
+ key: keyBytesResult.value,
801
+ description: jsonEntry.description,
802
+ createdAt: jsonEntry.createdAt
803
+ };
804
+ secrets.set(name, entry);
805
+ }
806
+ // All validation passed — commit state atomically
807
+ this._salt = saltResult.value;
808
+ this._secrets = secrets;
809
+ this._state = 'unlocked';
810
+ this._dirty = false;
811
+ return succeed(this);
812
+ }
764
813
  }
765
814
  //# sourceMappingURL=keyStore.js.map
@@ -1636,6 +1636,21 @@ declare class KeyStore_2 implements IEncryptionProvider {
1636
1636
  * @public
1637
1637
  */
1638
1638
  unlock(password: string): Promise<Result<KeyStore_2>>;
1639
+ /**
1640
+ * Unlocks an existing key store with a pre-derived key, bypassing
1641
+ * PBKDF2 key derivation. Use this when the derived key has been
1642
+ * stored externally (e.g., in another key store) and the original
1643
+ * password is no longer available.
1644
+ *
1645
+ * The supplied key must have been derived from the correct password
1646
+ * using the key store file's own PBKDF2 parameters (salt and
1647
+ * iteration count).
1648
+ *
1649
+ * @param derivedKey - The pre-derived master key (32 bytes for AES-256)
1650
+ * @returns Success with this instance when unlocked, Failure if key is incorrect
1651
+ * @public
1652
+ */
1653
+ unlockWithKey(derivedKey: Uint8Array): Promise<Result<KeyStore_2>>;
1639
1654
  /**
1640
1655
  * Locks the key store, clearing all secrets from memory.
1641
1656
  * @param force - If true, discards unsaved changes
@@ -1801,6 +1816,11 @@ declare class KeyStore_2 implements IEncryptionProvider {
1801
1816
  * @public
1802
1817
  */
1803
1818
  getEncryptionConfig(): Result<Pick<IEncryptionConfig, 'secretProvider' | 'cryptoProvider'>>;
1819
+ /**
1820
+ * Decrypts the vault with a derived key and loads secrets into memory.
1821
+ * Shared by `unlock()` and `unlockWithKey()`.
1822
+ */
1823
+ private _decryptVault;
1804
1824
  }
1805
1825
 
1806
1826
  /**
@@ -76,6 +76,21 @@ export declare class KeyStore implements IEncryptionProvider {
76
76
  * @public
77
77
  */
78
78
  unlock(password: string): Promise<Result<KeyStore>>;
79
+ /**
80
+ * Unlocks an existing key store with a pre-derived key, bypassing
81
+ * PBKDF2 key derivation. Use this when the derived key has been
82
+ * stored externally (e.g., in another key store) and the original
83
+ * password is no longer available.
84
+ *
85
+ * The supplied key must have been derived from the correct password
86
+ * using the key store file's own PBKDF2 parameters (salt and
87
+ * iteration count).
88
+ *
89
+ * @param derivedKey - The pre-derived master key (32 bytes for AES-256)
90
+ * @returns Success with this instance when unlocked, Failure if key is incorrect
91
+ * @public
92
+ */
93
+ unlockWithKey(derivedKey: Uint8Array): Promise<Result<KeyStore>>;
79
94
  /**
80
95
  * Locks the key store, clearing all secrets from memory.
81
96
  * @param force - If true, discards unsaved changes
@@ -241,5 +256,10 @@ export declare class KeyStore implements IEncryptionProvider {
241
256
  * @public
242
257
  */
243
258
  getEncryptionConfig(): Result<Pick<IEncryptionConfig, 'secretProvider' | 'cryptoProvider'>>;
259
+ /**
260
+ * Decrypts the vault with a derived key and loads secrets into memory.
261
+ * Shared by `unlock()` and `unlockWithKey()`.
262
+ */
263
+ private _decryptVault;
244
264
  }
245
265
  //# sourceMappingURL=keyStore.d.ts.map
@@ -182,7 +182,6 @@ class KeyStore {
182
182
  * @public
183
183
  */
184
184
  async unlock(password) {
185
- var _a;
186
185
  if (this._isNew) {
187
186
  return (0, ts_utils_1.fail)('Cannot unlock a new key store - use initialize() instead');
188
187
  }
@@ -206,57 +205,37 @@ class KeyStore {
206
205
  if (keyResult.isFailure()) {
207
206
  return (0, ts_utils_1.fail)(`Key derivation failed: ${keyResult.message}`);
208
207
  }
209
- // Decrypt the vault
210
- const ivResult = this._cryptoProvider.fromBase64(this._keystoreFile.iv);
211
- const authTagResult = this._cryptoProvider.fromBase64(this._keystoreFile.authTag);
212
- const encryptedDataResult = this._cryptoProvider.fromBase64(this._keystoreFile.encryptedData);
213
- /* c8 ignore next 9 - base64 decode errors tested but coverage intermittently missed */
214
- if (ivResult.isFailure()) {
215
- return (0, ts_utils_1.fail)(`Invalid IV in key store file: ${ivResult.message}`);
216
- }
217
- if (authTagResult.isFailure()) {
218
- return (0, ts_utils_1.fail)(`Invalid auth tag in key store file: ${authTagResult.message}`);
219
- }
220
- if (encryptedDataResult.isFailure()) {
221
- return (0, ts_utils_1.fail)(`Invalid encrypted data in key store file: ${encryptedDataResult.message}`);
222
- }
223
- const decryptResult = await this._cryptoProvider.decrypt(encryptedDataResult.value, keyResult.value, ivResult.value, authTagResult.value);
224
- if (decryptResult.isFailure()) {
225
- return (0, ts_utils_1.fail)('Incorrect password or corrupted key store');
208
+ return this._decryptVault(keyResult.value);
209
+ }
210
+ /**
211
+ * Unlocks an existing key store with a pre-derived key, bypassing
212
+ * PBKDF2 key derivation. Use this when the derived key has been
213
+ * stored externally (e.g., in another key store) and the original
214
+ * password is no longer available.
215
+ *
216
+ * The supplied key must have been derived from the correct password
217
+ * using the key store file's own PBKDF2 parameters (salt and
218
+ * iteration count).
219
+ *
220
+ * @param derivedKey - The pre-derived master key (32 bytes for AES-256)
221
+ * @returns Success with this instance when unlocked, Failure if key is incorrect
222
+ * @public
223
+ */
224
+ async unlockWithKey(derivedKey) {
225
+ if (this._isNew) {
226
+ return (0, ts_utils_1.fail)('Cannot unlock a new key store - use initialize() instead');
226
227
  }
227
- // Parse the vault contents
228
- const parseResult = (0, ts_utils_1.captureResult)(() => JSON.parse(decryptResult.value));
229
- /* c8 ignore next 3 - error path tested but coverage intermittently missed */
230
- if (parseResult.isFailure()) {
231
- return (0, ts_utils_1.fail)(`Failed to parse vault contents: ${parseResult.message}`);
228
+ if (this._state === 'unlocked') {
229
+ return (0, ts_utils_1.fail)('Key store is already unlocked');
232
230
  }
233
- const vaultResult = converters_1.keystoreVaultContents.convert(parseResult.value);
234
- /* c8 ignore next 3 - error path tested but coverage intermittently missed */
235
- if (vaultResult.isFailure()) {
236
- return (0, ts_utils_1.fail)(`Invalid vault format: ${vaultResult.message}`);
231
+ if (derivedKey.length !== Constants.AES_256_KEY_SIZE) {
232
+ return (0, ts_utils_1.fail)(`Key must be ${Constants.AES_256_KEY_SIZE} bytes, got ${derivedKey.length}`);
237
233
  }
238
- // Load secrets into memory
239
- this._salt = salt;
240
- this._secrets = new Map();
241
- for (const [name, jsonEntry] of Object.entries(vaultResult.value.secrets)) {
242
- const keyBytesResult = this._cryptoProvider.fromBase64(jsonEntry.key);
243
- /* c8 ignore next 3 - error path tested but coverage intermittently missed */
244
- if (keyBytesResult.isFailure()) {
245
- return (0, ts_utils_1.fail)(`Invalid key for secret '${name}': ${keyBytesResult.message}`);
246
- }
247
- const entry = {
248
- name,
249
- /* c8 ignore next 1 - backwards compatibility: old vaults may lack type field */
250
- type: (_a = jsonEntry.type) !== null && _a !== void 0 ? _a : 'encryption-key',
251
- key: keyBytesResult.value,
252
- description: jsonEntry.description,
253
- createdAt: jsonEntry.createdAt
254
- };
255
- this._secrets.set(name, entry);
234
+ /* c8 ignore next 3 - defensive coding: unreachable via public API (open sets file, create sets isNew) */
235
+ if (!this._keystoreFile) {
236
+ return (0, ts_utils_1.fail)('No key store file to unlock');
256
237
  }
257
- this._state = 'unlocked';
258
- this._dirty = false;
259
- return (0, ts_utils_1.succeed)(this);
238
+ return this._decryptVault(derivedKey);
260
239
  }
261
240
  /**
262
241
  * Locks the key store, clearing all secrets from memory.
@@ -797,6 +776,76 @@ class KeyStore {
797
776
  cryptoProvider: this._cryptoProvider
798
777
  });
799
778
  }
779
+ // ============================================================================
780
+ // Private: Vault Decryption
781
+ // ============================================================================
782
+ /**
783
+ * Decrypts the vault with a derived key and loads secrets into memory.
784
+ * Shared by `unlock()` and `unlockWithKey()`.
785
+ */
786
+ async _decryptVault(derivedKey) {
787
+ var _a;
788
+ const keystoreFile = this._keystoreFile;
789
+ if (keystoreFile === undefined) {
790
+ return (0, ts_utils_1.fail)('No key store file loaded');
791
+ }
792
+ const ivResult = this._cryptoProvider.fromBase64(keystoreFile.iv);
793
+ const authTagResult = this._cryptoProvider.fromBase64(keystoreFile.authTag);
794
+ const encryptedDataResult = this._cryptoProvider.fromBase64(keystoreFile.encryptedData);
795
+ /* c8 ignore next 9 - base64 decode errors tested but coverage intermittently missed */
796
+ if (ivResult.isFailure()) {
797
+ return (0, ts_utils_1.fail)(`Invalid IV in key store file: ${ivResult.message}`);
798
+ }
799
+ if (authTagResult.isFailure()) {
800
+ return (0, ts_utils_1.fail)(`Invalid auth tag in key store file: ${authTagResult.message}`);
801
+ }
802
+ if (encryptedDataResult.isFailure()) {
803
+ return (0, ts_utils_1.fail)(`Invalid encrypted data in key store file: ${encryptedDataResult.message}`);
804
+ }
805
+ const decryptResult = await this._cryptoProvider.decrypt(encryptedDataResult.value, derivedKey, ivResult.value, authTagResult.value);
806
+ if (decryptResult.isFailure()) {
807
+ return (0, ts_utils_1.fail)('Incorrect password or corrupted key store');
808
+ }
809
+ // Parse the vault contents
810
+ const parseResult = (0, ts_utils_1.captureResult)(() => JSON.parse(decryptResult.value));
811
+ /* c8 ignore next 3 - error path tested but coverage intermittently missed */
812
+ if (parseResult.isFailure()) {
813
+ return (0, ts_utils_1.fail)(`Failed to parse vault contents: ${parseResult.message}`);
814
+ }
815
+ const vaultResult = converters_1.keystoreVaultContents.convert(parseResult.value);
816
+ /* c8 ignore next 3 - error path tested but coverage intermittently missed */
817
+ if (vaultResult.isFailure()) {
818
+ return (0, ts_utils_1.fail)(`Invalid vault format: ${vaultResult.message}`);
819
+ }
820
+ // Build secrets into local variables to avoid partial state on failure
821
+ const saltResult = this._cryptoProvider.fromBase64(keystoreFile.keyDerivation.salt);
822
+ if (saltResult.isFailure()) {
823
+ return (0, ts_utils_1.fail)(`Invalid salt in key store file: ${saltResult.message}`);
824
+ }
825
+ const secrets = new Map();
826
+ for (const [name, jsonEntry] of Object.entries(vaultResult.value.secrets)) {
827
+ const keyBytesResult = this._cryptoProvider.fromBase64(jsonEntry.key);
828
+ /* c8 ignore next 3 - error path tested but coverage intermittently missed */
829
+ if (keyBytesResult.isFailure()) {
830
+ return (0, ts_utils_1.fail)(`Invalid key for secret '${name}': ${keyBytesResult.message}`);
831
+ }
832
+ const entry = {
833
+ name,
834
+ /* c8 ignore next 1 - backwards compatibility: old vaults may lack type field */
835
+ type: (_a = jsonEntry.type) !== null && _a !== void 0 ? _a : 'encryption-key',
836
+ key: keyBytesResult.value,
837
+ description: jsonEntry.description,
838
+ createdAt: jsonEntry.createdAt
839
+ };
840
+ secrets.set(name, entry);
841
+ }
842
+ // All validation passed — commit state atomically
843
+ this._salt = saltResult.value;
844
+ this._secrets = secrets;
845
+ this._state = 'unlocked';
846
+ this._dirty = false;
847
+ return (0, ts_utils_1.succeed)(this);
848
+ }
800
849
  }
801
850
  exports.KeyStore = KeyStore;
802
851
  //# sourceMappingURL=keyStore.js.map
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@fgv/ts-extras",
3
- "version": "5.1.0-14",
3
+ "version": "5.1.0-15",
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-14",
90
- "@fgv/typedoc-compact-theme": "5.1.0-14",
91
- "@fgv/ts-utils-jest": "5.1.0-14",
92
- "@fgv/ts-utils": "5.1.0-14"
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"
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-14"
100
+ "@fgv/ts-json-base": "5.1.0-15"
101
101
  },
102
102
  "peerDependencies": {
103
- "@fgv/ts-utils": "5.1.0-14"
103
+ "@fgv/ts-utils": "5.1.0-15"
104
104
  },
105
105
  "repository": {
106
106
  "type": "git",