@fgv/ts-extras 5.1.0-14 → 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.
|
@@ -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
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
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
|
-
|
|
192
|
-
|
|
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
|
-
|
|
198
|
-
|
|
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
|
-
|
|
203
|
-
this.
|
|
204
|
-
|
|
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.
|
|
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.
|
|
@@ -605,49 +584,29 @@ export class KeyStore {
|
|
|
605
584
|
if (keyResult.isFailure()) {
|
|
606
585
|
return fail(`Key derivation failed: ${keyResult.message}`);
|
|
607
586
|
}
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
if (jsonResult.isFailure()) {
|
|
627
|
-
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');
|
|
628
605
|
}
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
if (encryptResult.isFailure()) {
|
|
632
|
-
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}`);
|
|
633
608
|
}
|
|
634
|
-
|
|
635
|
-
const keystoreFileData = {
|
|
636
|
-
format: KEYSTORE_FORMAT,
|
|
637
|
-
algorithm: Constants.DEFAULT_ALGORITHM,
|
|
638
|
-
iv: this._cryptoProvider.toBase64(iv),
|
|
639
|
-
authTag: this._cryptoProvider.toBase64(authTag),
|
|
640
|
-
encryptedData: this._cryptoProvider.toBase64(encryptedData),
|
|
641
|
-
keyDerivation: {
|
|
642
|
-
kdf: 'pbkdf2',
|
|
643
|
-
salt: this._cryptoProvider.toBase64(this._salt),
|
|
644
|
-
iterations: this._iterations
|
|
645
|
-
}
|
|
646
|
-
};
|
|
647
|
-
this._keystoreFile = keystoreFileData;
|
|
648
|
-
this._dirty = false;
|
|
649
|
-
this._isNew = false;
|
|
650
|
-
return succeed(keystoreFileData);
|
|
609
|
+
return this._encryptVault(derivedKey);
|
|
651
610
|
}
|
|
652
611
|
/**
|
|
653
612
|
* Changes the master password.
|
|
@@ -761,5 +720,127 @@ export class KeyStore {
|
|
|
761
720
|
cryptoProvider: this._cryptoProvider
|
|
762
721
|
});
|
|
763
722
|
}
|
|
723
|
+
// ============================================================================
|
|
724
|
+
// Private: Vault Encryption / Decryption
|
|
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
|
+
}
|
|
778
|
+
/**
|
|
779
|
+
* Decrypts the vault with a derived key and loads secrets into memory.
|
|
780
|
+
* Shared by `unlock()` and `unlockWithKey()`.
|
|
781
|
+
*/
|
|
782
|
+
async _decryptVault(derivedKey) {
|
|
783
|
+
var _a;
|
|
784
|
+
const keystoreFile = this._keystoreFile;
|
|
785
|
+
if (keystoreFile === undefined) {
|
|
786
|
+
return fail('No key store file loaded');
|
|
787
|
+
}
|
|
788
|
+
const ivResult = this._cryptoProvider.fromBase64(keystoreFile.iv);
|
|
789
|
+
const authTagResult = this._cryptoProvider.fromBase64(keystoreFile.authTag);
|
|
790
|
+
const encryptedDataResult = this._cryptoProvider.fromBase64(keystoreFile.encryptedData);
|
|
791
|
+
/* c8 ignore next 9 - base64 decode errors tested but coverage intermittently missed */
|
|
792
|
+
if (ivResult.isFailure()) {
|
|
793
|
+
return fail(`Invalid IV in key store file: ${ivResult.message}`);
|
|
794
|
+
}
|
|
795
|
+
if (authTagResult.isFailure()) {
|
|
796
|
+
return fail(`Invalid auth tag in key store file: ${authTagResult.message}`);
|
|
797
|
+
}
|
|
798
|
+
if (encryptedDataResult.isFailure()) {
|
|
799
|
+
return fail(`Invalid encrypted data in key store file: ${encryptedDataResult.message}`);
|
|
800
|
+
}
|
|
801
|
+
const decryptResult = await this._cryptoProvider.decrypt(encryptedDataResult.value, derivedKey, ivResult.value, authTagResult.value);
|
|
802
|
+
if (decryptResult.isFailure()) {
|
|
803
|
+
return fail('Incorrect password or corrupted key store');
|
|
804
|
+
}
|
|
805
|
+
// Parse the vault contents
|
|
806
|
+
const parseResult = captureResult(() => JSON.parse(decryptResult.value));
|
|
807
|
+
/* c8 ignore next 3 - error path tested but coverage intermittently missed */
|
|
808
|
+
if (parseResult.isFailure()) {
|
|
809
|
+
return fail(`Failed to parse vault contents: ${parseResult.message}`);
|
|
810
|
+
}
|
|
811
|
+
const vaultResult = keystoreVaultContents.convert(parseResult.value);
|
|
812
|
+
/* c8 ignore next 3 - error path tested but coverage intermittently missed */
|
|
813
|
+
if (vaultResult.isFailure()) {
|
|
814
|
+
return fail(`Invalid vault format: ${vaultResult.message}`);
|
|
815
|
+
}
|
|
816
|
+
// Build secrets into local variables to avoid partial state on failure
|
|
817
|
+
const saltResult = this._cryptoProvider.fromBase64(keystoreFile.keyDerivation.salt);
|
|
818
|
+
if (saltResult.isFailure()) {
|
|
819
|
+
return fail(`Invalid salt in key store file: ${saltResult.message}`);
|
|
820
|
+
}
|
|
821
|
+
const secrets = new Map();
|
|
822
|
+
for (const [name, jsonEntry] of Object.entries(vaultResult.value.secrets)) {
|
|
823
|
+
const keyBytesResult = this._cryptoProvider.fromBase64(jsonEntry.key);
|
|
824
|
+
/* c8 ignore next 3 - error path tested but coverage intermittently missed */
|
|
825
|
+
if (keyBytesResult.isFailure()) {
|
|
826
|
+
return fail(`Invalid key for secret '${name}': ${keyBytesResult.message}`);
|
|
827
|
+
}
|
|
828
|
+
const entry = {
|
|
829
|
+
name,
|
|
830
|
+
/* c8 ignore next 1 - backwards compatibility: old vaults may lack type field */
|
|
831
|
+
type: (_a = jsonEntry.type) !== null && _a !== void 0 ? _a : 'encryption-key',
|
|
832
|
+
key: keyBytesResult.value,
|
|
833
|
+
description: jsonEntry.description,
|
|
834
|
+
createdAt: jsonEntry.createdAt
|
|
835
|
+
};
|
|
836
|
+
secrets.set(name, entry);
|
|
837
|
+
}
|
|
838
|
+
// All validation passed — commit state atomically
|
|
839
|
+
this._salt = saltResult.value;
|
|
840
|
+
this._secrets = secrets;
|
|
841
|
+
this._state = 'unlocked';
|
|
842
|
+
this._dirty = false;
|
|
843
|
+
return succeed(this);
|
|
844
|
+
}
|
|
764
845
|
}
|
|
765
846
|
//# sourceMappingURL=keyStore.js.map
|
package/dist/ts-extras.d.ts
CHANGED
|
@@ -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
|
|
@@ -1777,6 +1792,20 @@ declare class KeyStore_2 implements IEncryptionProvider {
|
|
|
1777
1792
|
* @public
|
|
1778
1793
|
*/
|
|
1779
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>>;
|
|
1780
1809
|
/**
|
|
1781
1810
|
* Changes the master password.
|
|
1782
1811
|
* Re-encrypts the vault with the new password-derived key.
|
|
@@ -1801,6 +1830,16 @@ declare class KeyStore_2 implements IEncryptionProvider {
|
|
|
1801
1830
|
* @public
|
|
1802
1831
|
*/
|
|
1803
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;
|
|
1838
|
+
/**
|
|
1839
|
+
* Decrypts the vault with a derived key and loads secrets into memory.
|
|
1840
|
+
* Shared by `unlock()` and `unlockWithKey()`.
|
|
1841
|
+
*/
|
|
1842
|
+
private _decryptVault;
|
|
1804
1843
|
}
|
|
1805
1844
|
|
|
1806
1845
|
/**
|
|
@@ -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
|
|
@@ -217,6 +232,20 @@ export declare class KeyStore implements IEncryptionProvider {
|
|
|
217
232
|
* @public
|
|
218
233
|
*/
|
|
219
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>>;
|
|
220
249
|
/**
|
|
221
250
|
* Changes the master password.
|
|
222
251
|
* Re-encrypts the vault with the new password-derived key.
|
|
@@ -241,5 +270,15 @@ export declare class KeyStore implements IEncryptionProvider {
|
|
|
241
270
|
* @public
|
|
242
271
|
*/
|
|
243
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;
|
|
278
|
+
/**
|
|
279
|
+
* Decrypts the vault with a derived key and loads secrets into memory.
|
|
280
|
+
* Shared by `unlock()` and `unlockWithKey()`.
|
|
281
|
+
*/
|
|
282
|
+
private _decryptVault;
|
|
244
283
|
}
|
|
245
284
|
//# 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
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
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
|
-
|
|
228
|
-
|
|
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
|
-
|
|
234
|
-
|
|
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
|
-
|
|
239
|
-
this.
|
|
240
|
-
|
|
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.
|
|
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.
|
|
@@ -641,49 +620,29 @@ class KeyStore {
|
|
|
641
620
|
if (keyResult.isFailure()) {
|
|
642
621
|
return (0, ts_utils_1.fail)(`Key derivation failed: ${keyResult.message}`);
|
|
643
622
|
}
|
|
644
|
-
|
|
645
|
-
|
|
646
|
-
|
|
647
|
-
|
|
648
|
-
|
|
649
|
-
|
|
650
|
-
|
|
651
|
-
|
|
652
|
-
|
|
653
|
-
|
|
654
|
-
|
|
655
|
-
|
|
656
|
-
|
|
657
|
-
|
|
658
|
-
|
|
659
|
-
|
|
660
|
-
|
|
661
|
-
|
|
662
|
-
if (jsonResult.isFailure()) {
|
|
663
|
-
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');
|
|
664
641
|
}
|
|
665
|
-
|
|
666
|
-
|
|
667
|
-
if (encryptResult.isFailure()) {
|
|
668
|
-
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}`);
|
|
669
644
|
}
|
|
670
|
-
|
|
671
|
-
const keystoreFileData = {
|
|
672
|
-
format: model_1.KEYSTORE_FORMAT,
|
|
673
|
-
algorithm: Constants.DEFAULT_ALGORITHM,
|
|
674
|
-
iv: this._cryptoProvider.toBase64(iv),
|
|
675
|
-
authTag: this._cryptoProvider.toBase64(authTag),
|
|
676
|
-
encryptedData: this._cryptoProvider.toBase64(encryptedData),
|
|
677
|
-
keyDerivation: {
|
|
678
|
-
kdf: 'pbkdf2',
|
|
679
|
-
salt: this._cryptoProvider.toBase64(this._salt),
|
|
680
|
-
iterations: this._iterations
|
|
681
|
-
}
|
|
682
|
-
};
|
|
683
|
-
this._keystoreFile = keystoreFileData;
|
|
684
|
-
this._dirty = false;
|
|
685
|
-
this._isNew = false;
|
|
686
|
-
return (0, ts_utils_1.succeed)(keystoreFileData);
|
|
645
|
+
return this._encryptVault(derivedKey);
|
|
687
646
|
}
|
|
688
647
|
/**
|
|
689
648
|
* Changes the master password.
|
|
@@ -797,6 +756,128 @@ class KeyStore {
|
|
|
797
756
|
cryptoProvider: this._cryptoProvider
|
|
798
757
|
});
|
|
799
758
|
}
|
|
759
|
+
// ============================================================================
|
|
760
|
+
// Private: Vault Encryption / Decryption
|
|
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
|
+
}
|
|
814
|
+
/**
|
|
815
|
+
* Decrypts the vault with a derived key and loads secrets into memory.
|
|
816
|
+
* Shared by `unlock()` and `unlockWithKey()`.
|
|
817
|
+
*/
|
|
818
|
+
async _decryptVault(derivedKey) {
|
|
819
|
+
var _a;
|
|
820
|
+
const keystoreFile = this._keystoreFile;
|
|
821
|
+
if (keystoreFile === undefined) {
|
|
822
|
+
return (0, ts_utils_1.fail)('No key store file loaded');
|
|
823
|
+
}
|
|
824
|
+
const ivResult = this._cryptoProvider.fromBase64(keystoreFile.iv);
|
|
825
|
+
const authTagResult = this._cryptoProvider.fromBase64(keystoreFile.authTag);
|
|
826
|
+
const encryptedDataResult = this._cryptoProvider.fromBase64(keystoreFile.encryptedData);
|
|
827
|
+
/* c8 ignore next 9 - base64 decode errors tested but coverage intermittently missed */
|
|
828
|
+
if (ivResult.isFailure()) {
|
|
829
|
+
return (0, ts_utils_1.fail)(`Invalid IV in key store file: ${ivResult.message}`);
|
|
830
|
+
}
|
|
831
|
+
if (authTagResult.isFailure()) {
|
|
832
|
+
return (0, ts_utils_1.fail)(`Invalid auth tag in key store file: ${authTagResult.message}`);
|
|
833
|
+
}
|
|
834
|
+
if (encryptedDataResult.isFailure()) {
|
|
835
|
+
return (0, ts_utils_1.fail)(`Invalid encrypted data in key store file: ${encryptedDataResult.message}`);
|
|
836
|
+
}
|
|
837
|
+
const decryptResult = await this._cryptoProvider.decrypt(encryptedDataResult.value, derivedKey, ivResult.value, authTagResult.value);
|
|
838
|
+
if (decryptResult.isFailure()) {
|
|
839
|
+
return (0, ts_utils_1.fail)('Incorrect password or corrupted key store');
|
|
840
|
+
}
|
|
841
|
+
// Parse the vault contents
|
|
842
|
+
const parseResult = (0, ts_utils_1.captureResult)(() => JSON.parse(decryptResult.value));
|
|
843
|
+
/* c8 ignore next 3 - error path tested but coverage intermittently missed */
|
|
844
|
+
if (parseResult.isFailure()) {
|
|
845
|
+
return (0, ts_utils_1.fail)(`Failed to parse vault contents: ${parseResult.message}`);
|
|
846
|
+
}
|
|
847
|
+
const vaultResult = converters_1.keystoreVaultContents.convert(parseResult.value);
|
|
848
|
+
/* c8 ignore next 3 - error path tested but coverage intermittently missed */
|
|
849
|
+
if (vaultResult.isFailure()) {
|
|
850
|
+
return (0, ts_utils_1.fail)(`Invalid vault format: ${vaultResult.message}`);
|
|
851
|
+
}
|
|
852
|
+
// Build secrets into local variables to avoid partial state on failure
|
|
853
|
+
const saltResult = this._cryptoProvider.fromBase64(keystoreFile.keyDerivation.salt);
|
|
854
|
+
if (saltResult.isFailure()) {
|
|
855
|
+
return (0, ts_utils_1.fail)(`Invalid salt in key store file: ${saltResult.message}`);
|
|
856
|
+
}
|
|
857
|
+
const secrets = new Map();
|
|
858
|
+
for (const [name, jsonEntry] of Object.entries(vaultResult.value.secrets)) {
|
|
859
|
+
const keyBytesResult = this._cryptoProvider.fromBase64(jsonEntry.key);
|
|
860
|
+
/* c8 ignore next 3 - error path tested but coverage intermittently missed */
|
|
861
|
+
if (keyBytesResult.isFailure()) {
|
|
862
|
+
return (0, ts_utils_1.fail)(`Invalid key for secret '${name}': ${keyBytesResult.message}`);
|
|
863
|
+
}
|
|
864
|
+
const entry = {
|
|
865
|
+
name,
|
|
866
|
+
/* c8 ignore next 1 - backwards compatibility: old vaults may lack type field */
|
|
867
|
+
type: (_a = jsonEntry.type) !== null && _a !== void 0 ? _a : 'encryption-key',
|
|
868
|
+
key: keyBytesResult.value,
|
|
869
|
+
description: jsonEntry.description,
|
|
870
|
+
createdAt: jsonEntry.createdAt
|
|
871
|
+
};
|
|
872
|
+
secrets.set(name, entry);
|
|
873
|
+
}
|
|
874
|
+
// All validation passed — commit state atomically
|
|
875
|
+
this._salt = saltResult.value;
|
|
876
|
+
this._secrets = secrets;
|
|
877
|
+
this._state = 'unlocked';
|
|
878
|
+
this._dirty = false;
|
|
879
|
+
return (0, ts_utils_1.succeed)(this);
|
|
880
|
+
}
|
|
800
881
|
}
|
|
801
882
|
exports.KeyStore = KeyStore;
|
|
802
883
|
//# 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-
|
|
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/
|
|
90
|
-
"@fgv/
|
|
91
|
-
"@fgv/ts-utils-jest": "5.1.0-
|
|
92
|
-
"@fgv/ts-utils": "5.1.0-
|
|
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-
|
|
100
|
+
"@fgv/ts-json-base": "5.1.0-16"
|
|
101
101
|
},
|
|
102
102
|
"peerDependencies": {
|
|
103
|
-
"@fgv/ts-utils": "5.1.0-
|
|
103
|
+
"@fgv/ts-utils": "5.1.0-16"
|
|
104
104
|
},
|
|
105
105
|
"repository": {
|
|
106
106
|
"type": "git",
|