@coffre/core 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.
- package/dist/kek.d.ts +16 -0
- package/dist/kek.js +36 -1
- package/dist/schemas.d.ts +1 -1
- package/dist/vault.d.ts +21 -0
- package/package.json +3 -2
- package/src/kek/check.ts +46 -0
- package/src/kek/index.ts +1 -0
- package/src/vault.ts +14 -0
package/dist/kek.d.ts
CHANGED
|
@@ -69,4 +69,20 @@ export declare class AwsKmsKekProvider implements KekProvider {
|
|
|
69
69
|
unwrap(wrapped: WrappedDek, ctx: SecretContext, operation?: KeyOperation): Promise<Buffer>;
|
|
70
70
|
}
|
|
71
71
|
//#endregion
|
|
72
|
+
//#region src/kek/check.d.ts
|
|
73
|
+
/** The action of the vault's entry that holds a vault key's check. */
|
|
74
|
+
export declare const KEY_CHECK = "key.check";
|
|
75
|
+
/** The known value a check wraps: the size of a data key. */
|
|
76
|
+
export declare const KEY_CHECK_VALUE: Buffer;
|
|
77
|
+
/** The context a check is wrapped in, which no secret has: the nil UUID throughout. */
|
|
78
|
+
export declare const KEY_CHECK_CONTEXT: SecretContext;
|
|
79
|
+
/**
|
|
80
|
+
* Whether `key`, a local vault key, is the one that wrapped `check`: it
|
|
81
|
+
* opens it, under the vault ID the check names, to the known value. The ID
|
|
82
|
+
* is bound into the wrap, so a match also says the ID is the escrowed one's.
|
|
83
|
+
*/
|
|
84
|
+
export declare function opensKeyCheck(key: Buffer, check: WrappedDek): Promise<boolean>;
|
|
85
|
+
/** The id the log records for entries the app signs with `key`, its APP_KEY. */
|
|
86
|
+
export declare function appLogKeyId(key: Uint8Array): string;
|
|
87
|
+
//#endregion
|
|
72
88
|
export { type AwsCredentials, type AwsKmsOptions, DEK_BYTES, KekBadClaimError, KekCancelledError, type KekProvider, KekRegistry, KekUnavailableError, type KeyOperation, type WrappedDek };
|
package/dist/kek.js
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { deriveLogKey } from "./audit.js";
|
|
1
2
|
import { i as KekUnavailableError, n as KekBadClaimError, o as checkContext, r as KekCancelledError, s as encodeAad, t as DEK_BYTES } from "./types-Mi7-Tzj9.js";
|
|
2
3
|
import { createCipheriv, createDecipheriv, createHash, createHmac, randomBytes, timingSafeEqual } from "node:crypto";
|
|
3
4
|
//#region src/kek/cancellation.ts
|
|
@@ -483,4 +484,38 @@ var Slots = class {
|
|
|
483
484
|
}
|
|
484
485
|
};
|
|
485
486
|
//#endregion
|
|
486
|
-
|
|
487
|
+
//#region src/kek/check.ts
|
|
488
|
+
/** The action of the vault's entry that holds a vault key's check. */
|
|
489
|
+
const KEY_CHECK = "key.check";
|
|
490
|
+
/** The known value a check wraps: the size of a data key. */
|
|
491
|
+
const KEY_CHECK_VALUE = createHash("sha256").update("coffre.kek.check.v1").digest();
|
|
492
|
+
const NIL = "00000000-0000-0000-0000-000000000000";
|
|
493
|
+
/** The context a check is wrapped in, which no secret has: the nil UUID throughout. */
|
|
494
|
+
const KEY_CHECK_CONTEXT = {
|
|
495
|
+
projectId: NIL,
|
|
496
|
+
environmentId: NIL,
|
|
497
|
+
secretId: NIL
|
|
498
|
+
};
|
|
499
|
+
/**
|
|
500
|
+
* Whether `key`, a local vault key, is the one that wrapped `check`: it
|
|
501
|
+
* opens it, under the vault ID the check names, to the known value. The ID
|
|
502
|
+
* is bound into the wrap, so a match also says the ID is the escrowed one's.
|
|
503
|
+
*/
|
|
504
|
+
async function opensKeyCheck(key, check) {
|
|
505
|
+
if (check.kekProvider !== "local" || key.length !== 32) return false;
|
|
506
|
+
try {
|
|
507
|
+
const value = await new LocalKekProvider(key, check.kekId, check.kekVersion).unwrap(check, KEY_CHECK_CONTEXT);
|
|
508
|
+
const right = equalBytes(value, KEY_CHECK_VALUE);
|
|
509
|
+
value.fill(0);
|
|
510
|
+
return right;
|
|
511
|
+
} catch (error) {
|
|
512
|
+
if (error instanceof KekBadClaimError) return false;
|
|
513
|
+
throw error;
|
|
514
|
+
}
|
|
515
|
+
}
|
|
516
|
+
/** The id the log records for entries the app signs with `key`, its APP_KEY. */
|
|
517
|
+
function appLogKeyId(key) {
|
|
518
|
+
return deriveLogKey("app", key).keyId;
|
|
519
|
+
}
|
|
520
|
+
//#endregion
|
|
521
|
+
export { AwsKmsKekProvider, DEK_BYTES, KEY_CHECK, KEY_CHECK_CONTEXT, KEY_CHECK_VALUE, KekBadClaimError, KekCancelledError, KekRegistry, KekUnavailableError, LocalKekProvider, appLogKeyId, awsKms, equalBytes, opensKeyCheck };
|
package/dist/schemas.d.ts
CHANGED
|
@@ -9,8 +9,8 @@ export declare const principalType: z.ZodEnum<{
|
|
|
9
9
|
}>;
|
|
10
10
|
export declare const principalId: z.ZodString;
|
|
11
11
|
export declare const instanceRole: z.ZodEnum<{
|
|
12
|
-
owner: "owner";
|
|
13
12
|
user: "user";
|
|
13
|
+
owner: "owner";
|
|
14
14
|
}>;
|
|
15
15
|
export declare const grantId: z.ZodString;
|
|
16
16
|
export declare const emailAddress: z.ZodString;
|
package/dist/vault.d.ts
CHANGED
|
@@ -155,6 +155,12 @@ export interface Vault {
|
|
|
155
155
|
checkpointKeys: Record<string, CheckpointKey>;
|
|
156
156
|
rootAdmins: string[];
|
|
157
157
|
}>;
|
|
158
|
+
/**
|
|
159
|
+
* What an escrowed vault key is checked against: the vault key it wraps
|
|
160
|
+
* under now, and each one's check, as the vault wrote it. None of it is
|
|
161
|
+
* secret, and the vault answers no guess: a check opens only under its key.
|
|
162
|
+
*/
|
|
163
|
+
keyChecks(): Promise<KeyChecks>;
|
|
158
164
|
/**
|
|
159
165
|
* Check the whole log: rehash it from the first entry, the vault's
|
|
160
166
|
* entries by their MACs, every checkpoint against the prefix it signed,
|
|
@@ -234,6 +240,21 @@ export type WrappedKey = {
|
|
|
234
240
|
kekVersion: string;
|
|
235
241
|
bytes: string;
|
|
236
242
|
};
|
|
243
|
+
/**
|
|
244
|
+
* The vault key the vault wraps under now, by provider and ID, and each
|
|
245
|
+
* vault key's check, newest first: a known value wrapped under it
|
|
246
|
+
* (`KEY_CHECK_VALUE` in `@coffre/core/kek`), in its newest `key.check`
|
|
247
|
+
* entry that carries the vault's MAC, at `seq`.
|
|
248
|
+
*/
|
|
249
|
+
export type KeyChecks = {
|
|
250
|
+
current: {
|
|
251
|
+
kekProvider: string;
|
|
252
|
+
kekId: string;
|
|
253
|
+
};
|
|
254
|
+
checks: (WrappedKey & {
|
|
255
|
+
seq: number;
|
|
256
|
+
})[];
|
|
257
|
+
};
|
|
237
258
|
/** Why someone reads: shown in the log, and the same rules apply to each. */
|
|
238
259
|
export type Purpose = 'reveal' | 'run' | 'compare' | 'sync';
|
|
239
260
|
/** Ties the vault's entries to the app's request and audit rows. */
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@coffre/core",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.7",
|
|
4
4
|
"description": "What coffre's packages share: roles, envelopes, identity, the audit chain and the vault's contract.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"repository": {
|
|
@@ -65,10 +65,11 @@
|
|
|
65
65
|
"zod": "4.4.3"
|
|
66
66
|
},
|
|
67
67
|
"devDependencies": {
|
|
68
|
+
"@hegeldev/hegel": "0.4.7",
|
|
68
69
|
"@types/node": "26.1.1",
|
|
69
70
|
"tsdown": "0.23.0",
|
|
70
71
|
"typescript": "5.9.3",
|
|
71
|
-
"@coffre/conformance": "0.1.
|
|
72
|
+
"@coffre/conformance": "0.1.7"
|
|
72
73
|
},
|
|
73
74
|
"scripts": {
|
|
74
75
|
"build": "tsdown",
|
package/src/kek/check.ts
ADDED
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
// What a vault key, or the app key, can be checked against without opening
|
|
2
|
+
// anything else: the vault's `key.check` entries, each a known value wrapped
|
|
3
|
+
// under one vault key, and the key id the log records for the app's entries,
|
|
4
|
+
// a fingerprint of the key they are signed with. The vault writes the
|
|
5
|
+
// checks; `coffre verify keys` holds an escrowed key to them, on the
|
|
6
|
+
// operator's machine, the key never sent anywhere.
|
|
7
|
+
import { createHash } from 'node:crypto';
|
|
8
|
+
|
|
9
|
+
import { deriveLogKey } from '../audit/chain.ts';
|
|
10
|
+
import type { SecretContext } from '../context.ts';
|
|
11
|
+
import { equalBytes, LocalKekProvider } from './local.ts';
|
|
12
|
+
import { KekBadClaimError, type WrappedDek } from './types.ts';
|
|
13
|
+
|
|
14
|
+
/** The action of the vault's entry that holds a vault key's check. */
|
|
15
|
+
export const KEY_CHECK = 'key.check';
|
|
16
|
+
|
|
17
|
+
/** The known value a check wraps: the size of a data key. */
|
|
18
|
+
export const KEY_CHECK_VALUE: Buffer = createHash('sha256').update('coffre.kek.check.v1').digest();
|
|
19
|
+
|
|
20
|
+
const NIL = '00000000-0000-0000-0000-000000000000';
|
|
21
|
+
|
|
22
|
+
/** The context a check is wrapped in, which no secret has: the nil UUID throughout. */
|
|
23
|
+
export const KEY_CHECK_CONTEXT: SecretContext = { projectId: NIL, environmentId: NIL, secretId: NIL };
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* Whether `key`, a local vault key, is the one that wrapped `check`: it
|
|
27
|
+
* opens it, under the vault ID the check names, to the known value. The ID
|
|
28
|
+
* is bound into the wrap, so a match also says the ID is the escrowed one's.
|
|
29
|
+
*/
|
|
30
|
+
export async function opensKeyCheck(key: Buffer, check: WrappedDek): Promise<boolean> {
|
|
31
|
+
if (check.kekProvider !== 'local' || key.length !== 32) return false;
|
|
32
|
+
try {
|
|
33
|
+
const value = await new LocalKekProvider(key, check.kekId, check.kekVersion).unwrap(check, KEY_CHECK_CONTEXT);
|
|
34
|
+
const right = equalBytes(value, KEY_CHECK_VALUE);
|
|
35
|
+
value.fill(0);
|
|
36
|
+
return right;
|
|
37
|
+
} catch (error) {
|
|
38
|
+
if (error instanceof KekBadClaimError) return false;
|
|
39
|
+
throw error;
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/** The id the log records for entries the app signs with `key`, its APP_KEY. */
|
|
44
|
+
export function appLogKeyId(key: Uint8Array): string {
|
|
45
|
+
return deriveLogKey('app', key).keyId;
|
|
46
|
+
}
|
package/src/kek/index.ts
CHANGED
|
@@ -3,4 +3,5 @@ export { DEK_BYTES, KekUnavailableError, KekCancelledError, KekBadClaimError } f
|
|
|
3
3
|
export { LocalKekProvider, equalBytes } from './local.ts';
|
|
4
4
|
export { KekRegistry } from './registry.ts';
|
|
5
5
|
export { AwsKmsKekProvider, awsKms, type AwsKmsOptions } from './aws-kms.ts';
|
|
6
|
+
export { appLogKeyId, KEY_CHECK, KEY_CHECK_CONTEXT, KEY_CHECK_VALUE, opensKeyCheck } from './check.ts';
|
|
6
7
|
export type { AwsCredentials } from './sigv4.ts';
|
package/src/vault.ts
CHANGED
|
@@ -70,6 +70,12 @@ export interface Vault {
|
|
|
70
70
|
* admins, as principals.
|
|
71
71
|
*/
|
|
72
72
|
about(): Promise<{ checkpointKeys: Record<string, CheckpointKey>; rootAdmins: string[] }>;
|
|
73
|
+
/**
|
|
74
|
+
* What an escrowed vault key is checked against: the vault key it wraps
|
|
75
|
+
* under now, and each one's check, as the vault wrote it. None of it is
|
|
76
|
+
* secret, and the vault answers no guess: a check opens only under its key.
|
|
77
|
+
*/
|
|
78
|
+
keyChecks(): Promise<KeyChecks>;
|
|
73
79
|
/**
|
|
74
80
|
* Check the whole log: rehash it from the first entry, the vault's
|
|
75
81
|
* entries by their MACs, every checkpoint against the prefix it signed,
|
|
@@ -143,6 +149,14 @@ export type SecretRef = {
|
|
|
143
149
|
/** A wrapped data key and the key encryption key that wrapped it; `bytes` is base64. */
|
|
144
150
|
export type WrappedKey = { kekProvider: string; kekId: string; kekVersion: string; bytes: string };
|
|
145
151
|
|
|
152
|
+
/**
|
|
153
|
+
* The vault key the vault wraps under now, by provider and ID, and each
|
|
154
|
+
* vault key's check, newest first: a known value wrapped under it
|
|
155
|
+
* (`KEY_CHECK_VALUE` in `@coffre/core/kek`), in its newest `key.check`
|
|
156
|
+
* entry that carries the vault's MAC, at `seq`.
|
|
157
|
+
*/
|
|
158
|
+
export type KeyChecks = { current: { kekProvider: string; kekId: string }; checks: (WrappedKey & { seq: number })[] };
|
|
159
|
+
|
|
146
160
|
/** Why someone reads: shown in the log, and the same rules apply to each. */
|
|
147
161
|
export type Purpose = 'reveal' | 'run' | 'compare' | 'sync';
|
|
148
162
|
|