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 +5 -1
- package/dist/index.cjs +127 -23
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +29 -1
- package/dist/index.d.ts +29 -1
- package/dist/index.js +127 -24
- package/dist/index.js.map +1 -1
- package/dist/one-password-worker.js.map +1 -1
- package/package.json +1 -1
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
|
-
|
|
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
|
-
|
|
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
|
|
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
|
|
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
|
|
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
|
-
|
|
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.#
|
|
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.#
|
|
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;
|