vaultkeeper 0.7.0 → 0.8.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/README.md CHANGED
@@ -480,7 +480,11 @@ consecutive required-presence operations each demand their own distinct fresh ac
480
480
  > intrinsic to the cryptographic operation.
481
481
 
482
482
  Real-hardware confirmation for each backend is documented as a manual verification test in
483
- [`docs/manual-tests/presence-per-use.md`](../../docs/manual-tests/presence-per-use.md).
483
+ [`docs/manual-tests/presence-per-use.md`](../../docs/manual-tests/presence-per-use.md). The
484
+ release-process-referenced register of what manual residue remains per backend once its paired
485
+ in-memory double runs the shared conformance corpus in CI — and the cadence/cost of running that
486
+ corpus against the real adapter — lives in
487
+ [`docs/manual-tests/manual-residue-register.md`](../../docs/manual-tests/manual-residue-register.md).
484
488
 
485
489
  ## Doctor / preflight checks
486
490
 
package/dist/index.cjs CHANGED
@@ -473,6 +473,21 @@ var RotationInProgressError = class extends VaultError {
473
473
  this.name = "RotationInProgressError";
474
474
  }
475
475
  };
476
+ var TestDoubleMisuseError = class extends VaultError {
477
+ /** The name of the test double class that refused to construct. */
478
+ doubleName;
479
+ /**
480
+ * The environment value (e.g. `process.env.NODE_ENV`) that triggered the
481
+ * refusal.
482
+ */
483
+ detectedEnvironment;
484
+ constructor(message, doubleName, detectedEnvironment) {
485
+ super(message);
486
+ this.name = "TestDoubleMisuseError";
487
+ this.doubleName = doubleName;
488
+ this.detectedEnvironment = detectedEnvironment;
489
+ }
490
+ };
476
491
 
477
492
  // src/backend/registry.ts
