vaultkeeper 0.3.0 → 0.5.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/dist/index.cjs +1213 -40
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +188 -8
- package/dist/index.d.ts +188 -8
- package/dist/index.js +1211 -38
- package/dist/index.js.map +1 -1
- package/dist/one-password-worker.js +104 -0
- package/dist/one-password-worker.js.map +1 -0
- package/package.json +3 -1
package/dist/index.js
CHANGED
|
@@ -1,9 +1,12 @@
|
|
|
1
|
-
import
|
|
2
|
-
import * as
|
|
3
|
-
import
|
|
1
|
+
import * as fs from 'fs/promises';
|
|
2
|
+
import * as path3 from 'path';
|
|
3
|
+
import { join, dirname, resolve } from 'path';
|
|
4
4
|
import * as os4 from 'os';
|
|
5
|
-
import * as
|
|
5
|
+
import * as crypto from 'crypto';
|
|
6
|
+
import { spawn } from 'child_process';
|
|
7
|
+
import { fileURLToPath } from 'url';
|
|
6
8
|
import * as fs4 from 'fs';
|
|
9
|
+
import { existsSync, readFileSync } from 'fs';
|
|
7
10
|
import { CompactEncrypt, compactDecrypt } from 'jose';
|
|
8
11
|
|
|
9
12
|
// src/errors.ts
|
|
@@ -136,6 +139,22 @@ var IdentityMismatchError = class extends VaultError {
|
|
|
136
139
|
this.currentHash = currentHash;
|
|
137
140
|
}
|
|
138
141
|
};
|
|
142
|
+
var InvalidAlgorithmError = class extends VaultError {
|
|
143
|
+
/**
|
|
144
|
+
* The algorithm that was requested.
|
|
145
|
+
*/
|
|
146
|
+
algorithm;
|
|
147
|
+
/**
|
|
148
|
+
* The set of algorithms that are allowed.
|
|
149
|
+
*/
|
|
150
|
+
allowed;
|
|
151
|
+
constructor(message, algorithm, allowed) {
|
|
152
|
+
super(message);
|
|
153
|
+
this.name = "InvalidAlgorithmError";
|
|
154
|
+
this.algorithm = algorithm;
|
|
155
|
+
this.allowed = allowed;
|
|
156
|
+
}
|
|
157
|
+
};
|
|
139
158
|
var SetupError = class extends VaultError {
|
|
140
159
|
/**
|
|
141
160
|
* The name of the dependency that caused the setup failure.
|
|
@@ -171,14 +190,10 @@ var RotationInProgressError = class extends VaultError {
|
|
|
171
190
|
}
|
|
172
191
|
};
|
|
173
192
|
|
|
174
|
-
// src/backend/types.ts
|
|
175
|
-
function isListableBackend(backend) {
|
|
176
|
-
return "list" in backend && typeof backend.list === "function";
|
|
177
|
-
}
|
|
178
|
-
|
|
179
193
|
// src/backend/registry.ts
|
|
180
194
|
var BackendRegistry = class {
|
|
181
195
|
static backends = /* @__PURE__ */ new Map();
|
|
196
|
+
static setups = /* @__PURE__ */ new Map();
|
|
182
197
|
/**
|
|
183
198
|
* Register a backend factory.
|
|
184
199
|
* @param type - Backend type identifier
|
|
@@ -190,10 +205,11 @@ var BackendRegistry = class {
|
|
|
190
205
|
/**
|
|
191
206
|
* Create a backend instance by type.
|
|
192
207
|
* @param type - Backend type identifier
|
|
208
|
+
* @param config - Optional backend configuration forwarded to the factory
|
|
193
209
|
* @returns A SecretBackend instance
|
|
194
|
-
* @throws
|
|
210
|
+
* @throws {@link BackendUnavailableError} if the backend type is not registered
|
|
195
211
|
*/
|
|
196
|
-
static create(type) {
|
|
212
|
+
static create(type, config) {
|
|
197
213
|
const factory = this.backends.get(type);
|
|
198
214
|
if (factory === void 0) {
|
|
199
215
|
throw new BackendUnavailableError(
|
|
@@ -202,7 +218,7 @@ var BackendRegistry = class {
|
|
|
202
218
|
Array.from(this.backends.keys())
|
|
203
219
|
);
|
|
204
220
|
}
|
|
205
|
-
return factory();
|
|
221
|
+
return factory(config);
|
|
206
222
|
}
|
|
207
223
|
/**
|
|
208
224
|
* Get all registered backend type identifiers.
|
|
@@ -238,18 +254,199 @@ var BackendRegistry = class {
|
|
|
238
254
|
);
|
|
239
255
|
return results.filter((type) => type !== null);
|
|
240
256
|
}
|
|
257
|
+
/**
|
|
258
|
+
* Register a setup factory for a backend type.
|
|
259
|
+
* @param type - Backend type identifier
|
|
260
|
+
* @param factory - Factory function that creates a setup generator
|
|
261
|
+
*/
|
|
262
|
+
static registerSetup(type, factory) {
|
|
263
|
+
this.setups.set(type, factory);
|
|
264
|
+
}
|
|
265
|
+
/**
|
|
266
|
+
* Get the setup factory for a backend type, if one is registered.
|
|
267
|
+
* @param type - Backend type identifier
|
|
268
|
+
* @returns The setup factory, or `undefined` if none is registered
|
|
269
|
+
*/
|
|
270
|
+
static getSetup(type) {
|
|
271
|
+
return this.setups.get(type);
|
|
272
|
+
}
|
|
273
|
+
/**
|
|
274
|
+
* Check whether a setup factory is registered for the given backend type.
|
|
275
|
+
* @param type - Backend type identifier
|
|
276
|
+
* @returns `true` if a setup factory is registered
|
|
277
|
+
*/
|
|
278
|
+
static hasSetup(type) {
|
|
279
|
+
return this.setups.has(type);
|
|
280
|
+
}
|
|
281
|
+
/**
|
|
282
|
+
* Clear all registered backend factories.
|
|
283
|
+
* Intended for use in tests only.
|
|
284
|
+
* @internal
|
|
285
|
+
*/
|
|
286
|
+
static clearBackends() {
|
|
287
|
+
this.backends.clear();
|
|
288
|
+
}
|
|
289
|
+
/**
|
|
290
|
+
* Clear all registered setup factories.
|
|
291
|
+
* Intended for use in tests only.
|
|
292
|
+
* @internal
|
|
293
|
+
*/
|
|
294
|
+
static clearSetups() {
|
|
295
|
+
this.setups.clear();
|
|
296
|
+
}
|
|
297
|
+
};
|
|
298
|
+
var STORAGE_DIR_NAME = path3.join(".vaultkeeper", "file");
|
|
299
|
+
var KEY_FILE = ".key";
|
|
300
|
+
var GCM_IV_BYTES = 12;
|
|
301
|
+
var GCM_KEY_BYTES = 32;
|
|
302
|
+
var GCM_TAG_LENGTH = 128;
|
|
303
|
+
function getStorageDir() {
|
|
304
|
+
return path3.join(os4.homedir(), STORAGE_DIR_NAME);
|
|
305
|
+
}
|
|
306
|
+
function getEntryPath(storageDir, id) {
|
|
307
|
+
const safeId = Buffer.from(id, "utf8").toString("hex");
|
|
308
|
+
return path3.join(storageDir, `${safeId}.enc`);
|
|
309
|
+
}
|
|
310
|
+
async function ensureStorageDir(storageDir) {
|
|
311
|
+
try {
|
|
312
|
+
await fs.mkdir(storageDir, { recursive: true, mode: 448 });
|
|
313
|
+
} catch (err) {
|
|
314
|
+
if (err instanceof Error && "code" in err && err.code !== "EEXIST") {
|
|
315
|
+
throw new FilesystemError(
|
|
316
|
+
`Failed to create storage directory: ${storageDir}`,
|
|
317
|
+
storageDir,
|
|
318
|
+
"rwx"
|
|
319
|
+
);
|
|
320
|
+
}
|
|
321
|
+
}
|
|
322
|
+
}
|
|
323
|
+
async function getOrCreateKey(storageDir) {
|
|
324
|
+
const keyPath = path3.join(storageDir, KEY_FILE);
|
|
325
|
+
try {
|
|
326
|
+
const data = await fs.readFile(keyPath);
|
|
327
|
+
return data;
|
|
328
|
+
} catch (err) {
|
|
329
|
+
if (err instanceof Error && "code" in err && err.code === "ENOENT") {
|
|
330
|
+
const key = crypto.randomBytes(GCM_KEY_BYTES);
|
|
331
|
+
await fs.writeFile(keyPath, key, { mode: 384 });
|
|
332
|
+
return key;
|
|
333
|
+
}
|
|
334
|
+
throw err;
|
|
335
|
+
}
|
|
336
|
+
}
|
|
337
|
+
function encryptGcm(key, plaintext) {
|
|
338
|
+
const iv = crypto.randomBytes(GCM_IV_BYTES);
|
|
339
|
+
const cipher = crypto.createCipheriv("aes-256-gcm", key, iv, {
|
|
340
|
+
authTagLength: GCM_TAG_LENGTH / 8
|
|
341
|
+
});
|
|
342
|
+
const encrypted = Buffer.concat([cipher.update(plaintext, "utf8"), cipher.final()]);
|
|
343
|
+
const authTag = cipher.getAuthTag();
|
|
344
|
+
return [iv.toString("base64"), authTag.toString("base64"), encrypted.toString("base64")].join(":");
|
|
345
|
+
}
|
|
346
|
+
function decryptGcm(key, encoded) {
|
|
347
|
+
const parts = encoded.split(":");
|
|
348
|
+
if (parts.length !== 3) {
|
|
349
|
+
throw new Error("Invalid encrypted file format: expected iv:authTag:ciphertext");
|
|
350
|
+
}
|
|
351
|
+
const [ivB64, authTagB64, ciphertextB64] = parts;
|
|
352
|
+
if (ivB64 === void 0 || authTagB64 === void 0 || ciphertextB64 === void 0) {
|
|
353
|
+
throw new Error("Invalid encrypted file format: missing part");
|
|
354
|
+
}
|
|
355
|
+
const iv = Buffer.from(ivB64, "base64");
|
|
356
|
+
const authTag = Buffer.from(authTagB64, "base64");
|
|
357
|
+
const ciphertext = Buffer.from(ciphertextB64, "base64");
|
|
358
|
+
const decipher = crypto.createDecipheriv("aes-256-gcm", key, iv, {
|
|
359
|
+
authTagLength: GCM_TAG_LENGTH / 8
|
|
360
|
+
});
|
|
361
|
+
decipher.setAuthTag(authTag);
|
|
362
|
+
const decrypted = Buffer.concat([decipher.update(ciphertext), decipher.final()]);
|
|
363
|
+
return decrypted.toString("utf8");
|
|
364
|
+
}
|
|
365
|
+
var FileBackend = class {
|
|
366
|
+
type = "file";
|
|
367
|
+
displayName = "Encrypted File Store";
|
|
368
|
+
async isAvailable() {
|
|
369
|
+
try {
|
|
370
|
+
const storageDir = getStorageDir();
|
|
371
|
+
await ensureStorageDir(storageDir);
|
|
372
|
+
return true;
|
|
373
|
+
} catch {
|
|
374
|
+
return false;
|
|
375
|
+
}
|
|
376
|
+
}
|
|
377
|
+
async store(id, secret) {
|
|
378
|
+
const storageDir = getStorageDir();
|
|
379
|
+
await ensureStorageDir(storageDir);
|
|
380
|
+
const key = await getOrCreateKey(storageDir);
|
|
381
|
+
const entryPath = getEntryPath(storageDir, id);
|
|
382
|
+
const encrypted = encryptGcm(key, secret);
|
|
383
|
+
await fs.writeFile(entryPath, encrypted, { mode: 384 });
|
|
384
|
+
}
|
|
385
|
+
async retrieve(id) {
|
|
386
|
+
const storageDir = getStorageDir();
|
|
387
|
+
const entryPath = getEntryPath(storageDir, id);
|
|
388
|
+
let encoded;
|
|
389
|
+
try {
|
|
390
|
+
encoded = await fs.readFile(entryPath, "utf8");
|
|
391
|
+
} catch (err) {
|
|
392
|
+
if (err instanceof Error && "code" in err && err.code === "ENOENT") {
|
|
393
|
+
throw new SecretNotFoundError(`Secret not found in file store: ${id}`);
|
|
394
|
+
}
|
|
395
|
+
throw err;
|
|
396
|
+
}
|
|
397
|
+
const key = await getOrCreateKey(storageDir);
|
|
398
|
+
try {
|
|
399
|
+
return decryptGcm(key, encoded);
|
|
400
|
+
} catch (err) {
|
|
401
|
+
throw new Error(
|
|
402
|
+
`Failed to decrypt secret: ${err instanceof Error ? err.message : String(err)}`
|
|
403
|
+
);
|
|
404
|
+
}
|
|
405
|
+
}
|
|
406
|
+
async delete(id) {
|
|
407
|
+
const storageDir = getStorageDir();
|
|
408
|
+
const entryPath = getEntryPath(storageDir, id);
|
|
409
|
+
try {
|
|
410
|
+
await fs.unlink(entryPath);
|
|
411
|
+
} catch (err) {
|
|
412
|
+
if (err instanceof Error && "code" in err && err.code === "ENOENT") {
|
|
413
|
+
throw new SecretNotFoundError(`Secret not found in file store: ${id}`);
|
|
414
|
+
}
|
|
415
|
+
throw err;
|
|
416
|
+
}
|
|
417
|
+
}
|
|
418
|
+
async exists(id) {
|
|
419
|
+
const storageDir = getStorageDir();
|
|
420
|
+
const entryPath = getEntryPath(storageDir, id);
|
|
421
|
+
try {
|
|
422
|
+
await fs.access(entryPath);
|
|
423
|
+
return true;
|
|
424
|
+
} catch {
|
|
425
|
+
return false;
|
|
426
|
+
}
|
|
427
|
+
}
|
|
428
|
+
async list() {
|
|
429
|
+
const storageDir = getStorageDir();
|
|
430
|
+
let entries;
|
|
431
|
+
try {
|
|
432
|
+
entries = await fs.readdir(storageDir);
|
|
433
|
+
} catch {
|
|
434
|
+
return [];
|
|
435
|
+
}
|
|
436
|
+
return entries.filter((f) => f.endsWith(".enc")).map((f) => Buffer.from(f.slice(0, -4), "hex").toString("utf8"));
|
|
437
|
+
}
|
|
241
438
|
};
|
|
242
439
|
async function execCommand(command, args, options) {
|
|
243
|
-
const result = await execCommandFull(command, args);
|
|
440
|
+
const result = await execCommandFull(command, args, options);
|
|
244
441
|
if (result.exitCode !== 0) {
|
|
245
442
|
throw new Error(`Command failed with exit code ${String(result.exitCode)}: ${result.stderr}`);
|
|
246
443
|
}
|
|
247
444
|
return result.stdout.trim();
|
|
248
445
|
}
|
|
249
446
|
function execCommandFull(command, args, options) {
|
|
250
|
-
return new Promise((
|
|
447
|
+
return new Promise((resolve2, reject) => {
|
|
251
448
|
const proc = spawn(command, args, {
|
|
252
|
-
stdio: ["ignore", "pipe", "pipe"]
|
|
449
|
+
stdio: [options?.stdin !== void 0 ? "pipe" : "ignore", "pipe", "pipe"]
|
|
253
450
|
});
|
|
254
451
|
let stdout = "";
|
|
255
452
|
let stderr = "";
|
|
@@ -259,25 +456,887 @@ function execCommandFull(command, args, options) {
|
|
|
259
456
|
proc.stderr?.on("data", (data) => {
|
|
260
457
|
stderr += data.toString();
|
|
261
458
|
});
|
|
459
|
+
if (options?.stdin !== void 0 && proc.stdin) {
|
|
460
|
+
proc.stdin.write(options.stdin);
|
|
461
|
+
proc.stdin.end();
|
|
462
|
+
}
|
|
463
|
+
if (options?.timeoutMs !== void 0) {
|
|
464
|
+
setTimeout(() => {
|
|
465
|
+
proc.kill("SIGTERM");
|
|
466
|
+
reject(new Error(`Command timed out after ${String(options.timeoutMs)}ms`));
|
|
467
|
+
}, options.timeoutMs);
|
|
468
|
+
}
|
|
262
469
|
proc.on("close", (code) => {
|
|
263
|
-
|
|
470
|
+
resolve2({ stdout, stderr, exitCode: code ?? 1 });
|
|
264
471
|
});
|
|
265
472
|
proc.on("error", (error) => {
|
|
266
473
|
reject(error);
|
|
267
474
|
});
|
|
268
475
|
});
|
|
269
476
|
}
|
|
270
|
-
|
|
271
|
-
|
|
477
|
+
|
|
478
|
+
// src/backend/keychain-backend.ts
|
|
479
|
+
var ACCOUNT = "vaultkeeper";
|
|
480
|
+
var SERVICE_PREFIX = "vaultkeeper:";
|
|
481
|
+
var KeychainBackend = class {
|
|
482
|
+
type = "keychain";
|
|
483
|
+
displayName = "macOS Keychain";
|
|
484
|
+
async isAvailable() {
|
|
485
|
+
if (process.platform !== "darwin") {
|
|
486
|
+
return false;
|
|
487
|
+
}
|
|
488
|
+
try {
|
|
489
|
+
const result = await execCommandFull("security", ["version"]);
|
|
490
|
+
return result.exitCode === 0;
|
|
491
|
+
} catch {
|
|
492
|
+
return false;
|
|
493
|
+
}
|
|
494
|
+
}
|
|
495
|
+
async store(id, secret) {
|
|
496
|
+
const service = `${SERVICE_PREFIX}${id}`;
|
|
497
|
+
const encoded = Buffer.from(secret, "utf8").toString("base64");
|
|
498
|
+
await execCommandFull("security", [
|
|
499
|
+
"delete-generic-password",
|
|
500
|
+
"-a",
|
|
501
|
+
ACCOUNT,
|
|
502
|
+
"-s",
|
|
503
|
+
service
|
|
504
|
+
]);
|
|
505
|
+
await execCommand("security", [
|
|
506
|
+
"add-generic-password",
|
|
507
|
+
"-a",
|
|
508
|
+
ACCOUNT,
|
|
509
|
+
"-s",
|
|
510
|
+
service,
|
|
511
|
+
"-w",
|
|
512
|
+
encoded
|
|
513
|
+
]);
|
|
514
|
+
}
|
|
515
|
+
async retrieve(id) {
|
|
516
|
+
const service = `${SERVICE_PREFIX}${id}`;
|
|
517
|
+
const result = await execCommandFull("security", [
|
|
518
|
+
"find-generic-password",
|
|
519
|
+
"-a",
|
|
520
|
+
ACCOUNT,
|
|
521
|
+
"-s",
|
|
522
|
+
service,
|
|
523
|
+
"-w"
|
|
524
|
+
]);
|
|
525
|
+
if (result.exitCode !== 0) {
|
|
526
|
+
throw new SecretNotFoundError(`Secret not found in macOS Keychain: ${id}`);
|
|
527
|
+
}
|
|
528
|
+
const encoded = result.stdout.trim();
|
|
529
|
+
return Buffer.from(encoded, "base64").toString("utf8");
|
|
530
|
+
}
|
|
531
|
+
async delete(id) {
|
|
532
|
+
const service = `${SERVICE_PREFIX}${id}`;
|
|
533
|
+
const result = await execCommandFull("security", [
|
|
534
|
+
"delete-generic-password",
|
|
535
|
+
"-a",
|
|
536
|
+
ACCOUNT,
|
|
537
|
+
"-s",
|
|
538
|
+
service
|
|
539
|
+
]);
|
|
540
|
+
if (result.exitCode !== 0) {
|
|
541
|
+
throw new SecretNotFoundError(`Secret not found in macOS Keychain: ${id}`);
|
|
542
|
+
}
|
|
543
|
+
}
|
|
544
|
+
async exists(id) {
|
|
545
|
+
const service = `${SERVICE_PREFIX}${id}`;
|
|
546
|
+
const result = await execCommandFull("security", [
|
|
547
|
+
"find-generic-password",
|
|
548
|
+
"-a",
|
|
549
|
+
ACCOUNT,
|
|
550
|
+
"-s",
|
|
551
|
+
service
|
|
552
|
+
]);
|
|
553
|
+
return result.exitCode === 0;
|
|
554
|
+
}
|
|
555
|
+
async list() {
|
|
556
|
+
const result = await execCommandFull("security", [
|
|
557
|
+
"dump-keychain"
|
|
558
|
+
]);
|
|
559
|
+
if (result.exitCode !== 0) {
|
|
560
|
+
return [];
|
|
561
|
+
}
|
|
562
|
+
const ids = [];
|
|
563
|
+
const servicePattern = /0x00000007 <blob>="vaultkeeper:([^"]+)"/g;
|
|
564
|
+
let match = servicePattern.exec(result.stdout);
|
|
565
|
+
while (match !== null) {
|
|
566
|
+
const id = match[1];
|
|
567
|
+
if (id !== void 0) {
|
|
568
|
+
ids.push(id);
|
|
569
|
+
}
|
|
570
|
+
match = servicePattern.exec(result.stdout);
|
|
571
|
+
}
|
|
572
|
+
return ids;
|
|
573
|
+
}
|
|
574
|
+
};
|
|
575
|
+
function getStoragePath() {
|
|
576
|
+
return path3.join(os4.homedir(), ".vaultkeeper", "dpapi");
|
|
577
|
+
}
|
|
578
|
+
function getEntryPath2(storageDir, id) {
|
|
579
|
+
const safeId = Buffer.from(id, "utf8").toString("hex");
|
|
580
|
+
return path3.join(storageDir, `${safeId}.enc`);
|
|
581
|
+
}
|
|
582
|
+
var DpapiBackend = class {
|
|
583
|
+
type = "dpapi";
|
|
584
|
+
displayName = "Windows DPAPI";
|
|
585
|
+
async isAvailable() {
|
|
586
|
+
if (process.platform !== "win32") {
|
|
587
|
+
return false;
|
|
588
|
+
}
|
|
589
|
+
try {
|
|
590
|
+
const result = await execCommandFull("powershell", [
|
|
591
|
+
"-NoProfile",
|
|
592
|
+
"-Command",
|
|
593
|
+
"[System.Security.Cryptography.ProtectedData] | Out-Null; exit 0"
|
|
594
|
+
]);
|
|
595
|
+
return result.exitCode === 0;
|
|
596
|
+
} catch {
|
|
597
|
+
return false;
|
|
598
|
+
}
|
|
599
|
+
}
|
|
600
|
+
async store(id, secret) {
|
|
601
|
+
const storageDir = getStoragePath();
|
|
602
|
+
await fs.mkdir(storageDir, { recursive: true });
|
|
603
|
+
const entryPath = getEntryPath2(storageDir, id);
|
|
604
|
+
const script = [
|
|
605
|
+
"Add-Type -AssemblyName System.Security",
|
|
606
|
+
`$bytes = [System.Text.Encoding]::UTF8.GetBytes(${JSON.stringify(secret)})`,
|
|
607
|
+
"$entropy = $null",
|
|
608
|
+
"$scope = [System.Security.Cryptography.DataProtectionScope]::CurrentUser",
|
|
609
|
+
"$encrypted = [System.Security.Cryptography.ProtectedData]::Protect($bytes, $entropy, $scope)",
|
|
610
|
+
`[System.IO.File]::WriteAllBytes(${JSON.stringify(entryPath)}, $encrypted)`
|
|
611
|
+
].join("; ");
|
|
612
|
+
await execCommand("powershell", ["-NoProfile", "-Command", script]);
|
|
613
|
+
}
|
|
614
|
+
async retrieve(id) {
|
|
615
|
+
const storageDir = getStoragePath();
|
|
616
|
+
const entryPath = getEntryPath2(storageDir, id);
|
|
617
|
+
try {
|
|
618
|
+
await fs.access(entryPath);
|
|
619
|
+
} catch {
|
|
620
|
+
throw new SecretNotFoundError(`Secret not found in Windows DPAPI store: ${id}`);
|
|
621
|
+
}
|
|
622
|
+
const script = [
|
|
623
|
+
"Add-Type -AssemblyName System.Security",
|
|
624
|
+
`$encrypted = [System.IO.File]::ReadAllBytes(${JSON.stringify(entryPath)})`,
|
|
625
|
+
"$entropy = $null",
|
|
626
|
+
"$scope = [System.Security.Cryptography.DataProtectionScope]::CurrentUser",
|
|
627
|
+
"$bytes = [System.Security.Cryptography.ProtectedData]::Unprotect($encrypted, $entropy, $scope)",
|
|
628
|
+
"Write-Output ([System.Text.Encoding]::UTF8.GetString($bytes))"
|
|
629
|
+
].join("; ");
|
|
630
|
+
return execCommand("powershell", ["-NoProfile", "-Command", script]);
|
|
631
|
+
}
|
|
632
|
+
async delete(id) {
|
|
633
|
+
const storageDir = getStoragePath();
|
|
634
|
+
const entryPath = getEntryPath2(storageDir, id);
|
|
635
|
+
try {
|
|
636
|
+
await fs.unlink(entryPath);
|
|
637
|
+
} catch (err) {
|
|
638
|
+
if (err instanceof Error && "code" in err && err.code === "ENOENT") {
|
|
639
|
+
throw new SecretNotFoundError(`Secret not found in Windows DPAPI store: ${id}`);
|
|
640
|
+
}
|
|
641
|
+
throw err;
|
|
642
|
+
}
|
|
643
|
+
}
|
|
644
|
+
async exists(id) {
|
|
645
|
+
const storageDir = getStoragePath();
|
|
646
|
+
const entryPath = getEntryPath2(storageDir, id);
|
|
647
|
+
try {
|
|
648
|
+
await fs.access(entryPath);
|
|
649
|
+
return true;
|
|
650
|
+
} catch {
|
|
651
|
+
return false;
|
|
652
|
+
}
|
|
653
|
+
}
|
|
654
|
+
async list() {
|
|
655
|
+
const storageDir = getStoragePath();
|
|
656
|
+
let entries;
|
|
657
|
+
try {
|
|
658
|
+
entries = await fs.readdir(storageDir);
|
|
659
|
+
} catch {
|
|
660
|
+
return [];
|
|
661
|
+
}
|
|
662
|
+
return entries.filter((f) => f.endsWith(".enc")).map((f) => Buffer.from(f.slice(0, -4), "hex").toString("utf8"));
|
|
663
|
+
}
|
|
664
|
+
};
|
|
665
|
+
|
|
666
|
+
// src/backend/secret-tool-backend.ts
|
|
667
|
+
var ATTRIBUTE_KEY = "vaultkeeper-id";
|
|
668
|
+
var LABEL_PREFIX = "vaultkeeper: ";
|
|
669
|
+
var SecretToolBackend = class {
|
|
670
|
+
type = "secret-tool";
|
|
671
|
+
displayName = "Linux Secret Service (secret-tool)";
|
|
672
|
+
async isAvailable() {
|
|
673
|
+
if (process.platform !== "linux") {
|
|
674
|
+
return false;
|
|
675
|
+
}
|
|
676
|
+
try {
|
|
677
|
+
const result = await execCommandFull("secret-tool", ["--version"]);
|
|
678
|
+
return result.exitCode === 0;
|
|
679
|
+
} catch {
|
|
680
|
+
return false;
|
|
681
|
+
}
|
|
682
|
+
}
|
|
683
|
+
async store(id, secret) {
|
|
684
|
+
const label = `${LABEL_PREFIX}${id}`;
|
|
685
|
+
await execCommand(
|
|
686
|
+
"secret-tool",
|
|
687
|
+
["store", "--label", label, ATTRIBUTE_KEY, id],
|
|
688
|
+
{ stdin: secret }
|
|
689
|
+
);
|
|
690
|
+
}
|
|
691
|
+
async retrieve(id) {
|
|
692
|
+
const result = await execCommandFull("secret-tool", ["lookup", ATTRIBUTE_KEY, id]);
|
|
693
|
+
if (result.exitCode !== 0 || result.stdout.trim() === "") {
|
|
694
|
+
throw new SecretNotFoundError(`Secret not found in Secret Service: ${id}`);
|
|
695
|
+
}
|
|
696
|
+
return result.stdout.trim();
|
|
697
|
+
}
|
|
698
|
+
async delete(id) {
|
|
699
|
+
const result = await execCommandFull("secret-tool", ["clear", ATTRIBUTE_KEY, id]);
|
|
700
|
+
if (result.exitCode !== 0) {
|
|
701
|
+
throw new SecretNotFoundError(`Secret not found in Secret Service: ${id}`);
|
|
702
|
+
}
|
|
703
|
+
}
|
|
704
|
+
async exists(id) {
|
|
705
|
+
const result = await execCommandFull("secret-tool", ["lookup", ATTRIBUTE_KEY, id]);
|
|
706
|
+
return result.exitCode === 0 && result.stdout.trim() !== "";
|
|
707
|
+
}
|
|
708
|
+
async list() {
|
|
709
|
+
const result = await execCommandFull("secret-tool", [
|
|
710
|
+
"search",
|
|
711
|
+
ATTRIBUTE_KEY,
|
|
712
|
+
""
|
|
713
|
+
]);
|
|
714
|
+
if (result.exitCode !== 0) {
|
|
715
|
+
return [];
|
|
716
|
+
}
|
|
717
|
+
const ids = [];
|
|
718
|
+
const attrPattern = new RegExp(`attribute\\.${ATTRIBUTE_KEY} = (.+)`, "g");
|
|
719
|
+
let match = attrPattern.exec(result.stdout);
|
|
720
|
+
while (match !== null) {
|
|
721
|
+
const id = match[1];
|
|
722
|
+
if (id !== void 0) {
|
|
723
|
+
ids.push(id);
|
|
724
|
+
}
|
|
725
|
+
match = attrPattern.exec(result.stdout);
|
|
726
|
+
}
|
|
727
|
+
return ids;
|
|
728
|
+
}
|
|
729
|
+
};
|
|
730
|
+
var INTEGRATION_NAME = "vaultkeeper";
|
|
731
|
+
var cachedVersion;
|
|
732
|
+
function getIntegrationVersion() {
|
|
733
|
+
if (cachedVersion !== void 0) return cachedVersion;
|
|
734
|
+
const dir = dirname(fileURLToPath(import.meta.url));
|
|
735
|
+
const candidates = [
|
|
736
|
+
resolve(dir, "..", "..", "package.json"),
|
|
737
|
+
resolve(dir, "..", "package.json")
|
|
738
|
+
];
|
|
739
|
+
for (const candidate of candidates) {
|
|
740
|
+
if (!existsSync(candidate)) continue;
|
|
741
|
+
const raw = JSON.parse(readFileSync(candidate, "utf8"));
|
|
742
|
+
if (raw !== null && typeof raw === "object" && "version" in raw && typeof raw.version === "string") {
|
|
743
|
+
cachedVersion = raw.version;
|
|
744
|
+
return cachedVersion;
|
|
745
|
+
}
|
|
746
|
+
}
|
|
747
|
+
throw new Error(
|
|
748
|
+
`Could not read version from vaultkeeper package.json. Tried paths: ${candidates.join(", ")}`
|
|
749
|
+
);
|
|
750
|
+
}
|
|
751
|
+
|
|
752
|
+
// src/backend/one-password-backend.ts
|
|
753
|
+
var SDK_INSTALL_URL = "https://developer.1password.com/docs/sdks/";
|
|
754
|
+
var TAG = "vaultkeeper";
|
|
755
|
+
var PASSWORD_FIELD_TITLE = "password";
|
|
756
|
+
var SESSION_TIMEOUT_MS = 3e4;
|
|
757
|
+
function isWorkerSuccess(res) {
|
|
758
|
+
return "value" in res;
|
|
759
|
+
}
|
|
760
|
+
function isWorkerResponse(value) {
|
|
761
|
+
if (value === null || typeof value !== "object") return false;
|
|
762
|
+
if ("value" in value && typeof value.value === "string") return true;
|
|
763
|
+
if ("error" in value && typeof value.error === "string" && "code" in value && typeof value.code === "string")
|
|
764
|
+
return true;
|
|
765
|
+
return false;
|
|
766
|
+
}
|
|
767
|
+
var OnePasswordBackend = class {
|
|
768
|
+
type = "1password";
|
|
769
|
+
displayName = "1Password";
|
|
770
|
+
vaultId;
|
|
771
|
+
account;
|
|
772
|
+
serviceAccountToken;
|
|
773
|
+
accessMode;
|
|
774
|
+
sessionTimeoutMs;
|
|
775
|
+
/** In-flight or resolved client promise — prevents duplicate createClient calls. */
|
|
776
|
+
clientPromise;
|
|
777
|
+
constructor(options) {
|
|
778
|
+
if (options.accessMode === "per-access" && options.serviceAccountToken !== void 0) {
|
|
779
|
+
throw new Error(
|
|
780
|
+
"per-access mode requires desktop biometric authentication and cannot be used with a service account token"
|
|
781
|
+
);
|
|
782
|
+
}
|
|
783
|
+
if (options.account !== void 0 && options.serviceAccountToken !== void 0) {
|
|
784
|
+
throw new Error(
|
|
785
|
+
"account and serviceAccountToken are mutually exclusive \u2014 provide one or the other, not both"
|
|
786
|
+
);
|
|
787
|
+
}
|
|
788
|
+
this.vaultId = options.vault;
|
|
789
|
+
this.sessionTimeoutMs = options.sessionTimeoutMs ?? SESSION_TIMEOUT_MS;
|
|
790
|
+
if (options.account !== void 0) {
|
|
791
|
+
this.account = options.account;
|
|
792
|
+
}
|
|
793
|
+
if (options.serviceAccountToken !== void 0) {
|
|
794
|
+
this.serviceAccountToken = options.serviceAccountToken;
|
|
795
|
+
}
|
|
796
|
+
this.accessMode = options.accessMode ?? "session";
|
|
797
|
+
}
|
|
798
|
+
async isAvailable() {
|
|
799
|
+
const sdk = await this.tryLoadSdk();
|
|
800
|
+
return sdk !== null;
|
|
801
|
+
}
|
|
802
|
+
// ---- Session client management ----
|
|
803
|
+
/**
|
|
804
|
+
* Dynamically import the SDK. Returns `null` if the SDK is not installed or
|
|
805
|
+
* the native library cannot be loaded.
|
|
806
|
+
*/
|
|
807
|
+
async tryLoadSdk() {
|
|
808
|
+
try {
|
|
809
|
+
const sdk = await import('@1password/sdk');
|
|
810
|
+
return sdk;
|
|
811
|
+
} catch {
|
|
812
|
+
return null;
|
|
813
|
+
}
|
|
814
|
+
}
|
|
815
|
+
/**
|
|
816
|
+
* Acquire (or create) a cached SDK client.
|
|
817
|
+
* Wraps `createClient` with a configurable timeout (default 30 s) to handle
|
|
818
|
+
* the known beta SDK hang after session expiry.
|
|
819
|
+
*/
|
|
820
|
+
acquireClient() {
|
|
821
|
+
this.clientPromise ??= this.createClientInternal().catch((err) => {
|
|
822
|
+
this.clientPromise = void 0;
|
|
823
|
+
throw err;
|
|
824
|
+
});
|
|
825
|
+
return this.clientPromise;
|
|
826
|
+
}
|
|
827
|
+
async createClientInternal() {
|
|
828
|
+
const sdk = await this.tryLoadSdk();
|
|
829
|
+
if (sdk === null) {
|
|
830
|
+
throw new PluginNotFoundError(
|
|
831
|
+
"1Password SDK (@1password/sdk) is not available. Install it to use this backend.",
|
|
832
|
+
"@1password/sdk",
|
|
833
|
+
SDK_INSTALL_URL
|
|
834
|
+
);
|
|
835
|
+
}
|
|
836
|
+
const auth = this.buildAuth(sdk);
|
|
837
|
+
let timerId;
|
|
838
|
+
const timeoutPromise = new Promise((_resolve, reject) => {
|
|
839
|
+
timerId = setTimeout(() => {
|
|
840
|
+
reject(new BackendLockedError("1Password session timed out waiting for authentication", true));
|
|
841
|
+
}, this.sessionTimeoutMs);
|
|
842
|
+
});
|
|
843
|
+
try {
|
|
844
|
+
const client = await Promise.race([
|
|
845
|
+
sdk.createClient({
|
|
846
|
+
auth,
|
|
847
|
+
integrationName: INTEGRATION_NAME,
|
|
848
|
+
integrationVersion: getIntegrationVersion()
|
|
849
|
+
}),
|
|
850
|
+
timeoutPromise
|
|
851
|
+
]);
|
|
852
|
+
return client;
|
|
853
|
+
} catch (err) {
|
|
854
|
+
if (err instanceof BackendLockedError) {
|
|
855
|
+
throw err;
|
|
856
|
+
}
|
|
857
|
+
if (err instanceof sdk.DesktopSessionExpiredError) {
|
|
858
|
+
throw new BackendLockedError(
|
|
859
|
+
"1Password session has expired. Please unlock the app.",
|
|
860
|
+
true
|
|
861
|
+
);
|
|
862
|
+
}
|
|
863
|
+
throw new AuthorizationDeniedError(
|
|
864
|
+
`1Password authentication failed: ${String(err)}`
|
|
865
|
+
);
|
|
866
|
+
} finally {
|
|
867
|
+
if (timerId !== void 0) {
|
|
868
|
+
clearTimeout(timerId);
|
|
869
|
+
}
|
|
870
|
+
}
|
|
871
|
+
}
|
|
872
|
+
buildAuth(sdk) {
|
|
873
|
+
if (this.serviceAccountToken !== void 0) {
|
|
874
|
+
return this.serviceAccountToken;
|
|
875
|
+
}
|
|
876
|
+
const accountName = this.account ?? "";
|
|
877
|
+
return new sdk.DesktopAuth(accountName);
|
|
878
|
+
}
|
|
879
|
+
// ---- Helpers for item lookup by title ----
|
|
880
|
+
/**
|
|
881
|
+
* List all items in the vault tagged "vaultkeeper" and find one with the
|
|
882
|
+
* matching title (= secret ID). Returns `undefined` if not found.
|
|
883
|
+
*/
|
|
884
|
+
async findItemOverview(client, id) {
|
|
885
|
+
const overviews = await client.items.list(this.vaultId);
|
|
886
|
+
for (const overview of overviews) {
|
|
887
|
+
if (overview.title === id && overview.tags.includes(TAG)) {
|
|
888
|
+
return overview;
|
|
889
|
+
}
|
|
890
|
+
}
|
|
891
|
+
return void 0;
|
|
892
|
+
}
|
|
893
|
+
/**
|
|
894
|
+
* Fetch the full item for a given secret id. Returns `undefined` if not found.
|
|
895
|
+
*/
|
|
896
|
+
async findItem(client, id) {
|
|
897
|
+
const overview = await this.findItemOverview(client, id);
|
|
898
|
+
if (overview === void 0) return void 0;
|
|
899
|
+
return client.items.get(this.vaultId, overview.id);
|
|
900
|
+
}
|
|
901
|
+
/**
|
|
902
|
+
* Extract the concealed password field value from an item.
|
|
903
|
+
*/
|
|
904
|
+
extractSecret(item, id) {
|
|
905
|
+
for (const field of item.fields) {
|
|
906
|
+
if (field.title === PASSWORD_FIELD_TITLE) {
|
|
907
|
+
return field.value;
|
|
908
|
+
}
|
|
909
|
+
}
|
|
910
|
+
throw new SecretNotFoundError(
|
|
911
|
+
`Secret found in 1Password but missing password field: ${id}`
|
|
912
|
+
);
|
|
913
|
+
}
|
|
914
|
+
// ---- SecretBackend / ListableBackend implementation ----
|
|
915
|
+
async store(id, secret) {
|
|
916
|
+
const { ItemCategory, ItemFieldType } = await this.requireSdk();
|
|
917
|
+
const client = await this.acquireClient();
|
|
918
|
+
const existing = await this.findItem(client, id);
|
|
919
|
+
if (existing !== void 0) {
|
|
920
|
+
const hasPasswordField = existing.fields.some((f) => f.title === PASSWORD_FIELD_TITLE);
|
|
921
|
+
const updatedFields = hasPasswordField ? existing.fields.map((f) => {
|
|
922
|
+
if (f.title === PASSWORD_FIELD_TITLE) {
|
|
923
|
+
return { ...f, value: secret };
|
|
924
|
+
}
|
|
925
|
+
return f;
|
|
926
|
+
}) : [
|
|
927
|
+
...existing.fields,
|
|
928
|
+
{
|
|
929
|
+
id: "password",
|
|
930
|
+
title: PASSWORD_FIELD_TITLE,
|
|
931
|
+
fieldType: ItemFieldType.Concealed,
|
|
932
|
+
value: secret
|
|
933
|
+
}
|
|
934
|
+
];
|
|
935
|
+
await client.items.put({ ...existing, fields: updatedFields });
|
|
936
|
+
} else {
|
|
937
|
+
await client.items.create({
|
|
938
|
+
category: ItemCategory.Password,
|
|
939
|
+
vaultId: this.vaultId,
|
|
940
|
+
title: id,
|
|
941
|
+
tags: [TAG],
|
|
942
|
+
fields: [
|
|
943
|
+
{
|
|
944
|
+
id: "password",
|
|
945
|
+
title: PASSWORD_FIELD_TITLE,
|
|
946
|
+
fieldType: ItemFieldType.Concealed,
|
|
947
|
+
value: secret
|
|
948
|
+
}
|
|
949
|
+
]
|
|
950
|
+
});
|
|
951
|
+
}
|
|
952
|
+
}
|
|
953
|
+
async retrieve(id) {
|
|
954
|
+
if (this.accessMode === "per-access") {
|
|
955
|
+
return this.retrieveViaWorker(id);
|
|
956
|
+
}
|
|
957
|
+
return this.retrieveViaSession(id);
|
|
958
|
+
}
|
|
959
|
+
async retrieveViaSession(id) {
|
|
960
|
+
const client = await this.acquireClient();
|
|
961
|
+
const item = await this.findItem(client, id);
|
|
962
|
+
if (item === void 0) {
|
|
963
|
+
throw new SecretNotFoundError(`Secret not found in 1Password: ${id}`);
|
|
964
|
+
}
|
|
965
|
+
return this.extractSecret(item, id);
|
|
966
|
+
}
|
|
967
|
+
/**
|
|
968
|
+
* Spawn the per-access worker script that triggers a fresh biometric prompt
|
|
969
|
+
* for each retrieval, then returns the secret from its stdout.
|
|
970
|
+
*/
|
|
971
|
+
retrieveViaWorker(id) {
|
|
972
|
+
return new Promise((resolve2, reject) => {
|
|
973
|
+
const workerPath = join(
|
|
974
|
+
dirname(fileURLToPath(import.meta.url)),
|
|
975
|
+
"one-password-worker.js"
|
|
976
|
+
);
|
|
977
|
+
const accountArg = this.account ?? "";
|
|
978
|
+
const child = spawn(
|
|
979
|
+
process.execPath,
|
|
980
|
+
[workerPath, accountArg, this.vaultId, id],
|
|
981
|
+
{ stdio: ["ignore", "pipe", "pipe"] }
|
|
982
|
+
);
|
|
983
|
+
const stdoutChunks = [];
|
|
984
|
+
const stderrChunks = [];
|
|
985
|
+
child.stdout.on("data", (chunk) => {
|
|
986
|
+
stdoutChunks.push(chunk);
|
|
987
|
+
});
|
|
988
|
+
child.stderr.on("data", (chunk) => {
|
|
989
|
+
stderrChunks.push(chunk);
|
|
990
|
+
});
|
|
991
|
+
child.on("close", (code) => {
|
|
992
|
+
const raw = Buffer.concat(stdoutChunks).toString("utf8").trim();
|
|
993
|
+
if (raw === "") {
|
|
994
|
+
const stderr = Buffer.concat(stderrChunks).toString("utf8").trim();
|
|
995
|
+
const detail = stderr !== "" ? stderr : `exit code ${String(code)}`;
|
|
996
|
+
reject(new Error(`1Password per-access worker crashed for secret ${id}: ${detail}`));
|
|
997
|
+
return;
|
|
998
|
+
}
|
|
999
|
+
let parsed;
|
|
1000
|
+
try {
|
|
1001
|
+
parsed = JSON.parse(raw);
|
|
1002
|
+
} catch {
|
|
1003
|
+
reject(new SecretNotFoundError(`Worker returned unparseable output for secret: ${id}`));
|
|
1004
|
+
return;
|
|
1005
|
+
}
|
|
1006
|
+
if (!isWorkerResponse(parsed)) {
|
|
1007
|
+
reject(new SecretNotFoundError(`Worker returned unexpected response shape for secret: ${id}`));
|
|
1008
|
+
return;
|
|
1009
|
+
}
|
|
1010
|
+
if (isWorkerSuccess(parsed)) {
|
|
1011
|
+
resolve2(parsed.value);
|
|
1012
|
+
} else {
|
|
1013
|
+
switch (parsed.code) {
|
|
1014
|
+
case "NOT_FOUND":
|
|
1015
|
+
reject(new SecretNotFoundError(`Secret not found in 1Password: ${id}`));
|
|
1016
|
+
break;
|
|
1017
|
+
case "AUTH_DENIED":
|
|
1018
|
+
reject(new AuthorizationDeniedError("1Password authentication was denied"));
|
|
1019
|
+
break;
|
|
1020
|
+
case "LOCKED":
|
|
1021
|
+
reject(new BackendLockedError("1Password is locked. Please unlock and retry.", true));
|
|
1022
|
+
break;
|
|
1023
|
+
default:
|
|
1024
|
+
reject(new SecretNotFoundError(`Worker failed for secret ${id}: ${parsed.error}`));
|
|
1025
|
+
}
|
|
1026
|
+
}
|
|
1027
|
+
});
|
|
1028
|
+
child.on("error", (err) => {
|
|
1029
|
+
reject(new Error(
|
|
1030
|
+
`Failed to spawn 1Password per-access worker at ${workerPath}: ${String(err)}`
|
|
1031
|
+
));
|
|
1032
|
+
});
|
|
1033
|
+
});
|
|
1034
|
+
}
|
|
1035
|
+
async delete(id) {
|
|
1036
|
+
const client = await this.acquireClient();
|
|
1037
|
+
const overview = await this.findItemOverview(client, id);
|
|
1038
|
+
if (overview === void 0) {
|
|
1039
|
+
throw new SecretNotFoundError(`Secret not found in 1Password: ${id}`);
|
|
1040
|
+
}
|
|
1041
|
+
await client.items.delete(this.vaultId, overview.id);
|
|
1042
|
+
}
|
|
1043
|
+
async exists(id) {
|
|
1044
|
+
const client = await this.acquireClient();
|
|
1045
|
+
const overview = await this.findItemOverview(client, id);
|
|
1046
|
+
return overview !== void 0;
|
|
1047
|
+
}
|
|
1048
|
+
async list() {
|
|
1049
|
+
const client = await this.acquireClient();
|
|
1050
|
+
const overviews = await client.items.list(this.vaultId);
|
|
1051
|
+
const ids = [];
|
|
1052
|
+
for (const overview of overviews) {
|
|
1053
|
+
if (overview.tags.includes(TAG)) {
|
|
1054
|
+
ids.push(overview.title);
|
|
1055
|
+
}
|
|
1056
|
+
}
|
|
1057
|
+
return ids;
|
|
1058
|
+
}
|
|
1059
|
+
// ---- Private helpers ----
|
|
1060
|
+
/** Load SDK and throw PluginNotFoundError if unavailable. */
|
|
1061
|
+
async requireSdk() {
|
|
1062
|
+
const sdk = await this.tryLoadSdk();
|
|
1063
|
+
if (sdk === null) {
|
|
1064
|
+
throw new PluginNotFoundError(
|
|
1065
|
+
"1Password SDK (@1password/sdk) is not available.",
|
|
1066
|
+
"@1password/sdk",
|
|
1067
|
+
SDK_INSTALL_URL
|
|
1068
|
+
);
|
|
1069
|
+
}
|
|
1070
|
+
return sdk;
|
|
1071
|
+
}
|
|
1072
|
+
};
|
|
1073
|
+
var YKMAN_INSTALL_URL = "https://developers.yubico.com/yubikey-manager/";
|
|
1074
|
+
var STORAGE_DIR_NAME2 = path3.join(".vaultkeeper", "yubikey");
|
|
1075
|
+
var METADATA_FILE = "metadata.json";
|
|
1076
|
+
var DEVICE_TIMEOUT_MS = 5e3;
|
|
1077
|
+
var GCM_IV_BYTES2 = 12;
|
|
1078
|
+
var GCM_KEY_BYTES2 = 32;
|
|
1079
|
+
var GCM_TAG_LENGTH_BITS = 128;
|
|
1080
|
+
var FORMAT_VERSION = "1";
|
|
1081
|
+
function getStorageDir2() {
|
|
1082
|
+
return path3.join(os4.homedir(), STORAGE_DIR_NAME2);
|
|
1083
|
+
}
|
|
1084
|
+
function getEntryPath3(storageDir, id) {
|
|
1085
|
+
const safeId = Buffer.from(id, "utf8").toString("hex");
|
|
1086
|
+
return path3.join(storageDir, `${safeId}.enc`);
|
|
1087
|
+
}
|
|
1088
|
+
function isStringRecord(value) {
|
|
1089
|
+
if (value === null || typeof value !== "object") {
|
|
1090
|
+
return false;
|
|
1091
|
+
}
|
|
1092
|
+
return Object.values(value).every((v) => typeof v === "string");
|
|
1093
|
+
}
|
|
1094
|
+
async function loadMetadata(storageDir) {
|
|
1095
|
+
const metaPath = path3.join(storageDir, METADATA_FILE);
|
|
1096
|
+
try {
|
|
1097
|
+
const raw = await fs.readFile(metaPath, "utf8");
|
|
1098
|
+
const parsed = JSON.parse(raw);
|
|
1099
|
+
if (parsed !== null && typeof parsed === "object" && "entries" in parsed && isStringRecord(parsed.entries)) {
|
|
1100
|
+
return { entries: parsed.entries };
|
|
1101
|
+
}
|
|
1102
|
+
return { entries: {} };
|
|
1103
|
+
} catch {
|
|
1104
|
+
return { entries: {} };
|
|
1105
|
+
}
|
|
1106
|
+
}
|
|
1107
|
+
async function saveMetadata(storageDir, metadata) {
|
|
1108
|
+
const metaPath = path3.join(storageDir, METADATA_FILE);
|
|
1109
|
+
await fs.writeFile(metaPath, JSON.stringify(metadata, null, 2), { mode: 384 });
|
|
1110
|
+
}
|
|
1111
|
+
var HMAC_RESPONSE_HEX_LENGTH = 40;
|
|
1112
|
+
var HMAC_RESPONSE_RE = /^[0-9a-fA-F]{40}$/;
|
|
1113
|
+
function deriveKey(hmacResponse, id) {
|
|
1114
|
+
const trimmed = hmacResponse.trim();
|
|
1115
|
+
if (!HMAC_RESPONSE_RE.test(trimmed)) {
|
|
1116
|
+
throw new Error(
|
|
1117
|
+
`Invalid YubiKey HMAC response: expected exactly ${String(HMAC_RESPONSE_HEX_LENGTH)} hex characters (20 bytes), got ${String(trimmed.length)} characters`
|
|
1118
|
+
);
|
|
1119
|
+
}
|
|
1120
|
+
const ikm = Buffer.from(trimmed, "hex");
|
|
1121
|
+
const info = Buffer.from(`vaultkeeper-yubikey:${id}`, "utf8");
|
|
1122
|
+
const keyMaterial = crypto.hkdfSync("sha256", ikm, Buffer.alloc(0), info, GCM_KEY_BYTES2);
|
|
1123
|
+
return Buffer.from(keyMaterial);
|
|
1124
|
+
}
|
|
1125
|
+
function encryptGcm2(key, plaintext) {
|
|
1126
|
+
const iv = crypto.randomBytes(GCM_IV_BYTES2);
|
|
1127
|
+
let encrypted;
|
|
1128
|
+
let authTag;
|
|
1129
|
+
try {
|
|
1130
|
+
const cipher = crypto.createCipheriv("aes-256-gcm", key, iv, {
|
|
1131
|
+
authTagLength: GCM_TAG_LENGTH_BITS / 8
|
|
1132
|
+
});
|
|
1133
|
+
encrypted = Buffer.concat([cipher.update(plaintext, "utf8"), cipher.final()]);
|
|
1134
|
+
authTag = cipher.getAuthTag();
|
|
1135
|
+
return [
|
|
1136
|
+
FORMAT_VERSION,
|
|
1137
|
+
iv.toString("base64"),
|
|
1138
|
+
authTag.toString("base64"),
|
|
1139
|
+
encrypted.toString("base64")
|
|
1140
|
+
].join(":");
|
|
1141
|
+
} finally {
|
|
1142
|
+
key.fill(0);
|
|
1143
|
+
iv.fill(0);
|
|
1144
|
+
encrypted?.fill(0);
|
|
1145
|
+
authTag?.fill(0);
|
|
1146
|
+
}
|
|
1147
|
+
}
|
|
1148
|
+
function decryptGcm2(key, encoded) {
|
|
1149
|
+
const parts = encoded.split(":");
|
|
1150
|
+
const versionSegment = parts[0] ?? "";
|
|
1151
|
+
const parsedVersion = parseInt(versionSegment, 10);
|
|
1152
|
+
const isNumericVersion = String(parsedVersion) === versionSegment && !Number.isNaN(parsedVersion);
|
|
1153
|
+
if (!isNumericVersion) {
|
|
1154
|
+
key.fill(0);
|
|
1155
|
+
throw new Error(
|
|
1156
|
+
"Encrypted file uses a legacy format (AES-256-CBC). Delete the secret and re-store it to migrate to AES-256-GCM."
|
|
1157
|
+
);
|
|
1158
|
+
}
|
|
1159
|
+
if (versionSegment !== FORMAT_VERSION) {
|
|
1160
|
+
key.fill(0);
|
|
1161
|
+
throw new Error(
|
|
1162
|
+
`Unsupported encrypted file version: ${versionSegment}. This vaultkeeper build only supports version ${FORMAT_VERSION}. Upgrade vaultkeeper to read this secret.`
|
|
1163
|
+
);
|
|
1164
|
+
}
|
|
1165
|
+
if (parts.length !== 4) {
|
|
1166
|
+
key.fill(0);
|
|
1167
|
+
throw new Error(
|
|
1168
|
+
`Invalid encrypted file format: expected ${FORMAT_VERSION}:iv:authTag:ciphertext`
|
|
1169
|
+
);
|
|
1170
|
+
}
|
|
1171
|
+
const [_version, ivB64, authTagB64, ciphertextB64] = parts;
|
|
1172
|
+
if (ivB64 === void 0 || authTagB64 === void 0 || ciphertextB64 === void 0) {
|
|
1173
|
+
key.fill(0);
|
|
1174
|
+
throw new Error("Invalid encrypted file format: missing part");
|
|
1175
|
+
}
|
|
1176
|
+
let decrypted;
|
|
1177
|
+
try {
|
|
1178
|
+
const iv = Buffer.from(ivB64, "base64");
|
|
1179
|
+
const authTag = Buffer.from(authTagB64, "base64");
|
|
1180
|
+
const ciphertext = Buffer.from(ciphertextB64, "base64");
|
|
1181
|
+
const decipher = crypto.createDecipheriv("aes-256-gcm", key, iv, {
|
|
1182
|
+
authTagLength: GCM_TAG_LENGTH_BITS / 8
|
|
1183
|
+
});
|
|
1184
|
+
decipher.setAuthTag(authTag);
|
|
1185
|
+
try {
|
|
1186
|
+
decrypted = Buffer.concat([decipher.update(ciphertext), decipher.final()]);
|
|
1187
|
+
} catch (err) {
|
|
1188
|
+
throw new Error(
|
|
1189
|
+
`GCM authentication failed \u2014 ciphertext may be tampered: ${err instanceof Error ? err.message : String(err)}`
|
|
1190
|
+
);
|
|
1191
|
+
}
|
|
1192
|
+
const plaintext = decrypted.toString("utf8");
|
|
1193
|
+
return plaintext;
|
|
1194
|
+
} finally {
|
|
1195
|
+
key.fill(0);
|
|
1196
|
+
decrypted?.fill(0);
|
|
1197
|
+
}
|
|
1198
|
+
}
|
|
1199
|
+
var YubikeyBackend = class {
|
|
1200
|
+
type = "yubikey";
|
|
1201
|
+
displayName = "YubiKey";
|
|
1202
|
+
async isAvailable() {
|
|
1203
|
+
try {
|
|
1204
|
+
const result = await execCommandFull("ykman", ["--version"]);
|
|
1205
|
+
if (result.exitCode !== 0) {
|
|
1206
|
+
return false;
|
|
1207
|
+
}
|
|
1208
|
+
const listResult = await execCommandFull("ykman", ["list"]);
|
|
1209
|
+
return listResult.exitCode === 0 && listResult.stdout.trim() !== "";
|
|
1210
|
+
} catch {
|
|
1211
|
+
return false;
|
|
1212
|
+
}
|
|
1213
|
+
}
|
|
1214
|
+
async requireDevice() {
|
|
1215
|
+
const available = await this.isAvailable();
|
|
1216
|
+
if (!available) {
|
|
1217
|
+
const hasYkman = await execCommandFull("ykman", ["--version"]).then(
|
|
1218
|
+
(r) => r.exitCode === 0,
|
|
1219
|
+
() => false
|
|
1220
|
+
);
|
|
1221
|
+
if (!hasYkman) {
|
|
1222
|
+
throw new PluginNotFoundError("ykman is not installed", "ykman", YKMAN_INSTALL_URL);
|
|
1223
|
+
}
|
|
1224
|
+
throw new DeviceNotPresentError("No YubiKey device detected", DEVICE_TIMEOUT_MS);
|
|
1225
|
+
}
|
|
1226
|
+
}
|
|
1227
|
+
/**
|
|
1228
|
+
* Perform the YubiKey HMAC-SHA1 challenge-response for `id` and return the
|
|
1229
|
+
* raw hex response string. Throws on device failure.
|
|
1230
|
+
*/
|
|
1231
|
+
async challengeResponse(id) {
|
|
1232
|
+
const challenge = Buffer.from(`vaultkeeper:${id}`, "utf8").toString("hex");
|
|
1233
|
+
const responseResult = await execCommandFull("ykman", ["otp", "calculate", "2", challenge]);
|
|
1234
|
+
if (responseResult.exitCode !== 0) {
|
|
1235
|
+
throw new Error(`YubiKey challenge-response failed: ${responseResult.stderr}`);
|
|
1236
|
+
}
|
|
1237
|
+
return responseResult.stdout.trim();
|
|
1238
|
+
}
|
|
1239
|
+
async store(id, secret) {
|
|
1240
|
+
await this.requireDevice();
|
|
1241
|
+
const storageDir = getStorageDir2();
|
|
1242
|
+
await fs.mkdir(storageDir, { recursive: true, mode: 448 });
|
|
1243
|
+
const hmacResponse = await this.challengeResponse(id);
|
|
1244
|
+
const key = deriveKey(hmacResponse, id);
|
|
1245
|
+
const encrypted = encryptGcm2(key, secret);
|
|
1246
|
+
const entryPath = getEntryPath3(storageDir, id);
|
|
1247
|
+
await fs.writeFile(entryPath, encrypted, { mode: 384 });
|
|
1248
|
+
const metadata = await loadMetadata(storageDir);
|
|
1249
|
+
metadata.entries[id] = entryPath;
|
|
1250
|
+
await saveMetadata(storageDir, metadata);
|
|
1251
|
+
}
|
|
1252
|
+
async retrieve(id) {
|
|
1253
|
+
await this.requireDevice();
|
|
1254
|
+
const storageDir = getStorageDir2();
|
|
1255
|
+
const entryPath = getEntryPath3(storageDir, id);
|
|
1256
|
+
try {
|
|
1257
|
+
await fs.access(entryPath);
|
|
1258
|
+
} catch {
|
|
1259
|
+
throw new SecretNotFoundError(`Secret not found in YubiKey store: ${id}`);
|
|
1260
|
+
}
|
|
1261
|
+
const encoded = await fs.readFile(entryPath, "utf8");
|
|
1262
|
+
const hmacResponse = await this.challengeResponse(id);
|
|
1263
|
+
const key = deriveKey(hmacResponse, id);
|
|
1264
|
+
return decryptGcm2(key, encoded);
|
|
1265
|
+
}
|
|
1266
|
+
async delete(id) {
|
|
1267
|
+
await this.requireDevice();
|
|
1268
|
+
const storageDir = getStorageDir2();
|
|
1269
|
+
const entryPath = getEntryPath3(storageDir, id);
|
|
1270
|
+
try {
|
|
1271
|
+
await fs.unlink(entryPath);
|
|
1272
|
+
} catch (err) {
|
|
1273
|
+
if (err instanceof Error && "code" in err && err.code === "ENOENT") {
|
|
1274
|
+
throw new SecretNotFoundError(`Secret not found in YubiKey store: ${id}`);
|
|
1275
|
+
}
|
|
1276
|
+
throw err;
|
|
1277
|
+
}
|
|
1278
|
+
const metadata = await loadMetadata(storageDir);
|
|
1279
|
+
delete metadata.entries[id];
|
|
1280
|
+
await saveMetadata(storageDir, metadata);
|
|
1281
|
+
}
|
|
1282
|
+
async exists(id) {
|
|
1283
|
+
const storageDir = getStorageDir2();
|
|
1284
|
+
const entryPath = getEntryPath3(storageDir, id);
|
|
1285
|
+
try {
|
|
1286
|
+
await fs.access(entryPath);
|
|
1287
|
+
return true;
|
|
1288
|
+
} catch {
|
|
1289
|
+
return false;
|
|
1290
|
+
}
|
|
1291
|
+
}
|
|
1292
|
+
async list() {
|
|
1293
|
+
const storageDir = getStorageDir2();
|
|
1294
|
+
const metadata = await loadMetadata(storageDir);
|
|
1295
|
+
return Object.keys(metadata.entries);
|
|
1296
|
+
}
|
|
1297
|
+
};
|
|
1298
|
+
|
|
1299
|
+
// src/backend/register-builtins.ts
|
|
1300
|
+
function registerBuiltinBackends() {
|
|
1301
|
+
BackendRegistry.register("file", () => new FileBackend());
|
|
1302
|
+
BackendRegistry.register("keychain", () => new KeychainBackend());
|
|
1303
|
+
BackendRegistry.register("dpapi", () => new DpapiBackend());
|
|
1304
|
+
BackendRegistry.register("secret-tool", () => new SecretToolBackend());
|
|
1305
|
+
BackendRegistry.register("1password", (config) => {
|
|
1306
|
+
const opts = config?.options;
|
|
1307
|
+
const vaultId = opts?.vault ?? "";
|
|
1308
|
+
const opOptions = {
|
|
1309
|
+
vault: vaultId,
|
|
1310
|
+
// 'session' is the safer default — 'per-access' re-prompts on every read.
|
|
1311
|
+
accessMode: opts?.accessMode === "per-access" ? "per-access" : "session"
|
|
1312
|
+
};
|
|
1313
|
+
const account = opts?.account;
|
|
1314
|
+
if (account !== void 0) {
|
|
1315
|
+
opOptions.account = account;
|
|
1316
|
+
}
|
|
1317
|
+
const serviceAccountToken = opts?.serviceAccountToken;
|
|
1318
|
+
if (serviceAccountToken !== void 0) {
|
|
1319
|
+
opOptions.serviceAccountToken = serviceAccountToken;
|
|
1320
|
+
}
|
|
1321
|
+
return new OnePasswordBackend(opOptions);
|
|
1322
|
+
});
|
|
1323
|
+
BackendRegistry.register("yubikey", () => new YubikeyBackend());
|
|
1324
|
+
}
|
|
1325
|
+
registerBuiltinBackends();
|
|
1326
|
+
|
|
1327
|
+
// src/backend/types.ts
|
|
1328
|
+
function isListableBackend(backend) {
|
|
1329
|
+
return "list" in backend && typeof backend.list === "function";
|
|
1330
|
+
}
|
|
272
1331
|
function hashExecutable(filePath) {
|
|
273
|
-
return new Promise((
|
|
274
|
-
const hash =
|
|
1332
|
+
return new Promise((resolve2, reject) => {
|
|
1333
|
+
const hash = crypto.createHash("sha256");
|
|
275
1334
|
const stream = fs4.createReadStream(filePath);
|
|
276
1335
|
stream.on("data", (chunk) => {
|
|
277
1336
|
hash.update(chunk);
|
|
278
1337
|
});
|
|
279
1338
|
stream.on("end", () => {
|
|
280
|
-
|
|
1339
|
+
resolve2(hash.digest("hex"));
|
|
281
1340
|
});
|
|
282
1341
|
stream.on("error", (err) => {
|
|
283
1342
|
reject(err);
|
|
@@ -300,10 +1359,10 @@ function isTrustManifestEntry(value) {
|
|
|
300
1359
|
return true;
|
|
301
1360
|
}
|
|
302
1361
|
async function loadManifest(configDir) {
|
|
303
|
-
const manifestPath =
|
|
1362
|
+
const manifestPath = path3.join(configDir, MANIFEST_FILENAME);
|
|
304
1363
|
let rawText;
|
|
305
1364
|
try {
|
|
306
|
-
rawText = await
|
|
1365
|
+
rawText = await fs.readFile(manifestPath, "utf8");
|
|
307
1366
|
} catch (err) {
|
|
308
1367
|
if (typeof err === "object" && err !== null && "code" in err && err.code === "ENOENT") {
|
|
309
1368
|
return /* @__PURE__ */ new Map();
|
|
@@ -323,14 +1382,14 @@ async function loadManifest(configDir) {
|
|
|
323
1382
|
return manifest;
|
|
324
1383
|
}
|
|
325
1384
|
async function saveManifest(configDir, manifest) {
|
|
326
|
-
await
|
|
1385
|
+
await fs.mkdir(configDir, { recursive: true });
|
|
327
1386
|
const entries = {};
|
|
328
1387
|
for (const [namespace, entry] of manifest) {
|
|
329
1388
|
entries[namespace] = entry;
|
|
330
1389
|
}
|
|
331
1390
|
const raw = { version: 1, entries };
|
|
332
|
-
const manifestPath =
|
|
333
|
-
await
|
|
1391
|
+
const manifestPath = path3.join(configDir, MANIFEST_FILENAME);
|
|
1392
|
+
await fs.writeFile(manifestPath, JSON.stringify(raw, null, 2), "utf8");
|
|
334
1393
|
}
|
|
335
1394
|
function addTrustedHash(manifest, namespace, hash) {
|
|
336
1395
|
const next = new Map(manifest);
|
|
@@ -441,14 +1500,18 @@ function validateCapabilityToken(token) {
|
|
|
441
1500
|
return claims;
|
|
442
1501
|
}
|
|
443
1502
|
function getDefaultConfigDir() {
|
|
1503
|
+
const envOverride = process.env.VAULTKEEPER_CONFIG_DIR;
|
|
1504
|
+
if (envOverride !== void 0 && envOverride !== "") {
|
|
1505
|
+
return envOverride;
|
|
1506
|
+
}
|
|
444
1507
|
if (process.platform === "win32") {
|
|
445
1508
|
const appData = process.env.APPDATA;
|
|
446
1509
|
if (appData !== void 0) {
|
|
447
|
-
return
|
|
1510
|
+
return path3.join(appData, "vaultkeeper");
|
|
448
1511
|
}
|
|
449
|
-
return
|
|
1512
|
+
return path3.join(os4.homedir(), "AppData", "Roaming", "vaultkeeper");
|
|
450
1513
|
}
|
|
451
|
-
return
|
|
1514
|
+
return path3.join(os4.homedir(), ".config", "vaultkeeper");
|
|
452
1515
|
}
|
|
453
1516
|
function defaultConfig() {
|
|
454
1517
|
return {
|
|
@@ -487,6 +1550,21 @@ function validateBackendEntry(entry, index) {
|
|
|
487
1550
|
}
|
|
488
1551
|
result.path = entry.path;
|
|
489
1552
|
}
|
|
1553
|
+
if (entry.options !== void 0) {
|
|
1554
|
+
if (!isObject(entry.options)) {
|
|
1555
|
+
throw new Error(`backends[${String(index)}].options must be an object`);
|
|
1556
|
+
}
|
|
1557
|
+
const opts = {};
|
|
1558
|
+
for (const [k, v] of Object.entries(entry.options)) {
|
|
1559
|
+
if (typeof v !== "string") {
|
|
1560
|
+
throw new Error(
|
|
1561
|
+
`backends[${String(index)}].options["${k}"] must be a string`
|
|
1562
|
+
);
|
|
1563
|
+
}
|
|
1564
|
+
opts[k] = v;
|
|
1565
|
+
}
|
|
1566
|
+
result.options = opts;
|
|
1567
|
+
}
|
|
490
1568
|
return result;
|
|
491
1569
|
}
|
|
492
1570
|
function validateConfig(config) {
|
|
@@ -549,10 +1627,10 @@ function validateConfig(config) {
|
|
|
549
1627
|
}
|
|
550
1628
|
async function loadConfig(configDir) {
|
|
551
1629
|
const dir = configDir ?? getDefaultConfigDir();
|
|
552
|
-
const configPath =
|
|
1630
|
+
const configPath = path3.join(dir, "config.json");
|
|
553
1631
|
let raw;
|
|
554
1632
|
try {
|
|
555
|
-
raw = await
|
|
1633
|
+
raw = await fs.readFile(configPath, "utf-8");
|
|
556
1634
|
} catch {
|
|
557
1635
|
return defaultConfig();
|
|
558
1636
|
}
|
|
@@ -571,10 +1649,10 @@ var KeyManager = class {
|
|
|
571
1649
|
#rotating = false;
|
|
572
1650
|
/** Generate a new 32-byte key with a timestamp-based id. */
|
|
573
1651
|
generateKey() {
|
|
574
|
-
const randomSuffix =
|
|
1652
|
+
const randomSuffix = crypto.randomBytes(4).toString("hex");
|
|
575
1653
|
return {
|
|
576
1654
|
id: `k-${String(Date.now())}-${randomSuffix}`,
|
|
577
|
-
key: new Uint8Array(
|
|
1655
|
+
key: new Uint8Array(crypto.randomBytes(32)),
|
|
578
1656
|
createdAt: /* @__PURE__ */ new Date()
|
|
579
1657
|
};
|
|
580
1658
|
}
|
|
@@ -876,7 +1954,7 @@ function replaceInRecord2(record, secret) {
|
|
|
876
1954
|
function delegatedExec(secret, request) {
|
|
877
1955
|
const args = (request.args ?? []).map((arg) => replacePlaceholder2(arg, secret));
|
|
878
1956
|
const env = request.env !== void 0 ? replaceInRecord2(request.env, secret) : void 0;
|
|
879
|
-
return new Promise((
|
|
1957
|
+
return new Promise((resolve2, reject) => {
|
|
880
1958
|
const spawnOptions = {
|
|
881
1959
|
stdio: ["ignore", "pipe", "pipe"]
|
|
882
1960
|
};
|
|
@@ -896,7 +1974,7 @@ function delegatedExec(secret, request) {
|
|
|
896
1974
|
stderr += data.toString();
|
|
897
1975
|
});
|
|
898
1976
|
proc.on("close", (code) => {
|
|
899
|
-
|
|
1977
|
+
resolve2({ stdout, stderr, exitCode: code ?? 1 });
|
|
900
1978
|
});
|
|
901
1979
|
proc.on("error", (error) => {
|
|
902
1980
|
reject(error);
|
|
@@ -990,6 +2068,55 @@ function createSecretAccessor(secretValue) {
|
|
|
990
2068
|
return proxy;
|
|
991
2069
|
}
|
|
992
2070
|
|
|
2071
|
+
// src/access/sign-util.ts
|
|
2072
|
+
var ALLOWED_ALGORITHMS = /* @__PURE__ */ new Set(["sha256", "sha384", "sha512"]);
|
|
2073
|
+
function resolveAlgorithmForKey(key, override) {
|
|
2074
|
+
const keyType = key.asymmetricKeyType;
|
|
2075
|
+
if (keyType === "ed25519" || keyType === "ed448") {
|
|
2076
|
+
return { signAlg: null, label: keyType };
|
|
2077
|
+
}
|
|
2078
|
+
const alg = (override ?? "sha256").toLowerCase();
|
|
2079
|
+
if (!ALLOWED_ALGORITHMS.has(alg)) {
|
|
2080
|
+
throw new InvalidAlgorithmError(
|
|
2081
|
+
`Unsupported algorithm '${alg}'. Allowed: ${[...ALLOWED_ALGORITHMS].join(", ")}`,
|
|
2082
|
+
alg,
|
|
2083
|
+
[...ALLOWED_ALGORITHMS]
|
|
2084
|
+
);
|
|
2085
|
+
}
|
|
2086
|
+
return { signAlg: alg, label: alg };
|
|
2087
|
+
}
|
|
2088
|
+
|
|
2089
|
+
// src/access/delegated-sign.ts
|
|
2090
|
+
function delegatedSign(secretPem, request) {
|
|
2091
|
+
const key = crypto.createPrivateKey(secretPem);
|
|
2092
|
+
const { signAlg, label } = resolveAlgorithmForKey(key, request.algorithm);
|
|
2093
|
+
const data = Buffer.isBuffer(request.data) ? request.data : Buffer.from(request.data);
|
|
2094
|
+
const signature = crypto.sign(signAlg, data, key);
|
|
2095
|
+
return {
|
|
2096
|
+
signature: signature.toString("base64"),
|
|
2097
|
+
algorithm: label
|
|
2098
|
+
};
|
|
2099
|
+
}
|
|
2100
|
+
function delegatedVerify(request) {
|
|
2101
|
+
let key;
|
|
2102
|
+
try {
|
|
2103
|
+
key = crypto.createPublicKey(request.publicKey);
|
|
2104
|
+
} catch {
|
|
2105
|
+
return false;
|
|
2106
|
+
}
|
|
2107
|
+
if (typeof request.publicKey === "string" && request.publicKey.includes("PRIVATE KEY")) {
|
|
2108
|
+
return false;
|
|
2109
|
+
}
|
|
2110
|
+
const { signAlg } = resolveAlgorithmForKey(key, request.algorithm);
|
|
2111
|
+
const sig = Buffer.from(request.signature, "base64");
|
|
2112
|
+
try {
|
|
2113
|
+
const data = Buffer.isBuffer(request.data) ? request.data : Buffer.from(request.data);
|
|
2114
|
+
return crypto.verify(signAlg, data, key, sig);
|
|
2115
|
+
} catch {
|
|
2116
|
+
return false;
|
|
2117
|
+
}
|
|
2118
|
+
}
|
|
2119
|
+
|
|
993
2120
|
// src/doctor/checks.ts
|
|
994
2121
|
function parseVersion(raw) {
|
|
995
2122
|
const match = /(\d+)\.(\d+)\.(\d+)/.exec(raw);
|
|
@@ -1242,7 +2369,7 @@ var VaultKeeper = class _VaultKeeper {
|
|
|
1242
2369
|
}
|
|
1243
2370
|
const now = Math.floor(Date.now() / 1e3);
|
|
1244
2371
|
const claims = {
|
|
1245
|
-
jti:
|
|
2372
|
+
jti: crypto.randomUUID(),
|
|
1246
2373
|
exp: now + ttlMinutes * 60,
|
|
1247
2374
|
iat: now,
|
|
1248
2375
|
sub: secretName,
|
|
@@ -1347,6 +2474,52 @@ var VaultKeeper = class _VaultKeeper {
|
|
|
1347
2474
|
const claims = validateCapabilityToken(token);
|
|
1348
2475
|
return createSecretAccessor(claims.val);
|
|
1349
2476
|
}
|
|
2477
|
+
/**
|
|
2478
|
+
* Sign data using the private key embedded in a capability token.
|
|
2479
|
+
*
|
|
2480
|
+
* The signing key is extracted from the token's encrypted claims, used
|
|
2481
|
+
* for a single `crypto.sign()` call, and never exposed to the caller.
|
|
2482
|
+
* The algorithm is auto-detected from the key type unless overridden
|
|
2483
|
+
* in the request.
|
|
2484
|
+
*
|
|
2485
|
+
* @param token - A `CapabilityToken` obtained from `authorize()`.
|
|
2486
|
+
* @param request - The data to sign and optional algorithm override.
|
|
2487
|
+
* @returns The base64-encoded signature and algorithm label, together
|
|
2488
|
+
* with the vault metadata (`vaultResponse`).
|
|
2489
|
+
* @throws {VaultError} If `token` is invalid or was not created by this
|
|
2490
|
+
* vault instance.
|
|
2491
|
+
* @throws {InvalidAlgorithmError} If `request.algorithm` is not in the
|
|
2492
|
+
* allowed set (e.g. `'md5'`).
|
|
2493
|
+
*/
|
|
2494
|
+
async sign(token, request) {
|
|
2495
|
+
const claims = validateCapabilityToken(token);
|
|
2496
|
+
const result = delegatedSign(claims.val, request);
|
|
2497
|
+
await Promise.resolve();
|
|
2498
|
+
return {
|
|
2499
|
+
result,
|
|
2500
|
+
vaultResponse: { keyStatus: "current" }
|
|
2501
|
+
};
|
|
2502
|
+
}
|
|
2503
|
+
/**
|
|
2504
|
+
* Verify a signature using a public key.
|
|
2505
|
+
*
|
|
2506
|
+
* This is a static method — no VaultKeeper instance, secrets, or
|
|
2507
|
+
* capability tokens are required. It is safe to call from CI or any
|
|
2508
|
+
* context that has access to public key material.
|
|
2509
|
+
*
|
|
2510
|
+
* Returns `false` for invalid key material, malformed signatures, or
|
|
2511
|
+
* any verification failure (except disallowed algorithms, which throw).
|
|
2512
|
+
*
|
|
2513
|
+
* @throws {InvalidAlgorithmError} If `request.algorithm` is not in the
|
|
2514
|
+
* allowed set (e.g. `'md5'`).
|
|
2515
|
+
*
|
|
2516
|
+
* @param request - The data, signature, public key, and optional
|
|
2517
|
+
* algorithm override.
|
|
2518
|
+
* @returns `true` if the signature is valid, `false` otherwise.
|
|
2519
|
+
*/
|
|
2520
|
+
static verify(request) {
|
|
2521
|
+
return delegatedVerify(request);
|
|
2522
|
+
}
|
|
1350
2523
|
/**
|
|
1351
2524
|
* Rotate the current encryption key.
|
|
1352
2525
|
*
|
|
@@ -1422,7 +2595,7 @@ var VaultKeeper = class _VaultKeeper {
|
|
|
1422
2595
|
[]
|
|
1423
2596
|
);
|
|
1424
2597
|
}
|
|
1425
|
-
return BackendRegistry.create(firstEnabled.type);
|
|
2598
|
+
return BackendRegistry.create(firstEnabled.type, firstEnabled);
|
|
1426
2599
|
}
|
|
1427
2600
|
#requireBackend() {
|
|
1428
2601
|
if (this.#backend === void 0) {
|
|
@@ -1463,6 +2636,6 @@ var VaultKeeper = class _VaultKeeper {
|
|
|
1463
2636
|
}
|
|
1464
2637
|
};
|
|
1465
2638
|
|
|
1466
|
-
export { AuthorizationDeniedError, BackendLockedError, BackendRegistry, BackendUnavailableError, CapabilityToken, DeviceNotPresentError, FilesystemError, IdentityMismatchError, KeyRevokedError, KeyRotatedError, PluginNotFoundError, RotationInProgressError, SecretNotFoundError, SetupError, TokenExpiredError, TokenRevokedError, UsageLimitExceededError, VaultError, VaultKeeper, isListableBackend };
|
|
2639
|
+
export { AuthorizationDeniedError, BackendLockedError, BackendRegistry, BackendUnavailableError, CapabilityToken, DeviceNotPresentError, FilesystemError, IdentityMismatchError, InvalidAlgorithmError, KeyRevokedError, KeyRotatedError, PluginNotFoundError, RotationInProgressError, SecretNotFoundError, SetupError, TokenExpiredError, TokenRevokedError, UsageLimitExceededError, VaultError, VaultKeeper, isListableBackend };
|
|
1467
2640
|
//# sourceMappingURL=index.js.map
|
|
1468
2641
|
//# sourceMappingURL=index.js.map
|