vaultkeeper 1.0.0 → 1.0.1
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 +1048 -44
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +9 -10
- package/dist/index.d.ts +9 -10
- package/dist/index.js +1046 -41
- package/dist/index.js.map +1 -1
- package/dist/one-password-worker.js +27 -3
- package/dist/one-password-worker.js.map +1 -1
- package/package.json +2 -1
package/dist/index.cjs
CHANGED
|
@@ -1,14 +1,15 @@
|
|
|
1
1
|
'use strict';
|
|
2
2
|
|
|
3
|
-
var
|
|
4
|
-
var
|
|
5
|
-
var path5 = require('path');
|
|
3
|
+
var fs = require('fs/promises');
|
|
4
|
+
var path3 = require('path');
|
|
6
5
|
var os4 = require('os');
|
|
7
|
-
var
|
|
8
|
-
require('
|
|
6
|
+
var crypto = require('crypto');
|
|
7
|
+
var child_process = require('child_process');
|
|
8
|
+
var url = require('url');
|
|
9
9
|
var fs4 = require('fs');
|
|
10
10
|
var jose = require('jose');
|
|
11
11
|
|
|
12
|
+
var _documentCurrentScript = typeof document !== 'undefined' ? document.currentScript : null;
|
|
12
13
|
function _interopNamespace(e) {
|
|
13
14
|
if (e && e.__esModule) return e;
|
|
14
15
|
var n = Object.create(null);
|
|
@@ -27,10 +28,10 @@ function _interopNamespace(e) {
|
|
|
27
28
|
return Object.freeze(n);
|
|
28
29
|
}
|
|
29
30
|
|
|
30
|
-
var
|
|
31
|
-
var
|
|
31
|
+
var fs__namespace = /*#__PURE__*/_interopNamespace(fs);
|
|
32
|
+
var path3__namespace = /*#__PURE__*/_interopNamespace(path3);
|
|
32
33
|
var os4__namespace = /*#__PURE__*/_interopNamespace(os4);
|
|
33
|
-
var
|
|
34
|
+
var crypto__namespace = /*#__PURE__*/_interopNamespace(crypto);
|
|
34
35
|
var fs4__namespace = /*#__PURE__*/_interopNamespace(fs4);
|
|
35
36
|
|
|
36
37
|
// src/errors.ts
|
|
@@ -214,11 +215,6 @@ var RotationInProgressError = class extends VaultError {
|
|
|
214
215
|
}
|
|
215
216
|
};
|
|
216
217
|
|
|
217
|
-
// src/backend/types.ts
|
|
218
|
-
function isListableBackend(backend) {
|
|
219
|
-
return "list" in backend && typeof backend.list === "function";
|
|
220
|
-
}
|
|
221
|
-
|
|
222
218
|
// src/backend/registry.ts
|
|
223
219
|
var BackendRegistry = class {
|
|
224
220
|
static backends = /* @__PURE__ */ new Map();
|
|
@@ -234,10 +230,11 @@ var BackendRegistry = class {
|
|
|
234
230
|
/**
|
|
235
231
|
* Create a backend instance by type.
|
|
236
232
|
* @param type - Backend type identifier
|
|
233
|
+
* @param config - Optional backend configuration forwarded to the factory
|
|
237
234
|
* @returns A SecretBackend instance
|
|
238
235
|
* @throws {@link BackendUnavailableError} if the backend type is not registered
|
|
239
236
|
*/
|
|
240
|
-
static create(type) {
|
|
237
|
+
static create(type, config) {
|
|
241
238
|
const factory = this.backends.get(type);
|
|
242
239
|
if (factory === void 0) {
|
|
243
240
|
throw new BackendUnavailableError(
|
|
@@ -246,7 +243,7 @@ var BackendRegistry = class {
|
|
|
246
243
|
Array.from(this.backends.keys())
|
|
247
244
|
);
|
|
248
245
|
}
|
|
249
|
-
return factory();
|
|
246
|
+
return factory(config);
|
|
250
247
|
}
|
|
251
248
|
/**
|
|
252
249
|
* Get all registered backend type identifiers.
|
|
@@ -323,17 +320,158 @@ var BackendRegistry = class {
|
|
|
323
320
|
this.setups.clear();
|
|
324
321
|
}
|
|
325
322
|
};
|
|
323
|
+
var STORAGE_DIR_NAME = path3__namespace.join(".vaultkeeper", "file");
|
|
324
|
+
var KEY_FILE = ".key";
|
|
325
|
+
var GCM_IV_BYTES = 12;
|
|
326
|
+
var GCM_KEY_BYTES = 32;
|
|
327
|
+
var GCM_TAG_LENGTH = 128;
|
|
328
|
+
function getStorageDir() {
|
|
329
|
+
return path3__namespace.join(os4__namespace.homedir(), STORAGE_DIR_NAME);
|
|
330
|
+
}
|
|
331
|
+
function getEntryPath(storageDir, id) {
|
|
332
|
+
const safeId = Buffer.from(id, "utf8").toString("hex");
|
|
333
|
+
return path3__namespace.join(storageDir, `${safeId}.enc`);
|
|
334
|
+
}
|
|
335
|
+
async function ensureStorageDir(storageDir) {
|
|
336
|
+
try {
|
|
337
|
+
await fs__namespace.mkdir(storageDir, { recursive: true, mode: 448 });
|
|
338
|
+
} catch (err) {
|
|
339
|
+
if (err instanceof Error && "code" in err && err.code !== "EEXIST") {
|
|
340
|
+
throw new FilesystemError(
|
|
341
|
+
`Failed to create storage directory: ${storageDir}`,
|
|
342
|
+
storageDir,
|
|
343
|
+
"rwx"
|
|
344
|
+
);
|
|
345
|
+
}
|
|
346
|
+
}
|
|
347
|
+
}
|
|
348
|
+
async function getOrCreateKey(storageDir) {
|
|
349
|
+
const keyPath = path3__namespace.join(storageDir, KEY_FILE);
|
|
350
|
+
try {
|
|
351
|
+
const data = await fs__namespace.readFile(keyPath);
|
|
352
|
+
return data;
|
|
353
|
+
} catch (err) {
|
|
354
|
+
if (err instanceof Error && "code" in err && err.code === "ENOENT") {
|
|
355
|
+
const key = crypto__namespace.randomBytes(GCM_KEY_BYTES);
|
|
356
|
+
await fs__namespace.writeFile(keyPath, key, { mode: 384 });
|
|
357
|
+
return key;
|
|
358
|
+
}
|
|
359
|
+
throw err;
|
|
360
|
+
}
|
|
361
|
+
}
|
|
362
|
+
function encryptGcm(key, plaintext) {
|
|
363
|
+
const iv = crypto__namespace.randomBytes(GCM_IV_BYTES);
|
|
364
|
+
const cipher = crypto__namespace.createCipheriv("aes-256-gcm", key, iv, {
|
|
365
|
+
authTagLength: GCM_TAG_LENGTH / 8
|
|
366
|
+
});
|
|
367
|
+
const encrypted = Buffer.concat([cipher.update(plaintext, "utf8"), cipher.final()]);
|
|
368
|
+
const authTag = cipher.getAuthTag();
|
|
369
|
+
return [iv.toString("base64"), authTag.toString("base64"), encrypted.toString("base64")].join(":");
|
|
370
|
+
}
|
|
371
|
+
function decryptGcm(key, encoded) {
|
|
372
|
+
const parts = encoded.split(":");
|
|
373
|
+
if (parts.length !== 3) {
|
|
374
|
+
throw new Error("Invalid encrypted file format: expected iv:authTag:ciphertext");
|
|
375
|
+
}
|
|
376
|
+
const [ivB64, authTagB64, ciphertextB64] = parts;
|
|
377
|
+
if (ivB64 === void 0 || authTagB64 === void 0 || ciphertextB64 === void 0) {
|
|
378
|
+
throw new Error("Invalid encrypted file format: missing part");
|
|
379
|
+
}
|
|
380
|
+
const iv = Buffer.from(ivB64, "base64");
|
|
381
|
+
const authTag = Buffer.from(authTagB64, "base64");
|
|
382
|
+
const ciphertext = Buffer.from(ciphertextB64, "base64");
|
|
383
|
+
const decipher = crypto__namespace.createDecipheriv("aes-256-gcm", key, iv, {
|
|
384
|
+
authTagLength: GCM_TAG_LENGTH / 8
|
|
385
|
+
});
|
|
386
|
+
decipher.setAuthTag(authTag);
|
|
387
|
+
const decrypted = Buffer.concat([decipher.update(ciphertext), decipher.final()]);
|
|
388
|
+
return decrypted.toString("utf8");
|
|
389
|
+
}
|
|
390
|
+
var FileBackend = class {
|
|
391
|
+
type = "file";
|
|
392
|
+
displayName = "Encrypted File Store";
|
|
393
|
+
async isAvailable() {
|
|
394
|
+
try {
|
|
395
|
+
const storageDir = getStorageDir();
|
|
396
|
+
await ensureStorageDir(storageDir);
|
|
397
|
+
return true;
|
|
398
|
+
} catch {
|
|
399
|
+
return false;
|
|
400
|
+
}
|
|
401
|
+
}
|
|
402
|
+
async store(id, secret) {
|
|
403
|
+
const storageDir = getStorageDir();
|
|
404
|
+
await ensureStorageDir(storageDir);
|
|
405
|
+
const key = await getOrCreateKey(storageDir);
|
|
406
|
+
const entryPath = getEntryPath(storageDir, id);
|
|
407
|
+
const encrypted = encryptGcm(key, secret);
|
|
408
|
+
await fs__namespace.writeFile(entryPath, encrypted, { mode: 384 });
|
|
409
|
+
}
|
|
410
|
+
async retrieve(id) {
|
|
411
|
+
const storageDir = getStorageDir();
|
|
412
|
+
const entryPath = getEntryPath(storageDir, id);
|
|
413
|
+
let encoded;
|
|
414
|
+
try {
|
|
415
|
+
encoded = await fs__namespace.readFile(entryPath, "utf8");
|
|
416
|
+
} catch (err) {
|
|
417
|
+
if (err instanceof Error && "code" in err && err.code === "ENOENT") {
|
|
418
|
+
throw new SecretNotFoundError(`Secret not found in file store: ${id}`);
|
|
419
|
+
}
|
|
420
|
+
throw err;
|
|
421
|
+
}
|
|
422
|
+
const key = await getOrCreateKey(storageDir);
|
|
423
|
+
try {
|
|
424
|
+
return decryptGcm(key, encoded);
|
|
425
|
+
} catch (err) {
|
|
426
|
+
throw new Error(
|
|
427
|
+
`Failed to decrypt secret: ${err instanceof Error ? err.message : String(err)}`
|
|
428
|
+
);
|
|
429
|
+
}
|
|
430
|
+
}
|
|
431
|
+
async delete(id) {
|
|
432
|
+
const storageDir = getStorageDir();
|
|
433
|
+
const entryPath = getEntryPath(storageDir, id);
|
|
434
|
+
try {
|
|
435
|
+
await fs__namespace.unlink(entryPath);
|
|
436
|
+
} catch (err) {
|
|
437
|
+
if (err instanceof Error && "code" in err && err.code === "ENOENT") {
|
|
438
|
+
throw new SecretNotFoundError(`Secret not found in file store: ${id}`);
|
|
439
|
+
}
|
|
440
|
+
throw err;
|
|
441
|
+
}
|
|
442
|
+
}
|
|
443
|
+
async exists(id) {
|
|
444
|
+
const storageDir = getStorageDir();
|
|
445
|
+
const entryPath = getEntryPath(storageDir, id);
|
|
446
|
+
try {
|
|
447
|
+
await fs__namespace.access(entryPath);
|
|
448
|
+
return true;
|
|
449
|
+
} catch {
|
|
450
|
+
return false;
|
|
451
|
+
}
|
|
452
|
+
}
|
|
453
|
+
async list() {
|
|
454
|
+
const storageDir = getStorageDir();
|
|
455
|
+
let entries;
|
|
456
|
+
try {
|
|
457
|
+
entries = await fs__namespace.readdir(storageDir);
|
|
458
|
+
} catch {
|
|
459
|
+
return [];
|
|
460
|
+
}
|
|
461
|
+
return entries.filter((f) => f.endsWith(".enc")).map((f) => Buffer.from(f.slice(0, -4), "hex").toString("utf8"));
|
|
462
|
+
}
|
|
463
|
+
};
|
|
326
464
|
async function execCommand(command, args, options) {
|
|
327
|
-
const result = await execCommandFull(command, args);
|
|
465
|
+
const result = await execCommandFull(command, args, options);
|
|
328
466
|
if (result.exitCode !== 0) {
|
|
329
467
|
throw new Error(`Command failed with exit code ${String(result.exitCode)}: ${result.stderr}`);
|
|
330
468
|
}
|
|
331
469
|
return result.stdout.trim();
|
|
332
470
|
}
|
|
333
471
|
function execCommandFull(command, args, options) {
|
|
334
|
-
return new Promise((
|
|
472
|
+
return new Promise((resolve2, reject) => {
|
|
335
473
|
const proc = child_process.spawn(command, args, {
|
|
336
|
-
stdio: ["ignore", "pipe", "pipe"]
|
|
474
|
+
stdio: [options?.stdin !== void 0 ? "pipe" : "ignore", "pipe", "pipe"]
|
|
337
475
|
});
|
|
338
476
|
let stdout = "";
|
|
339
477
|
let stderr = "";
|
|
@@ -343,25 +481,887 @@ function execCommandFull(command, args, options) {
|
|
|
343
481
|
proc.stderr?.on("data", (data) => {
|
|
344
482
|
stderr += data.toString();
|
|
345
483
|
});
|
|
484
|
+
if (options?.stdin !== void 0 && proc.stdin) {
|
|
485
|
+
proc.stdin.write(options.stdin);
|
|
486
|
+
proc.stdin.end();
|
|
487
|
+
}
|
|
488
|
+
if (options?.timeoutMs !== void 0) {
|
|
489
|
+
setTimeout(() => {
|
|
490
|
+
proc.kill("SIGTERM");
|
|
491
|
+
reject(new Error(`Command timed out after ${String(options.timeoutMs)}ms`));
|
|
492
|
+
}, options.timeoutMs);
|
|
493
|
+
}
|
|
346
494
|
proc.on("close", (code) => {
|
|
347
|
-
|
|
495
|
+
resolve2({ stdout, stderr, exitCode: code ?? 1 });
|
|
348
496
|
});
|
|
349
497
|
proc.on("error", (error) => {
|
|
350
498
|
reject(error);
|
|
351
499
|
});
|
|
352
500
|
});
|
|
353
501
|
}
|
|
354
|
-
|
|
355
|
-
|
|
502
|
+
|
|
503
|
+
// src/backend/keychain-backend.ts
|
|
504
|
+
var ACCOUNT = "vaultkeeper";
|
|
505
|
+
var SERVICE_PREFIX = "vaultkeeper:";
|
|
506
|
+
var KeychainBackend = class {
|
|
507
|
+
type = "keychain";
|
|
508
|
+
displayName = "macOS Keychain";
|
|
509
|
+
async isAvailable() {
|
|
510
|
+
if (process.platform !== "darwin") {
|
|
511
|
+
return false;
|
|
512
|
+
}
|
|
513
|
+
try {
|
|
514
|
+
const result = await execCommandFull("security", ["version"]);
|
|
515
|
+
return result.exitCode === 0;
|
|
516
|
+
} catch {
|
|
517
|
+
return false;
|
|
518
|
+
}
|
|
519
|
+
}
|
|
520
|
+
async store(id, secret) {
|
|
521
|
+
const service = `${SERVICE_PREFIX}${id}`;
|
|
522
|
+
const encoded = Buffer.from(secret, "utf8").toString("base64");
|
|
523
|
+
await execCommandFull("security", [
|
|
524
|
+
"delete-generic-password",
|
|
525
|
+
"-a",
|
|
526
|
+
ACCOUNT,
|
|
527
|
+
"-s",
|
|
528
|
+
service
|
|
529
|
+
]);
|
|
530
|
+
await execCommand("security", [
|
|
531
|
+
"add-generic-password",
|
|
532
|
+
"-a",
|
|
533
|
+
ACCOUNT,
|
|
534
|
+
"-s",
|
|
535
|
+
service,
|
|
536
|
+
"-w",
|
|
537
|
+
encoded
|
|
538
|
+
]);
|
|
539
|
+
}
|
|
540
|
+
async retrieve(id) {
|
|
541
|
+
const service = `${SERVICE_PREFIX}${id}`;
|
|
542
|
+
const result = await execCommandFull("security", [
|
|
543
|
+
"find-generic-password",
|
|
544
|
+
"-a",
|
|
545
|
+
ACCOUNT,
|
|
546
|
+
"-s",
|
|
547
|
+
service,
|
|
548
|
+
"-w"
|
|
549
|
+
]);
|
|
550
|
+
if (result.exitCode !== 0) {
|
|
551
|
+
throw new SecretNotFoundError(`Secret not found in macOS Keychain: ${id}`);
|
|
552
|
+
}
|
|
553
|
+
const encoded = result.stdout.trim();
|
|
554
|
+
return Buffer.from(encoded, "base64").toString("utf8");
|
|
555
|
+
}
|
|
556
|
+
async delete(id) {
|
|
557
|
+
const service = `${SERVICE_PREFIX}${id}`;
|
|
558
|
+
const result = await execCommandFull("security", [
|
|
559
|
+
"delete-generic-password",
|
|
560
|
+
"-a",
|
|
561
|
+
ACCOUNT,
|
|
562
|
+
"-s",
|
|
563
|
+
service
|
|
564
|
+
]);
|
|
565
|
+
if (result.exitCode !== 0) {
|
|
566
|
+
throw new SecretNotFoundError(`Secret not found in macOS Keychain: ${id}`);
|
|
567
|
+
}
|
|
568
|
+
}
|
|
569
|
+
async exists(id) {
|
|
570
|
+
const service = `${SERVICE_PREFIX}${id}`;
|
|
571
|
+
const result = await execCommandFull("security", [
|
|
572
|
+
"find-generic-password",
|
|
573
|
+
"-a",
|
|
574
|
+
ACCOUNT,
|
|
575
|
+
"-s",
|
|
576
|
+
service
|
|
577
|
+
]);
|
|
578
|
+
return result.exitCode === 0;
|
|
579
|
+
}
|
|
580
|
+
async list() {
|
|
581
|
+
const result = await execCommandFull("security", [
|
|
582
|
+
"dump-keychain"
|
|
583
|
+
]);
|
|
584
|
+
if (result.exitCode !== 0) {
|
|
585
|
+
return [];
|
|
586
|
+
}
|
|
587
|
+
const ids = [];
|
|
588
|
+
const servicePattern = /0x00000007 <blob>="vaultkeeper:([^"]+)"/g;
|
|
589
|
+
let match = servicePattern.exec(result.stdout);
|
|
590
|
+
while (match !== null) {
|
|
591
|
+
const id = match[1];
|
|
592
|
+
if (id !== void 0) {
|
|
593
|
+
ids.push(id);
|
|
594
|
+
}
|
|
595
|
+
match = servicePattern.exec(result.stdout);
|
|
596
|
+
}
|
|
597
|
+
return ids;
|
|
598
|
+
}
|
|
599
|
+
};
|
|
600
|
+
function getStoragePath() {
|
|
601
|
+
return path3__namespace.join(os4__namespace.homedir(), ".vaultkeeper", "dpapi");
|
|
602
|
+
}
|
|
603
|
+
function getEntryPath2(storageDir, id) {
|
|
604
|
+
const safeId = Buffer.from(id, "utf8").toString("hex");
|
|
605
|
+
return path3__namespace.join(storageDir, `${safeId}.enc`);
|
|
606
|
+
}
|
|
607
|
+
var DpapiBackend = class {
|
|
608
|
+
type = "dpapi";
|
|
609
|
+
displayName = "Windows DPAPI";
|
|
610
|
+
async isAvailable() {
|
|
611
|
+
if (process.platform !== "win32") {
|
|
612
|
+
return false;
|
|
613
|
+
}
|
|
614
|
+
try {
|
|
615
|
+
const result = await execCommandFull("powershell", [
|
|
616
|
+
"-NoProfile",
|
|
617
|
+
"-Command",
|
|
618
|
+
"[System.Security.Cryptography.ProtectedData] | Out-Null; exit 0"
|
|
619
|
+
]);
|
|
620
|
+
return result.exitCode === 0;
|
|
621
|
+
} catch {
|
|
622
|
+
return false;
|
|
623
|
+
}
|
|
624
|
+
}
|
|
625
|
+
async store(id, secret) {
|
|
626
|
+
const storageDir = getStoragePath();
|
|
627
|
+
await fs__namespace.mkdir(storageDir, { recursive: true });
|
|
628
|
+
const entryPath = getEntryPath2(storageDir, id);
|
|
629
|
+
const script = [
|
|
630
|
+
"Add-Type -AssemblyName System.Security",
|
|
631
|
+
`$bytes = [System.Text.Encoding]::UTF8.GetBytes(${JSON.stringify(secret)})`,
|
|
632
|
+
"$entropy = $null",
|
|
633
|
+
"$scope = [System.Security.Cryptography.DataProtectionScope]::CurrentUser",
|
|
634
|
+
"$encrypted = [System.Security.Cryptography.ProtectedData]::Protect($bytes, $entropy, $scope)",
|
|
635
|
+
`[System.IO.File]::WriteAllBytes(${JSON.stringify(entryPath)}, $encrypted)`
|
|
636
|
+
].join("; ");
|
|
637
|
+
await execCommand("powershell", ["-NoProfile", "-Command", script]);
|
|
638
|
+
}
|
|
639
|
+
async retrieve(id) {
|
|
640
|
+
const storageDir = getStoragePath();
|
|
641
|
+
const entryPath = getEntryPath2(storageDir, id);
|
|
642
|
+
try {
|
|
643
|
+
await fs__namespace.access(entryPath);
|
|
644
|
+
} catch {
|
|
645
|
+
throw new SecretNotFoundError(`Secret not found in Windows DPAPI store: ${id}`);
|
|
646
|
+
}
|
|
647
|
+
const script = [
|
|
648
|
+
"Add-Type -AssemblyName System.Security",
|
|
649
|
+
`$encrypted = [System.IO.File]::ReadAllBytes(${JSON.stringify(entryPath)})`,
|
|
650
|
+
"$entropy = $null",
|
|
651
|
+
"$scope = [System.Security.Cryptography.DataProtectionScope]::CurrentUser",
|
|
652
|
+
"$bytes = [System.Security.Cryptography.ProtectedData]::Unprotect($encrypted, $entropy, $scope)",
|
|
653
|
+
"Write-Output ([System.Text.Encoding]::UTF8.GetString($bytes))"
|
|
654
|
+
].join("; ");
|
|
655
|
+
return execCommand("powershell", ["-NoProfile", "-Command", script]);
|
|
656
|
+
}
|
|
657
|
+
async delete(id) {
|
|
658
|
+
const storageDir = getStoragePath();
|
|
659
|
+
const entryPath = getEntryPath2(storageDir, id);
|
|
660
|
+
try {
|
|
661
|
+
await fs__namespace.unlink(entryPath);
|
|
662
|
+
} catch (err) {
|
|
663
|
+
if (err instanceof Error && "code" in err && err.code === "ENOENT") {
|
|
664
|
+
throw new SecretNotFoundError(`Secret not found in Windows DPAPI store: ${id}`);
|
|
665
|
+
}
|
|
666
|
+
throw err;
|
|
667
|
+
}
|
|
668
|
+
}
|
|
669
|
+
async exists(id) {
|
|
670
|
+
const storageDir = getStoragePath();
|
|
671
|
+
const entryPath = getEntryPath2(storageDir, id);
|
|
672
|
+
try {
|
|
673
|
+
await fs__namespace.access(entryPath);
|
|
674
|
+
return true;
|
|
675
|
+
} catch {
|
|
676
|
+
return false;
|
|
677
|
+
}
|
|
678
|
+
}
|
|
679
|
+
async list() {
|
|
680
|
+
const storageDir = getStoragePath();
|
|
681
|
+
let entries;
|
|
682
|
+
try {
|
|
683
|
+
entries = await fs__namespace.readdir(storageDir);
|
|
684
|
+
} catch {
|
|
685
|
+
return [];
|
|
686
|
+
}
|
|
687
|
+
return entries.filter((f) => f.endsWith(".enc")).map((f) => Buffer.from(f.slice(0, -4), "hex").toString("utf8"));
|
|
688
|
+
}
|
|
689
|
+
};
|
|
690
|
+
|
|
691
|
+
// src/backend/secret-tool-backend.ts
|
|
692
|
+
var ATTRIBUTE_KEY = "vaultkeeper-id";
|
|
693
|
+
var LABEL_PREFIX = "vaultkeeper: ";
|
|
694
|
+
var SecretToolBackend = class {
|
|
695
|
+
type = "secret-tool";
|
|
696
|
+
displayName = "Linux Secret Service (secret-tool)";
|
|
697
|
+
async isAvailable() {
|
|
698
|
+
if (process.platform !== "linux") {
|
|
699
|
+
return false;
|
|
700
|
+
}
|
|
701
|
+
try {
|
|
702
|
+
const result = await execCommandFull("secret-tool", ["--version"]);
|
|
703
|
+
return result.exitCode === 0;
|
|
704
|
+
} catch {
|
|
705
|
+
return false;
|
|
706
|
+
}
|
|
707
|
+
}
|
|
708
|
+
async store(id, secret) {
|
|
709
|
+
const label = `${LABEL_PREFIX}${id}`;
|
|
710
|
+
await execCommand(
|
|
711
|
+
"secret-tool",
|
|
712
|
+
["store", "--label", label, ATTRIBUTE_KEY, id],
|
|
713
|
+
{ stdin: secret }
|
|
714
|
+
);
|
|
715
|
+
}
|
|
716
|
+
async retrieve(id) {
|
|
717
|
+
const result = await execCommandFull("secret-tool", ["lookup", ATTRIBUTE_KEY, id]);
|
|
718
|
+
if (result.exitCode !== 0 || result.stdout.trim() === "") {
|
|
719
|
+
throw new SecretNotFoundError(`Secret not found in Secret Service: ${id}`);
|
|
720
|
+
}
|
|
721
|
+
return result.stdout.trim();
|
|
722
|
+
}
|
|
723
|
+
async delete(id) {
|
|
724
|
+
const result = await execCommandFull("secret-tool", ["clear", ATTRIBUTE_KEY, id]);
|
|
725
|
+
if (result.exitCode !== 0) {
|
|
726
|
+
throw new SecretNotFoundError(`Secret not found in Secret Service: ${id}`);
|
|
727
|
+
}
|
|
728
|
+
}
|
|
729
|
+
async exists(id) {
|
|
730
|
+
const result = await execCommandFull("secret-tool", ["lookup", ATTRIBUTE_KEY, id]);
|
|
731
|
+
return result.exitCode === 0 && result.stdout.trim() !== "";
|
|
732
|
+
}
|
|
733
|
+
async list() {
|
|
734
|
+
const result = await execCommandFull("secret-tool", [
|
|
735
|
+
"search",
|
|
736
|
+
ATTRIBUTE_KEY,
|
|
737
|
+
""
|
|
738
|
+
]);
|
|
739
|
+
if (result.exitCode !== 0) {
|
|
740
|
+
return [];
|
|
741
|
+
}
|
|
742
|
+
const ids = [];
|
|
743
|
+
const attrPattern = new RegExp(`attribute\\.${ATTRIBUTE_KEY} = (.+)`, "g");
|
|
744
|
+
let match = attrPattern.exec(result.stdout);
|
|
745
|
+
while (match !== null) {
|
|
746
|
+
const id = match[1];
|
|
747
|
+
if (id !== void 0) {
|
|
748
|
+
ids.push(id);
|
|
749
|
+
}
|
|
750
|
+
match = attrPattern.exec(result.stdout);
|
|
751
|
+
}
|
|
752
|
+
return ids;
|
|
753
|
+
}
|
|
754
|
+
};
|
|
755
|
+
var INTEGRATION_NAME = "vaultkeeper";
|
|
756
|
+
var cachedVersion;
|
|
757
|
+
function getIntegrationVersion() {
|
|
758
|
+
if (cachedVersion !== void 0) return cachedVersion;
|
|
759
|
+
const dir = path3.dirname(url.fileURLToPath((typeof document === 'undefined' ? require('u' + 'rl').pathToFileURL(__filename).href : (_documentCurrentScript && _documentCurrentScript.tagName.toUpperCase() === 'SCRIPT' && _documentCurrentScript.src || new URL('index.cjs', document.baseURI).href))));
|
|
760
|
+
const candidates = [
|
|
761
|
+
path3.resolve(dir, "..", "..", "package.json"),
|
|
762
|
+
path3.resolve(dir, "..", "package.json")
|
|
763
|
+
];
|
|
764
|
+
for (const candidate of candidates) {
|
|
765
|
+
if (!fs4.existsSync(candidate)) continue;
|
|
766
|
+
const raw = JSON.parse(fs4.readFileSync(candidate, "utf8"));
|
|
767
|
+
if (raw !== null && typeof raw === "object" && "version" in raw && typeof raw.version === "string") {
|
|
768
|
+
cachedVersion = raw.version;
|
|
769
|
+
return cachedVersion;
|
|
770
|
+
}
|
|
771
|
+
}
|
|
772
|
+
throw new Error(
|
|
773
|
+
`Could not read version from vaultkeeper package.json. Tried paths: ${candidates.join(", ")}`
|
|
774
|
+
);
|
|
775
|
+
}
|
|
776
|
+
|
|
777
|
+
// src/backend/one-password-backend.ts
|
|
778
|
+
var SDK_INSTALL_URL = "https://developer.1password.com/docs/sdks/";
|
|
779
|
+
var TAG = "vaultkeeper";
|
|
780
|
+
var PASSWORD_FIELD_TITLE = "password";
|
|
781
|
+
var SESSION_TIMEOUT_MS = 3e4;
|
|
782
|
+
function isWorkerSuccess(res) {
|
|
783
|
+
return "value" in res;
|
|
784
|
+
}
|
|
785
|
+
function isWorkerResponse(value) {
|
|
786
|
+
if (value === null || typeof value !== "object") return false;
|
|
787
|
+
if ("value" in value && typeof value.value === "string") return true;
|
|
788
|
+
if ("error" in value && typeof value.error === "string" && "code" in value && typeof value.code === "string")
|
|
789
|
+
return true;
|
|
790
|
+
return false;
|
|
791
|
+
}
|
|
792
|
+
var OnePasswordBackend = class {
|
|
793
|
+
type = "1password";
|
|
794
|
+
displayName = "1Password";
|
|
795
|
+
vaultId;
|
|
796
|
+
account;
|
|
797
|
+
serviceAccountToken;
|
|
798
|
+
accessMode;
|
|
799
|
+
sessionTimeoutMs;
|
|
800
|
+
/** In-flight or resolved client promise — prevents duplicate createClient calls. */
|
|
801
|
+
clientPromise;
|
|
802
|
+
constructor(options) {
|
|
803
|
+
if (options.accessMode === "per-access" && options.serviceAccountToken !== void 0) {
|
|
804
|
+
throw new Error(
|
|
805
|
+
"per-access mode requires desktop biometric authentication and cannot be used with a service account token"
|
|
806
|
+
);
|
|
807
|
+
}
|
|
808
|
+
if (options.account !== void 0 && options.serviceAccountToken !== void 0) {
|
|
809
|
+
throw new Error(
|
|
810
|
+
"account and serviceAccountToken are mutually exclusive \u2014 provide one or the other, not both"
|
|
811
|
+
);
|
|
812
|
+
}
|
|
813
|
+
this.vaultId = options.vault;
|
|
814
|
+
this.sessionTimeoutMs = options.sessionTimeoutMs ?? SESSION_TIMEOUT_MS;
|
|
815
|
+
if (options.account !== void 0) {
|
|
816
|
+
this.account = options.account;
|
|
817
|
+
}
|
|
818
|
+
if (options.serviceAccountToken !== void 0) {
|
|
819
|
+
this.serviceAccountToken = options.serviceAccountToken;
|
|
820
|
+
}
|
|
821
|
+
this.accessMode = options.accessMode ?? "session";
|
|
822
|
+
}
|
|
823
|
+
async isAvailable() {
|
|
824
|
+
const sdk = await this.tryLoadSdk();
|
|
825
|
+
return sdk !== null;
|
|
826
|
+
}
|
|
827
|
+
// ---- Session client management ----
|
|
828
|
+
/**
|
|
829
|
+
* Dynamically import the SDK. Returns `null` if the SDK is not installed or
|
|
830
|
+
* the native library cannot be loaded.
|
|
831
|
+
*/
|
|
832
|
+
async tryLoadSdk() {
|
|
833
|
+
try {
|
|
834
|
+
const sdk = await import('@1password/sdk');
|
|
835
|
+
return sdk;
|
|
836
|
+
} catch {
|
|
837
|
+
return null;
|
|
838
|
+
}
|
|
839
|
+
}
|
|
840
|
+
/**
|
|
841
|
+
* Acquire (or create) a cached SDK client.
|
|
842
|
+
* Wraps `createClient` with a configurable timeout (default 30 s) to handle
|
|
843
|
+
* the known beta SDK hang after session expiry.
|
|
844
|
+
*/
|
|
845
|
+
acquireClient() {
|
|
846
|
+
this.clientPromise ??= this.createClientInternal().catch((err) => {
|
|
847
|
+
this.clientPromise = void 0;
|
|
848
|
+
throw err;
|
|
849
|
+
});
|
|
850
|
+
return this.clientPromise;
|
|
851
|
+
}
|
|
852
|
+
async createClientInternal() {
|
|
853
|
+
const sdk = await this.tryLoadSdk();
|
|
854
|
+
if (sdk === null) {
|
|
855
|
+
throw new PluginNotFoundError(
|
|
856
|
+
"1Password SDK (@1password/sdk) is not available. Install it to use this backend.",
|
|
857
|
+
"@1password/sdk",
|
|
858
|
+
SDK_INSTALL_URL
|
|
859
|
+
);
|
|
860
|
+
}
|
|
861
|
+
const auth = this.buildAuth(sdk);
|
|
862
|
+
let timerId;
|
|
863
|
+
const timeoutPromise = new Promise((_resolve, reject) => {
|
|
864
|
+
timerId = setTimeout(() => {
|
|
865
|
+
reject(new BackendLockedError("1Password session timed out waiting for authentication", true));
|
|
866
|
+
}, this.sessionTimeoutMs);
|
|
867
|
+
});
|
|
868
|
+
try {
|
|
869
|
+
const client = await Promise.race([
|
|
870
|
+
sdk.createClient({
|
|
871
|
+
auth,
|
|
872
|
+
integrationName: INTEGRATION_NAME,
|
|
873
|
+
integrationVersion: getIntegrationVersion()
|
|
874
|
+
}),
|
|
875
|
+
timeoutPromise
|
|
876
|
+
]);
|
|
877
|
+
return client;
|
|
878
|
+
} catch (err) {
|
|
879
|
+
if (err instanceof BackendLockedError) {
|
|
880
|
+
throw err;
|
|
881
|
+
}
|
|
882
|
+
if (err instanceof sdk.DesktopSessionExpiredError) {
|
|
883
|
+
throw new BackendLockedError(
|
|
884
|
+
"1Password session has expired. Please unlock the app.",
|
|
885
|
+
true
|
|
886
|
+
);
|
|
887
|
+
}
|
|
888
|
+
throw new AuthorizationDeniedError(
|
|
889
|
+
`1Password authentication failed: ${String(err)}`
|
|
890
|
+
);
|
|
891
|
+
} finally {
|
|
892
|
+
if (timerId !== void 0) {
|
|
893
|
+
clearTimeout(timerId);
|
|
894
|
+
}
|
|
895
|
+
}
|
|
896
|
+
}
|
|
897
|
+
buildAuth(sdk) {
|
|
898
|
+
if (this.serviceAccountToken !== void 0) {
|
|
899
|
+
return this.serviceAccountToken;
|
|
900
|
+
}
|
|
901
|
+
const accountName = this.account ?? "";
|
|
902
|
+
return new sdk.DesktopAuth(accountName);
|
|
903
|
+
}
|
|
904
|
+
// ---- Helpers for item lookup by title ----
|
|
905
|
+
/**
|
|
906
|
+
* List all items in the vault tagged "vaultkeeper" and find one with the
|
|
907
|
+
* matching title (= secret ID). Returns `undefined` if not found.
|
|
908
|
+
*/
|
|
909
|
+
async findItemOverview(client, id) {
|
|
910
|
+
const overviews = await client.items.list(this.vaultId);
|
|
911
|
+
for (const overview of overviews) {
|
|
912
|
+
if (overview.title === id && overview.tags.includes(TAG)) {
|
|
913
|
+
return overview;
|
|
914
|
+
}
|
|
915
|
+
}
|
|
916
|
+
return void 0;
|
|
917
|
+
}
|
|
918
|
+
/**
|
|
919
|
+
* Fetch the full item for a given secret id. Returns `undefined` if not found.
|
|
920
|
+
*/
|
|
921
|
+
async findItem(client, id) {
|
|
922
|
+
const overview = await this.findItemOverview(client, id);
|
|
923
|
+
if (overview === void 0) return void 0;
|
|
924
|
+
return client.items.get(this.vaultId, overview.id);
|
|
925
|
+
}
|
|
926
|
+
/**
|
|
927
|
+
* Extract the concealed password field value from an item.
|
|
928
|
+
*/
|
|
929
|
+
extractSecret(item, id) {
|
|
930
|
+
for (const field of item.fields) {
|
|
931
|
+
if (field.title === PASSWORD_FIELD_TITLE) {
|
|
932
|
+
return field.value;
|
|
933
|
+
}
|
|
934
|
+
}
|
|
935
|
+
throw new SecretNotFoundError(
|
|
936
|
+
`Secret found in 1Password but missing password field: ${id}`
|
|
937
|
+
);
|
|
938
|
+
}
|
|
939
|
+
// ---- SecretBackend / ListableBackend implementation ----
|
|
940
|
+
async store(id, secret) {
|
|
941
|
+
const { ItemCategory, ItemFieldType } = await this.requireSdk();
|
|
942
|
+
const client = await this.acquireClient();
|
|
943
|
+
const existing = await this.findItem(client, id);
|
|
944
|
+
if (existing !== void 0) {
|
|
945
|
+
const hasPasswordField = existing.fields.some((f) => f.title === PASSWORD_FIELD_TITLE);
|
|
946
|
+
const updatedFields = hasPasswordField ? existing.fields.map((f) => {
|
|
947
|
+
if (f.title === PASSWORD_FIELD_TITLE) {
|
|
948
|
+
return { ...f, value: secret };
|
|
949
|
+
}
|
|
950
|
+
return f;
|
|
951
|
+
}) : [
|
|
952
|
+
...existing.fields,
|
|
953
|
+
{
|
|
954
|
+
id: "password",
|
|
955
|
+
title: PASSWORD_FIELD_TITLE,
|
|
956
|
+
fieldType: ItemFieldType.Concealed,
|
|
957
|
+
value: secret
|
|
958
|
+
}
|
|
959
|
+
];
|
|
960
|
+
await client.items.put({ ...existing, fields: updatedFields });
|
|
961
|
+
} else {
|
|
962
|
+
await client.items.create({
|
|
963
|
+
category: ItemCategory.Password,
|
|
964
|
+
vaultId: this.vaultId,
|
|
965
|
+
title: id,
|
|
966
|
+
tags: [TAG],
|
|
967
|
+
fields: [
|
|
968
|
+
{
|
|
969
|
+
id: "password",
|
|
970
|
+
title: PASSWORD_FIELD_TITLE,
|
|
971
|
+
fieldType: ItemFieldType.Concealed,
|
|
972
|
+
value: secret
|
|
973
|
+
}
|
|
974
|
+
]
|
|
975
|
+
});
|
|
976
|
+
}
|
|
977
|
+
}
|
|
978
|
+
async retrieve(id) {
|
|
979
|
+
if (this.accessMode === "per-access") {
|
|
980
|
+
return this.retrieveViaWorker(id);
|
|
981
|
+
}
|
|
982
|
+
return this.retrieveViaSession(id);
|
|
983
|
+
}
|
|
984
|
+
async retrieveViaSession(id) {
|
|
985
|
+
const client = await this.acquireClient();
|
|
986
|
+
const item = await this.findItem(client, id);
|
|
987
|
+
if (item === void 0) {
|
|
988
|
+
throw new SecretNotFoundError(`Secret not found in 1Password: ${id}`);
|
|
989
|
+
}
|
|
990
|
+
return this.extractSecret(item, id);
|
|
991
|
+
}
|
|
992
|
+
/**
|
|
993
|
+
* Spawn the per-access worker script that triggers a fresh biometric prompt
|
|
994
|
+
* for each retrieval, then returns the secret from its stdout.
|
|
995
|
+
*/
|
|
996
|
+
retrieveViaWorker(id) {
|
|
997
|
+
return new Promise((resolve2, reject) => {
|
|
998
|
+
const workerPath = path3.join(
|
|
999
|
+
path3.dirname(url.fileURLToPath((typeof document === 'undefined' ? require('u' + 'rl').pathToFileURL(__filename).href : (_documentCurrentScript && _documentCurrentScript.tagName.toUpperCase() === 'SCRIPT' && _documentCurrentScript.src || new URL('index.cjs', document.baseURI).href)))),
|
|
1000
|
+
"one-password-worker.js"
|
|
1001
|
+
);
|
|
1002
|
+
const accountArg = this.account ?? "";
|
|
1003
|
+
const child = child_process.spawn(
|
|
1004
|
+
process.execPath,
|
|
1005
|
+
[workerPath, accountArg, this.vaultId, id],
|
|
1006
|
+
{ stdio: ["ignore", "pipe", "pipe"] }
|
|
1007
|
+
);
|
|
1008
|
+
const stdoutChunks = [];
|
|
1009
|
+
const stderrChunks = [];
|
|
1010
|
+
child.stdout.on("data", (chunk) => {
|
|
1011
|
+
stdoutChunks.push(chunk);
|
|
1012
|
+
});
|
|
1013
|
+
child.stderr.on("data", (chunk) => {
|
|
1014
|
+
stderrChunks.push(chunk);
|
|
1015
|
+
});
|
|
1016
|
+
child.on("close", (code) => {
|
|
1017
|
+
const raw = Buffer.concat(stdoutChunks).toString("utf8").trim();
|
|
1018
|
+
if (raw === "") {
|
|
1019
|
+
const stderr = Buffer.concat(stderrChunks).toString("utf8").trim();
|
|
1020
|
+
const detail = stderr !== "" ? stderr : `exit code ${String(code)}`;
|
|
1021
|
+
reject(new Error(`1Password per-access worker crashed for secret ${id}: ${detail}`));
|
|
1022
|
+
return;
|
|
1023
|
+
}
|
|
1024
|
+
let parsed;
|
|
1025
|
+
try {
|
|
1026
|
+
parsed = JSON.parse(raw);
|
|
1027
|
+
} catch {
|
|
1028
|
+
reject(new SecretNotFoundError(`Worker returned unparseable output for secret: ${id}`));
|
|
1029
|
+
return;
|
|
1030
|
+
}
|
|
1031
|
+
if (!isWorkerResponse(parsed)) {
|
|
1032
|
+
reject(new SecretNotFoundError(`Worker returned unexpected response shape for secret: ${id}`));
|
|
1033
|
+
return;
|
|
1034
|
+
}
|
|
1035
|
+
if (isWorkerSuccess(parsed)) {
|
|
1036
|
+
resolve2(parsed.value);
|
|
1037
|
+
} else {
|
|
1038
|
+
switch (parsed.code) {
|
|
1039
|
+
case "NOT_FOUND":
|
|
1040
|
+
reject(new SecretNotFoundError(`Secret not found in 1Password: ${id}`));
|
|
1041
|
+
break;
|
|
1042
|
+
case "AUTH_DENIED":
|
|
1043
|
+
reject(new AuthorizationDeniedError("1Password authentication was denied"));
|
|
1044
|
+
break;
|
|
1045
|
+
case "LOCKED":
|
|
1046
|
+
reject(new BackendLockedError("1Password is locked. Please unlock and retry.", true));
|
|
1047
|
+
break;
|
|
1048
|
+
default:
|
|
1049
|
+
reject(new SecretNotFoundError(`Worker failed for secret ${id}: ${parsed.error}`));
|
|
1050
|
+
}
|
|
1051
|
+
}
|
|
1052
|
+
});
|
|
1053
|
+
child.on("error", (err) => {
|
|
1054
|
+
reject(new Error(
|
|
1055
|
+
`Failed to spawn 1Password per-access worker at ${workerPath}: ${String(err)}`
|
|
1056
|
+
));
|
|
1057
|
+
});
|
|
1058
|
+
});
|
|
1059
|
+
}
|
|
1060
|
+
async delete(id) {
|
|
1061
|
+
const client = await this.acquireClient();
|
|
1062
|
+
const overview = await this.findItemOverview(client, id);
|
|
1063
|
+
if (overview === void 0) {
|
|
1064
|
+
throw new SecretNotFoundError(`Secret not found in 1Password: ${id}`);
|
|
1065
|
+
}
|
|
1066
|
+
await client.items.delete(this.vaultId, overview.id);
|
|
1067
|
+
}
|
|
1068
|
+
async exists(id) {
|
|
1069
|
+
const client = await this.acquireClient();
|
|
1070
|
+
const overview = await this.findItemOverview(client, id);
|
|
1071
|
+
return overview !== void 0;
|
|
1072
|
+
}
|
|
1073
|
+
async list() {
|
|
1074
|
+
const client = await this.acquireClient();
|
|
1075
|
+
const overviews = await client.items.list(this.vaultId);
|
|
1076
|
+
const ids = [];
|
|
1077
|
+
for (const overview of overviews) {
|
|
1078
|
+
if (overview.tags.includes(TAG)) {
|
|
1079
|
+
ids.push(overview.title);
|
|
1080
|
+
}
|
|
1081
|
+
}
|
|
1082
|
+
return ids;
|
|
1083
|
+
}
|
|
1084
|
+
// ---- Private helpers ----
|
|
1085
|
+
/** Load SDK and throw PluginNotFoundError if unavailable. */
|
|
1086
|
+
async requireSdk() {
|
|
1087
|
+
const sdk = await this.tryLoadSdk();
|
|
1088
|
+
if (sdk === null) {
|
|
1089
|
+
throw new PluginNotFoundError(
|
|
1090
|
+
"1Password SDK (@1password/sdk) is not available.",
|
|
1091
|
+
"@1password/sdk",
|
|
1092
|
+
SDK_INSTALL_URL
|
|
1093
|
+
);
|
|
1094
|
+
}
|
|
1095
|
+
return sdk;
|
|
1096
|
+
}
|
|
1097
|
+
};
|
|
1098
|
+
var YKMAN_INSTALL_URL = "https://developers.yubico.com/yubikey-manager/";
|
|
1099
|
+
var STORAGE_DIR_NAME2 = path3__namespace.join(".vaultkeeper", "yubikey");
|
|
1100
|
+
var METADATA_FILE = "metadata.json";
|
|
1101
|
+
var DEVICE_TIMEOUT_MS = 5e3;
|
|
1102
|
+
var GCM_IV_BYTES2 = 12;
|
|
1103
|
+
var GCM_KEY_BYTES2 = 32;
|
|
1104
|
+
var GCM_TAG_LENGTH_BITS = 128;
|
|
1105
|
+
var FORMAT_VERSION = "1";
|
|
1106
|
+
function getStorageDir2() {
|
|
1107
|
+
return path3__namespace.join(os4__namespace.homedir(), STORAGE_DIR_NAME2);
|
|
1108
|
+
}
|
|
1109
|
+
function getEntryPath3(storageDir, id) {
|
|
1110
|
+
const safeId = Buffer.from(id, "utf8").toString("hex");
|
|
1111
|
+
return path3__namespace.join(storageDir, `${safeId}.enc`);
|
|
1112
|
+
}
|
|
1113
|
+
function isStringRecord(value) {
|
|
1114
|
+
if (value === null || typeof value !== "object") {
|
|
1115
|
+
return false;
|
|
1116
|
+
}
|
|
1117
|
+
return Object.values(value).every((v) => typeof v === "string");
|
|
1118
|
+
}
|
|
1119
|
+
async function loadMetadata(storageDir) {
|
|
1120
|
+
const metaPath = path3__namespace.join(storageDir, METADATA_FILE);
|
|
1121
|
+
try {
|
|
1122
|
+
const raw = await fs__namespace.readFile(metaPath, "utf8");
|
|
1123
|
+
const parsed = JSON.parse(raw);
|
|
1124
|
+
if (parsed !== null && typeof parsed === "object" && "entries" in parsed && isStringRecord(parsed.entries)) {
|
|
1125
|
+
return { entries: parsed.entries };
|
|
1126
|
+
}
|
|
1127
|
+
return { entries: {} };
|
|
1128
|
+
} catch {
|
|
1129
|
+
return { entries: {} };
|
|
1130
|
+
}
|
|
1131
|
+
}
|
|
1132
|
+
async function saveMetadata(storageDir, metadata) {
|
|
1133
|
+
const metaPath = path3__namespace.join(storageDir, METADATA_FILE);
|
|
1134
|
+
await fs__namespace.writeFile(metaPath, JSON.stringify(metadata, null, 2), { mode: 384 });
|
|
1135
|
+
}
|
|
1136
|
+
var HMAC_RESPONSE_HEX_LENGTH = 40;
|
|
1137
|
+
var HMAC_RESPONSE_RE = /^[0-9a-fA-F]{40}$/;
|
|
1138
|
+
function deriveKey(hmacResponse, id) {
|
|
1139
|
+
const trimmed = hmacResponse.trim();
|
|
1140
|
+
if (!HMAC_RESPONSE_RE.test(trimmed)) {
|
|
1141
|
+
throw new Error(
|
|
1142
|
+
`Invalid YubiKey HMAC response: expected exactly ${String(HMAC_RESPONSE_HEX_LENGTH)} hex characters (20 bytes), got ${String(trimmed.length)} characters`
|
|
1143
|
+
);
|
|
1144
|
+
}
|
|
1145
|
+
const ikm = Buffer.from(trimmed, "hex");
|
|
1146
|
+
const info = Buffer.from(`vaultkeeper-yubikey:${id}`, "utf8");
|
|
1147
|
+
const keyMaterial = crypto__namespace.hkdfSync("sha256", ikm, Buffer.alloc(0), info, GCM_KEY_BYTES2);
|
|
1148
|
+
return Buffer.from(keyMaterial);
|
|
1149
|
+
}
|
|
1150
|
+
function encryptGcm2(key, plaintext) {
|
|
1151
|
+
const iv = crypto__namespace.randomBytes(GCM_IV_BYTES2);
|
|
1152
|
+
let encrypted;
|
|
1153
|
+
let authTag;
|
|
1154
|
+
try {
|
|
1155
|
+
const cipher = crypto__namespace.createCipheriv("aes-256-gcm", key, iv, {
|
|
1156
|
+
authTagLength: GCM_TAG_LENGTH_BITS / 8
|
|
1157
|
+
});
|
|
1158
|
+
encrypted = Buffer.concat([cipher.update(plaintext, "utf8"), cipher.final()]);
|
|
1159
|
+
authTag = cipher.getAuthTag();
|
|
1160
|
+
return [
|
|
1161
|
+
FORMAT_VERSION,
|
|
1162
|
+
iv.toString("base64"),
|
|
1163
|
+
authTag.toString("base64"),
|
|
1164
|
+
encrypted.toString("base64")
|
|
1165
|
+
].join(":");
|
|
1166
|
+
} finally {
|
|
1167
|
+
key.fill(0);
|
|
1168
|
+
iv.fill(0);
|
|
1169
|
+
encrypted?.fill(0);
|
|
1170
|
+
authTag?.fill(0);
|
|
1171
|
+
}
|
|
1172
|
+
}
|
|
1173
|
+
function decryptGcm2(key, encoded) {
|
|
1174
|
+
const parts = encoded.split(":");
|
|
1175
|
+
const versionSegment = parts[0] ?? "";
|
|
1176
|
+
const parsedVersion = parseInt(versionSegment, 10);
|
|
1177
|
+
const isNumericVersion = String(parsedVersion) === versionSegment && !Number.isNaN(parsedVersion);
|
|
1178
|
+
if (!isNumericVersion) {
|
|
1179
|
+
key.fill(0);
|
|
1180
|
+
throw new Error(
|
|
1181
|
+
"Encrypted file uses a legacy format (AES-256-CBC). Delete the secret and re-store it to migrate to AES-256-GCM."
|
|
1182
|
+
);
|
|
1183
|
+
}
|
|
1184
|
+
if (versionSegment !== FORMAT_VERSION) {
|
|
1185
|
+
key.fill(0);
|
|
1186
|
+
throw new Error(
|
|
1187
|
+
`Unsupported encrypted file version: ${versionSegment}. This vaultkeeper build only supports version ${FORMAT_VERSION}. Upgrade vaultkeeper to read this secret.`
|
|
1188
|
+
);
|
|
1189
|
+
}
|
|
1190
|
+
if (parts.length !== 4) {
|
|
1191
|
+
key.fill(0);
|
|
1192
|
+
throw new Error(
|
|
1193
|
+
`Invalid encrypted file format: expected ${FORMAT_VERSION}:iv:authTag:ciphertext`
|
|
1194
|
+
);
|
|
1195
|
+
}
|
|
1196
|
+
const [_version, ivB64, authTagB64, ciphertextB64] = parts;
|
|
1197
|
+
if (ivB64 === void 0 || authTagB64 === void 0 || ciphertextB64 === void 0) {
|
|
1198
|
+
key.fill(0);
|
|
1199
|
+
throw new Error("Invalid encrypted file format: missing part");
|
|
1200
|
+
}
|
|
1201
|
+
let decrypted;
|
|
1202
|
+
try {
|
|
1203
|
+
const iv = Buffer.from(ivB64, "base64");
|
|
1204
|
+
const authTag = Buffer.from(authTagB64, "base64");
|
|
1205
|
+
const ciphertext = Buffer.from(ciphertextB64, "base64");
|
|
1206
|
+
const decipher = crypto__namespace.createDecipheriv("aes-256-gcm", key, iv, {
|
|
1207
|
+
authTagLength: GCM_TAG_LENGTH_BITS / 8
|
|
1208
|
+
});
|
|
1209
|
+
decipher.setAuthTag(authTag);
|
|
1210
|
+
try {
|
|
1211
|
+
decrypted = Buffer.concat([decipher.update(ciphertext), decipher.final()]);
|
|
1212
|
+
} catch (err) {
|
|
1213
|
+
throw new Error(
|
|
1214
|
+
`GCM authentication failed \u2014 ciphertext may be tampered: ${err instanceof Error ? err.message : String(err)}`
|
|
1215
|
+
);
|
|
1216
|
+
}
|
|
1217
|
+
const plaintext = decrypted.toString("utf8");
|
|
1218
|
+
return plaintext;
|
|
1219
|
+
} finally {
|
|
1220
|
+
key.fill(0);
|
|
1221
|
+
decrypted?.fill(0);
|
|
1222
|
+
}
|
|
1223
|
+
}
|
|
1224
|
+
var YubikeyBackend = class {
|
|
1225
|
+
type = "yubikey";
|
|
1226
|
+
displayName = "YubiKey";
|
|
1227
|
+
async isAvailable() {
|
|
1228
|
+
try {
|
|
1229
|
+
const result = await execCommandFull("ykman", ["--version"]);
|
|
1230
|
+
if (result.exitCode !== 0) {
|
|
1231
|
+
return false;
|
|
1232
|
+
}
|
|
1233
|
+
const listResult = await execCommandFull("ykman", ["list"]);
|
|
1234
|
+
return listResult.exitCode === 0 && listResult.stdout.trim() !== "";
|
|
1235
|
+
} catch {
|
|
1236
|
+
return false;
|
|
1237
|
+
}
|
|
1238
|
+
}
|
|
1239
|
+
async requireDevice() {
|
|
1240
|
+
const available = await this.isAvailable();
|
|
1241
|
+
if (!available) {
|
|
1242
|
+
const hasYkman = await execCommandFull("ykman", ["--version"]).then(
|
|
1243
|
+
(r) => r.exitCode === 0,
|
|
1244
|
+
() => false
|
|
1245
|
+
);
|
|
1246
|
+
if (!hasYkman) {
|
|
1247
|
+
throw new PluginNotFoundError("ykman is not installed", "ykman", YKMAN_INSTALL_URL);
|
|
1248
|
+
}
|
|
1249
|
+
throw new DeviceNotPresentError("No YubiKey device detected", DEVICE_TIMEOUT_MS);
|
|
1250
|
+
}
|
|
1251
|
+
}
|
|
1252
|
+
/**
|
|
1253
|
+
* Perform the YubiKey HMAC-SHA1 challenge-response for `id` and return the
|
|
1254
|
+
* raw hex response string. Throws on device failure.
|
|
1255
|
+
*/
|
|
1256
|
+
async challengeResponse(id) {
|
|
1257
|
+
const challenge = Buffer.from(`vaultkeeper:${id}`, "utf8").toString("hex");
|
|
1258
|
+
const responseResult = await execCommandFull("ykman", ["otp", "calculate", "2", challenge]);
|
|
1259
|
+
if (responseResult.exitCode !== 0) {
|
|
1260
|
+
throw new Error(`YubiKey challenge-response failed: ${responseResult.stderr}`);
|
|
1261
|
+
}
|
|
1262
|
+
return responseResult.stdout.trim();
|
|
1263
|
+
}
|
|
1264
|
+
async store(id, secret) {
|
|
1265
|
+
await this.requireDevice();
|
|
1266
|
+
const storageDir = getStorageDir2();
|
|
1267
|
+
await fs__namespace.mkdir(storageDir, { recursive: true, mode: 448 });
|
|
1268
|
+
const hmacResponse = await this.challengeResponse(id);
|
|
1269
|
+
const key = deriveKey(hmacResponse, id);
|
|
1270
|
+
const encrypted = encryptGcm2(key, secret);
|
|
1271
|
+
const entryPath = getEntryPath3(storageDir, id);
|
|
1272
|
+
await fs__namespace.writeFile(entryPath, encrypted, { mode: 384 });
|
|
1273
|
+
const metadata = await loadMetadata(storageDir);
|
|
1274
|
+
metadata.entries[id] = entryPath;
|
|
1275
|
+
await saveMetadata(storageDir, metadata);
|
|
1276
|
+
}
|
|
1277
|
+
async retrieve(id) {
|
|
1278
|
+
await this.requireDevice();
|
|
1279
|
+
const storageDir = getStorageDir2();
|
|
1280
|
+
const entryPath = getEntryPath3(storageDir, id);
|
|
1281
|
+
try {
|
|
1282
|
+
await fs__namespace.access(entryPath);
|
|
1283
|
+
} catch {
|
|
1284
|
+
throw new SecretNotFoundError(`Secret not found in YubiKey store: ${id}`);
|
|
1285
|
+
}
|
|
1286
|
+
const encoded = await fs__namespace.readFile(entryPath, "utf8");
|
|
1287
|
+
const hmacResponse = await this.challengeResponse(id);
|
|
1288
|
+
const key = deriveKey(hmacResponse, id);
|
|
1289
|
+
return decryptGcm2(key, encoded);
|
|
1290
|
+
}
|
|
1291
|
+
async delete(id) {
|
|
1292
|
+
await this.requireDevice();
|
|
1293
|
+
const storageDir = getStorageDir2();
|
|
1294
|
+
const entryPath = getEntryPath3(storageDir, id);
|
|
1295
|
+
try {
|
|
1296
|
+
await fs__namespace.unlink(entryPath);
|
|
1297
|
+
} catch (err) {
|
|
1298
|
+
if (err instanceof Error && "code" in err && err.code === "ENOENT") {
|
|
1299
|
+
throw new SecretNotFoundError(`Secret not found in YubiKey store: ${id}`);
|
|
1300
|
+
}
|
|
1301
|
+
throw err;
|
|
1302
|
+
}
|
|
1303
|
+
const metadata = await loadMetadata(storageDir);
|
|
1304
|
+
delete metadata.entries[id];
|
|
1305
|
+
await saveMetadata(storageDir, metadata);
|
|
1306
|
+
}
|
|
1307
|
+
async exists(id) {
|
|
1308
|
+
const storageDir = getStorageDir2();
|
|
1309
|
+
const entryPath = getEntryPath3(storageDir, id);
|
|
1310
|
+
try {
|
|
1311
|
+
await fs__namespace.access(entryPath);
|
|
1312
|
+
return true;
|
|
1313
|
+
} catch {
|
|
1314
|
+
return false;
|
|
1315
|
+
}
|
|
1316
|
+
}
|
|
1317
|
+
async list() {
|
|
1318
|
+
const storageDir = getStorageDir2();
|
|
1319
|
+
const metadata = await loadMetadata(storageDir);
|
|
1320
|
+
return Object.keys(metadata.entries);
|
|
1321
|
+
}
|
|
1322
|
+
};
|
|
1323
|
+
|
|
1324
|
+
// src/backend/register-builtins.ts
|
|
1325
|
+
function registerBuiltinBackends() {
|
|
1326
|
+
BackendRegistry.register("file", () => new FileBackend());
|
|
1327
|
+
BackendRegistry.register("keychain", () => new KeychainBackend());
|
|
1328
|
+
BackendRegistry.register("dpapi", () => new DpapiBackend());
|
|
1329
|
+
BackendRegistry.register("secret-tool", () => new SecretToolBackend());
|
|
1330
|
+
BackendRegistry.register("1password", (config) => {
|
|
1331
|
+
const opts = config?.options;
|
|
1332
|
+
const vaultId = opts?.vault ?? "";
|
|
1333
|
+
const opOptions = {
|
|
1334
|
+
vault: vaultId,
|
|
1335
|
+
// 'session' is the safer default — 'per-access' re-prompts on every read.
|
|
1336
|
+
accessMode: opts?.accessMode === "per-access" ? "per-access" : "session"
|
|
1337
|
+
};
|
|
1338
|
+
const account = opts?.account;
|
|
1339
|
+
if (account !== void 0) {
|
|
1340
|
+
opOptions.account = account;
|
|
1341
|
+
}
|
|
1342
|
+
const serviceAccountToken = opts?.serviceAccountToken;
|
|
1343
|
+
if (serviceAccountToken !== void 0) {
|
|
1344
|
+
opOptions.serviceAccountToken = serviceAccountToken;
|
|
1345
|
+
}
|
|
1346
|
+
return new OnePasswordBackend(opOptions);
|
|
1347
|
+
});
|
|
1348
|
+
BackendRegistry.register("yubikey", () => new YubikeyBackend());
|
|
1349
|
+
}
|
|
1350
|
+
registerBuiltinBackends();
|
|
1351
|
+
|
|
1352
|
+
// src/backend/types.ts
|
|
1353
|
+
function isListableBackend(backend) {
|
|
1354
|
+
return "list" in backend && typeof backend.list === "function";
|
|
1355
|
+
}
|
|
356
1356
|
function hashExecutable(filePath) {
|
|
357
|
-
return new Promise((
|
|
358
|
-
const hash =
|
|
1357
|
+
return new Promise((resolve2, reject) => {
|
|
1358
|
+
const hash = crypto__namespace.createHash("sha256");
|
|
359
1359
|
const stream = fs4__namespace.createReadStream(filePath);
|
|
360
1360
|
stream.on("data", (chunk) => {
|
|
361
1361
|
hash.update(chunk);
|
|
362
1362
|
});
|
|
363
1363
|
stream.on("end", () => {
|
|
364
|
-
|
|
1364
|
+
resolve2(hash.digest("hex"));
|
|
365
1365
|
});
|
|
366
1366
|
stream.on("error", (err) => {
|
|
367
1367
|
reject(err);
|
|
@@ -384,10 +1384,10 @@ function isTrustManifestEntry(value) {
|
|
|
384
1384
|
return true;
|
|
385
1385
|
}
|
|
386
1386
|
async function loadManifest(configDir) {
|
|
387
|
-
const manifestPath =
|
|
1387
|
+
const manifestPath = path3__namespace.join(configDir, MANIFEST_FILENAME);
|
|
388
1388
|
let rawText;
|
|
389
1389
|
try {
|
|
390
|
-
rawText = await
|
|
1390
|
+
rawText = await fs__namespace.readFile(manifestPath, "utf8");
|
|
391
1391
|
} catch (err) {
|
|
392
1392
|
if (typeof err === "object" && err !== null && "code" in err && err.code === "ENOENT") {
|
|
393
1393
|
return /* @__PURE__ */ new Map();
|
|
@@ -407,14 +1407,14 @@ async function loadManifest(configDir) {
|
|
|
407
1407
|
return manifest;
|
|
408
1408
|
}
|
|
409
1409
|
async function saveManifest(configDir, manifest) {
|
|
410
|
-
await
|
|
1410
|
+
await fs__namespace.mkdir(configDir, { recursive: true });
|
|
411
1411
|
const entries = {};
|
|
412
1412
|
for (const [namespace, entry] of manifest) {
|
|
413
1413
|
entries[namespace] = entry;
|
|
414
1414
|
}
|
|
415
1415
|
const raw = { version: 1, entries };
|
|
416
|
-
const manifestPath =
|
|
417
|
-
await
|
|
1416
|
+
const manifestPath = path3__namespace.join(configDir, MANIFEST_FILENAME);
|
|
1417
|
+
await fs__namespace.writeFile(manifestPath, JSON.stringify(raw, null, 2), "utf8");
|
|
418
1418
|
}
|
|
419
1419
|
function addTrustedHash(manifest, namespace, hash) {
|
|
420
1420
|
const next = new Map(manifest);
|
|
@@ -525,14 +1525,18 @@ function validateCapabilityToken(token) {
|
|
|
525
1525
|
return claims;
|
|
526
1526
|
}
|
|
527
1527
|
function getDefaultConfigDir() {
|
|
1528
|
+
const envOverride = process.env.VAULTKEEPER_CONFIG_DIR;
|
|
1529
|
+
if (envOverride !== void 0 && envOverride !== "") {
|
|
1530
|
+
return envOverride;
|
|
1531
|
+
}
|
|
528
1532
|
if (process.platform === "win32") {
|
|
529
1533
|
const appData = process.env.APPDATA;
|
|
530
1534
|
if (appData !== void 0) {
|
|
531
|
-
return
|
|
1535
|
+
return path3__namespace.join(appData, "vaultkeeper");
|
|
532
1536
|
}
|
|
533
|
-
return
|
|
1537
|
+
return path3__namespace.join(os4__namespace.homedir(), "AppData", "Roaming", "vaultkeeper");
|
|
534
1538
|
}
|
|
535
|
-
return
|
|
1539
|
+
return path3__namespace.join(os4__namespace.homedir(), ".config", "vaultkeeper");
|
|
536
1540
|
}
|
|
537
1541
|
function defaultConfig() {
|
|
538
1542
|
return {
|
|
@@ -648,10 +1652,10 @@ function validateConfig(config) {
|
|
|
648
1652
|
}
|
|
649
1653
|
async function loadConfig(configDir) {
|
|
650
1654
|
const dir = configDir ?? getDefaultConfigDir();
|
|
651
|
-
const configPath =
|
|
1655
|
+
const configPath = path3__namespace.join(dir, "config.json");
|
|
652
1656
|
let raw;
|
|
653
1657
|
try {
|
|
654
|
-
raw = await
|
|
1658
|
+
raw = await fs__namespace.readFile(configPath, "utf-8");
|
|
655
1659
|
} catch {
|
|
656
1660
|
return defaultConfig();
|
|
657
1661
|
}
|
|
@@ -670,10 +1674,10 @@ var KeyManager = class {
|
|
|
670
1674
|
#rotating = false;
|
|
671
1675
|
/** Generate a new 32-byte key with a timestamp-based id. */
|
|
672
1676
|
generateKey() {
|
|
673
|
-
const randomSuffix =
|
|
1677
|
+
const randomSuffix = crypto__namespace.randomBytes(4).toString("hex");
|
|
674
1678
|
return {
|
|
675
1679
|
id: `k-${String(Date.now())}-${randomSuffix}`,
|
|
676
|
-
key: new Uint8Array(
|
|
1680
|
+
key: new Uint8Array(crypto__namespace.randomBytes(32)),
|
|
677
1681
|
createdAt: /* @__PURE__ */ new Date()
|
|
678
1682
|
};
|
|
679
1683
|
}
|
|
@@ -975,7 +1979,7 @@ function replaceInRecord2(record, secret) {
|
|
|
975
1979
|
function delegatedExec(secret, request) {
|
|
976
1980
|
const args = (request.args ?? []).map((arg) => replacePlaceholder2(arg, secret));
|
|
977
1981
|
const env = request.env !== void 0 ? replaceInRecord2(request.env, secret) : void 0;
|
|
978
|
-
return new Promise((
|
|
1982
|
+
return new Promise((resolve2, reject) => {
|
|
979
1983
|
const spawnOptions = {
|
|
980
1984
|
stdio: ["ignore", "pipe", "pipe"]
|
|
981
1985
|
};
|
|
@@ -995,7 +1999,7 @@ function delegatedExec(secret, request) {
|
|
|
995
1999
|
stderr += data.toString();
|
|
996
2000
|
});
|
|
997
2001
|
proc.on("close", (code) => {
|
|
998
|
-
|
|
2002
|
+
resolve2({ stdout, stderr, exitCode: code ?? 1 });
|
|
999
2003
|
});
|
|
1000
2004
|
proc.on("error", (error) => {
|
|
1001
2005
|
reject(error);
|
|
@@ -1109,10 +2113,10 @@ function resolveAlgorithmForKey(key, override) {
|
|
|
1109
2113
|
|
|
1110
2114
|
// src/access/delegated-sign.ts
|
|
1111
2115
|
function delegatedSign(secretPem, request) {
|
|
1112
|
-
const key =
|
|
2116
|
+
const key = crypto__namespace.createPrivateKey(secretPem);
|
|
1113
2117
|
const { signAlg, label } = resolveAlgorithmForKey(key, request.algorithm);
|
|
1114
2118
|
const data = Buffer.isBuffer(request.data) ? request.data : Buffer.from(request.data);
|
|
1115
|
-
const signature =
|
|
2119
|
+
const signature = crypto__namespace.sign(signAlg, data, key);
|
|
1116
2120
|
return {
|
|
1117
2121
|
signature: signature.toString("base64"),
|
|
1118
2122
|
algorithm: label
|
|
@@ -1121,7 +2125,7 @@ function delegatedSign(secretPem, request) {
|
|
|
1121
2125
|
function delegatedVerify(request) {
|
|
1122
2126
|
let key;
|
|
1123
2127
|
try {
|
|
1124
|
-
key =
|
|
2128
|
+
key = crypto__namespace.createPublicKey(request.publicKey);
|
|
1125
2129
|
} catch {
|
|
1126
2130
|
return false;
|
|
1127
2131
|
}
|
|
@@ -1132,7 +2136,7 @@ function delegatedVerify(request) {
|
|
|
1132
2136
|
const sig = Buffer.from(request.signature, "base64");
|
|
1133
2137
|
try {
|
|
1134
2138
|
const data = Buffer.isBuffer(request.data) ? request.data : Buffer.from(request.data);
|
|
1135
|
-
return
|
|
2139
|
+
return crypto__namespace.verify(signAlg, data, key, sig);
|
|
1136
2140
|
} catch {
|
|
1137
2141
|
return false;
|
|
1138
2142
|
}
|
|
@@ -1390,7 +2394,7 @@ var VaultKeeper = class _VaultKeeper {
|
|
|
1390
2394
|
}
|
|
1391
2395
|
const now = Math.floor(Date.now() / 1e3);
|
|
1392
2396
|
const claims = {
|
|
1393
|
-
jti:
|
|
2397
|
+
jti: crypto__namespace.randomUUID(),
|
|
1394
2398
|
exp: now + ttlMinutes * 60,
|
|
1395
2399
|
iat: now,
|
|
1396
2400
|
sub: secretName,
|
|
@@ -1616,7 +2620,7 @@ var VaultKeeper = class _VaultKeeper {
|
|
|
1616
2620
|
[]
|
|
1617
2621
|
);
|
|
1618
2622
|
}
|
|
1619
|
-
return BackendRegistry.create(firstEnabled.type);
|
|
2623
|
+
return BackendRegistry.create(firstEnabled.type, firstEnabled);
|
|
1620
2624
|
}
|
|
1621
2625
|
#requireBackend() {
|
|
1622
2626
|
if (this.#backend === void 0) {
|