@coffre/vault 0.1.6 → 0.1.7

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.
@@ -45,6 +45,7 @@ export declare class VaultEntrypoint extends WorkerEntrypoint implements Vault {
45
45
  checkpointKeys: Record<string, import("@coffre/core/vault").CheckpointKey>;
46
46
  rootAdmins: string[];
47
47
  }>;
48
+ keyChecks(): Promise<import("@coffre/core/vault").KeyChecks>;
48
49
  verifyLog(input: VerifyLogInput): Promise<import("@coffre/core/vault").LogVerification>;
49
50
  /** No HTTP surface: only the app's service binding reaches the vault. */
50
51
  fetch(): Response;
@@ -1,5 +1,5 @@
1
1
  import { a as verifyCheckpoint, i as checkpointMessage, n as KekUnavailableError, o as derivedSigningKey, r as awsKms, s as resolveVaultConfig, t as KekBadClaimError } from "./src-DHuqw-IB.js";
2
- import { n as prepareVault, t as openVault } from "./vault-PQvFopA6.js";
2
+ import { n as prepareVault, t as openVault } from "./vault-BV36qTZy.js";
3
3
  import { createDatabase } from "@coffre/db";
4
4
  import { HyperdrivePool, postgres } from "@coffre/db/hyperdrive";
5
5
  import { WorkerEntrypoint } from "cloudflare:workers";
