@coffre/vault 0.1.1 → 0.1.3
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 +1 -2
- package/dist/cloudflare.d.ts +3 -3
- package/dist/cloudflare.js +3 -3
- package/dist/{index-qXbB_vlp.d.ts → index-DPxyTJNN.d.ts} +16 -6
- package/dist/index.d.ts +2 -2
- package/dist/index.js +2 -3
- package/dist/node.d.ts +2 -2
- package/dist/node.js +3 -3
- package/dist/src-DHuqw-IB.js +113 -0
- package/dist/{vault-DvTsSAOX.js → vault-pexzy7Z-.js} +233 -109
- package/package.json +3 -3
- package/src/cloudflare.ts +1 -2
- package/src/config.ts +66 -17
- package/src/index.ts +2 -0
- package/src/log.ts +54 -7
- package/src/store.ts +16 -0
- package/src/vault.ts +198 -29
package/README.md
CHANGED
|
@@ -10,9 +10,8 @@ import { postgres, vault } from '@coffre/vault/cloudflare';
|
|
|
10
10
|
|
|
11
11
|
export default vault((env) => ({
|
|
12
12
|
database: postgres(env.VAULT_HYPERDRIVE), // coffre_vault_runtime, caching disabled
|
|
13
|
-
kek: { id: env.
|
|
13
|
+
kek: { id: env.VAULT_KEY_ID, key: env.VAULT_KEY }, // or awsKms({ keyArn, credentials }), with a signingKey
|
|
14
14
|
rootAdmins: env.ROOT_ADMINS.split(','),
|
|
15
|
-
signingKey: env.SIGNING_KEY,
|
|
16
15
|
}));
|
|
17
16
|
```
|
|
18
17
|
|
package/dist/cloudflare.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { a as KekUnavailableError, c as awsKms, d as Kek, f as VaultConfig, i as KekProvider, l as checkpointMessage, n as AwsKmsOptions, o as SecretContext, r as KekBadClaimError, s as WrappedDek, t as AwsCredentials, u as verifyCheckpoint } from "./index-
|
|
1
|
+
import { a as KekUnavailableError, c as awsKms, d as Kek, f as VaultConfig, i as KekProvider, l as checkpointMessage, n as AwsKmsOptions, o as SecretContext, p as derivedSigningKey, r as KekBadClaimError, s as WrappedDek, t as AwsCredentials, u as verifyCheckpoint } from "./index-DPxyTJNN.js";
|
|
2
2
|
import { AdmitInput, RemoveInput, RewrapInput, SetAccessInput, UnwrapInput, Vault, VerifyLogInput, WrapInput } from "@coffre/core/vault";
|
|
3
3
|
import { PostgresDatabase, PostgresDatabase as PostgresDatabase$1, postgres } from "@coffre/db/hyperdrive";
|
|
4
4
|
import { WorkerEntrypoint } from "cloudflare:workers";
|
|
@@ -42,7 +42,7 @@ export declare class VaultEntrypoint extends WorkerEntrypoint implements Vault {
|
|
|
42
42
|
checkpoint: import("@coffre/core/vault").Checkpoint;
|
|
43
43
|
}>>;
|
|
44
44
|
about(): Promise<{
|
|
45
|
-
|
|
45
|
+
checkpointKeys: Record<string, import("@coffre/core/vault").CheckpointKey>;
|
|
46
46
|
rootAdmins: string[];
|
|
47
47
|
}>;
|
|
48
48
|
verifyLog(input: VerifyLogInput): Promise<import("@coffre/core/vault").LogVerification>;
|
|
@@ -52,4 +52,4 @@ export declare class VaultEntrypoint extends WorkerEntrypoint implements Vault {
|
|
|
52
52
|
/** The vault Worker's default export, `VaultEntrypoint`, configured from its `env`. */
|
|
53
53
|
export declare function vault<Env>(configure_: (env: Env) => WorkersVaultConfig): typeof VaultEntrypoint;
|
|
54
54
|
//#endregion
|
|
55
|
-
export { type AwsCredentials, type AwsKmsOptions, type Kek, KekBadClaimError, type KekProvider, KekUnavailableError, type PostgresDatabase, type SecretContext, type Vault, type VaultConfig, type WrappedDek, awsKms, checkpointMessage, postgres, verifyCheckpoint };
|
|
55
|
+
export { type AwsCredentials, type AwsKmsOptions, type Kek, KekBadClaimError, type KekProvider, KekUnavailableError, type PostgresDatabase, type SecretContext, type Vault, type VaultConfig, type WrappedDek, awsKms, checkpointMessage, derivedSigningKey, postgres, verifyCheckpoint };
|
package/dist/cloudflare.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import { n as prepareVault,
|
|
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-pexzy7Z-.js";
|
|
3
3
|
import { createDatabase } from "@coffre/db";
|
|
4
4
|
import { HyperdrivePool, postgres } from "@coffre/db/hyperdrive";
|
|
5
5
|
import { WorkerEntrypoint } from "cloudflare:workers";
|
|
@@ -70,4 +70,4 @@ function vault(configure_) {
|
|
|
70
70
|
return VaultEntrypoint;
|
|
71
71
|
}
|
|
72
72
|
//#endregion
|
|
73
|
-
export { KekBadClaimError, KekUnavailableError, VaultEntrypoint, awsKms, checkpointMessage, postgres, vault, verifyCheckpoint };
|
|
73
|
+
export { KekBadClaimError, KekUnavailableError, VaultEntrypoint, awsKms, checkpointMessage, derivedSigningKey, postgres, vault, verifyCheckpoint };
|
|
@@ -16,25 +16,35 @@ type Kek = {
|
|
|
16
16
|
*
|
|
17
17
|
* {
|
|
18
18
|
* kek: awsKms({ keyArn: env.KMS_KEY_ARN, credentials: { … } }),
|
|
19
|
-
* previousKeks: [{ id: '
|
|
19
|
+
* previousKeks: [{ id: 'vault-2025-01-10-k7q2xm', key: env.OLD_VAULT_KEY }],
|
|
20
20
|
* rootAdmins: ['admin@acme.example'],
|
|
21
|
-
* signingKey: env.SIGNING_KEY,
|
|
21
|
+
* signingKey: env.SIGNING_KEY, // required with a key service; derived from a local KEK otherwise
|
|
22
22
|
* }
|
|
23
23
|
*/
|
|
24
24
|
type VaultConfig = {
|
|
25
25
|
/** Wraps every new data key. */
|
|
26
26
|
kek: Kek;
|
|
27
|
-
/**
|
|
27
|
+
/**
|
|
28
|
+
* Older KEKs, still unwrapping what they wrapped until a rewrap moves it
|
|
29
|
+
* on, and verifying what the vault wrote under them before the rotation.
|
|
30
|
+
*/
|
|
28
31
|
previousKeks?: readonly Kek[];
|
|
29
32
|
/** At least one email: the only way into a fresh instance, and the only members nobody can remove. */
|
|
30
33
|
rootAdmins: readonly string[];
|
|
31
|
-
/**
|
|
32
|
-
|
|
34
|
+
/**
|
|
35
|
+
* 32 random bytes, base64: the seed of the vault's own keys, which MAC its
|
|
36
|
+
* log entries, seal member rows and sign checkpoints. Leave it out with a
|
|
37
|
+
* local KEK, and the vault derives it from the KEK. Required when a key
|
|
38
|
+
* service holds the KEK, as AWS KMS does: the vault never sees that key.
|
|
39
|
+
*/
|
|
40
|
+
signingKey?: string;
|
|
33
41
|
/** At most `count` data keys unwrapped per principal in any `windowMinutes`; 1000 in 15 unless set. */
|
|
34
42
|
bulkLimit?: {
|
|
35
43
|
count: number;
|
|
36
44
|
windowMinutes: number;
|
|
37
45
|
};
|
|
38
46
|
};
|
|
47
|
+
/** The signing key a local KEK stands for, when the vault is given none: HKDF-SHA-256 of the KEK. */
|
|
48
|
+
declare function derivedSigningKey(kek: Uint8Array): Buffer;
|
|
39
49
|
//#endregion
|
|
40
|
-
export { KekUnavailableError$1 as a, awsKms as c, Kek as d, VaultConfig as f, KekProvider$1 as i, checkpointMessage$1 as l, AwsKmsOptions as n, SecretContext as o, KekBadClaimError$1 as r, WrappedDek as s, AwsCredentials as t, verifyCheckpoint$1 as u };
|
|
50
|
+
export { KekUnavailableError$1 as a, awsKms as c, Kek as d, VaultConfig as f, KekProvider$1 as i, checkpointMessage$1 as l, AwsKmsOptions as n, SecretContext as o, derivedSigningKey as p, KekBadClaimError$1 as r, WrappedDek as s, AwsCredentials as t, verifyCheckpoint$1 as u };
|
package/dist/index.d.ts
CHANGED
|
@@ -1,3 +1,3 @@
|
|
|
1
|
-
import { a as KekUnavailableError, c as awsKms, d as Kek, f as VaultConfig, i as KekProvider, l as checkpointMessage, n as AwsKmsOptions, o as SecretContext, r as KekBadClaimError, s as WrappedDek, t as AwsCredentials, u as verifyCheckpoint } from "./index-
|
|
1
|
+
import { a as KekUnavailableError, c as awsKms, d as Kek, f as VaultConfig, i as KekProvider, l as checkpointMessage, n as AwsKmsOptions, o as SecretContext, p as derivedSigningKey, r as KekBadClaimError, s as WrappedDek, t as AwsCredentials, u as verifyCheckpoint } from "./index-DPxyTJNN.js";
|
|
2
2
|
export type * from "@coffre/core/vault";
|
|
3
|
-
export { type AwsCredentials, type AwsKmsOptions, type Kek, KekBadClaimError, type KekProvider, KekUnavailableError, type SecretContext, type VaultConfig, type WrappedDek, awsKms, checkpointMessage, verifyCheckpoint };
|
|
3
|
+
export { type AwsCredentials, type AwsKmsOptions, type Kek, KekBadClaimError, type KekProvider, KekUnavailableError, type SecretContext, type VaultConfig, type WrappedDek, awsKms, checkpointMessage, derivedSigningKey, verifyCheckpoint };
|
package/dist/index.js
CHANGED
|
@@ -1,3 +1,2 @@
|
|
|
1
|
-
import { checkpointMessage,
|
|
2
|
-
|
|
3
|
-
export { KekBadClaimError, KekUnavailableError, awsKms, checkpointMessage, verifyCheckpoint };
|
|
1
|
+
import { a as verifyCheckpoint, i as checkpointMessage, n as KekUnavailableError, o as derivedSigningKey, r as awsKms, t as KekBadClaimError } from "./src-DHuqw-IB.js";
|
|
2
|
+
export { KekBadClaimError, KekUnavailableError, awsKms, checkpointMessage, derivedSigningKey, verifyCheckpoint };
|
package/dist/node.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { a as KekUnavailableError, c as awsKms, d as Kek, f as VaultConfig, i as KekProvider, l as checkpointMessage, n as AwsKmsOptions, o as SecretContext, r as KekBadClaimError, s as WrappedDek, t as AwsCredentials, u as verifyCheckpoint } from "./index-
|
|
1
|
+
import { a as KekUnavailableError, c as awsKms, d as Kek, f as VaultConfig, i as KekProvider, l as checkpointMessage, n as AwsKmsOptions, o as SecretContext, p as derivedSigningKey, r as KekBadClaimError, s as WrappedDek, t as AwsCredentials, u as verifyCheckpoint } from "./index-DPxyTJNN.js";
|
|
2
2
|
import { Vault, Vault as Vault$1 } from "@coffre/core/vault";
|
|
3
3
|
import { Database } from "@coffre/db";
|
|
4
4
|
import "@coffre/core/audit";
|
|
@@ -45,4 +45,4 @@ export declare function serveVault(config: NodeVaultConfig & {
|
|
|
45
45
|
/** The vault that `serveVault` runs, from the server's side of its socket. */
|
|
46
46
|
export declare function connectVault(socket: string): Vault;
|
|
47
47
|
//#endregion
|
|
48
|
-
export { type AwsCredentials, type AwsKmsOptions, type Kek, KekBadClaimError, type KekProvider, KekUnavailableError, type LocalVault, type SecretContext, type Vault, type VaultConfig, type VaultOptions, type WrappedDek, awsKms, checkpointMessage, verifyCheckpoint };
|
|
48
|
+
export { type AwsCredentials, type AwsKmsOptions, type Kek, KekBadClaimError, type KekProvider, KekUnavailableError, type LocalVault, type SecretContext, type Vault, type VaultConfig, type VaultOptions, type WrappedDek, awsKms, checkpointMessage, derivedSigningKey, verifyCheckpoint };
|
package/dist/node.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import { n as prepareVault,
|
|
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-pexzy7Z-.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";
|
|
@@ -139,4 +139,4 @@ async function readBody(stream) {
|
|
|
139
139
|
return Buffer.concat(chunks).toString("utf8");
|
|
140
140
|
}
|
|
141
141
|
//#endregion
|
|
142
|
-
export { KekBadClaimError, KekUnavailableError, awsKms, checkpointMessage, connectVault, localVault, serveVault, verifyCheckpoint };
|
|
142
|
+
export { KekBadClaimError, KekUnavailableError, awsKms, checkpointMessage, connectVault, derivedSigningKey, localVault, serveVault, verifyCheckpoint };
|
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
import { checkpointMessage as checkpointMessage$1, verifyCheckpoint as verifyCheckpoint$1 } from "@coffre/core/vault";
|
|
2
|
+
import { KekBadClaimError as KekBadClaimError$1, KekRegistry, KekUnavailableError as KekUnavailableError$1, LocalKekProvider, awsKms } from "@coffre/core/kek";
|
|
3
|
+
import { hkdfSync } from "node:crypto";
|
|
4
|
+
//#region src/config.ts
|
|
5
|
+
/**
|
|
6
|
+
* 1000 keys in 15 minutes: twenty `coffre run`s of a 50-key environment
|
|
7
|
+
* back to back, which no person or deploy pipeline does, while a script
|
|
8
|
+
* pulling every secret it can reach stops within a few seconds.
|
|
9
|
+
*/
|
|
10
|
+
const DEFAULT_BULK_LIMIT = {
|
|
11
|
+
count: 1e3,
|
|
12
|
+
windowMs: 9e5
|
|
13
|
+
};
|
|
14
|
+
function key32(value, what) {
|
|
15
|
+
const raw = Buffer.from(value, "base64");
|
|
16
|
+
if (raw.length !== 32) throw new Error(`${what} must be 32 bytes, base64; got ${raw.length} bytes`);
|
|
17
|
+
return raw;
|
|
18
|
+
}
|
|
19
|
+
const KEK_ID = /^[A-Za-z0-9._-]{1,64}$/;
|
|
20
|
+
const KEK_PROVIDER = /^[a-z0-9][a-z0-9-]{0,31}$/;
|
|
21
|
+
/** Every row records it, and `provider:keyId` finds the KEK again: visible ASCII, bounded. */
|
|
22
|
+
const KEK_NAME = /^[\x21-\x7e]{1,255}$/;
|
|
23
|
+
/** The label no other use of a KEK shares: a local KEK is otherwise only ever an AES-256-GCM key. */
|
|
24
|
+
const SIGNING_KEY_LABEL = "coffre.vault.signing-key.v1";
|
|
25
|
+
/** The signing key a local KEK stands for, when the vault is given none: HKDF-SHA-256 of the KEK. */
|
|
26
|
+
function derivedSigningKey(kek) {
|
|
27
|
+
return Buffer.from(hkdfSync("sha256", kek, /* @__PURE__ */ new Uint8Array(0), SIGNING_KEY_LABEL, 32));
|
|
28
|
+
}
|
|
29
|
+
/** A KEK as the registry takes it, and the signing key it stands for when it is one the vault holds. */
|
|
30
|
+
function kek(entry) {
|
|
31
|
+
if (!("wrap" in entry)) {
|
|
32
|
+
const { id, key } = entry;
|
|
33
|
+
if (!KEK_ID.test(id)) throw new Error(`the vault key's ID "${id}" (kek.id) must be 1-64 letters, digits, dots, dashes or underscores`);
|
|
34
|
+
const raw = key32(key, `the vault key ${id}`);
|
|
35
|
+
return {
|
|
36
|
+
provider: new LocalKekProvider(raw, id),
|
|
37
|
+
signingKey: derivedSigningKey(raw)
|
|
38
|
+
};
|
|
39
|
+
}
|
|
40
|
+
const { provider, keyId, keyVersion } = entry;
|
|
41
|
+
if (typeof provider !== "string" || !KEK_PROVIDER.test(provider)) throw new Error(`a key service's name (kek.provider) must be 1-32 lowercase letters, digits or dashes; got "${String(provider)}"`);
|
|
42
|
+
if (typeof keyId !== "string" || !KEK_NAME.test(keyId) || typeof keyVersion !== "string" || !KEK_NAME.test(keyVersion)) throw new Error(`vault key ${provider}: keyId and keyVersion must be 1-255 visible ASCII characters`);
|
|
43
|
+
if (typeof entry.wrap !== "function" || typeof entry.unwrap !== "function") throw new Error(`vault key ${provider}:${keyId} needs wrap() and unwrap()`);
|
|
44
|
+
return {
|
|
45
|
+
provider: entry,
|
|
46
|
+
signingKey: null
|
|
47
|
+
};
|
|
48
|
+
}
|
|
49
|
+
/** Check a deployment's vault configuration, failing on the first problem. */
|
|
50
|
+
function resolveVaultConfig(config) {
|
|
51
|
+
const keks = [config.kek, ...config.previousKeks ?? []].map(kek);
|
|
52
|
+
const [current, ...previous] = keks.map((entry) => entry.provider);
|
|
53
|
+
const refs = [current, ...previous].map(({ provider, keyId }) => `${provider}:${keyId}`);
|
|
54
|
+
const twice = refs.find((ref, i) => refs.indexOf(ref) !== i);
|
|
55
|
+
if (twice !== void 0) throw new Error(`two vault keys share an id: ${twice}`);
|
|
56
|
+
return {
|
|
57
|
+
keks: new KekRegistry(current, previous),
|
|
58
|
+
rootAdmins: checkRootAdmins(config.rootAdmins),
|
|
59
|
+
signingKeys: signingKeys(config.signingKey, keks),
|
|
60
|
+
bulkLimit: config.bulkLimit === void 0 ? DEFAULT_BULK_LIMIT : checkBulkLimit(config.bulkLimit)
|
|
61
|
+
};
|
|
62
|
+
}
|
|
63
|
+
/**
|
|
64
|
+
* The key the vault signs with, then every other it verifies with. It signs
|
|
65
|
+
* with `signingKey` when it is given one, and otherwise with the key the
|
|
66
|
+
* primary KEK stands for. It verifies with the keys every local KEK stands
|
|
67
|
+
* for too, the previous ones included: a KEK rotation changes the derived
|
|
68
|
+
* signing key, and what the old one signed before the rotation must still
|
|
69
|
+
* verify, for as long as the old KEK stays configured. Nothing after it does
|
|
70
|
+
* (`#settle` in vault.ts).
|
|
71
|
+
*/
|
|
72
|
+
function signingKeys(given, keks) {
|
|
73
|
+
const derived = keks.flatMap((entry) => entry.signingKey === null ? [] : [entry.signingKey]);
|
|
74
|
+
let signing;
|
|
75
|
+
if (given !== void 0) signing = key32(given, "the signing key");
|
|
76
|
+
else if (keks[0].signingKey !== null) signing = keks[0].signingKey;
|
|
77
|
+
else {
|
|
78
|
+
const { provider, keyId } = keks[0].provider;
|
|
79
|
+
throw new Error(`signingKey is required with the ${provider} vault key ${keyId}: the vault derives its signing key only from a vault key it holds, and a key service never hands its key over. Set signingKey to 32 random bytes, base64 (openssl rand -base64 32), and keep it with the vault key.`);
|
|
80
|
+
}
|
|
81
|
+
return [signing, ...derived].filter((key, i, all) => all.findIndex((other) => other.equals(key)) === i);
|
|
82
|
+
}
|
|
83
|
+
function checkBulkLimit({ count, windowMinutes }) {
|
|
84
|
+
if (!Number.isInteger(count) || count < 1 || !(windowMinutes > 0)) throw new Error("bulkLimit needs a whole count of at least 1 and a window above 0 minutes");
|
|
85
|
+
return {
|
|
86
|
+
count,
|
|
87
|
+
windowMs: windowMinutes * 6e4
|
|
88
|
+
};
|
|
89
|
+
}
|
|
90
|
+
function isHumanEmail(value) {
|
|
91
|
+
if (value.length > 254) return false;
|
|
92
|
+
const parts = value.split("@");
|
|
93
|
+
if (parts.length !== 2) return false;
|
|
94
|
+
const [local, domain] = parts;
|
|
95
|
+
if (local.length === 0 || local.length > 64 || local.startsWith(".") || local.endsWith(".") || local.includes("..") || !/^[A-Za-z0-9.!#$%&'*+/=?^_`{|}~-]+$/.test(local)) return false;
|
|
96
|
+
const labels = domain.split(".");
|
|
97
|
+
if (labels.length < 2) return false;
|
|
98
|
+
return labels.every((label) => label.length > 0 && label.length <= 63 && /^[A-Za-z0-9](?:[A-Za-z0-9-]*[A-Za-z0-9])?$/.test(label));
|
|
99
|
+
}
|
|
100
|
+
/**
|
|
101
|
+
* Root admins, lowercased the way they are compared. At least one, in every
|
|
102
|
+
* mode: they are the only way into a fresh instance, and the only members
|
|
103
|
+
* nobody can remove.
|
|
104
|
+
*/
|
|
105
|
+
function checkRootAdmins(emails) {
|
|
106
|
+
const rootAdmins = [...new Set(emails.map((entry) => entry.trim()).filter((entry) => entry.length > 0))];
|
|
107
|
+
if (rootAdmins.length === 0) throw new Error("rootAdmins must name at least one email, or nobody can manage coffre");
|
|
108
|
+
const invalid = rootAdmins.find((entry) => !isHumanEmail(entry));
|
|
109
|
+
if (invalid) throw new Error(`rootAdmins must be human email identities; invalid: ${invalid}`);
|
|
110
|
+
return rootAdmins.map((entry) => entry.toLowerCase());
|
|
111
|
+
}
|
|
112
|
+
//#endregion
|
|
113
|
+
export { verifyCheckpoint$1 as a, checkpointMessage$1 as i, KekUnavailableError$1 as n, derivedSigningKey as o, awsKms as r, resolveVaultConfig as s, KekBadClaimError$1 as t };
|