478
493
  var BackendRegistry = class {
@@ -1391,13 +1406,19 @@ var DpapiBackend = class {
1391
1406
  const entryPath = getEntryPath2(storageDir, id);
1392
1407
  const script = [
1393
1408
  "Add-Type -AssemblyName System.Security",
1394
- `$bytes = [System.Text.Encoding]::UTF8.GetBytes(${JSON.stringify(secret)})`,
1409
+ "$b64 = [Console]::In.ReadToEnd()",
1410
+ "$bytes = [System.Convert]::FromBase64String($b64.Trim())",
1395
1411
  "$entropy = $null",
1396
1412
  "$scope = [System.Security.Cryptography.DataProtectionScope]::CurrentUser",
1397
1413
  "$encrypted = [System.Security.Cryptography.ProtectedData]::Protect($bytes, $entropy, $scope)",
1398
1414
  `[System.IO.File]::WriteAllBytes(${JSON.stringify(entryPath)}, $encrypted)`
1399
1415
  ].join("; ");
1400
- await execCommand("powershell", ["-NoProfile", "-Command", script]);
1416
+ const secretBuf = Buffer.from(secret, "utf8");
1417
+ const stdinPayload = secretBuf.toString("base64");
1418
+ secretBuf.fill(0);
1419
+ await execCommand("powershell", ["-NoProfile", "-Command", script], {
1420
+ stdin: stdinPayload
1421
+ });
1401
1422
  }
1402
1423
  async retrieve(id) {
1403
1424
  const storageDir = this.#storageDir;
@@ -1454,6 +1475,9 @@ var DpapiBackend = class {
1454
1475
  // src/backend/secret-tool-backend.ts
1455
1476
  var ATTRIBUTE_KEY = "vaultkeeper-id";
1456
1477
  var LABEL_PREFIX = "vaultkeeper: ";
1478
+ function stripSecretToolTrailingNewline(stdout) {
1479
+ return stdout.endsWith("\n") ? stdout.slice(0, -1) : stdout;
1480
+ }
1457
1481
  var SecretToolBackend = class {
1458
1482
  type = "secret-tool";
1459
1483
  displayName = "Linux Secret Service (secret-tool)";
@@ -1470,29 +1494,29 @@ var SecretToolBackend = class {
1470
1494
  }
1471
1495
  async store(id, secret) {
1472
1496
  const label = `${LABEL_PREFIX}${id}`;
1473
- await execCommand("secret-tool", ["store", "--label", label, ATTRIBUTE_KEY, id], {
1497
+ await execCommand("secret-tool", ["store", "--label", label, "--", ATTRIBUTE_KEY, id], {
1474
1498
  stdin: secret
1475
1499
  });
1476
1500
  }
1477
1501
  async retrieve(id) {
1478
- const result = await execCommandFull("secret-tool", ["lookup", ATTRIBUTE_KEY, id]);
1479
- if (result.exitCode !== 0 || result.stdout.trim() === "") {
1502
+ const result = await execCommandFull("secret-tool", ["lookup", "--", ATTRIBUTE_KEY, id]);
1503
+ if (result.exitCode !== 0) {
1480
1504
  throw new SecretNotFoundError(`Secret not found in Secret Service: ${id}`);
1481
1505
  }
1482
- return result.stdout.trim();
1506
+ return stripSecretToolTrailingNewline(result.stdout);
1483
1507
  }
1484
1508
  async delete(id) {
1485
- const result = await execCommandFull("secret-tool", ["clear", ATTRIBUTE_KEY, id]);
1509
+ const result = await execCommandFull("secret-tool", ["clear", "--", ATTRIBUTE_KEY, id]);
1486
1510
  if (result.exitCode !== 0) {
1487
1511
  throw new SecretNotFoundError(`Secret not found in Secret Service: ${id}`);
1488
1512
  }
1489
1513
  }
1490
1514
  async exists(id) {
1491
- const result = await execCommandFull("secret-tool", ["lookup", ATTRIBUTE_KEY, id]);
1492
- return result.exitCode === 0 && result.stdout.trim() !== "";
1515
+ const result = await execCommandFull("secret-tool", ["lookup", "--", ATTRIBUTE_KEY, id]);
1516
+ return result.exitCode === 0;
1493
1517
  }
1494
1518
  async list() {
1495
- const result = await execCommandFull("secret-tool", ["search", ATTRIBUTE_KEY, ""]);
1519
+ const result = await execCommandFull("secret-tool", ["search", "--", ATTRIBUTE_KEY, ""]);
1496
1520
  if (result.exitCode !== 0) {
1497
1521
  return [];
1498
1522
  }
@@ -2950,11 +2974,16 @@ async function createToken(key, claims, options) {
2950
2974
  function isObject2(value) {
2951
2975
  return typeof value === "object" && value !== null && !Array.isArray(value);
2952
2976
  }
2977
+ function isLeasePresence(value) {
2978
+ if (!isObject2(value)) return false;
2979
+ const { op, at, method, backend } = value;
2980
+ return typeof op === "string" && typeof at === "number" && typeof method === "string" && typeof backend === "string";
2981
+ }
2953
2982
  function parseVaultClaims(raw) {
2954
2983
  if (!isObject2(raw)) {
2955
2984
  return void 0;
2956
2985
  }
2957
- const { jti, exp, iat, sub, exe, use, tid, bkd, val, ref } = raw;
2986
+ const { jti, exp, iat, sub, exe, use, tid, bkd, val, ref, kty, kid, kgen, pres } = raw;
2958
2987
  if (typeof jti !== "string") return void 0;
2959
2988
  if (typeof exp !== "number") return void 0;
2960
2989
  if (typeof iat !== "number") return void 0;
@@ -2962,10 +2991,29 @@ function parseVaultClaims(raw) {
2962
2991
  if (typeof exe !== "string") return void 0;
2963
2992
  if (use !== null && typeof use !== "number") return void 0;
2964
2993
  if (tid !== 1 && tid !== 2 && tid !== 3) return void 0;
2965
- if (typeof bkd !== "string") return void 0;
2966
- if (typeof val !== "string") return void 0;
2994
+ if (bkd !== void 0 && typeof bkd !== "string") return void 0;
2995
+ if (val !== void 0 && typeof val !== "string") return void 0;
2967
2996
  if (typeof ref !== "string") return void 0;
2968
- return { jti, exp, iat, sub, exe, use: use ?? null, tid, bkd, val, ref };
2997
+ if (kty !== void 0 && kty !== "secret" && kty !== "signing-key") return void 0;
2998
+ if (kid !== void 0 && typeof kid !== "string") return void 0;
2999
+ if (kgen !== void 0 && typeof kgen !== "number") return void 0;
3000
+ if (pres !== void 0 && !isLeasePresence(pres)) return void 0;
3001
+ return {
3002
+ jti,
3003
+ exp,
3004
+ iat,
3005
+ sub,
3006
+ exe,
3007
+ use: use ?? null,
3008
+ tid,
3009
+ bkd,
3010
+ val,
3011
+ ref,
3012
+ kty,
3013
+ kid,
3014
+ kgen,
3015
+ pres
3016
+ };
2969
3017
  }
2970
3018
  async function decryptToken(key, jwe) {
2971
3019
  let plaintext;
@@ -3046,15 +3094,44 @@ function validateClaims(claims, usedCount = 0) {
3046
3094
  if (claims.exe.trim() === "") {
3047
3095
  throw new VaultError("Invalid token: exe must not be empty");
3048
3096
  }
3049
- if (claims.bkd.trim() === "") {
3050
- throw new VaultError("Invalid token: bkd must not be empty");
3051
- }
3052
- if (claims.val.trim() === "") {
3053
- throw new VaultError("Invalid token: val must not be empty");
3054
- }
3055
3097
  if (claims.ref.trim() === "") {
3056
3098
  throw new VaultError("Invalid token: ref must not be empty");
3057
3099
  }
3100
+ switch (claims.kty) {
3101
+ case "signing-key": {
3102
+ if (claims.val !== void 0) {
3103
+ throw new VaultError("Invalid token: signing lease must not carry a val");
3104
+ }
3105
+ if (claims.kid === void 0 || claims.kid.trim() === "") {
3106
+ throw new VaultError("Invalid token: kid must not be empty");
3107
+ }
3108
+ if (claims.kgen === void 0) {
3109
+ throw new VaultError("Invalid token: kgen is required for a signing lease");
3110
+ }
3111
+ if (!Number.isSafeInteger(claims.kgen) || claims.kgen < 0) {
3112
+ throw new VaultError("Invalid token: kgen must be a non-negative integer");
3113
+ }
3114
+ break;
3115
+ }
3116
+ case "secret":
3117
+ case void 0: {
3118
+ if (claims.bkd === void 0 || claims.bkd.trim() === "") {
3119
+ throw new VaultError("Invalid token: bkd must not be empty");
3120
+ }
3121
+ if (claims.val === void 0 || claims.val.trim() === "") {
3122
+ throw new VaultError("Invalid token: val must not be empty");
3123
+ }
3124
+ if (claims.kid !== void 0 || claims.kgen !== void 0 || claims.pres !== void 0) {
3125
+ throw new VaultError(
3126
+ "Invalid token: secret claim must not carry signing-lease fields (kid/kgen/pres)"
3127
+ );
3128
+ }
3129
+ break;
3130
+ }
3131
+ default: {
3132
+ throw new VaultError(`Invalid token: unrecognized claim kind kty=${String(claims.kty)}`);
3133
+ }
3134
+ }
3058
3135
  if (claims.iat > claims.exp) {
3059
3136
  throw new VaultError("Invalid token: iat must not be after exp");
3060
3137
  }
@@ -4069,6 +4146,16 @@ var VaultKeeper = class _VaultKeeper {
4069
4146
  "This capability token authorizes a signing key, not a secret \u2014 it cannot be read with getSecret(). Use sign() instead."
4070
4147
  );
4071
4148
  }
4149
+ if (claims.kty === "signing-key") {
4150
+ throw new AuthorizationDeniedError(
4151
+ "This capability token authorizes a signing key, not a secret \u2014 it cannot be read with getSecret(). Use sign() instead."
4152
+ );
4153
+ }
4154
+ if (claims.val === void 0) {
4155
+ throw new AuthorizationDeniedError(
4156
+ "This capability token does not carry a secret value \u2014 it is malformed or was not minted as a secret token."
4157
+ );
4158
+ }
4072
4159
  return createSecretAccessor(claims.val);
4073
4160
  }
4074
4161
  /**
@@ -4411,7 +4498,7 @@ var VaultKeeper = class _VaultKeeper {
4411
4498
  // ---------------------------------------------------------------------------
4412
4499
  static #resolveSecrets(token) {
4413
4500
  if (token instanceof CapabilityToken) {
4414
- return _VaultKeeper.#requireSecretClaims(token).val;
4501
+ return _VaultKeeper.#requireSecretValue(token);
4415
4502
  }
4416
4503
  const result = {};
4417
4504
  for (const [name, t] of Object.entries(token)) {
@@ -4420,7 +4507,7 @@ var VaultKeeper = class _VaultKeeper {
4420
4507
  `Invalid capability token for secret "${name}" \u2014 expected a CapabilityToken from authorize()`
4421
4508
  );
4422
4509
  }
4423
- result[name] = _VaultKeeper.#requireSecretClaims(t).val;
4510
+ result[name] = _VaultKeeper.#requireSecretValue(t);
4424
4511
  }
4425
4512
  return result;
4426
4513
  }
@@ -4428,7 +4515,10 @@ var VaultKeeper = class _VaultKeeper {
4428
4515
  * Resolve a token to its secret claims, rejecting a signing-key token.
4429
4516
  *
4430
4517
  * Defense in depth: a signing-key capability must never be injectable as a
4431
- * secret through `fetch()`/`exec()`.
4518
+ * secret through `fetch()`/`exec()`. This rejects both the in-memory
4519
+ * signing-key token (`isSigningClaims`) and a JWE-based signing-key lease
4520
+ * (`kty: 'signing-key'`) — the two capability shapes are discriminated
4521
+ * differently but neither ever carries a secret value.
4432
4522
  */
4433
4523
  static #requireSecretClaims(token) {
4434
4524
  const claims = validateCapabilityToken(token);
@@ -4437,8 +4527,21 @@ var VaultKeeper = class _VaultKeeper {
4437
4527
  "This capability token authorizes a signing key, not a secret \u2014 it cannot be injected into fetch() or exec()."
4438
4528
  );
4439
4529
  }
4530
+ if (claims.kty === "signing-key") {
4531
+ throw new AuthorizationDeniedError(
4532
+ "This capability token authorizes a signing-key lease, not a secret \u2014 it cannot be injected into fetch() or exec()."
4533
+ );
4534
+ }
4440
4535
  return claims;
4441
4536
  }
4537
+ /** Resolve a token to its secret value, rejecting a claims shape with no `val`. */
4538
+ static #requireSecretValue(token) {
4539
+ const claims = _VaultKeeper.#requireSecretClaims(token);
4540
+ if (claims.val === void 0 || claims.val.trim() === "") {
4541
+ throw new AuthorizationDeniedError("This capability token does not authorize a secret value.");
4542
+ }
4543
+ return claims.val;
4544
+ }
4442
4545
  /**
4443
4546
  * Validate a caller-supplied resource name. `kind` names the resource in the
4444
4547
  * error so a signing-key caller is not told about a "secret".
@@ -4631,6 +4734,7 @@ exports.SetupError = SetupError;
4631
4734
  exports.SigningKeyAlreadyExistsError = SigningKeyAlreadyExistsError;
4632
4735
  exports.SigningKeyNotFoundError = SigningKeyNotFoundError;
4633
4736
  exports.SigningNotSupportedError = SigningNotSupportedError;
4737
+ exports.TestDoubleMisuseError = TestDoubleMisuseError;
4634
4738
  exports.TokenExpiredError = TokenExpiredError;
4635
4739
  exports.TokenRevokedError = TokenRevokedError;
4636
4740
  exports.UnknownBackendTypeError = UnknownBackendTypeError;