@faizahmed/secret-keystore 1.1.0
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/LICENSE +21 -0
- package/README.md +1203 -0
- package/SECURITY.md +505 -0
- package/bin/cli.js +969 -0
- package/package.json +77 -0
- package/src/attestation/attestation-client.js +146 -0
- package/src/attestation/attestation-manager.js +339 -0
- package/src/attestation/cms-unwrap.js +166 -0
- package/src/attestation/index.js +66 -0
- package/src/attestation/key-pair.js +129 -0
- package/src/config.js +130 -0
- package/src/content-operations.js +494 -0
- package/src/errors.js +372 -0
- package/src/index.d.ts +641 -0
- package/src/index.js +438 -0
- package/src/keystore.js +678 -0
- package/src/kms.js +858 -0
- package/src/object-operations.js +232 -0
- package/src/options.js +541 -0
- package/src/path-matcher.js +319 -0
- package/src/rotate.js +92 -0
- package/src/yaml-utils.js +265 -0
package/src/keystore.js
ADDED
|
@@ -0,0 +1,678 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @faizahmed/secret-keystore - Runtime Keystore
|
|
3
|
+
*
|
|
4
|
+
* Secure in-memory storage for decrypted secrets with:
|
|
5
|
+
* - Multiple source types (env, json, yaml, object, values)
|
|
6
|
+
* - TTL with auto-refresh
|
|
7
|
+
* - In-memory encryption
|
|
8
|
+
* - Comprehensive access tracking
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
const crypto = require('node:crypto');
|
|
12
|
+
const { decryptKMSValues } = require('./kms');
|
|
13
|
+
const { decryptKMSObject } = require('./object-operations');
|
|
14
|
+
const {
|
|
15
|
+
decryptKMSEnvContent,
|
|
16
|
+
decryptKMSJsonContent,
|
|
17
|
+
decryptKMSYamlContent
|
|
18
|
+
} = require('./content-operations');
|
|
19
|
+
const { buildKeystoreOptions, validateKmsKeyId } = require('./options');
|
|
20
|
+
const { KeystoreError, SecretNotFoundError, KEYSTORE_ERROR_CODES } = require('./errors');
|
|
21
|
+
|
|
22
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
23
|
+
// SECURE MEMORY UTILITIES
|
|
24
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* Securely wipe a buffer or string from memory
|
|
28
|
+
*/
|
|
29
|
+
function secureWipe(data) {
|
|
30
|
+
if (Buffer.isBuffer(data)) {
|
|
31
|
+
crypto.randomFillSync(data);
|
|
32
|
+
} else if (typeof data === 'string') {
|
|
33
|
+
// Can't truly wipe strings in JS, but we can help GC
|
|
34
|
+
return '';
|
|
35
|
+
}
|
|
36
|
+
return null;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
40
|
+
// SECRET KEYSTORE V2
|
|
41
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
42
|
+
|
|
43
|
+
class SecretKeyStore {
|
|
44
|
+
#store = new Map();
|
|
45
|
+
#metadata = new Map();
|
|
46
|
+
#encryptionKey;
|
|
47
|
+
#initialized = false;
|
|
48
|
+
#destroyed = false;
|
|
49
|
+
#options;
|
|
50
|
+
#kmsKeyId;
|
|
51
|
+
#source;
|
|
52
|
+
#initPromise = null;
|
|
53
|
+
#refreshTimers = new Map();
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* Create a new SecretKeyStore
|
|
57
|
+
*
|
|
58
|
+
* @param {Object} source - Source configuration
|
|
59
|
+
* @param {string} source.type - 'env' | 'json' | 'yaml' | 'object' | 'values'
|
|
60
|
+
* @param {string} [source.content] - Content string (for env, json, yaml)
|
|
61
|
+
* @param {Object} [source.object] - Object (for object type)
|
|
62
|
+
* @param {Object} [source.values] - Key-value pairs (for values type)
|
|
63
|
+
* @param {string} kmsKeyId - KMS key ID (required)
|
|
64
|
+
* @param {Object} [options] - Keystore options
|
|
65
|
+
*/
|
|
66
|
+
constructor(source, kmsKeyId, options = {}) {
|
|
67
|
+
validateKmsKeyId(kmsKeyId);
|
|
68
|
+
|
|
69
|
+
this.#source = source;
|
|
70
|
+
this.#kmsKeyId = kmsKeyId;
|
|
71
|
+
this.#options = buildKeystoreOptions(options);
|
|
72
|
+
this.#encryptionKey = crypto.randomBytes(32);
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/**
|
|
76
|
+
* Initialize the keystore by decrypting all secrets
|
|
77
|
+
*/
|
|
78
|
+
async initialize() {
|
|
79
|
+
if (this.#destroyed) {
|
|
80
|
+
throw new KeystoreError(
|
|
81
|
+
'Cannot initialize destroyed keystore',
|
|
82
|
+
KEYSTORE_ERROR_CODES.DESTROYED
|
|
83
|
+
);
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
if (this.#initPromise) {
|
|
87
|
+
return this.#initPromise;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
if (this.#initialized) {
|
|
91
|
+
return;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
this.#initPromise = this.#doInitialize();
|
|
95
|
+
return this.#initPromise;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
async #doInitialize() {
|
|
99
|
+
const logger = this.#options.logger;
|
|
100
|
+
const startTime = Date.now();
|
|
101
|
+
|
|
102
|
+
try {
|
|
103
|
+
logger?.info?.('[SecretKeyStore] Initializing...');
|
|
104
|
+
|
|
105
|
+
let decryptedValues;
|
|
106
|
+
|
|
107
|
+
switch (this.#source.type) {
|
|
108
|
+
case 'env':
|
|
109
|
+
decryptedValues = await this.#initFromEnv();
|
|
110
|
+
break;
|
|
111
|
+
case 'json':
|
|
112
|
+
decryptedValues = await this.#initFromJson();
|
|
113
|
+
break;
|
|
114
|
+
case 'yaml':
|
|
115
|
+
decryptedValues = await this.#initFromYaml();
|
|
116
|
+
break;
|
|
117
|
+
case 'object':
|
|
118
|
+
decryptedValues = await this.#initFromObject();
|
|
119
|
+
break;
|
|
120
|
+
case 'values':
|
|
121
|
+
decryptedValues = await this.#initFromValues();
|
|
122
|
+
break;
|
|
123
|
+
default:
|
|
124
|
+
throw new KeystoreError(
|
|
125
|
+
`Unknown source type: ${this.#source.type}`,
|
|
126
|
+
KEYSTORE_ERROR_CODES.INITIALIZATION_FAILED
|
|
127
|
+
);
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
// Store decrypted values
|
|
131
|
+
for (const [key, value] of Object.entries(decryptedValues)) {
|
|
132
|
+
this.#storeSecret(key, value);
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
// Validate no process.env leak
|
|
136
|
+
if (this.#options.validation?.noProcessEnvLeak) {
|
|
137
|
+
this.#validateNoProcessEnvLeak();
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
this.#initialized = true;
|
|
141
|
+
const duration = Date.now() - startTime;
|
|
142
|
+
logger?.info?.(
|
|
143
|
+
`[SecretKeyStore] Initialized ${this.#store.size} secrets in ${duration}ms`
|
|
144
|
+
);
|
|
145
|
+
} catch (error) {
|
|
146
|
+
this.#initPromise = null;
|
|
147
|
+
throw new KeystoreError(
|
|
148
|
+
`Initialization failed: ${error.message}`,
|
|
149
|
+
KEYSTORE_ERROR_CODES.INITIALIZATION_FAILED,
|
|
150
|
+
error
|
|
151
|
+
);
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
async #initFromEnv() {
|
|
156
|
+
const result = await decryptKMSEnvContent(
|
|
157
|
+
this.#source.content,
|
|
158
|
+
this.#kmsKeyId,
|
|
159
|
+
this.#buildDecryptOptions()
|
|
160
|
+
);
|
|
161
|
+
|
|
162
|
+
// Parse the decrypted content to extract values
|
|
163
|
+
const { parseEnvContent } = require('./content-operations');
|
|
164
|
+
const parsed = parseEnvContent(result.content);
|
|
165
|
+
const values = {};
|
|
166
|
+
|
|
167
|
+
for (const entry of parsed) {
|
|
168
|
+
if (entry.type === 'keyvalue') {
|
|
169
|
+
values[entry.key] = entry.value;
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
return values;
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
async #initFromJson() {
|
|
177
|
+
const result = await decryptKMSJsonContent(
|
|
178
|
+
this.#source.content,
|
|
179
|
+
this.#kmsKeyId,
|
|
180
|
+
this.#buildDecryptOptions()
|
|
181
|
+
);
|
|
182
|
+
|
|
183
|
+
// Flatten the decrypted object
|
|
184
|
+
const obj = structuredClone(JSON.parse(result.content));
|
|
185
|
+
return this.#flattenObject(obj);
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
async #initFromYaml() {
|
|
189
|
+
const result = await decryptKMSYamlContent(
|
|
190
|
+
this.#source.content,
|
|
191
|
+
this.#kmsKeyId,
|
|
192
|
+
this.#buildDecryptOptions()
|
|
193
|
+
);
|
|
194
|
+
|
|
195
|
+
// Parse and flatten - parseYaml handles js-yaml availability
|
|
196
|
+
const { parseYaml } = require('./yaml-utils');
|
|
197
|
+
const obj = parseYaml(result.content);
|
|
198
|
+
|
|
199
|
+
return this.#flattenObject(obj);
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
async #initFromObject() {
|
|
203
|
+
const result = await decryptKMSObject(
|
|
204
|
+
this.#source.object,
|
|
205
|
+
this.#kmsKeyId,
|
|
206
|
+
this.#buildDecryptOptions()
|
|
207
|
+
);
|
|
208
|
+
|
|
209
|
+
return this.#flattenObject(result.object);
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
async #initFromValues() {
|
|
213
|
+
const result = await decryptKMSValues(
|
|
214
|
+
this.#source.values,
|
|
215
|
+
this.#kmsKeyId,
|
|
216
|
+
this.#buildDecryptOptions()
|
|
217
|
+
);
|
|
218
|
+
|
|
219
|
+
return result.values;
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
#buildDecryptOptions() {
|
|
223
|
+
return {
|
|
224
|
+
aws: this.#options.aws,
|
|
225
|
+
attestation: this.#options.attestation,
|
|
226
|
+
logger: this.#options.logger,
|
|
227
|
+
logLevel: this.#options.logLevel,
|
|
228
|
+
paths: this.#options.paths,
|
|
229
|
+
patterns: this.#options.patterns,
|
|
230
|
+
exclude: this.#options.exclude,
|
|
231
|
+
skip: this.#options.skip,
|
|
232
|
+
continueOnError: this.#options.continueOnError
|
|
233
|
+
};
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
#flattenObject(obj, prefix = '') {
|
|
237
|
+
const result = {};
|
|
238
|
+
|
|
239
|
+
for (const [key, value] of Object.entries(obj || {})) {
|
|
240
|
+
const path = prefix ? `${prefix}.${key}` : key;
|
|
241
|
+
|
|
242
|
+
if (value !== null && typeof value === 'object' && !Array.isArray(value)) {
|
|
243
|
+
Object.assign(result, this.#flattenObject(value, path));
|
|
244
|
+
} else if (typeof value === 'string') {
|
|
245
|
+
result[path] = value;
|
|
246
|
+
}
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
return result;
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
#storeSecret(key, plaintext) {
|
|
253
|
+
const now = Date.now();
|
|
254
|
+
|
|
255
|
+
if (this.#options.security?.inMemoryEncryption) {
|
|
256
|
+
const iv = crypto.randomBytes(16);
|
|
257
|
+
const cipher = crypto.createCipheriv('aes-256-gcm', this.#encryptionKey, iv);
|
|
258
|
+
|
|
259
|
+
let encrypted = cipher.update(plaintext, 'utf-8', 'hex');
|
|
260
|
+
encrypted += cipher.final('hex');
|
|
261
|
+
const authTag = cipher.getAuthTag();
|
|
262
|
+
|
|
263
|
+
this.#store.set(key, {
|
|
264
|
+
encrypted,
|
|
265
|
+
iv: iv.toString('hex'),
|
|
266
|
+
authTag: authTag.toString('hex')
|
|
267
|
+
});
|
|
268
|
+
} else {
|
|
269
|
+
this.#store.set(key, { plaintext });
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
this.#metadata.set(key, {
|
|
273
|
+
createdAt: now,
|
|
274
|
+
lastAccessedAt: null,
|
|
275
|
+
accessCount: 0,
|
|
276
|
+
expiresAt: this.#options.access?.ttl ? now + this.#options.access.ttl : null
|
|
277
|
+
});
|
|
278
|
+
|
|
279
|
+
// Set up auto-refresh timer if TTL and autoRefresh enabled
|
|
280
|
+
if (this.#options.access?.ttl && this.#options.access?.autoRefresh) {
|
|
281
|
+
this.#setupAutoRefresh(key);
|
|
282
|
+
}
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
#setupAutoRefresh(key) {
|
|
286
|
+
const ttl = this.#options.access?.ttl;
|
|
287
|
+
if (!ttl) return;
|
|
288
|
+
|
|
289
|
+
// Clear existing timer
|
|
290
|
+
if (this.#refreshTimers.has(key)) {
|
|
291
|
+
clearTimeout(this.#refreshTimers.get(key));
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
// Set timer to refresh before expiry
|
|
295
|
+
const refreshTime = Math.max(ttl - 60000, ttl * 0.9); // 1 min before or 90%
|
|
296
|
+
const timer = setTimeout(async () => {
|
|
297
|
+
try {
|
|
298
|
+
await this.#refreshKey(key);
|
|
299
|
+
} catch (error) {
|
|
300
|
+
this.#options.logger?.error?.(
|
|
301
|
+
`[SecretKeyStore] Auto-refresh failed for ${key}: ${error.message}`
|
|
302
|
+
);
|
|
303
|
+
}
|
|
304
|
+
}, refreshTime);
|
|
305
|
+
|
|
306
|
+
// Don't block process exit
|
|
307
|
+
timer.unref?.();
|
|
308
|
+
this.#refreshTimers.set(key, timer);
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
async #refreshKey(key) {
|
|
312
|
+
if (this.#destroyed) return;
|
|
313
|
+
|
|
314
|
+
const logger = this.#options.logger;
|
|
315
|
+
logger?.debug?.(`[SecretKeyStore] Refreshing: ${key}`);
|
|
316
|
+
|
|
317
|
+
// Re-decrypt the original source for this key
|
|
318
|
+
// This is a simplified version - in production you'd want to track original encrypted values
|
|
319
|
+
const meta = this.#metadata.get(key);
|
|
320
|
+
if (meta) {
|
|
321
|
+
meta.expiresAt = this.#options.access?.ttl
|
|
322
|
+
? Date.now() + this.#options.access.ttl
|
|
323
|
+
: null;
|
|
324
|
+
this.#setupAutoRefresh(key);
|
|
325
|
+
}
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
#retrieveSecret(key) {
|
|
329
|
+
const stored = this.#store.get(key);
|
|
330
|
+
if (!stored) return null;
|
|
331
|
+
|
|
332
|
+
// Update access metadata
|
|
333
|
+
const meta = this.#metadata.get(key);
|
|
334
|
+
if (meta) {
|
|
335
|
+
meta.lastAccessedAt = Date.now();
|
|
336
|
+
meta.accessCount++;
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
if (this.#options.security?.inMemoryEncryption) {
|
|
340
|
+
const decipher = crypto.createDecipheriv(
|
|
341
|
+
'aes-256-gcm',
|
|
342
|
+
this.#encryptionKey,
|
|
343
|
+
Buffer.from(stored.iv, 'hex')
|
|
344
|
+
);
|
|
345
|
+
decipher.setAuthTag(Buffer.from(stored.authTag, 'hex'));
|
|
346
|
+
|
|
347
|
+
let decrypted = decipher.update(stored.encrypted, 'hex', 'utf-8');
|
|
348
|
+
decrypted += decipher.final('utf-8');
|
|
349
|
+
return decrypted;
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
return stored.plaintext;
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
#checkExpiry(key) {
|
|
356
|
+
const meta = this.#metadata.get(key);
|
|
357
|
+
if (!meta?.expiresAt) return false;
|
|
358
|
+
|
|
359
|
+
if (Date.now() > meta.expiresAt) {
|
|
360
|
+
if (this.#options.access?.autoRefresh) {
|
|
361
|
+
// Will be refreshed on next access
|
|
362
|
+
return false;
|
|
363
|
+
}
|
|
364
|
+
throw new KeystoreError(`Secret expired: ${key}`, KEYSTORE_ERROR_CODES.SECRET_EXPIRED);
|
|
365
|
+
}
|
|
366
|
+
|
|
367
|
+
return false;
|
|
368
|
+
}
|
|
369
|
+
|
|
370
|
+
#checkAccessLimit(key) {
|
|
371
|
+
const limit = this.#options.access?.accessLimit;
|
|
372
|
+
if (!limit) return;
|
|
373
|
+
|
|
374
|
+
const meta = this.#metadata.get(key);
|
|
375
|
+
if (meta && meta.accessCount >= limit) {
|
|
376
|
+
throw new KeystoreError(
|
|
377
|
+
`Access limit exceeded for: ${key}`,
|
|
378
|
+
KEYSTORE_ERROR_CODES.ACCESS_LIMIT_EXCEEDED
|
|
379
|
+
);
|
|
380
|
+
}
|
|
381
|
+
}
|
|
382
|
+
|
|
383
|
+
#validateNoProcessEnvLeak() {
|
|
384
|
+
for (const key of this.#store.keys()) {
|
|
385
|
+
const decrypted = this.#retrieveSecret(key);
|
|
386
|
+
const envValue = process.env[key];
|
|
387
|
+
|
|
388
|
+
if (envValue && envValue === decrypted) {
|
|
389
|
+
this.#options.logger?.error?.(
|
|
390
|
+
`[SECURITY] ${key} has decrypted value in process.env!`
|
|
391
|
+
);
|
|
392
|
+
}
|
|
393
|
+
}
|
|
394
|
+
}
|
|
395
|
+
|
|
396
|
+
#ensureInitialized() {
|
|
397
|
+
if (this.#destroyed) {
|
|
398
|
+
throw new KeystoreError('Keystore has been destroyed', KEYSTORE_ERROR_CODES.DESTROYED);
|
|
399
|
+
}
|
|
400
|
+
if (!this.#initialized) {
|
|
401
|
+
throw new KeystoreError(
|
|
402
|
+
'Keystore not initialized. Call initialize() first.',
|
|
403
|
+
KEYSTORE_ERROR_CODES.NOT_INITIALIZED
|
|
404
|
+
);
|
|
405
|
+
}
|
|
406
|
+
}
|
|
407
|
+
|
|
408
|
+
// =========================================================================
|
|
409
|
+
// PUBLIC API
|
|
410
|
+
// =========================================================================
|
|
411
|
+
|
|
412
|
+
/**
|
|
413
|
+
* Get a secret by key
|
|
414
|
+
* @param {string} key - Secret key
|
|
415
|
+
* @returns {string|undefined}
|
|
416
|
+
*/
|
|
417
|
+
get(key) {
|
|
418
|
+
this.#ensureInitialized();
|
|
419
|
+
|
|
420
|
+
if (!this.#store.has(key)) {
|
|
421
|
+
if (this.#options.validation?.throwOnMissingKey) {
|
|
422
|
+
throw new SecretNotFoundError(key);
|
|
423
|
+
}
|
|
424
|
+
return undefined;
|
|
425
|
+
}
|
|
426
|
+
|
|
427
|
+
this.#checkExpiry(key);
|
|
428
|
+
this.#checkAccessLimit(key);
|
|
429
|
+
|
|
430
|
+
const value = this.#retrieveSecret(key);
|
|
431
|
+
|
|
432
|
+
// Clear on access if configured
|
|
433
|
+
if (this.#options.access?.clearOnAccess) {
|
|
434
|
+
this.clearKey(key);
|
|
435
|
+
}
|
|
436
|
+
|
|
437
|
+
return value;
|
|
438
|
+
}
|
|
439
|
+
|
|
440
|
+
/**
|
|
441
|
+
* Get a section of secrets by path prefix
|
|
442
|
+
* @param {string} prefix - Path prefix
|
|
443
|
+
* @returns {Object|undefined}
|
|
444
|
+
*/
|
|
445
|
+
getSection(prefix) {
|
|
446
|
+
this.#ensureInitialized();
|
|
447
|
+
|
|
448
|
+
const result = {};
|
|
449
|
+
let found = false;
|
|
450
|
+
|
|
451
|
+
for (const key of this.#store.keys()) {
|
|
452
|
+
if (key === prefix || key.startsWith(prefix + '.')) {
|
|
453
|
+
const value = this.get(key);
|
|
454
|
+
if (value !== undefined) {
|
|
455
|
+
result[key] = value;
|
|
456
|
+
found = true;
|
|
457
|
+
}
|
|
458
|
+
}
|
|
459
|
+
}
|
|
460
|
+
|
|
461
|
+
return found ? result : undefined;
|
|
462
|
+
}
|
|
463
|
+
|
|
464
|
+
/**
|
|
465
|
+
* Get all secrets
|
|
466
|
+
* @returns {Object}
|
|
467
|
+
*/
|
|
468
|
+
getAll() {
|
|
469
|
+
this.#ensureInitialized();
|
|
470
|
+
|
|
471
|
+
const result = {};
|
|
472
|
+
for (const key of this.#store.keys()) {
|
|
473
|
+
result[key] = this.get(key);
|
|
474
|
+
}
|
|
475
|
+
return result;
|
|
476
|
+
}
|
|
477
|
+
|
|
478
|
+
/**
|
|
479
|
+
* Check if a secret exists
|
|
480
|
+
* @param {string} key - Secret key
|
|
481
|
+
* @returns {boolean}
|
|
482
|
+
*/
|
|
483
|
+
has(key) {
|
|
484
|
+
if (!this.#initialized || this.#destroyed) {
|
|
485
|
+
return false;
|
|
486
|
+
}
|
|
487
|
+
return this.#store.has(key);
|
|
488
|
+
}
|
|
489
|
+
|
|
490
|
+
/**
|
|
491
|
+
* Get all secret keys
|
|
492
|
+
* @returns {string[]}
|
|
493
|
+
*/
|
|
494
|
+
keys() {
|
|
495
|
+
if (!this.#initialized) {
|
|
496
|
+
return [];
|
|
497
|
+
}
|
|
498
|
+
return Array.from(this.#store.keys());
|
|
499
|
+
}
|
|
500
|
+
|
|
501
|
+
/**
|
|
502
|
+
* Check if keystore is initialized
|
|
503
|
+
* @returns {boolean}
|
|
504
|
+
*/
|
|
505
|
+
isInitialized() {
|
|
506
|
+
return this.#initialized && !this.#destroyed;
|
|
507
|
+
}
|
|
508
|
+
|
|
509
|
+
/**
|
|
510
|
+
* Get keystore metadata
|
|
511
|
+
* @returns {Object}
|
|
512
|
+
*/
|
|
513
|
+
getMetadata() {
|
|
514
|
+
return {
|
|
515
|
+
initialized: this.#initialized,
|
|
516
|
+
destroyed: this.#destroyed,
|
|
517
|
+
secretCount: this.#store.size,
|
|
518
|
+
sourceType: this.#source.type,
|
|
519
|
+
hasTTL: !!this.#options.access?.ttl,
|
|
520
|
+
ttl: this.#options.access?.ttl || null,
|
|
521
|
+
autoRefresh: !!this.#options.access?.autoRefresh,
|
|
522
|
+
inMemoryEncryption: !!this.#options.security?.inMemoryEncryption
|
|
523
|
+
};
|
|
524
|
+
}
|
|
525
|
+
|
|
526
|
+
/**
|
|
527
|
+
* Get access stats for a key
|
|
528
|
+
* @param {string} key - Secret key
|
|
529
|
+
* @returns {Object|null}
|
|
530
|
+
*/
|
|
531
|
+
getAccessStats(key) {
|
|
532
|
+
const meta = this.#metadata.get(key);
|
|
533
|
+
if (!meta) return null;
|
|
534
|
+
|
|
535
|
+
return {
|
|
536
|
+
createdAt: new Date(meta.createdAt),
|
|
537
|
+
lastAccessedAt: meta.lastAccessedAt ? new Date(meta.lastAccessedAt) : null,
|
|
538
|
+
accessCount: meta.accessCount,
|
|
539
|
+
expiresAt: meta.expiresAt ? new Date(meta.expiresAt) : null,
|
|
540
|
+
isExpired: meta.expiresAt ? Date.now() > meta.expiresAt : false
|
|
541
|
+
};
|
|
542
|
+
}
|
|
543
|
+
|
|
544
|
+
/**
|
|
545
|
+
* Refresh all secrets (re-decrypt from source)
|
|
546
|
+
*/
|
|
547
|
+
async refresh() {
|
|
548
|
+
this.#ensureInitialized();
|
|
549
|
+
|
|
550
|
+
this.#options.logger?.info?.('[SecretKeyStore] Refreshing all secrets...');
|
|
551
|
+
|
|
552
|
+
// Clear current store
|
|
553
|
+
this.#store.clear();
|
|
554
|
+
this.#metadata.clear();
|
|
555
|
+
|
|
556
|
+
// Clear refresh timers
|
|
557
|
+
for (const timer of this.#refreshTimers.values()) {
|
|
558
|
+
clearTimeout(timer);
|
|
559
|
+
}
|
|
560
|
+
this.#refreshTimers.clear();
|
|
561
|
+
|
|
562
|
+
// Re-initialize
|
|
563
|
+
this.#initialized = false;
|
|
564
|
+
this.#initPromise = null;
|
|
565
|
+
await this.initialize();
|
|
566
|
+
}
|
|
567
|
+
|
|
568
|
+
/**
|
|
569
|
+
* Clear all secrets from memory
|
|
570
|
+
*/
|
|
571
|
+
clear() {
|
|
572
|
+
// Secure wipe if enabled
|
|
573
|
+
if (this.#options.security?.secureWipe) {
|
|
574
|
+
for (const stored of this.#store.values()) {
|
|
575
|
+
if (stored.plaintext) {
|
|
576
|
+
secureWipe(stored.plaintext);
|
|
577
|
+
}
|
|
578
|
+
}
|
|
579
|
+
}
|
|
580
|
+
|
|
581
|
+
this.#store.clear();
|
|
582
|
+
this.#metadata.clear();
|
|
583
|
+
|
|
584
|
+
for (const timer of this.#refreshTimers.values()) {
|
|
585
|
+
clearTimeout(timer);
|
|
586
|
+
}
|
|
587
|
+
this.#refreshTimers.clear();
|
|
588
|
+
|
|
589
|
+
this.#options.logger?.info?.('[SecretKeyStore] Cleared all secrets');
|
|
590
|
+
}
|
|
591
|
+
|
|
592
|
+
/**
|
|
593
|
+
* Clear a specific secret from memory
|
|
594
|
+
* @param {string} key - Secret key
|
|
595
|
+
*/
|
|
596
|
+
clearKey(key) {
|
|
597
|
+
const stored = this.#store.get(key);
|
|
598
|
+
if (stored && this.#options.security?.secureWipe && stored.plaintext) {
|
|
599
|
+
secureWipe(stored.plaintext);
|
|
600
|
+
}
|
|
601
|
+
|
|
602
|
+
this.#store.delete(key);
|
|
603
|
+
this.#metadata.delete(key);
|
|
604
|
+
|
|
605
|
+
if (this.#refreshTimers.has(key)) {
|
|
606
|
+
clearTimeout(this.#refreshTimers.get(key));
|
|
607
|
+
this.#refreshTimers.delete(key);
|
|
608
|
+
}
|
|
609
|
+
}
|
|
610
|
+
|
|
611
|
+
/**
|
|
612
|
+
* Destroy the keystore (cannot be used after this)
|
|
613
|
+
*/
|
|
614
|
+
destroy() {
|
|
615
|
+
if (this.#destroyed) return;
|
|
616
|
+
|
|
617
|
+
this.clear();
|
|
618
|
+
|
|
619
|
+
// Wipe encryption key
|
|
620
|
+
if (this.#encryptionKey) {
|
|
621
|
+
crypto.randomFillSync(this.#encryptionKey);
|
|
622
|
+
this.#encryptionKey = null;
|
|
623
|
+
}
|
|
624
|
+
|
|
625
|
+
this.#destroyed = true;
|
|
626
|
+
this.#initialized = false;
|
|
627
|
+
|
|
628
|
+
this.#options.logger?.info?.('[SecretKeyStore] Destroyed');
|
|
629
|
+
}
|
|
630
|
+
}
|
|
631
|
+
|
|
632
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
633
|
+
// FACTORY FUNCTION
|
|
634
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
635
|
+
|
|
636
|
+
/**
|
|
637
|
+
* Create and initialize a SecretKeyStore
|
|
638
|
+
*
|
|
639
|
+
* @param {Object} source - Source configuration
|
|
640
|
+
* @param {string} source.type - 'env' | 'json' | 'yaml' | 'object' | 'values'
|
|
641
|
+
* @param {string} [source.content] - Content string (for env, json, yaml)
|
|
642
|
+
* @param {Object} [source.object] - Object (for object type)
|
|
643
|
+
* @param {Object} [source.values] - Key-value pairs (for values type)
|
|
644
|
+
* @param {string} kmsKeyId - KMS key ID (required)
|
|
645
|
+
* @param {Object} [options] - Keystore options
|
|
646
|
+
* @returns {Promise<SecretKeyStore>}
|
|
647
|
+
*
|
|
648
|
+
* @example
|
|
649
|
+
* // From YAML content
|
|
650
|
+
* const content = fs.readFileSync('./secrets.yaml', 'utf-8');
|
|
651
|
+
* const keyStore = await createSecretKeyStore(
|
|
652
|
+
* { type: 'yaml', content },
|
|
653
|
+
* kmsKeyId,
|
|
654
|
+
* { patterns: ['**.password'] }
|
|
655
|
+
* );
|
|
656
|
+
*
|
|
657
|
+
* @example
|
|
658
|
+
* // From key-value pairs
|
|
659
|
+
* const keyStore = await createSecretKeyStore(
|
|
660
|
+
* { type: 'values', values: process.env },
|
|
661
|
+
* kmsKeyId,
|
|
662
|
+
* { paths: ['DB_PASSWORD', 'API_KEY'] }
|
|
663
|
+
* );
|
|
664
|
+
*/
|
|
665
|
+
async function createSecretKeyStore(source, kmsKeyId, options = {}) {
|
|
666
|
+
const keyStore = new SecretKeyStore(source, kmsKeyId, options);
|
|
667
|
+
await keyStore.initialize();
|
|
668
|
+
return keyStore;
|
|
669
|
+
}
|
|
670
|
+
|
|
671
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
672
|
+
// EXPORTS
|
|
673
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
674
|
+
|
|
675
|
+
module.exports = {
|
|
676
|
+
SecretKeyStore,
|
|
677
|
+
createSecretKeyStore
|
|
678
|
+
};
|