@fgv/ts-extras 5.1.0-12 → 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.
- package/dist/packlets/crypto-utils/keystore/keyStore.js +97 -48
- package/dist/packlets/crypto-utils/nodeCryptoProvider.js +12 -0
- package/dist/packlets/yaml/index.js +1 -0
- package/dist/packlets/yaml/serializers.js +48 -0
- package/dist/ts-extras.d.ts +80 -1
- package/lib/packlets/crypto-utils/keystore/keyStore.d.ts +20 -0
- package/lib/packlets/crypto-utils/keystore/keyStore.js +97 -48
- package/lib/packlets/crypto-utils/model.d.ts +6 -0
- package/lib/packlets/crypto-utils/nodeCryptoProvider.d.ts +6 -0
- package/lib/packlets/crypto-utils/nodeCryptoProvider.js +12 -0
- package/lib/packlets/yaml/index.d.ts +1 -0
- package/lib/packlets/yaml/index.js +1 -0
- package/lib/packlets/yaml/serializers.d.ts +45 -0
- package/lib/packlets/yaml/serializers.js +84 -0
- package/package.json +7 -7
|
@@ -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.
|
|
@@ -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
|
|
@@ -116,6 +116,18 @@ export class NodeCryptoProvider {
|
|
|
116
116
|
});
|
|
117
117
|
});
|
|
118
118
|
}
|
|
119
|
+
/**
|
|
120
|
+
* Computes a SHA-256 hash of the given data.
|
|
121
|
+
* @param data - UTF-8 string to hash
|
|
122
|
+
* @returns `Success` with hex-encoded hash string, or `Failure` with an error.
|
|
123
|
+
*/
|
|
124
|
+
async sha256(data) {
|
|
125
|
+
return captureResult(() => {
|
|
126
|
+
const hash = crypto.createHash('sha256');
|
|
127
|
+
hash.update(data, 'utf8');
|
|
128
|
+
return hash.digest('hex');
|
|
129
|
+
});
|
|
130
|
+
}
|
|
119
131
|
// ============================================================================
|
|
120
132
|
// Platform Utility Methods
|
|
121
133
|
// ============================================================================
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* Copyright (c) 2024 Erik Fortune
|
|
3
|
+
*
|
|
4
|
+
* Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
5
|
+
* of this software and associated documentation files (the "Software"), to deal
|
|
6
|
+
* in the Software without restriction, including without limitation the rights
|
|
7
|
+
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
8
|
+
* copies of the Software, and to permit persons to whom the Software is
|
|
9
|
+
* furnished to do so, subject to the following conditions:
|
|
10
|
+
*
|
|
11
|
+
* The above copyright notice and this permission notice shall be included in all
|
|
12
|
+
* copies or substantial portions of the Software.
|
|
13
|
+
*
|
|
14
|
+
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
15
|
+
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
16
|
+
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
17
|
+
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
18
|
+
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
19
|
+
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
20
|
+
* SOFTWARE.
|
|
21
|
+
*/
|
|
22
|
+
import { captureResult, fail } from '@fgv/ts-utils';
|
|
23
|
+
import * as yaml from 'js-yaml';
|
|
24
|
+
/**
|
|
25
|
+
* Serializes a value to a YAML string.
|
|
26
|
+
* @param value - The value to serialize (must be an object or array)
|
|
27
|
+
* @param options - Optional serialization options
|
|
28
|
+
* @returns `Success` with YAML string, or `Failure` with error
|
|
29
|
+
* @public
|
|
30
|
+
*/
|
|
31
|
+
export function yamlStringify(value, options) {
|
|
32
|
+
if (value === null || value === undefined) {
|
|
33
|
+
return fail('Cannot serialize null or undefined to YAML');
|
|
34
|
+
}
|
|
35
|
+
if (typeof value !== 'object') {
|
|
36
|
+
return fail('YAML serialization requires an object or array');
|
|
37
|
+
}
|
|
38
|
+
return captureResult(() => yaml.dump(value, {
|
|
39
|
+
indent: options === null || options === void 0 ? void 0 : options.indent,
|
|
40
|
+
flowLevel: options === null || options === void 0 ? void 0 : options.flowLevel,
|
|
41
|
+
sortKeys: options === null || options === void 0 ? void 0 : options.sortKeys,
|
|
42
|
+
lineWidth: options === null || options === void 0 ? void 0 : options.lineWidth,
|
|
43
|
+
noRefs: options === null || options === void 0 ? void 0 : options.noRefs,
|
|
44
|
+
noArrayIndent: options === null || options === void 0 ? void 0 : options.noArrayIndent,
|
|
45
|
+
forceQuotes: options === null || options === void 0 ? void 0 : options.forceQuotes
|
|
46
|
+
}));
|
|
47
|
+
}
|
|
48
|
+
//# sourceMappingURL=serializers.js.map
|
package/dist/ts-extras.d.ts
CHANGED
|
@@ -939,6 +939,12 @@ declare interface ICryptoProvider {
|
|
|
939
939
|
* @returns Success with derived 32-byte key, or Failure with error
|
|
940
940
|
*/
|
|
941
941
|
deriveKey(password: string, salt: Uint8Array, iterations: number): Promise<Result<Uint8Array>>;
|
|
942
|
+
/**
|
|
943
|
+
* Computes a SHA-256 hash of the given data.
|
|
944
|
+
* @param data - UTF-8 string to hash
|
|
945
|
+
* @returns Success with hex-encoded hash string, or Failure with error
|
|
946
|
+
*/
|
|
947
|
+
sha256(data: string): Promise<Result<string>>;
|
|
942
948
|
/**
|
|
943
949
|
* Generates cryptographically secure random bytes.
|
|
944
950
|
* @param length - Number of bytes to generate
|
|
@@ -1445,6 +1451,42 @@ declare interface IVariableRef {
|
|
|
1445
1451
|
readonly isSection: boolean;
|
|
1446
1452
|
}
|
|
1447
1453
|
|
|
1454
|
+
/**
|
|
1455
|
+
* Options for YAML serialization, mirroring commonly-used `js-yaml` `DumpOptions`.
|
|
1456
|
+
* @public
|
|
1457
|
+
*/
|
|
1458
|
+
declare interface IYamlSerializeOptions {
|
|
1459
|
+
/**
|
|
1460
|
+
* Indentation width in spaces (default: 2).
|
|
1461
|
+
*/
|
|
1462
|
+
readonly indent?: number;
|
|
1463
|
+
/**
|
|
1464
|
+
* Nesting level at which to switch from block to flow style.
|
|
1465
|
+
* -1 means block style everywhere (default: -1).
|
|
1466
|
+
*/
|
|
1467
|
+
readonly flowLevel?: number;
|
|
1468
|
+
/**
|
|
1469
|
+
* If true, sort keys when dumping (default: false).
|
|
1470
|
+
*/
|
|
1471
|
+
readonly sortKeys?: boolean;
|
|
1472
|
+
/**
|
|
1473
|
+
* Maximum line width (default: 80).
|
|
1474
|
+
*/
|
|
1475
|
+
readonly lineWidth?: number;
|
|
1476
|
+
/**
|
|
1477
|
+
* If true, don't convert duplicate objects into references (default: false).
|
|
1478
|
+
*/
|
|
1479
|
+
readonly noRefs?: boolean;
|
|
1480
|
+
/**
|
|
1481
|
+
* If true, don't add an indentation level to array elements (default: false).
|
|
1482
|
+
*/
|
|
1483
|
+
readonly noArrayIndent?: boolean;
|
|
1484
|
+
/**
|
|
1485
|
+
* If true, all non-key strings will be quoted (default: false).
|
|
1486
|
+
*/
|
|
1487
|
+
readonly forceQuotes?: boolean;
|
|
1488
|
+
}
|
|
1489
|
+
|
|
1448
1490
|
/**
|
|
1449
1491
|
* Simple interface for a file to be added to a zip file.
|
|
1450
1492
|
* @public
|
|
@@ -1594,6 +1636,21 @@ declare class KeyStore_2 implements IEncryptionProvider {
|
|
|
1594
1636
|
* @public
|
|
1595
1637
|
*/
|
|
1596
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>>;
|
|
1597
1654
|
/**
|
|
1598
1655
|
* Locks the key store, clearing all secrets from memory.
|
|
1599
1656
|
* @param force - If true, discards unsaved changes
|
|
@@ -1759,6 +1816,11 @@ declare class KeyStore_2 implements IEncryptionProvider {
|
|
|
1759
1816
|
* @public
|
|
1760
1817
|
*/
|
|
1761
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;
|
|
1762
1824
|
}
|
|
1763
1825
|
|
|
1764
1826
|
/**
|
|
@@ -2000,6 +2062,12 @@ declare class NodeCryptoProvider implements ICryptoProvider {
|
|
|
2000
2062
|
* @returns `Success` with derived 32-byte key, or `Failure` with an error.
|
|
2001
2063
|
*/
|
|
2002
2064
|
deriveKey(password: string, salt: Uint8Array, iterations: number): Promise<Result<Uint8Array>>;
|
|
2065
|
+
/**
|
|
2066
|
+
* Computes a SHA-256 hash of the given data.
|
|
2067
|
+
* @param data - UTF-8 string to hash
|
|
2068
|
+
* @returns `Success` with hex-encoded hash string, or `Failure` with an error.
|
|
2069
|
+
*/
|
|
2070
|
+
sha256(data: string): Promise<Result<string>>;
|
|
2003
2071
|
/**
|
|
2004
2072
|
* Generates cryptographically secure random bytes.
|
|
2005
2073
|
* @param length - Number of bytes to generate
|
|
@@ -2312,7 +2380,9 @@ declare const uint8ArrayFromBase64: Converter<Uint8Array>;
|
|
|
2312
2380
|
|
|
2313
2381
|
declare namespace Yaml {
|
|
2314
2382
|
export {
|
|
2315
|
-
yamlConverter
|
|
2383
|
+
yamlConverter,
|
|
2384
|
+
yamlStringify,
|
|
2385
|
+
IYamlSerializeOptions
|
|
2316
2386
|
}
|
|
2317
2387
|
}
|
|
2318
2388
|
export { Yaml }
|
|
@@ -2325,6 +2395,15 @@ export { Yaml }
|
|
|
2325
2395
|
*/
|
|
2326
2396
|
declare function yamlConverter<T>(converter: Converter<T>): Converter<T>;
|
|
2327
2397
|
|
|
2398
|
+
/**
|
|
2399
|
+
* Serializes a value to a YAML string.
|
|
2400
|
+
* @param value - The value to serialize (must be an object or array)
|
|
2401
|
+
* @param options - Optional serialization options
|
|
2402
|
+
* @returns `Success` with YAML string, or `Failure` with error
|
|
2403
|
+
* @public
|
|
2404
|
+
*/
|
|
2405
|
+
declare function yamlStringify(value: unknown, options?: IYamlSerializeOptions): Result<string>;
|
|
2406
|
+
|
|
2328
2407
|
/**
|
|
2329
2408
|
* Supported compression levels for zip files.
|
|
2330
2409
|
* @public
|
|
@@ -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
|
-
|
|
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.
|
|
@@ -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
|
|
@@ -145,6 +145,12 @@ export interface ICryptoProvider {
|
|
|
145
145
|
* @returns Success with derived 32-byte key, or Failure with error
|
|
146
146
|
*/
|
|
147
147
|
deriveKey(password: string, salt: Uint8Array, iterations: number): Promise<Result<Uint8Array>>;
|
|
148
|
+
/**
|
|
149
|
+
* Computes a SHA-256 hash of the given data.
|
|
150
|
+
* @param data - UTF-8 string to hash
|
|
151
|
+
* @returns Success with hex-encoded hash string, or Failure with error
|
|
152
|
+
*/
|
|
153
|
+
sha256(data: string): Promise<Result<string>>;
|
|
148
154
|
/**
|
|
149
155
|
* Generates cryptographically secure random bytes.
|
|
150
156
|
* @param length - Number of bytes to generate
|
|
@@ -35,6 +35,12 @@ export declare class NodeCryptoProvider implements ICryptoProvider {
|
|
|
35
35
|
* @returns `Success` with derived 32-byte key, or `Failure` with an error.
|
|
36
36
|
*/
|
|
37
37
|
deriveKey(password: string, salt: Uint8Array, iterations: number): Promise<Result<Uint8Array>>;
|
|
38
|
+
/**
|
|
39
|
+
* Computes a SHA-256 hash of the given data.
|
|
40
|
+
* @param data - UTF-8 string to hash
|
|
41
|
+
* @returns `Success` with hex-encoded hash string, or `Failure` with an error.
|
|
42
|
+
*/
|
|
43
|
+
sha256(data: string): Promise<Result<string>>;
|
|
38
44
|
/**
|
|
39
45
|
* Generates cryptographically secure random bytes.
|
|
40
46
|
* @param length - Number of bytes to generate
|
|
@@ -152,6 +152,18 @@ class NodeCryptoProvider {
|
|
|
152
152
|
});
|
|
153
153
|
});
|
|
154
154
|
}
|
|
155
|
+
/**
|
|
156
|
+
* Computes a SHA-256 hash of the given data.
|
|
157
|
+
* @param data - UTF-8 string to hash
|
|
158
|
+
* @returns `Success` with hex-encoded hash string, or `Failure` with an error.
|
|
159
|
+
*/
|
|
160
|
+
async sha256(data) {
|
|
161
|
+
return (0, ts_utils_1.captureResult)(() => {
|
|
162
|
+
const hash = crypto.createHash('sha256');
|
|
163
|
+
hash.update(data, 'utf8');
|
|
164
|
+
return hash.digest('hex');
|
|
165
|
+
});
|
|
166
|
+
}
|
|
155
167
|
// ============================================================================
|
|
156
168
|
// Platform Utility Methods
|
|
157
169
|
// ============================================================================
|
|
@@ -36,4 +36,5 @@ var __exportStar = (this && this.__exportStar) || function(m, exports) {
|
|
|
36
36
|
};
|
|
37
37
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
38
38
|
__exportStar(require("./converters"), exports);
|
|
39
|
+
__exportStar(require("./serializers"), exports);
|
|
39
40
|
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
import { Result } from '@fgv/ts-utils';
|
|
2
|
+
/**
|
|
3
|
+
* Options for YAML serialization, mirroring commonly-used `js-yaml` `DumpOptions`.
|
|
4
|
+
* @public
|
|
5
|
+
*/
|
|
6
|
+
export interface IYamlSerializeOptions {
|
|
7
|
+
/**
|
|
8
|
+
* Indentation width in spaces (default: 2).
|
|
9
|
+
*/
|
|
10
|
+
readonly indent?: number;
|
|
11
|
+
/**
|
|
12
|
+
* Nesting level at which to switch from block to flow style.
|
|
13
|
+
* -1 means block style everywhere (default: -1).
|
|
14
|
+
*/
|
|
15
|
+
readonly flowLevel?: number;
|
|
16
|
+
/**
|
|
17
|
+
* If true, sort keys when dumping (default: false).
|
|
18
|
+
*/
|
|
19
|
+
readonly sortKeys?: boolean;
|
|
20
|
+
/**
|
|
21
|
+
* Maximum line width (default: 80).
|
|
22
|
+
*/
|
|
23
|
+
readonly lineWidth?: number;
|
|
24
|
+
/**
|
|
25
|
+
* If true, don't convert duplicate objects into references (default: false).
|
|
26
|
+
*/
|
|
27
|
+
readonly noRefs?: boolean;
|
|
28
|
+
/**
|
|
29
|
+
* If true, don't add an indentation level to array elements (default: false).
|
|
30
|
+
*/
|
|
31
|
+
readonly noArrayIndent?: boolean;
|
|
32
|
+
/**
|
|
33
|
+
* If true, all non-key strings will be quoted (default: false).
|
|
34
|
+
*/
|
|
35
|
+
readonly forceQuotes?: boolean;
|
|
36
|
+
}
|
|
37
|
+
/**
|
|
38
|
+
* Serializes a value to a YAML string.
|
|
39
|
+
* @param value - The value to serialize (must be an object or array)
|
|
40
|
+
* @param options - Optional serialization options
|
|
41
|
+
* @returns `Success` with YAML string, or `Failure` with error
|
|
42
|
+
* @public
|
|
43
|
+
*/
|
|
44
|
+
export declare function yamlStringify(value: unknown, options?: IYamlSerializeOptions): Result<string>;
|
|
45
|
+
//# sourceMappingURL=serializers.d.ts.map
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/*
|
|
3
|
+
* Copyright (c) 2024 Erik Fortune
|
|
4
|
+
*
|
|
5
|
+
* Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
* of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
* in the Software without restriction, including without limitation the rights
|
|
8
|
+
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
* copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
* furnished to do so, subject to the following conditions:
|
|
11
|
+
*
|
|
12
|
+
* The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
* copies or substantial portions of the Software.
|
|
14
|
+
*
|
|
15
|
+
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
* SOFTWARE.
|
|
22
|
+
*/
|
|
23
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
24
|
+
if (k2 === undefined) k2 = k;
|
|
25
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
26
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
27
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
28
|
+
}
|
|
29
|
+
Object.defineProperty(o, k2, desc);
|
|
30
|
+
}) : (function(o, m, k, k2) {
|
|
31
|
+
if (k2 === undefined) k2 = k;
|
|
32
|
+
o[k2] = m[k];
|
|
33
|
+
}));
|
|
34
|
+
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
35
|
+
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
36
|
+
}) : function(o, v) {
|
|
37
|
+
o["default"] = v;
|
|
38
|
+
});
|
|
39
|
+
var __importStar = (this && this.__importStar) || (function () {
|
|
40
|
+
var ownKeys = function(o) {
|
|
41
|
+
ownKeys = Object.getOwnPropertyNames || function (o) {
|
|
42
|
+
var ar = [];
|
|
43
|
+
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
|
|
44
|
+
return ar;
|
|
45
|
+
};
|
|
46
|
+
return ownKeys(o);
|
|
47
|
+
};
|
|
48
|
+
return function (mod) {
|
|
49
|
+
if (mod && mod.__esModule) return mod;
|
|
50
|
+
var result = {};
|
|
51
|
+
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
|
|
52
|
+
__setModuleDefault(result, mod);
|
|
53
|
+
return result;
|
|
54
|
+
};
|
|
55
|
+
})();
|
|
56
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
57
|
+
exports.yamlStringify = yamlStringify;
|
|
58
|
+
const ts_utils_1 = require("@fgv/ts-utils");
|
|
59
|
+
const yaml = __importStar(require("js-yaml"));
|
|
60
|
+
/**
|
|
61
|
+
* Serializes a value to a YAML string.
|
|
62
|
+
* @param value - The value to serialize (must be an object or array)
|
|
63
|
+
* @param options - Optional serialization options
|
|
64
|
+
* @returns `Success` with YAML string, or `Failure` with error
|
|
65
|
+
* @public
|
|
66
|
+
*/
|
|
67
|
+
function yamlStringify(value, options) {
|
|
68
|
+
if (value === null || value === undefined) {
|
|
69
|
+
return (0, ts_utils_1.fail)('Cannot serialize null or undefined to YAML');
|
|
70
|
+
}
|
|
71
|
+
if (typeof value !== 'object') {
|
|
72
|
+
return (0, ts_utils_1.fail)('YAML serialization requires an object or array');
|
|
73
|
+
}
|
|
74
|
+
return (0, ts_utils_1.captureResult)(() => yaml.dump(value, {
|
|
75
|
+
indent: options === null || options === void 0 ? void 0 : options.indent,
|
|
76
|
+
flowLevel: options === null || options === void 0 ? void 0 : options.flowLevel,
|
|
77
|
+
sortKeys: options === null || options === void 0 ? void 0 : options.sortKeys,
|
|
78
|
+
lineWidth: options === null || options === void 0 ? void 0 : options.lineWidth,
|
|
79
|
+
noRefs: options === null || options === void 0 ? void 0 : options.noRefs,
|
|
80
|
+
noArrayIndent: options === null || options === void 0 ? void 0 : options.noArrayIndent,
|
|
81
|
+
forceQuotes: options === null || options === void 0 ? void 0 : options.forceQuotes
|
|
82
|
+
}));
|
|
83
|
+
}
|
|
84
|
+
//# sourceMappingURL=serializers.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-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-
|
|
90
|
-
"@fgv/
|
|
91
|
-
"@fgv/ts-utils": "5.1.0-
|
|
92
|
-
"@fgv/
|
|
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-
|
|
100
|
+
"@fgv/ts-json-base": "5.1.0-15"
|
|
101
101
|
},
|
|
102
102
|
"peerDependencies": {
|
|
103
|
-
"@fgv/ts-utils": "5.1.0-
|
|
103
|
+
"@fgv/ts-utils": "5.1.0-15"
|
|
104
104
|
},
|
|
105
105
|
"repository": {
|
|
106
106
|
"type": "git",
|