vaultkeeper 0.4.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 +1137 -49
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +103 -10
- package/dist/index.d.ts +103 -10
- package/dist/index.js +1135 -47
- 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.cjs
CHANGED
|
@@ -1,13 +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
|
|
6
|
+
var crypto = require('crypto');
|
|
7
|
+
var child_process = require('child_process');
|
|
8
|
+
var url = require('url');
|
|
8
9
|
var fs4 = require('fs');
|
|
9
10
|
var jose = require('jose');
|
|
10
11
|
|
|
12
|
+
var _documentCurrentScript = typeof document !== 'undefined' ? document.currentScript : null;
|
|
11
13
|
function _interopNamespace(e) {
|
|
12
14
|
if (e && e.__esModule) return e;
|
|
13
15
|
var n = Object.create(null);
|
|
@@ -26,10 +28,10 @@ function _interopNamespace(e) {
|
|
|
26
28
|
return Object.freeze(n);
|
|
27
29
|
}
|
|
28
30
|
|
|
29
|
-
var
|
|
30
|
-
var
|
|
31
|
+
var fs__namespace = /*#__PURE__*/_interopNamespace(fs);
|
|
32
|
+
var path3__namespace = /*#__PURE__*/_interopNamespace(path3);
|
|
31
33
|
var os4__namespace = /*#__PURE__*/_interopNamespace(os4);
|
|
32
|
-
var
|
|
34
|
+
var crypto__namespace = /*#__PURE__*/_interopNamespace(crypto);
|
|
33
35
|
var fs4__namespace = /*#__PURE__*/_interopNamespace(fs4);
|
|
34
36
|
|
|
35
37
|
// src/errors.ts
|
|
@@ -162,6 +164,22 @@ var IdentityMismatchError = class extends VaultError {
|
|
|
162
164
|
this.currentHash = currentHash;
|
|
163
165
|
}
|
|
164
166
|
};
|
|
167
|
+
var InvalidAlgorithmError = class extends VaultError {
|
|
168
|
+
/**
|
|
169
|
+
* The algorithm that was requested.
|
|
170
|
+
*/
|
|
171
|
+
algorithm;
|
|
172
|
+
/**
|
|
173
|
+
* The set of algorithms that are allowed.
|
|
174
|
+
*/
|
|
175
|
+
allowed;
|
|
176
|
+
constructor(message, algorithm, allowed) {
|
|
177
|
+
super(message);
|
|
178
|
+
this.name = "InvalidAlgorithmError";
|
|
179
|
+
this.algorithm = algorithm;
|
|
180
|
+
this.allowed = allowed;
|
|
181
|
+
}
|
|
182
|
+
};
|
|
165
183
|
var SetupError = class extends VaultError {
|
|
166
184
|
/**
|
|
167
185
|
* The name of the dependency that caused the setup failure.
|
|
@@ -197,14 +215,10 @@ var RotationInProgressError = class extends VaultError {
|
|
|
197
215
|
}
|
|
198
216
|
};
|
|
199
217
|
|
|
200
|
-
// src/backend/types.ts
|
|
201
|
-
function isListableBackend(backend) {
|
|
202
|
-
return "list" in backend && typeof backend.list === "function";
|
|
203
|
-
}
|
|
204
|
-
|
|
205
218
|
// src/backend/registry.ts
|
|
206
219
|
var BackendRegistry = class {
|
|
207
220
|
static backends = /* @__PURE__ */ new Map();
|
|
221
|
+
static setups = /* @__PURE__ */ new Map();
|
|
208
222
|
/**
|
|
209
223
|
* Register a backend factory.
|
|
210
224
|
* @param type - Backend type identifier
|
|
@@ -216,10 +230,11 @@ var BackendRegistry = class {
|
|
|
216
230
|
/**
|
|
217
231
|
* Create a backend instance by type.
|
|
218
232
|
* @param type - Backend type identifier
|
|
233
|
+
* @param config - Optional backend configuration forwarded to the factory
|
|
219
234
|
* @returns A SecretBackend instance
|
|
220
|
-
* @throws
|
|
235
|
+
* @throws {@link BackendUnavailableError} if the backend type is not registered
|
|
221
236
|
*/
|
|
222
|
-
static create(type) {
|
|
237
|
+
static create(type, config) {
|
|
223
238
|
const factory = this.backends.get(type);
|
|
224
239
|
if (factory === void 0) {
|
|
225
240
|
throw new BackendUnavailableError(
|
|
@@ -228,7 +243,7 @@ var BackendRegistry = class {
|
|
|
228
243
|
Array.from(this.backends.keys())
|
|
229
244
|
);
|
|
230
245
|
}
|
|
231
|
-
return factory();
|
|
246
|
+
return factory(config);
|
|
232
247
|
}
|
|
233
248
|
/**
|
|
234
249
|
* Get all registered backend type identifiers.
|
|
@@ -264,18 +279,199 @@ var BackendRegistry = class {
|
|
|
264
279
|
);
|
|
265
280
|
return results.filter((type) => type !== null);
|
|
266
281
|
}
|
|
282
|
+
/**
|
|
283
|
+
* Register a setup factory for a backend type.
|
|
284
|
+
* @param type - Backend type identifier
|
|
285
|
+
* @param factory - Factory function that creates a setup generator
|
|
286
|
+
*/
|
|
287
|
+
static registerSetup(type, factory) {
|
|
288
|
+
this.setups.set(type, factory);
|
|
289
|
+
}
|
|
290
|
+
/**
|
|
291
|
+
* Get the setup factory for a backend type, if one is registered.
|
|
292
|
+
* @param type - Backend type identifier
|
|
293
|
+
* @returns The setup factory, or `undefined` if none is registered
|
|
294
|
+
*/
|
|
295
|
+
static getSetup(type) {
|
|
296
|
+
return this.setups.get(type);
|
|
297
|
+
}
|
|
298
|
+
/**
|
|
299
|
+
* Check whether a setup factory is registered for the given backend type.
|
|
300
|
+
* @param type - Backend type identifier
|
|
301
|
+
* @returns `true` if a setup factory is registered
|
|
302
|
+
*/
|
|
303
|
+
static hasSetup(type) {
|
|
304
|
+
return this.setups.has(type);
|
|
305
|
+
}
|
|
306
|
+
/**
|
|
307
|
+
* Clear all registered backend factories.
|
|
308
|
+
* Intended for use in tests only.
|
|
309
|
+
* @internal
|
|
310
|
+
*/
|
|
311
|
+
static clearBackends() {
|
|
312
|
+
this.backends.clear();
|
|
313
|
+
}
|
|
314
|
+
/**
|
|
315
|
+
* Clear all registered setup factories.
|
|
316
|
+
* Intended for use in tests only.
|
|
317
|
+
* @internal
|
|
318
|
+
*/
|
|
319
|
+
static clearSetups() {
|
|
320
|
+
this.setups.clear();
|
|
321
|
+
}
|
|
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
|
+
}
|
|
267
463
|
};
|
|
268
464
|
async function execCommand(command, args, options) {
|
|
269
|
-
const result = await execCommandFull(command, args);
|
|
465
|
+
const result = await execCommandFull(command, args, options);
|
|
270
466
|
if (result.exitCode !== 0) {
|
|
271
467
|
throw new Error(`Command failed with exit code ${String(result.exitCode)}: ${result.stderr}`);
|
|
272
468
|
}
|
|
273
469
|
return result.stdout.trim();
|
|
274
470
|
}
|
|
275
471
|
function execCommandFull(command, args, options) {
|
|
276
|
-
return new Promise((
|
|
472
|
+
return new Promise((resolve2, reject) => {
|
|
277
473
|
const proc = child_process.spawn(command, args, {
|
|
278
|
-
stdio: ["ignore", "pipe", "pipe"]
|
|
474
|
+
stdio: [options?.stdin !== void 0 ? "pipe" : "ignore", "pipe", "pipe"]
|
|
279
475
|
});
|
|
280
476
|
let stdout = "";
|
|
281
477
|
let stderr = "";
|
|
@@ -285,25 +481,887 @@ function execCommandFull(command, args, options) {
|
|
|
285
481
|
proc.stderr?.on("data", (data) => {
|
|
286
482
|
stderr += data.toString();
|
|
287
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
|
+
}
|
|
288
494
|
proc.on("close", (code) => {
|
|
289
|
-
|
|
495
|
+
resolve2({ stdout, stderr, exitCode: code ?? 1 });
|
|
290
496
|
});
|
|
291
497
|
proc.on("error", (error) => {
|
|
292
498
|
reject(error);
|
|
293
499
|
});
|
|
294
500
|
});
|
|
295
501
|
}
|
|
296
|
-
|
|
297
|
-
|
|
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
|
+
}
|
|
298
1356
|
function hashExecutable(filePath) {
|
|
299
|
-
return new Promise((
|
|
300
|
-
const hash =
|
|
1357
|
+
return new Promise((resolve2, reject) => {
|
|
1358
|
+
const hash = crypto__namespace.createHash("sha256");
|
|
301
1359
|
const stream = fs4__namespace.createReadStream(filePath);
|
|
302
1360
|
stream.on("data", (chunk) => {
|
|
303
1361
|
hash.update(chunk);
|
|
304
1362
|
});
|
|
305
1363
|
stream.on("end", () => {
|
|
306
|
-
|
|
1364
|
+
resolve2(hash.digest("hex"));
|
|
307
1365
|
});
|
|
308
1366
|
stream.on("error", (err) => {
|
|
309
1367
|
reject(err);
|
|
@@ -326,10 +1384,10 @@ function isTrustManifestEntry(value) {
|
|
|
326
1384
|
return true;
|
|
327
1385
|
}
|
|
328
1386
|
async function loadManifest(configDir) {
|
|
329
|
-
const manifestPath =
|
|
1387
|
+
const manifestPath = path3__namespace.join(configDir, MANIFEST_FILENAME);
|
|
330
1388
|
let rawText;
|
|
331
1389
|
try {
|
|
332
|
-
rawText = await
|
|
1390
|
+
rawText = await fs__namespace.readFile(manifestPath, "utf8");
|
|
333
1391
|
} catch (err) {
|
|
334
1392
|
if (typeof err === "object" && err !== null && "code" in err && err.code === "ENOENT") {
|
|
335
1393
|
return /* @__PURE__ */ new Map();
|
|
@@ -349,14 +1407,14 @@ async function loadManifest(configDir) {
|
|
|
349
1407
|
return manifest;
|
|
350
1408
|
}
|
|
351
1409
|
async function saveManifest(configDir, manifest) {
|
|
352
|
-
await
|
|
1410
|
+
await fs__namespace.mkdir(configDir, { recursive: true });
|
|
353
1411
|
const entries = {};
|
|
354
1412
|
for (const [namespace, entry] of manifest) {
|
|
355
1413
|
entries[namespace] = entry;
|
|
356
1414
|
}
|
|
357
1415
|
const raw = { version: 1, entries };
|
|
358
|
-
const manifestPath =
|
|
359
|
-
await
|
|
1416
|
+
const manifestPath = path3__namespace.join(configDir, MANIFEST_FILENAME);
|
|
1417
|
+
await fs__namespace.writeFile(manifestPath, JSON.stringify(raw, null, 2), "utf8");
|
|
360
1418
|
}
|
|
361
1419
|
function addTrustedHash(manifest, namespace, hash) {
|
|
362
1420
|
const next = new Map(manifest);
|
|
@@ -467,14 +1525,18 @@ function validateCapabilityToken(token) {
|
|
|
467
1525
|
return claims;
|
|
468
1526
|
}
|
|
469
1527
|
function getDefaultConfigDir() {
|
|
1528
|
+
const envOverride = process.env.VAULTKEEPER_CONFIG_DIR;
|
|
1529
|
+
if (envOverride !== void 0 && envOverride !== "") {
|
|
1530
|
+
return envOverride;
|
|
1531
|
+
}
|
|
470
1532
|
if (process.platform === "win32") {
|
|
471
1533
|
const appData = process.env.APPDATA;
|
|
472
1534
|
if (appData !== void 0) {
|
|
473
|
-
return
|
|
1535
|
+
return path3__namespace.join(appData, "vaultkeeper");
|
|
474
1536
|
}
|
|
475
|
-
return
|
|
1537
|
+
return path3__namespace.join(os4__namespace.homedir(), "AppData", "Roaming", "vaultkeeper");
|
|
476
1538
|
}
|
|
477
|
-
return
|
|
1539
|
+
return path3__namespace.join(os4__namespace.homedir(), ".config", "vaultkeeper");
|
|
478
1540
|
}
|
|
479
1541
|
function defaultConfig() {
|
|
480
1542
|
return {
|
|
@@ -513,6 +1575,21 @@ function validateBackendEntry(entry, index) {
|
|
|
513
1575
|
}
|
|
514
1576
|
result.path = entry.path;
|
|
515
1577
|
}
|
|
1578
|
+
if (entry.options !== void 0) {
|
|
1579
|
+
if (!isObject(entry.options)) {
|
|
1580
|
+
throw new Error(`backends[${String(index)}].options must be an object`);
|
|
1581
|
+
}
|
|
1582
|
+
const opts = {};
|
|
1583
|
+
for (const [k, v] of Object.entries(entry.options)) {
|
|
1584
|
+
if (typeof v !== "string") {
|
|
1585
|
+
throw new Error(
|
|
1586
|
+
`backends[${String(index)}].options["${k}"] must be a string`
|
|
1587
|
+
);
|
|
1588
|
+
}
|
|
1589
|
+
opts[k] = v;
|
|
1590
|
+
}
|
|
1591
|
+
result.options = opts;
|
|
1592
|
+
}
|
|
516
1593
|
return result;
|
|
517
1594
|
}
|
|
518
1595
|
function validateConfig(config) {
|
|
@@ -575,10 +1652,10 @@ function validateConfig(config) {
|
|
|
575
1652
|
}
|
|
576
1653
|
async function loadConfig(configDir) {
|
|
577
1654
|
const dir = configDir ?? getDefaultConfigDir();
|
|
578
|
-
const configPath =
|
|
1655
|
+
const configPath = path3__namespace.join(dir, "config.json");
|
|
579
1656
|
let raw;
|
|
580
1657
|
try {
|
|
581
|
-
raw = await
|
|
1658
|
+
raw = await fs__namespace.readFile(configPath, "utf-8");
|
|
582
1659
|
} catch {
|
|
583
1660
|
return defaultConfig();
|
|
584
1661
|
}
|
|
@@ -597,10 +1674,10 @@ var KeyManager = class {
|
|
|
597
1674
|
#rotating = false;
|
|
598
1675
|
/** Generate a new 32-byte key with a timestamp-based id. */
|
|
599
1676
|
generateKey() {
|
|
600
|
-
const randomSuffix =
|
|
1677
|
+
const randomSuffix = crypto__namespace.randomBytes(4).toString("hex");
|
|
601
1678
|
return {
|
|
602
1679
|
id: `k-${String(Date.now())}-${randomSuffix}`,
|
|
603
|
-
key: new Uint8Array(
|
|
1680
|
+
key: new Uint8Array(crypto__namespace.randomBytes(32)),
|
|
604
1681
|
createdAt: /* @__PURE__ */ new Date()
|
|
605
1682
|
};
|
|
606
1683
|
}
|
|
@@ -902,7 +1979,7 @@ function replaceInRecord2(record, secret) {
|
|
|
902
1979
|
function delegatedExec(secret, request) {
|
|
903
1980
|
const args = (request.args ?? []).map((arg) => replacePlaceholder2(arg, secret));
|
|
904
1981
|
const env = request.env !== void 0 ? replaceInRecord2(request.env, secret) : void 0;
|
|
905
|
-
return new Promise((
|
|
1982
|
+
return new Promise((resolve2, reject) => {
|
|
906
1983
|
const spawnOptions = {
|
|
907
1984
|
stdio: ["ignore", "pipe", "pipe"]
|
|
908
1985
|
};
|
|
@@ -922,7 +1999,7 @@ function delegatedExec(secret, request) {
|
|
|
922
1999
|
stderr += data.toString();
|
|
923
2000
|
});
|
|
924
2001
|
proc.on("close", (code) => {
|
|
925
|
-
|
|
2002
|
+
resolve2({ stdout, stderr, exitCode: code ?? 1 });
|
|
926
2003
|
});
|
|
927
2004
|
proc.on("error", (error) => {
|
|
928
2005
|
reject(error);
|
|
@@ -1023,10 +2100,12 @@ function resolveAlgorithmForKey(key, override) {
|
|
|
1023
2100
|
if (keyType === "ed25519" || keyType === "ed448") {
|
|
1024
2101
|
return { signAlg: null, label: keyType };
|
|
1025
2102
|
}
|
|
1026
|
-
const alg = override ?? "sha256";
|
|
2103
|
+
const alg = (override ?? "sha256").toLowerCase();
|
|
1027
2104
|
if (!ALLOWED_ALGORITHMS.has(alg)) {
|
|
1028
|
-
throw new
|
|
1029
|
-
`Unsupported
|
|
2105
|
+
throw new InvalidAlgorithmError(
|
|
2106
|
+
`Unsupported algorithm '${alg}'. Allowed: ${[...ALLOWED_ALGORITHMS].join(", ")}`,
|
|
2107
|
+
alg,
|
|
2108
|
+
[...ALLOWED_ALGORITHMS]
|
|
1030
2109
|
);
|
|
1031
2110
|
}
|
|
1032
2111
|
return { signAlg: alg, label: alg };
|
|
@@ -1034,10 +2113,10 @@ function resolveAlgorithmForKey(key, override) {
|
|
|
1034
2113
|
|
|
1035
2114
|
// src/access/delegated-sign.ts
|
|
1036
2115
|
function delegatedSign(secretPem, request) {
|
|
1037
|
-
const key =
|
|
2116
|
+
const key = crypto__namespace.createPrivateKey(secretPem);
|
|
1038
2117
|
const { signAlg, label } = resolveAlgorithmForKey(key, request.algorithm);
|
|
1039
2118
|
const data = Buffer.isBuffer(request.data) ? request.data : Buffer.from(request.data);
|
|
1040
|
-
const signature =
|
|
2119
|
+
const signature = crypto__namespace.sign(signAlg, data, key);
|
|
1041
2120
|
return {
|
|
1042
2121
|
signature: signature.toString("base64"),
|
|
1043
2122
|
algorithm: label
|
|
@@ -1046,15 +2125,18 @@ function delegatedSign(secretPem, request) {
|
|
|
1046
2125
|
function delegatedVerify(request) {
|
|
1047
2126
|
let key;
|
|
1048
2127
|
try {
|
|
1049
|
-
key =
|
|
2128
|
+
key = crypto__namespace.createPublicKey(request.publicKey);
|
|
1050
2129
|
} catch {
|
|
1051
2130
|
return false;
|
|
1052
2131
|
}
|
|
2132
|
+
if (typeof request.publicKey === "string" && request.publicKey.includes("PRIVATE KEY")) {
|
|
2133
|
+
return false;
|
|
2134
|
+
}
|
|
1053
2135
|
const { signAlg } = resolveAlgorithmForKey(key, request.algorithm);
|
|
1054
2136
|
const sig = Buffer.from(request.signature, "base64");
|
|
1055
2137
|
try {
|
|
1056
2138
|
const data = Buffer.isBuffer(request.data) ? request.data : Buffer.from(request.data);
|
|
1057
|
-
return
|
|
2139
|
+
return crypto__namespace.verify(signAlg, data, key, sig);
|
|
1058
2140
|
} catch {
|
|
1059
2141
|
return false;
|
|
1060
2142
|
}
|
|
@@ -1312,7 +2394,7 @@ var VaultKeeper = class _VaultKeeper {
|
|
|
1312
2394
|
}
|
|
1313
2395
|
const now = Math.floor(Date.now() / 1e3);
|
|
1314
2396
|
const claims = {
|
|
1315
|
-
jti:
|
|
2397
|
+
jti: crypto__namespace.randomUUID(),
|
|
1316
2398
|
exp: now + ttlMinutes * 60,
|
|
1317
2399
|
iat: now,
|
|
1318
2400
|
sub: secretName,
|
|
@@ -1431,6 +2513,8 @@ var VaultKeeper = class _VaultKeeper {
|
|
|
1431
2513
|
* with the vault metadata (`vaultResponse`).
|
|
1432
2514
|
* @throws {VaultError} If `token` is invalid or was not created by this
|
|
1433
2515
|
* vault instance.
|
|
2516
|
+
* @throws {InvalidAlgorithmError} If `request.algorithm` is not in the
|
|
2517
|
+
* allowed set (e.g. `'md5'`).
|
|
1434
2518
|
*/
|
|
1435
2519
|
async sign(token, request) {
|
|
1436
2520
|
const claims = validateCapabilityToken(token);
|
|
@@ -1448,8 +2532,11 @@ var VaultKeeper = class _VaultKeeper {
|
|
|
1448
2532
|
* capability tokens are required. It is safe to call from CI or any
|
|
1449
2533
|
* context that has access to public key material.
|
|
1450
2534
|
*
|
|
1451
|
-
*
|
|
1452
|
-
*
|
|
2535
|
+
* Returns `false` for invalid key material, malformed signatures, or
|
|
2536
|
+
* any verification failure (except disallowed algorithms, which throw).
|
|
2537
|
+
*
|
|
2538
|
+
* @throws {InvalidAlgorithmError} If `request.algorithm` is not in the
|
|
2539
|
+
* allowed set (e.g. `'md5'`).
|
|
1453
2540
|
*
|
|
1454
2541
|
* @param request - The data, signature, public key, and optional
|
|
1455
2542
|
* algorithm override.
|
|
@@ -1533,7 +2620,7 @@ var VaultKeeper = class _VaultKeeper {
|
|
|
1533
2620
|
[]
|
|
1534
2621
|
);
|
|
1535
2622
|
}
|
|
1536
|
-
return BackendRegistry.create(firstEnabled.type);
|
|
2623
|
+
return BackendRegistry.create(firstEnabled.type, firstEnabled);
|
|
1537
2624
|
}
|
|
1538
2625
|
#requireBackend() {
|
|
1539
2626
|
if (this.#backend === void 0) {
|
|
@@ -1582,6 +2669,7 @@ exports.CapabilityToken = CapabilityToken;
|
|
|
1582
2669
|
exports.DeviceNotPresentError = DeviceNotPresentError;
|
|
1583
2670
|
exports.FilesystemError = FilesystemError;
|
|
1584
2671
|
exports.IdentityMismatchError = IdentityMismatchError;
|
|
2672
|
+
exports.InvalidAlgorithmError = InvalidAlgorithmError;
|
|
1585
2673
|
exports.KeyRevokedError = KeyRevokedError;
|
|
1586
2674
|
exports.KeyRotatedError = KeyRotatedError;
|
|
1587
2675
|
exports.PluginNotFoundError = PluginNotFoundError;
|