@geekmidas/constructs 10.0.0-alpha.46 → 10.0.0-alpha.48
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/adaptors/testing.cjs +1 -1
- package/dist/adaptors/testing.mjs +1 -1
- package/dist/cipher-C1lEFX63.mjs +225 -0
- package/dist/cipher-C1lEFX63.mjs.map +1 -0
- package/dist/cipher-CVxfwLjk.cjs +332 -0
- package/dist/cipher-CVxfwLjk.cjs.map +1 -0
- package/dist/encryption.cjs +96 -0
- package/dist/encryption.cjs.map +1 -0
- package/dist/encryption.d.cts +117 -0
- package/dist/encryption.d.cts.map +1 -0
- package/dist/encryption.d.mts +117 -0
- package/dist/encryption.d.mts.map +1 -0
- package/dist/encryption.mjs +83 -0
- package/dist/encryption.mjs.map +1 -0
- package/dist/kms-B-Hm3d5r.cjs +85 -0
- package/dist/kms-B-Hm3d5r.cjs.map +1 -0
- package/dist/kms-BdUlMJEZ.mjs +85 -0
- package/dist/kms-BdUlMJEZ.mjs.map +1 -0
- package/dist/mobile-app.cjs +3 -0
- package/dist/mobile-app.cjs.map +1 -1
- package/dist/mobile-app.d.cts.map +1 -1
- package/dist/mobile-app.d.mts.map +1 -1
- package/dist/mobile-app.mjs +3 -0
- package/dist/mobile-app.mjs.map +1 -1
- package/package.json +46 -31
|
@@ -12,6 +12,7 @@ const require_Topic = require("../Topic-CkxOTrMv.cjs");
|
|
|
12
12
|
const require_HonoEndpointAdaptor = require("../HonoEndpointAdaptor-CMi0MJsF.cjs");
|
|
13
13
|
const require_processAudits = require("../processAudits-D_2W5x8k.cjs");
|
|
14
14
|
let _geekmidas_manifest = require("@geekmidas/manifest");
|
|
15
|
+
let node_crypto = require("node:crypto");
|
|
15
16
|
let _geekmidas_errors = require("@geekmidas/errors");
|
|
16
17
|
let _geekmidas_envkit = require("@geekmidas/envkit");
|
|
17
18
|
let _geekmidas_services = require("@geekmidas/services");
|
|
@@ -19,7 +20,6 @@ let hono = require("hono");
|
|
|
19
20
|
let _geekmidas_audit = require("@geekmidas/audit");
|
|
20
21
|
let _geekmidas_testkit_context = require("@geekmidas/testkit/context");
|
|
21
22
|
let msw = require("msw");
|
|
22
|
-
let node_crypto = require("node:crypto");
|
|
23
23
|
let _geekmidas_testkit_browser = require("@geekmidas/testkit/browser");
|
|
24
24
|
let _geekmidas_testkit_faker = require("@geekmidas/testkit/faker");
|
|
25
25
|
let _geekmidas_testkit_mailbox = require("@geekmidas/testkit/mailbox");
|
|
@@ -10,6 +10,7 @@ import { t as Topic } from "../Topic-hPffOsot.mjs";
|
|
|
10
10
|
import { t as HonoEndpoint } from "../HonoEndpointAdaptor-BIS0O3M9.mjs";
|
|
11
11
|
import { n as executeWithAuditTransaction } from "../processAudits-BpRlPQMW.mjs";
|
|
12
12
|
import { provideKey } from "@geekmidas/manifest";
|
|
13
|
+
import { randomUUID } from "node:crypto";
|
|
13
14
|
import { ForbiddenError } from "@geekmidas/errors";
|
|
14
15
|
import { EnvironmentParser } from "@geekmidas/envkit";
|
|
15
16
|
import { ServiceDiscovery, runWithRequestContext, serviceContext } from "@geekmidas/services";
|
|
@@ -17,7 +18,6 @@ import { Hono } from "hono";
|
|
|
17
18
|
import { DefaultAuditor } from "@geekmidas/audit";
|
|
18
19
|
import { TEST_CONTEXT_HEADER, currentTestContext, installContextFetch, runAsServer, runInTestContext, testContextOf } from "@geekmidas/testkit/context";
|
|
19
20
|
import { http } from "msw";
|
|
20
|
-
import { randomUUID } from "node:crypto";
|
|
21
21
|
import { Browser } from "@geekmidas/testkit/browser";
|
|
22
22
|
import { faker } from "@geekmidas/testkit/faker";
|
|
23
23
|
import { createMailbox } from "@geekmidas/testkit/mailbox";
|
|
@@ -0,0 +1,225 @@
|
|
|
1
|
+
import { createCipheriv, createDecipheriv, createHmac, randomBytes } from "node:crypto";
|
|
2
|
+
//#region src/encryption/cipher.ts
|
|
3
|
+
/**
|
|
4
|
+
* The cipher behind an `Encryption` construct, and the format it writes.
|
|
5
|
+
*
|
|
6
|
+
* Every ciphertext names the key that wrote it — `gkm1.<keyId>.<payload>` — so
|
|
7
|
+
* `decrypt` never has to guess, a rotated key still opens what it wrote, and
|
|
8
|
+
* retiring a key is a decision made against data rather than a hope. The
|
|
9
|
+
* format version leads, so the day it has to change, both can be read.
|
|
10
|
+
*
|
|
11
|
+
* Two backends, picked by the scheme of the construct's URL, the way a queue's
|
|
12
|
+
* connection string picks its broker:
|
|
13
|
+
*
|
|
14
|
+
* - `aes256gcm://` — a keyring this process holds: AES-256-GCM with the newest
|
|
15
|
+
* key, HMAC-SHA256 for the blind index. Local, test and server stages.
|
|
16
|
+
* - `kms://` — envelope encryption under an AWS KMS key, and a KMS HMAC key for
|
|
17
|
+
* the index. The key material never leaves KMS. See `./kms`.
|
|
18
|
+
*/
|
|
19
|
+
/** The format every ciphertext leads with. */
|
|
20
|
+
const CIPHERTEXT_VERSION = "gkm1";
|
|
21
|
+
/** AES-256-GCM's nonce and tag, in bytes. */
|
|
22
|
+
const IV_BYTES = 12;
|
|
23
|
+
const KEY_BYTES = 32;
|
|
24
|
+
const LOCAL_SCHEME = "aes256gcm:";
|
|
25
|
+
var EncryptionUrlInvalid = class extends Error {
|
|
26
|
+
construct;
|
|
27
|
+
reason;
|
|
28
|
+
constructor(construct, reason) {
|
|
29
|
+
super(`${construct}'s encryption URL is not usable: ${reason}. gkm composes it — derived locally, generated into a server stage's secrets on its first deploy, provisioned on AWS — so it should not be set by hand.`);
|
|
30
|
+
this.construct = construct;
|
|
31
|
+
this.reason = reason;
|
|
32
|
+
this.name = "EncryptionUrlInvalid";
|
|
33
|
+
}
|
|
34
|
+
};
|
|
35
|
+
var UnknownEncryptionScheme = class extends Error {
|
|
36
|
+
construct;
|
|
37
|
+
scheme;
|
|
38
|
+
constructor(construct, scheme) {
|
|
39
|
+
super(`${construct}'s encryption URL uses '${scheme}', which no cipher handles. Expected 'aes256gcm:' (a local keyring) or 'kms:' (AWS KMS).`);
|
|
40
|
+
this.construct = construct;
|
|
41
|
+
this.scheme = scheme;
|
|
42
|
+
this.name = "UnknownEncryptionScheme";
|
|
43
|
+
}
|
|
44
|
+
};
|
|
45
|
+
var CiphertextMalformed = class extends Error {
|
|
46
|
+
construct;
|
|
47
|
+
constructor(construct) {
|
|
48
|
+
super(`${construct} was asked to decrypt something it did not write: a ciphertext starts '${CIPHERTEXT_VERSION}.<key>.'. Check the column holds what \`encrypt\` returned, untrimmed.`);
|
|
49
|
+
this.construct = construct;
|
|
50
|
+
this.name = "CiphertextMalformed";
|
|
51
|
+
}
|
|
52
|
+
};
|
|
53
|
+
var EncryptionKeyRetired = class extends Error {
|
|
54
|
+
construct;
|
|
55
|
+
keyId;
|
|
56
|
+
constructor(construct, keyId) {
|
|
57
|
+
super(`${construct} has no key '${keyId}' any more, so a value it wrote cannot be read. It was retired before every value was re-encrypted; restore it from the stage's secrets history, run the sweep, then retire it again.`);
|
|
58
|
+
this.construct = construct;
|
|
59
|
+
this.keyId = keyId;
|
|
60
|
+
this.name = "EncryptionKeyRetired";
|
|
61
|
+
}
|
|
62
|
+
};
|
|
63
|
+
var CiphertextNotAuthentic = class extends Error {
|
|
64
|
+
construct;
|
|
65
|
+
constructor(construct) {
|
|
66
|
+
super(`${construct} could not authenticate a ciphertext: it was altered, or written by a different Encryption construct. Ciphertexts are bound to the construct that wrote them.`);
|
|
67
|
+
this.construct = construct;
|
|
68
|
+
this.name = "CiphertextNotAuthentic";
|
|
69
|
+
}
|
|
70
|
+
};
|
|
71
|
+
var CurrentKeyCannotRetire = class extends Error {
|
|
72
|
+
construct;
|
|
73
|
+
keyId;
|
|
74
|
+
constructor(construct, keyId) {
|
|
75
|
+
super(`'${keyId}' is ${construct}'s current key — everything new is written with it. Rotate first, sweep, then retire '${keyId}'.`);
|
|
76
|
+
this.construct = construct;
|
|
77
|
+
this.keyId = keyId;
|
|
78
|
+
this.name = "CurrentKeyCannotRetire";
|
|
79
|
+
}
|
|
80
|
+
};
|
|
81
|
+
/** A ciphertext's three parts. */
|
|
82
|
+
function parseCiphertext(construct, ciphertext) {
|
|
83
|
+
const [version, keyId, payload, ...rest] = ciphertext.split(".");
|
|
84
|
+
if (version !== "gkm1" || !keyId || !payload || rest.length) throw new CiphertextMalformed(construct);
|
|
85
|
+
return {
|
|
86
|
+
keyId,
|
|
87
|
+
payload: Buffer.from(payload, "base64url")
|
|
88
|
+
};
|
|
89
|
+
}
|
|
90
|
+
function formatCiphertext(keyId, payload) {
|
|
91
|
+
return `${CIPHERTEXT_VERSION}.${keyId}.${payload.toString("base64url")}`;
|
|
92
|
+
}
|
|
93
|
+
/**
|
|
94
|
+
* What the cipher authenticates alongside the data: the construct and the key.
|
|
95
|
+
*
|
|
96
|
+
* Binding the construct means a value copied from one `Encryption` into
|
|
97
|
+
* another's column fails instead of decrypting under the wrong policy.
|
|
98
|
+
*/
|
|
99
|
+
function associatedData(construct, keyId) {
|
|
100
|
+
return Buffer.from(`${CIPHERTEXT_VERSION}:${construct}:${keyId}`);
|
|
101
|
+
}
|
|
102
|
+
/** `iv | tag | ciphertext`, under one AES-256-GCM key. */
|
|
103
|
+
function seal(key, plaintext, aad) {
|
|
104
|
+
const iv = randomBytes(IV_BYTES);
|
|
105
|
+
const cipher = createCipheriv("aes-256-gcm", key, iv);
|
|
106
|
+
cipher.setAAD(aad);
|
|
107
|
+
const body = Buffer.concat([cipher.update(plaintext), cipher.final()]);
|
|
108
|
+
return Buffer.concat([
|
|
109
|
+
iv,
|
|
110
|
+
cipher.getAuthTag(),
|
|
111
|
+
body
|
|
112
|
+
]);
|
|
113
|
+
}
|
|
114
|
+
/** The inverse of {@link seal}; `undefined` when it does not authenticate. */
|
|
115
|
+
function open(key, sealed, aad) {
|
|
116
|
+
if (sealed.length < 28) return void 0;
|
|
117
|
+
const decipher = createDecipheriv("aes-256-gcm", key, sealed.subarray(0, IV_BYTES));
|
|
118
|
+
decipher.setAAD(aad);
|
|
119
|
+
decipher.setAuthTag(sealed.subarray(IV_BYTES, 28));
|
|
120
|
+
try {
|
|
121
|
+
return Buffer.concat([decipher.update(sealed.subarray(28)), decipher.final()]);
|
|
122
|
+
} catch {
|
|
123
|
+
return;
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
/** Parse `aes256gcm://local?keys=k2.<key>,k1.<key>&index=<key>`. */
|
|
127
|
+
function parseKeyring(construct, url) {
|
|
128
|
+
let parsed;
|
|
129
|
+
try {
|
|
130
|
+
parsed = new URL(url);
|
|
131
|
+
} catch {
|
|
132
|
+
throw new EncryptionUrlInvalid(construct, "it is not a URL");
|
|
133
|
+
}
|
|
134
|
+
const keys = (parsed.searchParams.get("keys") ?? "").split(",").filter(Boolean).map((entry) => {
|
|
135
|
+
const [id, encoded] = entry.split(".");
|
|
136
|
+
const key = Buffer.from(encoded ?? "", "base64url");
|
|
137
|
+
if (!id || key.length !== KEY_BYTES) throw new EncryptionUrlInvalid(construct, `'${id ?? entry}' is not a 256-bit key`);
|
|
138
|
+
return {
|
|
139
|
+
id,
|
|
140
|
+
key
|
|
141
|
+
};
|
|
142
|
+
});
|
|
143
|
+
if (keys.length === 0) throw new EncryptionUrlInvalid(construct, "it holds no keys");
|
|
144
|
+
const index = Buffer.from(parsed.searchParams.get("index") ?? "", "base64url");
|
|
145
|
+
if (index.length !== KEY_BYTES) throw new EncryptionUrlInvalid(construct, "its index key is missing");
|
|
146
|
+
return {
|
|
147
|
+
keys,
|
|
148
|
+
index
|
|
149
|
+
};
|
|
150
|
+
}
|
|
151
|
+
function formatKeyring(keyring) {
|
|
152
|
+
const keys = keyring.keys.map(({ id, key }) => `${id}.${key.toString("base64url")}`).join(",");
|
|
153
|
+
return `${LOCAL_SCHEME}//local?keys=${keys}&index=${keyring.index.toString("base64url")}`;
|
|
154
|
+
}
|
|
155
|
+
/** A keyring with one fresh key, `k1`, and a fresh index key. */
|
|
156
|
+
function generateKeyring() {
|
|
157
|
+
return formatKeyring({
|
|
158
|
+
keys: [{
|
|
159
|
+
id: "k1",
|
|
160
|
+
key: randomBytes(KEY_BYTES)
|
|
161
|
+
}],
|
|
162
|
+
index: randomBytes(KEY_BYTES)
|
|
163
|
+
});
|
|
164
|
+
}
|
|
165
|
+
/**
|
|
166
|
+
* The keyring with a new current key in front, and every older one kept.
|
|
167
|
+
*
|
|
168
|
+
* Nothing is dropped: what the old keys wrote still has to open until a sweep
|
|
169
|
+
* has re-encrypted it — see {@link retireKey}.
|
|
170
|
+
*/
|
|
171
|
+
function rotateKeyring(construct, url) {
|
|
172
|
+
const keyring = parseKeyring(construct, url);
|
|
173
|
+
const next = Math.max(...keyring.keys.map(({ id }) => Number(id.slice(1)) || 0)) + 1;
|
|
174
|
+
return formatKeyring({
|
|
175
|
+
...keyring,
|
|
176
|
+
keys: [{
|
|
177
|
+
id: `k${next}`,
|
|
178
|
+
key: randomBytes(KEY_BYTES)
|
|
179
|
+
}, ...keyring.keys]
|
|
180
|
+
});
|
|
181
|
+
}
|
|
182
|
+
/** The keyring without `keyId`, which must not be the current key. */
|
|
183
|
+
function retireKey(construct, url, keyId) {
|
|
184
|
+
const keyring = parseKeyring(construct, url);
|
|
185
|
+
if (keyring.keys[0]?.id === keyId) throw new CurrentKeyCannotRetire(construct, keyId);
|
|
186
|
+
if (!keyring.keys.some(({ id }) => id === keyId)) throw new EncryptionKeyRetired(construct, keyId);
|
|
187
|
+
return formatKeyring({
|
|
188
|
+
...keyring,
|
|
189
|
+
keys: keyring.keys.filter(({ id }) => id !== keyId)
|
|
190
|
+
});
|
|
191
|
+
}
|
|
192
|
+
/** The `aes256gcm://` cipher: a keyring held in process. */
|
|
193
|
+
function keyringCipher(construct, url, onStaleKey = () => {}) {
|
|
194
|
+
const keyring = parseKeyring(construct, url);
|
|
195
|
+
const current = keyring.keys[0];
|
|
196
|
+
const byId = new Map(keyring.keys.map(({ id, key }) => [id, key]));
|
|
197
|
+
const reported = /* @__PURE__ */ new Set();
|
|
198
|
+
const decrypt = async (ciphertext) => {
|
|
199
|
+
const { keyId, payload } = parseCiphertext(construct, ciphertext);
|
|
200
|
+
const key = byId.get(keyId);
|
|
201
|
+
if (!key) throw new EncryptionKeyRetired(construct, keyId);
|
|
202
|
+
const plaintext = open(key, payload, associatedData(construct, keyId));
|
|
203
|
+
if (!plaintext) throw new CiphertextNotAuthentic(construct);
|
|
204
|
+
if (keyId !== current.id && !reported.has(keyId)) {
|
|
205
|
+
reported.add(keyId);
|
|
206
|
+
onStaleKey(keyId, current.id);
|
|
207
|
+
}
|
|
208
|
+
return plaintext.toString("utf8");
|
|
209
|
+
};
|
|
210
|
+
const encrypt = async (plaintext) => formatCiphertext(current.id, seal(current.key, Buffer.from(plaintext, "utf8"), associatedData(construct, current.id)));
|
|
211
|
+
return {
|
|
212
|
+
encrypt,
|
|
213
|
+
decrypt,
|
|
214
|
+
index: async (value) => createHmac("sha256", keyring.index).update(value, "utf8").digest("base64url"),
|
|
215
|
+
reencrypt: async (ciphertext) => {
|
|
216
|
+
const { keyId } = parseCiphertext(construct, ciphertext);
|
|
217
|
+
if (keyId === current.id) return ciphertext;
|
|
218
|
+
return encrypt(await decrypt(ciphertext));
|
|
219
|
+
}
|
|
220
|
+
};
|
|
221
|
+
}
|
|
222
|
+
//#endregion
|
|
223
|
+
export { rotateKeyring as _, EncryptionUrlInvalid as a, associatedData as c, generateKeyring as d, keyringCipher as f, retireKey as g, parseKeyring as h, EncryptionKeyRetired as i, formatCiphertext as l, parseCiphertext as m, CiphertextNotAuthentic as n, LOCAL_SCHEME as o, open as p, CurrentKeyCannotRetire as r, UnknownEncryptionScheme as s, CiphertextMalformed as t, formatKeyring as u, seal as v };
|
|
224
|
+
|
|
225
|
+
//# sourceMappingURL=cipher-C1lEFX63.mjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"cipher-C1lEFX63.mjs","names":[],"sources":["../src/encryption/cipher.ts"],"sourcesContent":["/**\n * The cipher behind an `Encryption` construct, and the format it writes.\n *\n * Every ciphertext names the key that wrote it — `gkm1.<keyId>.<payload>` — so\n * `decrypt` never has to guess, a rotated key still opens what it wrote, and\n * retiring a key is a decision made against data rather than a hope. The\n * format version leads, so the day it has to change, both can be read.\n *\n * Two backends, picked by the scheme of the construct's URL, the way a queue's\n * connection string picks its broker:\n *\n * - `aes256gcm://` — a keyring this process holds: AES-256-GCM with the newest\n * key, HMAC-SHA256 for the blind index. Local, test and server stages.\n * - `kms://` — envelope encryption under an AWS KMS key, and a KMS HMAC key for\n * the index. The key material never leaves KMS. See `./kms`.\n */\n\nimport {\n\tcreateCipheriv,\n\tcreateDecipheriv,\n\tcreateHmac,\n\trandomBytes,\n} from 'node:crypto';\n\n/** What a handler is given: `services.pii.encrypt(…)` and the rest. */\nexport interface Cipher {\n\t/** Plaintext in, a self-describing ciphertext string out. */\n\tencrypt(plaintext: string): Promise<string>;\n\t/** Opens any ciphertext this construct wrote, under whichever key wrote it. */\n\tdecrypt(ciphertext: string): Promise<string>;\n\t/**\n\t * A blind index: the same value always gives the same string, so an\n\t * encrypted column can still be looked up by an indexed column beside it.\n\t *\n\t * Deterministic on purpose, which is also why its key never rotates — a\n\t * rotated index key would make every stored index miss.\n\t */\n\tindex(value: string): Promise<string>;\n\t/**\n\t * The same plaintext under the current key — what a sweep runs over a\n\t * column before an old key is retired. Unchanged when it is already current.\n\t */\n\treencrypt(ciphertext: string): Promise<string>;\n}\n\n/** The format every ciphertext leads with. */\nexport const CIPHERTEXT_VERSION = 'gkm1';\n\n/** AES-256-GCM's nonce and tag, in bytes. */\nconst IV_BYTES = 12;\nconst TAG_BYTES = 16;\nconst KEY_BYTES = 32;\n\nexport const LOCAL_SCHEME = 'aes256gcm:';\n\nexport class EncryptionUrlInvalid extends Error {\n\tconstructor(\n\t\treadonly construct: string,\n\t\treadonly reason: string,\n\t) {\n\t\tsuper(\n\t\t\t`${construct}'s encryption URL is not usable: ${reason}. gkm composes it — derived locally, generated into a server stage's secrets on its first deploy, provisioned on AWS — so it should not be set by hand.`,\n\t\t);\n\t\tthis.name = 'EncryptionUrlInvalid';\n\t}\n}\n\nexport class UnknownEncryptionScheme extends Error {\n\tconstructor(\n\t\treadonly construct: string,\n\t\treadonly scheme: string,\n\t) {\n\t\tsuper(\n\t\t\t`${construct}'s encryption URL uses '${scheme}', which no cipher handles. Expected 'aes256gcm:' (a local keyring) or 'kms:' (AWS KMS).`,\n\t\t);\n\t\tthis.name = 'UnknownEncryptionScheme';\n\t}\n}\n\nexport class CiphertextMalformed extends Error {\n\tconstructor(readonly construct: string) {\n\t\tsuper(\n\t\t\t`${construct} was asked to decrypt something it did not write: a ciphertext starts '${CIPHERTEXT_VERSION}.<key>.'. Check the column holds what \\`encrypt\\` returned, untrimmed.`,\n\t\t);\n\t\tthis.name = 'CiphertextMalformed';\n\t}\n}\n\nexport class EncryptionKeyRetired extends Error {\n\tconstructor(\n\t\treadonly construct: string,\n\t\treadonly keyId: string,\n\t) {\n\t\tsuper(\n\t\t\t`${construct} has no key '${keyId}' any more, so a value it wrote cannot be read. It was retired before every value was re-encrypted; restore it from the stage's secrets history, run the sweep, then retire it again.`,\n\t\t);\n\t\tthis.name = 'EncryptionKeyRetired';\n\t}\n}\n\nexport class CiphertextNotAuthentic extends Error {\n\tconstructor(readonly construct: string) {\n\t\tsuper(\n\t\t\t`${construct} could not authenticate a ciphertext: it was altered, or written by a different Encryption construct. Ciphertexts are bound to the construct that wrote them.`,\n\t\t);\n\t\tthis.name = 'CiphertextNotAuthentic';\n\t}\n}\n\nexport class CurrentKeyCannotRetire extends Error {\n\tconstructor(\n\t\treadonly construct: string,\n\t\treadonly keyId: string,\n\t) {\n\t\tsuper(\n\t\t\t`'${keyId}' is ${construct}'s current key — everything new is written with it. Rotate first, sweep, then retire '${keyId}'.`,\n\t\t);\n\t\tthis.name = 'CurrentKeyCannotRetire';\n\t}\n}\n\n/** A ciphertext's three parts. */\nexport function parseCiphertext(\n\tconstruct: string,\n\tciphertext: string,\n): { keyId: string; payload: Buffer } {\n\tconst [version, keyId, payload, ...rest] = ciphertext.split('.');\n\tif (version !== CIPHERTEXT_VERSION || !keyId || !payload || rest.length) {\n\t\tthrow new CiphertextMalformed(construct);\n\t}\n\treturn { keyId, payload: Buffer.from(payload, 'base64url') };\n}\n\nexport function formatCiphertext(keyId: string, payload: Buffer): string {\n\treturn `${CIPHERTEXT_VERSION}.${keyId}.${payload.toString('base64url')}`;\n}\n\n/**\n * What the cipher authenticates alongside the data: the construct and the key.\n *\n * Binding the construct means a value copied from one `Encryption` into\n * another's column fails instead of decrypting under the wrong policy.\n */\nexport function associatedData(construct: string, keyId: string): Buffer {\n\treturn Buffer.from(`${CIPHERTEXT_VERSION}:${construct}:${keyId}`);\n}\n\n/** `iv | tag | ciphertext`, under one AES-256-GCM key. */\nexport function seal(key: Buffer, plaintext: Buffer, aad: Buffer): Buffer {\n\tconst iv = randomBytes(IV_BYTES);\n\tconst cipher = createCipheriv('aes-256-gcm', key, iv);\n\tcipher.setAAD(aad);\n\tconst body = Buffer.concat([cipher.update(plaintext), cipher.final()]);\n\treturn Buffer.concat([iv, cipher.getAuthTag(), body]);\n}\n\n/** The inverse of {@link seal}; `undefined` when it does not authenticate. */\nexport function open(\n\tkey: Buffer,\n\tsealed: Buffer,\n\taad: Buffer,\n): Buffer | undefined {\n\tif (sealed.length < IV_BYTES + TAG_BYTES) return undefined;\n\n\tconst decipher = createDecipheriv(\n\t\t'aes-256-gcm',\n\t\tkey,\n\t\tsealed.subarray(0, IV_BYTES),\n\t);\n\tdecipher.setAAD(aad);\n\tdecipher.setAuthTag(sealed.subarray(IV_BYTES, IV_BYTES + TAG_BYTES));\n\n\ttry {\n\t\treturn Buffer.concat([\n\t\t\tdecipher.update(sealed.subarray(IV_BYTES + TAG_BYTES)),\n\t\t\tdecipher.final(),\n\t\t]);\n\t} catch {\n\t\treturn undefined;\n\t}\n}\n\n/** A local keyring: newest key first, and the index key that never rotates. */\nexport interface Keyring {\n\tkeys: { id: string; key: Buffer }[];\n\tindex: Buffer;\n}\n\n/** Parse `aes256gcm://local?keys=k2.<key>,k1.<key>&index=<key>`. */\nexport function parseKeyring(construct: string, url: string): Keyring {\n\tlet parsed: URL;\n\ttry {\n\t\tparsed = new URL(url);\n\t} catch {\n\t\tthrow new EncryptionUrlInvalid(construct, 'it is not a URL');\n\t}\n\n\tconst keys = (parsed.searchParams.get('keys') ?? '')\n\t\t.split(',')\n\t\t.filter(Boolean)\n\t\t.map((entry) => {\n\t\t\tconst [id, encoded] = entry.split('.');\n\t\t\tconst key = Buffer.from(encoded ?? '', 'base64url');\n\t\t\tif (!id || key.length !== KEY_BYTES) {\n\t\t\t\tthrow new EncryptionUrlInvalid(\n\t\t\t\t\tconstruct,\n\t\t\t\t\t`'${id ?? entry}' is not a 256-bit key`,\n\t\t\t\t);\n\t\t\t}\n\t\t\treturn { id, key };\n\t\t});\n\tif (keys.length === 0) {\n\t\tthrow new EncryptionUrlInvalid(construct, 'it holds no keys');\n\t}\n\n\tconst index = Buffer.from(\n\t\tparsed.searchParams.get('index') ?? '',\n\t\t'base64url',\n\t);\n\tif (index.length !== KEY_BYTES) {\n\t\tthrow new EncryptionUrlInvalid(construct, 'its index key is missing');\n\t}\n\n\treturn { keys, index };\n}\n\nexport function formatKeyring(keyring: Keyring): string {\n\tconst keys = keyring.keys\n\t\t.map(({ id, key }) => `${id}.${key.toString('base64url')}`)\n\t\t.join(',');\n\treturn `${LOCAL_SCHEME}//local?keys=${keys}&index=${keyring.index.toString('base64url')}`;\n}\n\n/** A keyring with one fresh key, `k1`, and a fresh index key. */\nexport function generateKeyring(): string {\n\treturn formatKeyring({\n\t\tkeys: [{ id: 'k1', key: randomBytes(KEY_BYTES) }],\n\t\tindex: randomBytes(KEY_BYTES),\n\t});\n}\n\n/**\n * The keyring with a new current key in front, and every older one kept.\n *\n * Nothing is dropped: what the old keys wrote still has to open until a sweep\n * has re-encrypted it — see {@link retireKey}.\n */\nexport function rotateKeyring(construct: string, url: string): string {\n\tconst keyring = parseKeyring(construct, url);\n\tconst next =\n\t\tMath.max(...keyring.keys.map(({ id }) => Number(id.slice(1)) || 0)) + 1;\n\n\treturn formatKeyring({\n\t\t...keyring,\n\t\tkeys: [{ id: `k${next}`, key: randomBytes(KEY_BYTES) }, ...keyring.keys],\n\t});\n}\n\n/** The keyring without `keyId`, which must not be the current key. */\nexport function retireKey(\n\tconstruct: string,\n\turl: string,\n\tkeyId: string,\n): string {\n\tconst keyring = parseKeyring(construct, url);\n\tif (keyring.keys[0]?.id === keyId) {\n\t\tthrow new CurrentKeyCannotRetire(construct, keyId);\n\t}\n\tif (!keyring.keys.some(({ id }) => id === keyId)) {\n\t\tthrow new EncryptionKeyRetired(construct, keyId);\n\t}\n\n\treturn formatKeyring({\n\t\t...keyring,\n\t\tkeys: keyring.keys.filter(({ id }) => id !== keyId),\n\t});\n}\n\n/** Says, once per key per process, that a value is still under an old key. */\nexport type StaleKeyReporter = (keyId: string, current: string) => void;\n\n/** The `aes256gcm://` cipher: a keyring held in process. */\nexport function keyringCipher(\n\tconstruct: string,\n\turl: string,\n\tonStaleKey: StaleKeyReporter = () => {},\n): Cipher {\n\tconst keyring = parseKeyring(construct, url);\n\tconst current = keyring.keys[0]!;\n\tconst byId = new Map(keyring.keys.map(({ id, key }) => [id, key]));\n\tconst reported = new Set<string>();\n\n\tconst decrypt = async (ciphertext: string) => {\n\t\tconst { keyId, payload } = parseCiphertext(construct, ciphertext);\n\t\tconst key = byId.get(keyId);\n\t\tif (!key) throw new EncryptionKeyRetired(construct, keyId);\n\n\t\tconst plaintext = open(key, payload, associatedData(construct, keyId));\n\t\tif (!plaintext) throw new CiphertextNotAuthentic(construct);\n\n\t\tif (keyId !== current.id && !reported.has(keyId)) {\n\t\t\treported.add(keyId);\n\t\t\tonStaleKey(keyId, current.id);\n\t\t}\n\n\t\treturn plaintext.toString('utf8');\n\t};\n\n\tconst encrypt = async (plaintext: string) =>\n\t\tformatCiphertext(\n\t\t\tcurrent.id,\n\t\t\tseal(\n\t\t\t\tcurrent.key,\n\t\t\t\tBuffer.from(plaintext, 'utf8'),\n\t\t\t\tassociatedData(construct, current.id),\n\t\t\t),\n\t\t);\n\n\treturn {\n\t\tencrypt,\n\t\tdecrypt,\n\t\tindex: async (value) =>\n\t\t\tcreateHmac('sha256', keyring.index)\n\t\t\t\t.update(value, 'utf8')\n\t\t\t\t.digest('base64url'),\n\t\treencrypt: async (ciphertext) => {\n\t\t\tconst { keyId } = parseCiphertext(construct, ciphertext);\n\t\t\tif (keyId === current.id) return ciphertext;\n\t\t\treturn encrypt(await decrypt(ciphertext));\n\t\t},\n\t};\n}\n"],"mappings":"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"}
|
|
@@ -0,0 +1,332 @@
|
|
|
1
|
+
let node_crypto = require("node:crypto");
|
|
2
|
+
//#region src/encryption/cipher.ts
|
|
3
|
+
/**
|
|
4
|
+
* The cipher behind an `Encryption` construct, and the format it writes.
|
|
5
|
+
*
|
|
6
|
+
* Every ciphertext names the key that wrote it — `gkm1.<keyId>.<payload>` — so
|
|
7
|
+
* `decrypt` never has to guess, a rotated key still opens what it wrote, and
|
|
8
|
+
* retiring a key is a decision made against data rather than a hope. The
|
|
9
|
+
* format version leads, so the day it has to change, both can be read.
|
|
10
|
+
*
|
|
11
|
+
* Two backends, picked by the scheme of the construct's URL, the way a queue's
|
|
12
|
+
* connection string picks its broker:
|
|
13
|
+
*
|
|
14
|
+
* - `aes256gcm://` — a keyring this process holds: AES-256-GCM with the newest
|
|
15
|
+
* key, HMAC-SHA256 for the blind index. Local, test and server stages.
|
|
16
|
+
* - `kms://` — envelope encryption under an AWS KMS key, and a KMS HMAC key for
|
|
17
|
+
* the index. The key material never leaves KMS. See `./kms`.
|
|
18
|
+
*/
|
|
19
|
+
/** The format every ciphertext leads with. */
|
|
20
|
+
const CIPHERTEXT_VERSION = "gkm1";
|
|
21
|
+
/** AES-256-GCM's nonce and tag, in bytes. */
|
|
22
|
+
const IV_BYTES = 12;
|
|
23
|
+
const KEY_BYTES = 32;
|
|
24
|
+
const LOCAL_SCHEME = "aes256gcm:";
|
|
25
|
+
var EncryptionUrlInvalid = class extends Error {
|
|
26
|
+
construct;
|
|
27
|
+
reason;
|
|
28
|
+
constructor(construct, reason) {
|
|
29
|
+
super(`${construct}'s encryption URL is not usable: ${reason}. gkm composes it — derived locally, generated into a server stage's secrets on its first deploy, provisioned on AWS — so it should not be set by hand.`);
|
|
30
|
+
this.construct = construct;
|
|
31
|
+
this.reason = reason;
|
|
32
|
+
this.name = "EncryptionUrlInvalid";
|
|
33
|
+
}
|
|
34
|
+
};
|
|
35
|
+
var UnknownEncryptionScheme = class extends Error {
|
|
36
|
+
construct;
|
|
37
|
+
scheme;
|
|
38
|
+
constructor(construct, scheme) {
|
|
39
|
+
super(`${construct}'s encryption URL uses '${scheme}', which no cipher handles. Expected 'aes256gcm:' (a local keyring) or 'kms:' (AWS KMS).`);
|
|
40
|
+
this.construct = construct;
|
|
41
|
+
this.scheme = scheme;
|
|
42
|
+
this.name = "UnknownEncryptionScheme";
|
|
43
|
+
}
|
|
44
|
+
};
|
|
45
|
+
var CiphertextMalformed = class extends Error {
|
|
46
|
+
construct;
|
|
47
|
+
constructor(construct) {
|
|
48
|
+
super(`${construct} was asked to decrypt something it did not write: a ciphertext starts '${CIPHERTEXT_VERSION}.<key>.'. Check the column holds what \`encrypt\` returned, untrimmed.`);
|
|
49
|
+
this.construct = construct;
|
|
50
|
+
this.name = "CiphertextMalformed";
|
|
51
|
+
}
|
|
52
|
+
};
|
|
53
|
+
var EncryptionKeyRetired = class extends Error {
|
|
54
|
+
construct;
|
|
55
|
+
keyId;
|
|
56
|
+
constructor(construct, keyId) {
|
|
57
|
+
super(`${construct} has no key '${keyId}' any more, so a value it wrote cannot be read. It was retired before every value was re-encrypted; restore it from the stage's secrets history, run the sweep, then retire it again.`);
|
|
58
|
+
this.construct = construct;
|
|
59
|
+
this.keyId = keyId;
|
|
60
|
+
this.name = "EncryptionKeyRetired";
|
|
61
|
+
}
|
|
62
|
+
};
|
|
63
|
+
var CiphertextNotAuthentic = class extends Error {
|
|
64
|
+
construct;
|
|
65
|
+
constructor(construct) {
|
|
66
|
+
super(`${construct} could not authenticate a ciphertext: it was altered, or written by a different Encryption construct. Ciphertexts are bound to the construct that wrote them.`);
|
|
67
|
+
this.construct = construct;
|
|
68
|
+
this.name = "CiphertextNotAuthentic";
|
|
69
|
+
}
|
|
70
|
+
};
|
|
71
|
+
var CurrentKeyCannotRetire = class extends Error {
|
|
72
|
+
construct;
|
|
73
|
+
keyId;
|
|
74
|
+
constructor(construct, keyId) {
|
|
75
|
+
super(`'${keyId}' is ${construct}'s current key — everything new is written with it. Rotate first, sweep, then retire '${keyId}'.`);
|
|
76
|
+
this.construct = construct;
|
|
77
|
+
this.keyId = keyId;
|
|
78
|
+
this.name = "CurrentKeyCannotRetire";
|
|
79
|
+
}
|
|
80
|
+
};
|
|
81
|
+
/** A ciphertext's three parts. */
|
|
82
|
+
function parseCiphertext(construct, ciphertext) {
|
|
83
|
+
const [version, keyId, payload, ...rest] = ciphertext.split(".");
|
|
84
|
+
if (version !== "gkm1" || !keyId || !payload || rest.length) throw new CiphertextMalformed(construct);
|
|
85
|
+
return {
|
|
86
|
+
keyId,
|
|
87
|
+
payload: Buffer.from(payload, "base64url")
|
|
88
|
+
};
|
|
89
|
+
}
|
|
90
|
+
function formatCiphertext(keyId, payload) {
|
|
91
|
+
return `${CIPHERTEXT_VERSION}.${keyId}.${payload.toString("base64url")}`;
|
|
92
|
+
}
|
|
93
|
+
/**
|
|
94
|
+
* What the cipher authenticates alongside the data: the construct and the key.
|
|
95
|
+
*
|
|
96
|
+
* Binding the construct means a value copied from one `Encryption` into
|
|
97
|
+
* another's column fails instead of decrypting under the wrong policy.
|
|
98
|
+
*/
|
|
99
|
+
function associatedData(construct, keyId) {
|
|
100
|
+
return Buffer.from(`${CIPHERTEXT_VERSION}:${construct}:${keyId}`);
|
|
101
|
+
}
|
|
102
|
+
/** `iv | tag | ciphertext`, under one AES-256-GCM key. */
|
|
103
|
+
function seal(key, plaintext, aad) {
|
|
104
|
+
const iv = (0, node_crypto.randomBytes)(IV_BYTES);
|
|
105
|
+
const cipher = (0, node_crypto.createCipheriv)("aes-256-gcm", key, iv);
|
|
106
|
+
cipher.setAAD(aad);
|
|
107
|
+
const body = Buffer.concat([cipher.update(plaintext), cipher.final()]);
|
|
108
|
+
return Buffer.concat([
|
|
109
|
+
iv,
|
|
110
|
+
cipher.getAuthTag(),
|
|
111
|
+
body
|
|
112
|
+
]);
|
|
113
|
+
}
|
|
114
|
+
/** The inverse of {@link seal}; `undefined` when it does not authenticate. */
|
|
115
|
+
function open(key, sealed, aad) {
|
|
116
|
+
if (sealed.length < 28) return void 0;
|
|
117
|
+
const decipher = (0, node_crypto.createDecipheriv)("aes-256-gcm", key, sealed.subarray(0, IV_BYTES));
|
|
118
|
+
decipher.setAAD(aad);
|
|
119
|
+
decipher.setAuthTag(sealed.subarray(IV_BYTES, 28));
|
|
120
|
+
try {
|
|
121
|
+
return Buffer.concat([decipher.update(sealed.subarray(28)), decipher.final()]);
|
|
122
|
+
} catch {
|
|
123
|
+
return;
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
/** Parse `aes256gcm://local?keys=k2.<key>,k1.<key>&index=<key>`. */
|
|
127
|
+
function parseKeyring(construct, url) {
|
|
128
|
+
let parsed;
|
|
129
|
+
try {
|
|
130
|
+
parsed = new URL(url);
|
|
131
|
+
} catch {
|
|
132
|
+
throw new EncryptionUrlInvalid(construct, "it is not a URL");
|
|
133
|
+
}
|
|
134
|
+
const keys = (parsed.searchParams.get("keys") ?? "").split(",").filter(Boolean).map((entry) => {
|
|
135
|
+
const [id, encoded] = entry.split(".");
|
|
136
|
+
const key = Buffer.from(encoded ?? "", "base64url");
|
|
137
|
+
if (!id || key.length !== KEY_BYTES) throw new EncryptionUrlInvalid(construct, `'${id ?? entry}' is not a 256-bit key`);
|
|
138
|
+
return {
|
|
139
|
+
id,
|
|
140
|
+
key
|
|
141
|
+
};
|
|
142
|
+
});
|
|
143
|
+
if (keys.length === 0) throw new EncryptionUrlInvalid(construct, "it holds no keys");
|
|
144
|
+
const index = Buffer.from(parsed.searchParams.get("index") ?? "", "base64url");
|
|
145
|
+
if (index.length !== KEY_BYTES) throw new EncryptionUrlInvalid(construct, "its index key is missing");
|
|
146
|
+
return {
|
|
147
|
+
keys,
|
|
148
|
+
index
|
|
149
|
+
};
|
|
150
|
+
}
|
|
151
|
+
function formatKeyring(keyring) {
|
|
152
|
+
const keys = keyring.keys.map(({ id, key }) => `${id}.${key.toString("base64url")}`).join(",");
|
|
153
|
+
return `${LOCAL_SCHEME}//local?keys=${keys}&index=${keyring.index.toString("base64url")}`;
|
|
154
|
+
}
|
|
155
|
+
/** A keyring with one fresh key, `k1`, and a fresh index key. */
|
|
156
|
+
function generateKeyring() {
|
|
157
|
+
return formatKeyring({
|
|
158
|
+
keys: [{
|
|
159
|
+
id: "k1",
|
|
160
|
+
key: (0, node_crypto.randomBytes)(KEY_BYTES)
|
|
161
|
+
}],
|
|
162
|
+
index: (0, node_crypto.randomBytes)(KEY_BYTES)
|
|
163
|
+
});
|
|
164
|
+
}
|
|
165
|
+
/**
|
|
166
|
+
* The keyring with a new current key in front, and every older one kept.
|
|
167
|
+
*
|
|
168
|
+
* Nothing is dropped: what the old keys wrote still has to open until a sweep
|
|
169
|
+
* has re-encrypted it — see {@link retireKey}.
|
|
170
|
+
*/
|
|
171
|
+
function rotateKeyring(construct, url) {
|
|
172
|
+
const keyring = parseKeyring(construct, url);
|
|
173
|
+
const next = Math.max(...keyring.keys.map(({ id }) => Number(id.slice(1)) || 0)) + 1;
|
|
174
|
+
return formatKeyring({
|
|
175
|
+
...keyring,
|
|
176
|
+
keys: [{
|
|
177
|
+
id: `k${next}`,
|
|
178
|
+
key: (0, node_crypto.randomBytes)(KEY_BYTES)
|
|
179
|
+
}, ...keyring.keys]
|
|
180
|
+
});
|
|
181
|
+
}
|
|
182
|
+
/** The keyring without `keyId`, which must not be the current key. */
|
|
183
|
+
function retireKey(construct, url, keyId) {
|
|
184
|
+
const keyring = parseKeyring(construct, url);
|
|
185
|
+
if (keyring.keys[0]?.id === keyId) throw new CurrentKeyCannotRetire(construct, keyId);
|
|
186
|
+
if (!keyring.keys.some(({ id }) => id === keyId)) throw new EncryptionKeyRetired(construct, keyId);
|
|
187
|
+
return formatKeyring({
|
|
188
|
+
...keyring,
|
|
189
|
+
keys: keyring.keys.filter(({ id }) => id !== keyId)
|
|
190
|
+
});
|
|
191
|
+
}
|
|
192
|
+
/** The `aes256gcm://` cipher: a keyring held in process. */
|
|
193
|
+
function keyringCipher(construct, url, onStaleKey = () => {}) {
|
|
194
|
+
const keyring = parseKeyring(construct, url);
|
|
195
|
+
const current = keyring.keys[0];
|
|
196
|
+
const byId = new Map(keyring.keys.map(({ id, key }) => [id, key]));
|
|
197
|
+
const reported = /* @__PURE__ */ new Set();
|
|
198
|
+
const decrypt = async (ciphertext) => {
|
|
199
|
+
const { keyId, payload } = parseCiphertext(construct, ciphertext);
|
|
200
|
+
const key = byId.get(keyId);
|
|
201
|
+
if (!key) throw new EncryptionKeyRetired(construct, keyId);
|
|
202
|
+
const plaintext = open(key, payload, associatedData(construct, keyId));
|
|
203
|
+
if (!plaintext) throw new CiphertextNotAuthentic(construct);
|
|
204
|
+
if (keyId !== current.id && !reported.has(keyId)) {
|
|
205
|
+
reported.add(keyId);
|
|
206
|
+
onStaleKey(keyId, current.id);
|
|
207
|
+
}
|
|
208
|
+
return plaintext.toString("utf8");
|
|
209
|
+
};
|
|
210
|
+
const encrypt = async (plaintext) => formatCiphertext(current.id, seal(current.key, Buffer.from(plaintext, "utf8"), associatedData(construct, current.id)));
|
|
211
|
+
return {
|
|
212
|
+
encrypt,
|
|
213
|
+
decrypt,
|
|
214
|
+
index: async (value) => (0, node_crypto.createHmac)("sha256", keyring.index).update(value, "utf8").digest("base64url"),
|
|
215
|
+
reencrypt: async (ciphertext) => {
|
|
216
|
+
const { keyId } = parseCiphertext(construct, ciphertext);
|
|
217
|
+
if (keyId === current.id) return ciphertext;
|
|
218
|
+
return encrypt(await decrypt(ciphertext));
|
|
219
|
+
}
|
|
220
|
+
};
|
|
221
|
+
}
|
|
222
|
+
//#endregion
|
|
223
|
+
Object.defineProperty(exports, "CiphertextMalformed", {
|
|
224
|
+
enumerable: true,
|
|
225
|
+
get: function() {
|
|
226
|
+
return CiphertextMalformed;
|
|
227
|
+
}
|
|
228
|
+
});
|
|
229
|
+
Object.defineProperty(exports, "CiphertextNotAuthentic", {
|
|
230
|
+
enumerable: true,
|
|
231
|
+
get: function() {
|
|
232
|
+
return CiphertextNotAuthentic;
|
|
233
|
+
}
|
|
234
|
+
});
|
|
235
|
+
Object.defineProperty(exports, "CurrentKeyCannotRetire", {
|
|
236
|
+
enumerable: true,
|
|
237
|
+
get: function() {
|
|
238
|
+
return CurrentKeyCannotRetire;
|
|
239
|
+
}
|
|
240
|
+
});
|
|
241
|
+
Object.defineProperty(exports, "EncryptionKeyRetired", {
|
|
242
|
+
enumerable: true,
|
|
243
|
+
get: function() {
|
|
244
|
+
return EncryptionKeyRetired;
|
|
245
|
+
}
|
|
246
|
+
});
|
|
247
|
+
Object.defineProperty(exports, "EncryptionUrlInvalid", {
|
|
248
|
+
enumerable: true,
|
|
249
|
+
get: function() {
|
|
250
|
+
return EncryptionUrlInvalid;
|
|
251
|
+
}
|
|
252
|
+
});
|
|
253
|
+
Object.defineProperty(exports, "LOCAL_SCHEME", {
|
|
254
|
+
enumerable: true,
|
|
255
|
+
get: function() {
|
|
256
|
+
return LOCAL_SCHEME;
|
|
257
|
+
}
|
|
258
|
+
});
|
|
259
|
+
Object.defineProperty(exports, "UnknownEncryptionScheme", {
|
|
260
|
+
enumerable: true,
|
|
261
|
+
get: function() {
|
|
262
|
+
return UnknownEncryptionScheme;
|
|
263
|
+
}
|
|
264
|
+
});
|
|
265
|
+
Object.defineProperty(exports, "associatedData", {
|
|
266
|
+
enumerable: true,
|
|
267
|
+
get: function() {
|
|
268
|
+
return associatedData;
|
|
269
|
+
}
|
|
270
|
+
});
|
|
271
|
+
Object.defineProperty(exports, "formatCiphertext", {
|
|
272
|
+
enumerable: true,
|
|
273
|
+
get: function() {
|
|
274
|
+
return formatCiphertext;
|
|
275
|
+
}
|
|
276
|
+
});
|
|
277
|
+
Object.defineProperty(exports, "formatKeyring", {
|
|
278
|
+
enumerable: true,
|
|
279
|
+
get: function() {
|
|
280
|
+
return formatKeyring;
|
|
281
|
+
}
|
|
282
|
+
});
|
|
283
|
+
Object.defineProperty(exports, "generateKeyring", {
|
|
284
|
+
enumerable: true,
|
|
285
|
+
get: function() {
|
|
286
|
+
return generateKeyring;
|
|
287
|
+
}
|
|
288
|
+
});
|
|
289
|
+
Object.defineProperty(exports, "keyringCipher", {
|
|
290
|
+
enumerable: true,
|
|
291
|
+
get: function() {
|
|
292
|
+
return keyringCipher;
|
|
293
|
+
}
|
|
294
|
+
});
|
|
295
|
+
Object.defineProperty(exports, "open", {
|
|
296
|
+
enumerable: true,
|
|
297
|
+
get: function() {
|
|
298
|
+
return open;
|
|
299
|
+
}
|
|
300
|
+
});
|
|
301
|
+
Object.defineProperty(exports, "parseCiphertext", {
|
|
302
|
+
enumerable: true,
|
|
303
|
+
get: function() {
|
|
304
|
+
return parseCiphertext;
|
|
305
|
+
}
|
|
306
|
+
});
|
|
307
|
+
Object.defineProperty(exports, "parseKeyring", {
|
|
308
|
+
enumerable: true,
|
|
309
|
+
get: function() {
|
|
310
|
+
return parseKeyring;
|
|
311
|
+
}
|
|
312
|
+
});
|
|
313
|
+
Object.defineProperty(exports, "retireKey", {
|
|
314
|
+
enumerable: true,
|
|
315
|
+
get: function() {
|
|
316
|
+
return retireKey;
|
|
317
|
+
}
|
|
318
|
+
});
|
|
319
|
+
Object.defineProperty(exports, "rotateKeyring", {
|
|
320
|
+
enumerable: true,
|
|
321
|
+
get: function() {
|
|
322
|
+
return rotateKeyring;
|
|
323
|
+
}
|
|
324
|
+
});
|
|
325
|
+
Object.defineProperty(exports, "seal", {
|
|
326
|
+
enumerable: true,
|
|
327
|
+
get: function() {
|
|
328
|
+
return seal;
|
|
329
|
+
}
|
|
330
|
+
});
|
|
331
|
+
|
|
332
|
+
//# sourceMappingURL=cipher-CVxfwLjk.cjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"cipher-CVxfwLjk.cjs","names":["randomBytes","createCipheriv","createDecipheriv","createHmac"],"sources":["../src/encryption/cipher.ts"],"sourcesContent":["/**\n * The cipher behind an `Encryption` construct, and the format it writes.\n *\n * Every ciphertext names the key that wrote it — `gkm1.<keyId>.<payload>` — so\n * `decrypt` never has to guess, a rotated key still opens what it wrote, and\n * retiring a key is a decision made against data rather than a hope. The\n * format version leads, so the day it has to change, both can be read.\n *\n * Two backends, picked by the scheme of the construct's URL, the way a queue's\n * connection string picks its broker:\n *\n * - `aes256gcm://` — a keyring this process holds: AES-256-GCM with the newest\n * key, HMAC-SHA256 for the blind index. Local, test and server stages.\n * - `kms://` — envelope encryption under an AWS KMS key, and a KMS HMAC key for\n * the index. The key material never leaves KMS. See `./kms`.\n */\n\nimport {\n\tcreateCipheriv,\n\tcreateDecipheriv,\n\tcreateHmac,\n\trandomBytes,\n} from 'node:crypto';\n\n/** What a handler is given: `services.pii.encrypt(…)` and the rest. */\nexport interface Cipher {\n\t/** Plaintext in, a self-describing ciphertext string out. */\n\tencrypt(plaintext: string): Promise<string>;\n\t/** Opens any ciphertext this construct wrote, under whichever key wrote it. */\n\tdecrypt(ciphertext: string): Promise<string>;\n\t/**\n\t * A blind index: the same value always gives the same string, so an\n\t * encrypted column can still be looked up by an indexed column beside it.\n\t *\n\t * Deterministic on purpose, which is also why its key never rotates — a\n\t * rotated index key would make every stored index miss.\n\t */\n\tindex(value: string): Promise<string>;\n\t/**\n\t * The same plaintext under the current key — what a sweep runs over a\n\t * column before an old key is retired. Unchanged when it is already current.\n\t */\n\treencrypt(ciphertext: string): Promise<string>;\n}\n\n/** The format every ciphertext leads with. */\nexport const CIPHERTEXT_VERSION = 'gkm1';\n\n/** AES-256-GCM's nonce and tag, in bytes. */\nconst IV_BYTES = 12;\nconst TAG_BYTES = 16;\nconst KEY_BYTES = 32;\n\nexport const LOCAL_SCHEME = 'aes256gcm:';\n\nexport class EncryptionUrlInvalid extends Error {\n\tconstructor(\n\t\treadonly construct: string,\n\t\treadonly reason: string,\n\t) {\n\t\tsuper(\n\t\t\t`${construct}'s encryption URL is not usable: ${reason}. gkm composes it — derived locally, generated into a server stage's secrets on its first deploy, provisioned on AWS — so it should not be set by hand.`,\n\t\t);\n\t\tthis.name = 'EncryptionUrlInvalid';\n\t}\n}\n\nexport class UnknownEncryptionScheme extends Error {\n\tconstructor(\n\t\treadonly construct: string,\n\t\treadonly scheme: string,\n\t) {\n\t\tsuper(\n\t\t\t`${construct}'s encryption URL uses '${scheme}', which no cipher handles. Expected 'aes256gcm:' (a local keyring) or 'kms:' (AWS KMS).`,\n\t\t);\n\t\tthis.name = 'UnknownEncryptionScheme';\n\t}\n}\n\nexport class CiphertextMalformed extends Error {\n\tconstructor(readonly construct: string) {\n\t\tsuper(\n\t\t\t`${construct} was asked to decrypt something it did not write: a ciphertext starts '${CIPHERTEXT_VERSION}.<key>.'. Check the column holds what \\`encrypt\\` returned, untrimmed.`,\n\t\t);\n\t\tthis.name = 'CiphertextMalformed';\n\t}\n}\n\nexport class EncryptionKeyRetired extends Error {\n\tconstructor(\n\t\treadonly construct: string,\n\t\treadonly keyId: string,\n\t) {\n\t\tsuper(\n\t\t\t`${construct} has no key '${keyId}' any more, so a value it wrote cannot be read. It was retired before every value was re-encrypted; restore it from the stage's secrets history, run the sweep, then retire it again.`,\n\t\t);\n\t\tthis.name = 'EncryptionKeyRetired';\n\t}\n}\n\nexport class CiphertextNotAuthentic extends Error {\n\tconstructor(readonly construct: string) {\n\t\tsuper(\n\t\t\t`${construct} could not authenticate a ciphertext: it was altered, or written by a different Encryption construct. Ciphertexts are bound to the construct that wrote them.`,\n\t\t);\n\t\tthis.name = 'CiphertextNotAuthentic';\n\t}\n}\n\nexport class CurrentKeyCannotRetire extends Error {\n\tconstructor(\n\t\treadonly construct: string,\n\t\treadonly keyId: string,\n\t) {\n\t\tsuper(\n\t\t\t`'${keyId}' is ${construct}'s current key — everything new is written with it. Rotate first, sweep, then retire '${keyId}'.`,\n\t\t);\n\t\tthis.name = 'CurrentKeyCannotRetire';\n\t}\n}\n\n/** A ciphertext's three parts. */\nexport function parseCiphertext(\n\tconstruct: string,\n\tciphertext: string,\n): { keyId: string; payload: Buffer } {\n\tconst [version, keyId, payload, ...rest] = ciphertext.split('.');\n\tif (version !== CIPHERTEXT_VERSION || !keyId || !payload || rest.length) {\n\t\tthrow new CiphertextMalformed(construct);\n\t}\n\treturn { keyId, payload: Buffer.from(payload, 'base64url') };\n}\n\nexport function formatCiphertext(keyId: string, payload: Buffer): string {\n\treturn `${CIPHERTEXT_VERSION}.${keyId}.${payload.toString('base64url')}`;\n}\n\n/**\n * What the cipher authenticates alongside the data: the construct and the key.\n *\n * Binding the construct means a value copied from one `Encryption` into\n * another's column fails instead of decrypting under the wrong policy.\n */\nexport function associatedData(construct: string, keyId: string): Buffer {\n\treturn Buffer.from(`${CIPHERTEXT_VERSION}:${construct}:${keyId}`);\n}\n\n/** `iv | tag | ciphertext`, under one AES-256-GCM key. */\nexport function seal(key: Buffer, plaintext: Buffer, aad: Buffer): Buffer {\n\tconst iv = randomBytes(IV_BYTES);\n\tconst cipher = createCipheriv('aes-256-gcm', key, iv);\n\tcipher.setAAD(aad);\n\tconst body = Buffer.concat([cipher.update(plaintext), cipher.final()]);\n\treturn Buffer.concat([iv, cipher.getAuthTag(), body]);\n}\n\n/** The inverse of {@link seal}; `undefined` when it does not authenticate. */\nexport function open(\n\tkey: Buffer,\n\tsealed: Buffer,\n\taad: Buffer,\n): Buffer | undefined {\n\tif (sealed.length < IV_BYTES + TAG_BYTES) return undefined;\n\n\tconst decipher = createDecipheriv(\n\t\t'aes-256-gcm',\n\t\tkey,\n\t\tsealed.subarray(0, IV_BYTES),\n\t);\n\tdecipher.setAAD(aad);\n\tdecipher.setAuthTag(sealed.subarray(IV_BYTES, IV_BYTES + TAG_BYTES));\n\n\ttry {\n\t\treturn Buffer.concat([\n\t\t\tdecipher.update(sealed.subarray(IV_BYTES + TAG_BYTES)),\n\t\t\tdecipher.final(),\n\t\t]);\n\t} catch {\n\t\treturn undefined;\n\t}\n}\n\n/** A local keyring: newest key first, and the index key that never rotates. */\nexport interface Keyring {\n\tkeys: { id: string; key: Buffer }[];\n\tindex: Buffer;\n}\n\n/** Parse `aes256gcm://local?keys=k2.<key>,k1.<key>&index=<key>`. */\nexport function parseKeyring(construct: string, url: string): Keyring {\n\tlet parsed: URL;\n\ttry {\n\t\tparsed = new URL(url);\n\t} catch {\n\t\tthrow new EncryptionUrlInvalid(construct, 'it is not a URL');\n\t}\n\n\tconst keys = (parsed.searchParams.get('keys') ?? '')\n\t\t.split(',')\n\t\t.filter(Boolean)\n\t\t.map((entry) => {\n\t\t\tconst [id, encoded] = entry.split('.');\n\t\t\tconst key = Buffer.from(encoded ?? '', 'base64url');\n\t\t\tif (!id || key.length !== KEY_BYTES) {\n\t\t\t\tthrow new EncryptionUrlInvalid(\n\t\t\t\t\tconstruct,\n\t\t\t\t\t`'${id ?? entry}' is not a 256-bit key`,\n\t\t\t\t);\n\t\t\t}\n\t\t\treturn { id, key };\n\t\t});\n\tif (keys.length === 0) {\n\t\tthrow new EncryptionUrlInvalid(construct, 'it holds no keys');\n\t}\n\n\tconst index = Buffer.from(\n\t\tparsed.searchParams.get('index') ?? '',\n\t\t'base64url',\n\t);\n\tif (index.length !== KEY_BYTES) {\n\t\tthrow new EncryptionUrlInvalid(construct, 'its index key is missing');\n\t}\n\n\treturn { keys, index };\n}\n\nexport function formatKeyring(keyring: Keyring): string {\n\tconst keys = keyring.keys\n\t\t.map(({ id, key }) => `${id}.${key.toString('base64url')}`)\n\t\t.join(',');\n\treturn `${LOCAL_SCHEME}//local?keys=${keys}&index=${keyring.index.toString('base64url')}`;\n}\n\n/** A keyring with one fresh key, `k1`, and a fresh index key. */\nexport function generateKeyring(): string {\n\treturn formatKeyring({\n\t\tkeys: [{ id: 'k1', key: randomBytes(KEY_BYTES) }],\n\t\tindex: randomBytes(KEY_BYTES),\n\t});\n}\n\n/**\n * The keyring with a new current key in front, and every older one kept.\n *\n * Nothing is dropped: what the old keys wrote still has to open until a sweep\n * has re-encrypted it — see {@link retireKey}.\n */\nexport function rotateKeyring(construct: string, url: string): string {\n\tconst keyring = parseKeyring(construct, url);\n\tconst next =\n\t\tMath.max(...keyring.keys.map(({ id }) => Number(id.slice(1)) || 0)) + 1;\n\n\treturn formatKeyring({\n\t\t...keyring,\n\t\tkeys: [{ id: `k${next}`, key: randomBytes(KEY_BYTES) }, ...keyring.keys],\n\t});\n}\n\n/** The keyring without `keyId`, which must not be the current key. */\nexport function retireKey(\n\tconstruct: string,\n\turl: string,\n\tkeyId: string,\n): string {\n\tconst keyring = parseKeyring(construct, url);\n\tif (keyring.keys[0]?.id === keyId) {\n\t\tthrow new CurrentKeyCannotRetire(construct, keyId);\n\t}\n\tif (!keyring.keys.some(({ id }) => id === keyId)) {\n\t\tthrow new EncryptionKeyRetired(construct, keyId);\n\t}\n\n\treturn formatKeyring({\n\t\t...keyring,\n\t\tkeys: keyring.keys.filter(({ id }) => id !== keyId),\n\t});\n}\n\n/** Says, once per key per process, that a value is still under an old key. */\nexport type StaleKeyReporter = (keyId: string, current: string) => void;\n\n/** The `aes256gcm://` cipher: a keyring held in process. */\nexport function keyringCipher(\n\tconstruct: string,\n\turl: string,\n\tonStaleKey: StaleKeyReporter = () => {},\n): Cipher {\n\tconst keyring = parseKeyring(construct, url);\n\tconst current = keyring.keys[0]!;\n\tconst byId = new Map(keyring.keys.map(({ id, key }) => [id, key]));\n\tconst reported = new Set<string>();\n\n\tconst decrypt = async (ciphertext: string) => {\n\t\tconst { keyId, payload } = parseCiphertext(construct, ciphertext);\n\t\tconst key = byId.get(keyId);\n\t\tif (!key) throw new EncryptionKeyRetired(construct, keyId);\n\n\t\tconst plaintext = open(key, payload, associatedData(construct, keyId));\n\t\tif (!plaintext) throw new CiphertextNotAuthentic(construct);\n\n\t\tif (keyId !== current.id && !reported.has(keyId)) {\n\t\t\treported.add(keyId);\n\t\t\tonStaleKey(keyId, current.id);\n\t\t}\n\n\t\treturn plaintext.toString('utf8');\n\t};\n\n\tconst encrypt = async (plaintext: string) =>\n\t\tformatCiphertext(\n\t\t\tcurrent.id,\n\t\t\tseal(\n\t\t\t\tcurrent.key,\n\t\t\t\tBuffer.from(plaintext, 'utf8'),\n\t\t\t\tassociatedData(construct, current.id),\n\t\t\t),\n\t\t);\n\n\treturn {\n\t\tencrypt,\n\t\tdecrypt,\n\t\tindex: async (value) =>\n\t\t\tcreateHmac('sha256', keyring.index)\n\t\t\t\t.update(value, 'utf8')\n\t\t\t\t.digest('base64url'),\n\t\treencrypt: async (ciphertext) => {\n\t\t\tconst { keyId } = parseCiphertext(construct, ciphertext);\n\t\t\tif (keyId === current.id) return ciphertext;\n\t\t\treturn encrypt(await decrypt(ciphertext));\n\t\t},\n\t};\n}\n"],"mappings":"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"}
|