@@ -60,6 +60,9 @@ var VaultEntrypoint = class extends WorkerEntrypoint {
60
60
  async about() {
61
61
  return (await this.#vault()).about();
62
62
  }
63
+ async keyChecks() {
64
+ return (await this.#vault()).keyChecks();
65
+ }
63
66
  async verifyLog(input) {
64
67
  return (await this.#vault()).verifyLog(input);
65
68
  }
package/dist/node.js CHANGED
@@ -1,5 +1,5 @@
1
1
  import { a as verifyCheckpoint, i as checkpointMessage, n as KekUnavailableError, o as derivedSigningKey, r as awsKms, s as resolveVaultConfig, t as KekBadClaimError } from "./src-DHuqw-IB.js";
2
- import { n as prepareVault, t as openVault } from "./vault-PQvFopA6.js";
2
+ import { n as prepareVault, t as openVault } from "./vault-BV36qTZy.js";
3
3
  import { chmodSync, existsSync, lstatSync, rmSync } from "node:fs";
4
4
  import { Agent, createServer, request } from "node:http";
5
5
  import { openDatabase } from "@coffre/db/connect";
@@ -15,6 +15,7 @@ const METHODS = [
15
15
  "remove",
16
16
  "checkpoint",
17
17
  "about",
18
+ "keyChecks",
18
19
  "verifyLog"
19
20
  ];
20
21
  /**
@@ -1,6 +1,6 @@
1
1
  import { ACCESS_ACTIONS, checkpointMessage, describeAccessFault, verifyCheckpoint } from "@coffre/core/vault";
2
- import { DEK_BYTES, KekBadClaimError, KekCancelledError, KekUnavailableError, LocalKekProvider } from "@coffre/core/kek";
3
- import { createHash, createHmac, hkdfSync, randomUUID, timingSafeEqual } from "node:crypto";
2
+ import { DEK_BYTES, KEY_CHECK, KEY_CHECK_CONTEXT, KEY_CHECK_VALUE, KekBadClaimError, KekCancelledError, KekUnavailableError, LocalKekProvider } from "@coffre/core/kek";
3
+ import { createHmac, hkdfSync, randomUUID, timingSafeEqual } from "node:crypto";
4
4
  import { tablesOf } from "@coffre/db";
5
5
  import { allows, assignableToEnvironment, isRole, isSyncPrincipal, mayManageAccess } from "@coffre/core/access";
6
6
  import { GENESIS_HASH, deriveLogKey, verifyEntries } from "@coffre/core/audit";
@@ -821,20 +821,6 @@ const LOCK_TIMEOUT_MS = 15e3;
821
821
  const PRINCIPAL = /^(user|token|sync):[^\s:][^\s]*$/;
822
822
  /** Who acts for the vault itself, as when it gives a root admin a member row. */
823
823
  const VAULT_ACTOR = "system:vault";
824
- /**
825
- * A KEK's check: a known value, the size of a data key, wrapped under it in
826
- * a context no secret has (the nil UUID), and kept in a `key.check` entry.
827
- * Opening it again tells the vault its KEK is the one that wrapped the
828
- * data, without opening any data.
829
- */
830
- const KEY_CHECK = "key.check";
831
- const KEY_CHECK_VALUE = createHash("sha256").update("coffre.kek.check.v1").digest();
832
- const NIL = "00000000-0000-0000-0000-000000000000";
833
- const KEY_CHECK_CONTEXT = {
834
- projectId: NIL,
835
- environmentId: NIL,
836
- secretId: NIL
837
- };
838
824
  /** How many stored keys a KEK with no check yet is tried on: one that opens proves it. */
839
825
  const KEY_CHECK_SAMPLE = 3;
840
826
  /** The vault's first entry under a new key, which the keys it replaced count only before (`#settle`). */
@@ -1950,7 +1936,7 @@ var VaultService = class {
1950
1936
  const mismatch = `the vault key ${kek.keyId}${kek.provider === "local" ? "" : ` (${kek.provider})`}, kek or previousKeks in the vault's config, is not the one that wrapped these values`;
1951
1937
  const check = checks.get(`${kek.provider}:${kek.keyId}`);
1952
1938
  if (check !== void 0) {
1953
- if (!await opens(check, KEY_CHECK_CONTEXT, KEY_CHECK_VALUE)) return mismatch;
1939
+ if (!await opens(unwrappable(check.wrapped), KEY_CHECK_CONTEXT, KEY_CHECK_VALUE)) return mismatch;
1954
1940
  continue;
1955
1941
  }
1956
1942
  const samples = await wrappedUnder(this.#db, kek.provider, kek.keyId, KEY_CHECK_SAMPLE);
@@ -1984,13 +1970,21 @@ var VaultService = class {
1984
1970
  }
1985
1971
  return null;
1986
1972
  }
1987
- /** Each KEK's recorded check value, by `provider:keyId`. */
1973
+ /** Each KEK's recorded check value, by `provider:keyId`, and the entry that holds it. */
1988
1974
  async #checkValues(db) {
1989
1975
  const checks = /* @__PURE__ */ new Map();
1990
1976
  for (const entry of await vaultEntriesOf(db, [KEY_CHECK], -1n, VERIFY_BATCH)) {
1991
1977
  if (!this.#authentic(entry)) continue;
1992
- const wrapped = JSON.parse(entry.metadata);
1993
- checks.set(`${wrapped.kekProvider}:${wrapped.kekId}`, unwrappable(wrapped));
1978
+ const { kekProvider, kekId, kekVersion, bytes } = JSON.parse(entry.metadata);
1979
+ checks.set(`${kekProvider}:${kekId}`, {
1980
+ seq: entry.seq,
1981
+ wrapped: {
1982
+ kekProvider,
1983
+ kekId,
1984
+ kekVersion,
1985
+ bytes
1986
+ }
1987
+ });
1994
1988
  }
1995
1989
  return checks;
1996
1990
  }
@@ -2066,6 +2060,21 @@ var VaultService = class {
2066
2060
  rootAdmins: this.#config.rootAdmins.map((email) => `user:${email}`)
2067
2061
  };
2068
2062
  }
2063
+ async keyChecks() {
2064
+ await this.#kekMismatch();
2065
+ const { provider, keyId } = this.#config.keks.primary;
2066
+ const checks = [...(await this.#checkValues(this.#db)).values()].sort((a, b) => a.seq < b.seq ? 1 : -1).map(({ seq, wrapped }) => ({
2067
+ ...wrapped,
2068
+ seq: Number(seq)
2069
+ }));
2070
+ return {
2071
+ current: {
2072
+ kekProvider: provider,
2073
+ kekId: keyId
2074
+ },
2075
+ checks
2076
+ };
2077
+ }
2069
2078
  verifyLog(input) {
2070
2079
  return this.#verifyAll([], input.upTo ?? null);
2071
2080
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@coffre/vault",
3
- "version": "0.1.6",
3
+ "version": "0.1.7",
4
4
  "description": "coffre's vault: keys, grants and members, in the database the app uses, as a Cloudflare Worker or on Node.",
5
5
  "license": "MIT",
6
6
  "repository": {
@@ -36,8 +36,8 @@
36
36
  },
37
37
  "dependencies": {
38
38
  "drizzle-orm": "0.45.2",
39
- "@coffre/core": "0.1.6",
40
- "@coffre/db": "0.1.6"
39
+ "@coffre/core": "0.1.7",
40
+ "@coffre/db": "0.1.7"
41
41
  },
42
42
  "devDependencies": {
43
43
  "@types/node": "26.1.1",
package/src/cloudflare.ts CHANGED
@@ -78,6 +78,7 @@ export class VaultEntrypoint extends WorkerEntrypoint implements Vault {
78
78
  async remove(input: RemoveInput) { return (await this.#vault()).remove(input); }
79
79
  async checkpoint() { return (await this.#vault()).checkpoint(); }
80
80
  async about() { return (await this.#vault()).about(); }
81
+ async keyChecks() { return (await this.#vault()).keyChecks(); }
81
82
  async verifyLog(input: VerifyLogInput) { return (await this.#vault()).verifyLog(input); }
82
83
 
83
84
  /** No HTTP surface: only the app's service binding reaches the vault. */
package/src/local.ts CHANGED
@@ -15,6 +15,7 @@ export const METHODS = [
15
15
  'remove',
16
16
  'checkpoint',
17
17
  'about',
18
+ 'keyChecks',
18
19
  'verifyLog',
19
20
  ] as const satisfies readonly (keyof Vault)[];
20
21
 
package/src/vault.ts CHANGED
@@ -1,4 +1,4 @@
1
- import { createHash, randomUUID, timingSafeEqual } from 'node:crypto';
1
+ import { randomUUID, timingSafeEqual } from 'node:crypto';
2
2
 
3
3
  import {
4
4
  allows,
@@ -14,6 +14,9 @@ import { verifyEntries, type LogKey, type StoredEntry } from '@coffre/core/audit
14
14
  import { checkContext, type SecretContext } from '@coffre/core/envelope';
15
15
  import {
16
16
  DEK_BYTES,
17
+ KEY_CHECK,
18
+ KEY_CHECK_CONTEXT,
19
+ KEY_CHECK_VALUE,
17
20
  KekBadClaimError,
18
21
  KekCancelledError,
19
22
  KekUnavailableError,
@@ -34,6 +37,7 @@ import {
34
37
  type CheckpointKey,
35
38
  type Grant,
36
39
  type GrantChange,
40
+ type KeyChecks,
37
41
  type LogHead,
38
42
  type LogVerification,
39
43
  type Outcome,
@@ -150,16 +154,11 @@ const PRINCIPAL = /^(user|token|sync):[^\s:][^\s]*$/;
150
154
  /** Who acts for the vault itself, as when it gives a root admin a member row. */
151
155
  const VAULT_ACTOR = 'system:vault';
152
156
 
153
- /**
154
- * A KEK's check: a known value, the size of a data key, wrapped under it in
155
- * a context no secret has (the nil UUID), and kept in a `key.check` entry.
156
- * Opening it again tells the vault its KEK is the one that wrapped the
157
- * data, without opening any data.
158
- */
159
- const KEY_CHECK = 'key.check';
160
- const KEY_CHECK_VALUE = createHash('sha256').update('coffre.kek.check.v1').digest();
161
- const NIL = '00000000-0000-0000-0000-000000000000';
162
- const KEY_CHECK_CONTEXT: SecretContext = { projectId: NIL, environmentId: NIL, secretId: NIL };
157
+ // A KEK's check: a known value, the size of a data key, wrapped under it in
158
+ // a context no secret has (the nil UUID), and kept in a `key.check` entry
159
+ // (@coffre/core/kek). Opening it again tells the vault its KEK is the one
160
+ // that wrapped the data, without opening any data; `coffre verify keys`
161
+ // holds an escrowed key to it the same way.
163
162
 
164
163
  /** How many stored keys a KEK with no check yet is tried on: one that opens proves it. */
165
164
  const KEY_CHECK_SAMPLE = 3;
@@ -1352,7 +1351,7 @@ class VaultService implements Vault {
1352
1351
  const mismatch = `the vault key ${kek.keyId}${kek.provider === 'local' ? '' : ` (${kek.provider})`}, kek or previousKeks in the vault's config, is not the one that wrapped these values`;
1353
1352
  const check = checks.get(`${kek.provider}:${kek.keyId}`);
1354
1353
  if (check !== undefined) {
1355
- if (!(await opens(check, KEY_CHECK_CONTEXT, KEY_CHECK_VALUE))) return mismatch;
1354
+ if (!(await opens(unwrappable(check.wrapped), KEY_CHECK_CONTEXT, KEY_CHECK_VALUE))) return mismatch;
1356
1355
  continue;
1357
1356
  }
1358
1357
  const samples = await store.wrappedUnder(this.#db, kek.provider, kek.keyId, KEY_CHECK_SAMPLE);
@@ -1378,14 +1377,14 @@ class VaultService implements Vault {
1378
1377
  return null;
1379
1378
  }
1380
1379
 
1381
- /** Each KEK's recorded check value, by `provider:keyId`. */
1382
- async #checkValues(db: Queryable): Promise<Map<string, WrappedDek>> {
1383
- const checks = new Map<string, WrappedDek>();
1380
+ /** Each KEK's recorded check value, by `provider:keyId`, and the entry that holds it. */
1381
+ async #checkValues(db: Queryable): Promise<Map<string, { seq: bigint; wrapped: WrappedKey }>> {
1382
+ const checks = new Map<string, { seq: bigint; wrapped: WrappedKey }>();
1384
1383
  for (const entry of await store.vaultEntriesOf(db, [KEY_CHECK], -1n, VERIFY_BATCH)) {
1385
1384
  // A check in the vault's name that the vault did not write proves nothing either way.
1386
1385
  if (!this.#authentic(entry)) continue;
1387
- const wrapped = JSON.parse(entry.metadata) as WrappedKey;
1388
- checks.set(`${wrapped.kekProvider}:${wrapped.kekId}`, unwrappable(wrapped));
1386
+ const { kekProvider, kekId, kekVersion, bytes } = JSON.parse(entry.metadata) as WrappedKey;
1387
+ checks.set(`${kekProvider}:${kekId}`, { seq: entry.seq, wrapped: { kekProvider, kekId, kekVersion, bytes } });
1389
1388
  }
1390
1389
  return checks;
1391
1390
  }
@@ -1464,6 +1463,16 @@ class VaultService implements Vault {
1464
1463
  };
1465
1464
  }
1466
1465
 
1466
+ async keyChecks(): Promise<KeyChecks> {
1467
+ // A vault key with no check yet gets one first, as before the first key operation.
1468
+ await this.#kekMismatch();
1469
+ const { provider, keyId } = this.#config.keks.primary;
1470
+ const checks = [...(await this.#checkValues(this.#db)).values()]
1471
+ .sort((a, b) => (a.seq < b.seq ? 1 : -1))
1472
+ .map(({ seq, wrapped }) => ({ ...wrapped, seq: Number(seq) }));
1473
+ return { current: { kekProvider: provider, kekId: keyId }, checks };
1474
+ }
1475
+
1467
1476
  verifyLog(input: VerifyLogInput): Promise<LogVerification> {
1468
1477
  return this.#verifyAll([], input.upTo ?? null);
1469
1478
  }