@geekmidas/constructs 10.0.0-alpha.116 → 10.0.0-alpha.118
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.d.cts +1 -1
- package/dist/adaptors/testing.d.mts +1 -1
- package/dist/adaptors/testing.mjs +1 -1
- package/dist/cipher-C1lEFX63.mjs.map +1 -1
- package/dist/cipher-CVxfwLjk.cjs.map +1 -1
- package/dist/cipher-DSDxPRIp.d.cts +106 -0
- package/dist/cipher-DSDxPRIp.d.cts.map +1 -0
- package/dist/cipher-DSDxPRIp.d.mts +106 -0
- package/dist/cipher-DSDxPRIp.d.mts.map +1 -0
- package/dist/database/kysely.cjs +1 -1
- package/dist/database/kysely.d.cts +2 -2
- package/dist/database/kysely.d.mts +2 -2
- package/dist/database/kysely.mjs +1 -1
- package/dist/{kms-B-Hm3d5r.cjs → encryption/kms.cjs} +14 -4
- package/dist/encryption/kms.cjs.map +1 -0
- package/dist/encryption/kms.d.cts +7 -0
- package/dist/encryption/kms.d.cts.map +1 -0
- package/dist/encryption/kms.d.mts +7 -0
- package/dist/encryption/kms.d.mts.map +1 -0
- package/dist/{kms-BdUlMJEZ.mjs → encryption/kms.mjs} +13 -5
- package/dist/encryption/kms.mjs.map +1 -0
- package/dist/encryption.cjs +24 -4
- package/dist/encryption.cjs.map +1 -1
- package/dist/encryption.d.cts +8 -88
- package/dist/encryption.d.cts.map +1 -1
- package/dist/encryption.d.mts +8 -88
- package/dist/encryption.d.mts.map +1 -1
- package/dist/encryption.mjs +23 -5
- package/dist/encryption.mjs.map +1 -1
- package/dist/{kysely-D0bpzgPs.d.cts → kysely-BHYV-WC-.d.cts} +53 -12
- package/dist/kysely-BHYV-WC-.d.cts.map +1 -0
- package/dist/{kysely-DKwnl0nf.d.mts → kysely-CAW3QSqp.d.mts} +53 -12
- package/dist/kysely-CAW3QSqp.d.mts.map +1 -0
- package/dist/{kysely-B_0BAr0T.mjs → kysely-Cs4uPO8k.mjs} +6 -5
- package/dist/kysely-Cs4uPO8k.mjs.map +1 -0
- package/dist/{kysely-B3zfg9ti.cjs → kysely-CukArHJ0.cjs} +6 -5
- package/dist/kysely-CukArHJ0.cjs.map +1 -0
- package/package.json +41 -31
- package/dist/kms-B-Hm3d5r.cjs.map +0 -1
- package/dist/kms-BdUlMJEZ.mjs.map +0 -1
- package/dist/kysely-B3zfg9ti.cjs.map +0 -1
- package/dist/kysely-B_0BAr0T.mjs.map +0 -1
- package/dist/kysely-D0bpzgPs.d.cts.map +0 -1
- package/dist/kysely-DKwnl0nf.d.mts.map +0 -1
|
@@ -6,7 +6,7 @@ const require_functions = require("../functions-D7O5SGVc.cjs");
|
|
|
6
6
|
const require_Endpoint = require("../Endpoint-yhR2T2Wt.cjs");
|
|
7
7
|
const require_Queue = require("../Queue-BhuofM3k.cjs");
|
|
8
8
|
const require_email = require("../email.cjs");
|
|
9
|
-
const require_kysely = require("../kysely-
|
|
9
|
+
const require_kysely = require("../kysely-CukArHJ0.cjs");
|
|
10
10
|
const require_readRequestBody = require("../readRequestBody-Dej_L3c4.cjs");
|
|
11
11
|
const require_Topic = require("../Topic-DwS3QO0m.cjs");
|
|
12
12
|
const require_HonoEndpointAdaptor = require("../HonoEndpointAdaptor-CN1jjUBJ.cjs");
|
|
@@ -4,7 +4,7 @@ import { t as HttpMethod } from "../types-DMuSQeLL.cjs";
|
|
|
4
4
|
import { c as EndpointSchemas, i as Endpoint } from "../Endpoint-BWyKCa2x.cjs";
|
|
5
5
|
import { t as Queue } from "../Queue-HPyi0e1i.cjs";
|
|
6
6
|
import { t as Subscriber } from "../Subscriber-Bpp02cGr.cjs";
|
|
7
|
-
import { t as KyselyDatabase } from "../kysely-
|
|
7
|
+
import { t as KyselyDatabase } from "../kysely-BHYV-WC-.cjs";
|
|
8
8
|
import { t as Fake } from "../fake-Dvion_Fh.cjs";
|
|
9
9
|
import { o as TestStage } from "../stages-BSCF19o2.cjs";
|
|
10
10
|
import { Logger } from "@geekmidas/logger";
|
|
@@ -4,7 +4,7 @@ import { t as HttpMethod } from "../types-DMuSQeLL.mjs";
|
|
|
4
4
|
import { c as EndpointSchemas, i as Endpoint } from "../Endpoint-CrE9Ppdk.mjs";
|
|
5
5
|
import { t as Queue } from "../Queue-DlsHRzUp.mjs";
|
|
6
6
|
import { t as Subscriber } from "../Subscriber-CtN9R48-.mjs";
|
|
7
|
-
import { t as KyselyDatabase } from "../kysely-
|
|
7
|
+
import { t as KyselyDatabase } from "../kysely-CAW3QSqp.mjs";
|
|
8
8
|
import { t as Fake } from "../fake-DqKo8INJ.mjs";
|
|
9
9
|
import { o as TestStage } from "../stages-BSCF19o2.mjs";
|
|
10
10
|
import { EventPublisher, ExtractPublisherMessage } from "@geekmidas/events";
|
|
@@ -4,7 +4,7 @@ import { a as publishEvents, i as publishConstructEvents, t as FunctionBuilder }
|
|
|
4
4
|
import { r as ResponseBuilder, t as Endpoint } from "../Endpoint-gPXhHKhJ.mjs";
|
|
5
5
|
import { t as Queue } from "../Queue-cmhqEBOB.mjs";
|
|
6
6
|
import { Email } from "../email.mjs";
|
|
7
|
-
import { t as KyselyDatabase } from "../kysely-
|
|
7
|
+
import { t as KyselyDatabase } from "../kysely-Cs4uPO8k.mjs";
|
|
8
8
|
import { f as createObjectHeaders, s as createCookieHeaderAccessor } from "../readRequestBody-BTIl3k-D.mjs";
|
|
9
9
|
import { t as Topic } from "../Topic-D2R0r24C.mjs";
|
|
10
10
|
import { t as HonoEndpoint } from "../HonoEndpointAdaptor-euH1oSdY.mjs";
|
|
@@ -1 +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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"}
|
|
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\n/**\n * A cipher for a URL scheme other than `aes256gcm:` — `kms:` on AWS.\n *\n * Registered by the entry point a build generates, and only for a target\n * whose keys live there, so a process that never holds such a key never\n * imports the SDK it would need.\n */\nexport interface CipherDriver {\n\t/** The scheme it opens, with its colon: `'kms:'`. */\n\treadonly scheme: string;\n\topen(construct: string, url: string): Cipher;\n}\n"],"mappings":"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"}
|
|
@@ -1 +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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"}
|
|
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\n/**\n * A cipher for a URL scheme other than `aes256gcm:` — `kms:` on AWS.\n *\n * Registered by the entry point a build generates, and only for a target\n * whose keys live there, so a process that never holds such a key never\n * imports the SDK it would need.\n */\nexport interface CipherDriver {\n\t/** The scheme it opens, with its colon: `'kms:'`. */\n\treadonly scheme: string;\n\topen(construct: string, url: string): Cipher;\n}\n"],"mappings":"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"}
|
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
//#region src/encryption/cipher.d.ts
|
|
2
|
+
/**
|
|
3
|
+
* The cipher behind an `Encryption` construct, and the format it writes.
|
|
4
|
+
*
|
|
5
|
+
* Every ciphertext names the key that wrote it — `gkm1.<keyId>.<payload>` — so
|
|
6
|
+
* `decrypt` never has to guess, a rotated key still opens what it wrote, and
|
|
7
|
+
* retiring a key is a decision made against data rather than a hope. The
|
|
8
|
+
* format version leads, so the day it has to change, both can be read.
|
|
9
|
+
*
|
|
10
|
+
* Two backends, picked by the scheme of the construct's URL, the way a queue's
|
|
11
|
+
* connection string picks its broker:
|
|
12
|
+
*
|
|
13
|
+
* - `aes256gcm://` — a keyring this process holds: AES-256-GCM with the newest
|
|
14
|
+
* key, HMAC-SHA256 for the blind index. Local, test and server stages.
|
|
15
|
+
* - `kms://` — envelope encryption under an AWS KMS key, and a KMS HMAC key for
|
|
16
|
+
* the index. The key material never leaves KMS. See `./kms`.
|
|
17
|
+
*/
|
|
18
|
+
/** What a handler is given: `services.pii.encrypt(…)` and the rest. */
|
|
19
|
+
interface Cipher {
|
|
20
|
+
/** Plaintext in, a self-describing ciphertext string out. */
|
|
21
|
+
encrypt(plaintext: string): Promise<string>;
|
|
22
|
+
/** Opens any ciphertext this construct wrote, under whichever key wrote it. */
|
|
23
|
+
decrypt(ciphertext: string): Promise<string>;
|
|
24
|
+
/**
|
|
25
|
+
* A blind index: the same value always gives the same string, so an
|
|
26
|
+
* encrypted column can still be looked up by an indexed column beside it.
|
|
27
|
+
*
|
|
28
|
+
* Deterministic on purpose, which is also why its key never rotates — a
|
|
29
|
+
* rotated index key would make every stored index miss.
|
|
30
|
+
*/
|
|
31
|
+
index(value: string): Promise<string>;
|
|
32
|
+
/**
|
|
33
|
+
* The same plaintext under the current key — what a sweep runs over a
|
|
34
|
+
* column before an old key is retired. Unchanged when it is already current.
|
|
35
|
+
*/
|
|
36
|
+
reencrypt(ciphertext: string): Promise<string>;
|
|
37
|
+
}
|
|
38
|
+
declare class EncryptionUrlInvalid extends Error {
|
|
39
|
+
readonly construct: string;
|
|
40
|
+
readonly reason: string;
|
|
41
|
+
constructor(construct: string, reason: string);
|
|
42
|
+
}
|
|
43
|
+
declare class UnknownEncryptionScheme extends Error {
|
|
44
|
+
readonly construct: string;
|
|
45
|
+
readonly scheme: string;
|
|
46
|
+
constructor(construct: string, scheme: string);
|
|
47
|
+
}
|
|
48
|
+
declare class CiphertextMalformed extends Error {
|
|
49
|
+
readonly construct: string;
|
|
50
|
+
constructor(construct: string);
|
|
51
|
+
}
|
|
52
|
+
declare class EncryptionKeyRetired extends Error {
|
|
53
|
+
readonly construct: string;
|
|
54
|
+
readonly keyId: string;
|
|
55
|
+
constructor(construct: string, keyId: string);
|
|
56
|
+
}
|
|
57
|
+
declare class CiphertextNotAuthentic extends Error {
|
|
58
|
+
readonly construct: string;
|
|
59
|
+
constructor(construct: string);
|
|
60
|
+
}
|
|
61
|
+
declare class CurrentKeyCannotRetire extends Error {
|
|
62
|
+
readonly construct: string;
|
|
63
|
+
readonly keyId: string;
|
|
64
|
+
constructor(construct: string, keyId: string);
|
|
65
|
+
}
|
|
66
|
+
/** A local keyring: newest key first, and the index key that never rotates. */
|
|
67
|
+
interface Keyring {
|
|
68
|
+
keys: {
|
|
69
|
+
id: string;
|
|
70
|
+
key: Buffer;
|
|
71
|
+
}[];
|
|
72
|
+
index: Buffer;
|
|
73
|
+
}
|
|
74
|
+
/** Parse `aes256gcm://local?keys=k2.<key>,k1.<key>&index=<key>`. */
|
|
75
|
+
declare function parseKeyring(construct: string, url: string): Keyring;
|
|
76
|
+
declare function formatKeyring(keyring: Keyring): string;
|
|
77
|
+
/** A keyring with one fresh key, `k1`, and a fresh index key. */
|
|
78
|
+
declare function generateKeyring(): string;
|
|
79
|
+
/**
|
|
80
|
+
* The keyring with a new current key in front, and every older one kept.
|
|
81
|
+
*
|
|
82
|
+
* Nothing is dropped: what the old keys wrote still has to open until a sweep
|
|
83
|
+
* has re-encrypted it — see {@link retireKey}.
|
|
84
|
+
*/
|
|
85
|
+
declare function rotateKeyring(construct: string, url: string): string;
|
|
86
|
+
/** The keyring without `keyId`, which must not be the current key. */
|
|
87
|
+
declare function retireKey(construct: string, url: string, keyId: string): string;
|
|
88
|
+
/** Says, once per key per process, that a value is still under an old key. */
|
|
89
|
+
type StaleKeyReporter = (keyId: string, current: string) => void;
|
|
90
|
+
/** The `aes256gcm://` cipher: a keyring held in process. */
|
|
91
|
+
declare function keyringCipher(construct: string, url: string, onStaleKey?: StaleKeyReporter): Cipher;
|
|
92
|
+
/**
|
|
93
|
+
* A cipher for a URL scheme other than `aes256gcm:` — `kms:` on AWS.
|
|
94
|
+
*
|
|
95
|
+
* Registered by the entry point a build generates, and only for a target
|
|
96
|
+
* whose keys live there, so a process that never holds such a key never
|
|
97
|
+
* imports the SDK it would need.
|
|
98
|
+
*/
|
|
99
|
+
interface CipherDriver {
|
|
100
|
+
/** The scheme it opens, with its colon: `'kms:'`. */
|
|
101
|
+
readonly scheme: string;
|
|
102
|
+
open(construct: string, url: string): Cipher;
|
|
103
|
+
}
|
|
104
|
+
//#endregion
|
|
105
|
+
export { CurrentKeyCannotRetire as a, Keyring as c, generateKeyring as d, keyringCipher as f, rotateKeyring as h, CiphertextNotAuthentic as i, UnknownEncryptionScheme as l, retireKey as m, CipherDriver as n, EncryptionKeyRetired as o, parseKeyring as p, CiphertextMalformed as r, EncryptionUrlInvalid as s, Cipher as t, formatKeyring as u };
|
|
106
|
+
//# sourceMappingURL=cipher-DSDxPRIp.d.cts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"cipher-DSDxPRIp.d.cts","names":[],"sources":["../src/encryption/cipher.ts"],"mappings":";;;;;;;;;;;;;;;;;;UAyBiB;;EAEhB,QAAQ,oBAAoB;;EAE5B,QAAQ,qBAAqB;;;;;;;;EAQ7B,MAAM,gBAAgB;;;;;EAKtB,UAAU,qBAAqB;;cAanB,6BAA6B;WAE/B;WACA;EAFV,YACU,mBACA;;cASE,gCAAgC;WAElC;WACA;EAFV,YACU,mBACA;;cASE,4BAA4B;WACnB;EAArB,YAAqB;;cAQT,6BAA6B;WAE/B;WACA;EAFV,YACU,mBACA;;cASE,+BAA+B;WACtB;EAArB,YAAqB;;cAQT,+BAA+B;WAEjC;WACA;EAFV,YACU,mBACA;;;UAuEM;EAChB;IAAQ;IAAY,KAAK;;EACzB,OAAO;;;iBAIQ,aAAa,mBAAmB,cAAc;iBAqC9C,cAAc,SAAS;;iBAQvB;;;;;;;iBAaA,cAAc,mBAAmB;;iBAYjC,UACf,mBACA,aACA;;KAiBW,oBAAoB,eAAe;;iBAG/B,cACf,mBACA,aACA,aAAY,mBACV;;;;;;;;UAsDc;;WAEP;EACT,KAAK,mBAAmB,cAAc"}
|
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
//#region src/encryption/cipher.d.ts
|
|
2
|
+
/**
|
|
3
|
+
* The cipher behind an `Encryption` construct, and the format it writes.
|
|
4
|
+
*
|
|
5
|
+
* Every ciphertext names the key that wrote it — `gkm1.<keyId>.<payload>` — so
|
|
6
|
+
* `decrypt` never has to guess, a rotated key still opens what it wrote, and
|
|
7
|
+
* retiring a key is a decision made against data rather than a hope. The
|
|
8
|
+
* format version leads, so the day it has to change, both can be read.
|
|
9
|
+
*
|
|
10
|
+
* Two backends, picked by the scheme of the construct's URL, the way a queue's
|
|
11
|
+
* connection string picks its broker:
|
|
12
|
+
*
|
|
13
|
+
* - `aes256gcm://` — a keyring this process holds: AES-256-GCM with the newest
|
|
14
|
+
* key, HMAC-SHA256 for the blind index. Local, test and server stages.
|
|
15
|
+
* - `kms://` — envelope encryption under an AWS KMS key, and a KMS HMAC key for
|
|
16
|
+
* the index. The key material never leaves KMS. See `./kms`.
|
|
17
|
+
*/
|
|
18
|
+
/** What a handler is given: `services.pii.encrypt(…)` and the rest. */
|
|
19
|
+
interface Cipher {
|
|
20
|
+
/** Plaintext in, a self-describing ciphertext string out. */
|
|
21
|
+
encrypt(plaintext: string): Promise<string>;
|
|
22
|
+
/** Opens any ciphertext this construct wrote, under whichever key wrote it. */
|
|
23
|
+
decrypt(ciphertext: string): Promise<string>;
|
|
24
|
+
/**
|
|
25
|
+
* A blind index: the same value always gives the same string, so an
|
|
26
|
+
* encrypted column can still be looked up by an indexed column beside it.
|
|
27
|
+
*
|
|
28
|
+
* Deterministic on purpose, which is also why its key never rotates — a
|
|
29
|
+
* rotated index key would make every stored index miss.
|
|
30
|
+
*/
|
|
31
|
+
index(value: string): Promise<string>;
|
|
32
|
+
/**
|
|
33
|
+
* The same plaintext under the current key — what a sweep runs over a
|
|
34
|
+
* column before an old key is retired. Unchanged when it is already current.
|
|
35
|
+
*/
|
|
36
|
+
reencrypt(ciphertext: string): Promise<string>;
|
|
37
|
+
}
|
|
38
|
+
declare class EncryptionUrlInvalid extends Error {
|
|
39
|
+
readonly construct: string;
|
|
40
|
+
readonly reason: string;
|
|
41
|
+
constructor(construct: string, reason: string);
|
|
42
|
+
}
|
|
43
|
+
declare class UnknownEncryptionScheme extends Error {
|
|
44
|
+
readonly construct: string;
|
|
45
|
+
readonly scheme: string;
|
|
46
|
+
constructor(construct: string, scheme: string);
|
|
47
|
+
}
|
|
48
|
+
declare class CiphertextMalformed extends Error {
|
|
49
|
+
readonly construct: string;
|
|
50
|
+
constructor(construct: string);
|
|
51
|
+
}
|
|
52
|
+
declare class EncryptionKeyRetired extends Error {
|
|
53
|
+
readonly construct: string;
|
|
54
|
+
readonly keyId: string;
|
|
55
|
+
constructor(construct: string, keyId: string);
|
|
56
|
+
}
|
|
57
|
+
declare class CiphertextNotAuthentic extends Error {
|
|
58
|
+
readonly construct: string;
|
|
59
|
+
constructor(construct: string);
|
|
60
|
+
}
|
|
61
|
+
declare class CurrentKeyCannotRetire extends Error {
|
|
62
|
+
readonly construct: string;
|
|
63
|
+
readonly keyId: string;
|
|
64
|
+
constructor(construct: string, keyId: string);
|
|
65
|
+
}
|
|
66
|
+
/** A local keyring: newest key first, and the index key that never rotates. */
|
|
67
|
+
interface Keyring {
|
|
68
|
+
keys: {
|
|
69
|
+
id: string;
|
|
70
|
+
key: Buffer;
|
|
71
|
+
}[];
|
|
72
|
+
index: Buffer;
|
|
73
|
+
}
|
|
74
|
+
/** Parse `aes256gcm://local?keys=k2.<key>,k1.<key>&index=<key>`. */
|
|
75
|
+
declare function parseKeyring(construct: string, url: string): Keyring;
|
|
76
|
+
declare function formatKeyring(keyring: Keyring): string;
|
|
77
|
+
/** A keyring with one fresh key, `k1`, and a fresh index key. */
|
|
78
|
+
declare function generateKeyring(): string;
|
|
79
|
+
/**
|
|
80
|
+
* The keyring with a new current key in front, and every older one kept.
|
|
81
|
+
*
|
|
82
|
+
* Nothing is dropped: what the old keys wrote still has to open until a sweep
|
|
83
|
+
* has re-encrypted it — see {@link retireKey}.
|
|
84
|
+
*/
|
|
85
|
+
declare function rotateKeyring(construct: string, url: string): string;
|
|
86
|
+
/** The keyring without `keyId`, which must not be the current key. */
|
|
87
|
+
declare function retireKey(construct: string, url: string, keyId: string): string;
|
|
88
|
+
/** Says, once per key per process, that a value is still under an old key. */
|
|
89
|
+
type StaleKeyReporter = (keyId: string, current: string) => void;
|
|
90
|
+
/** The `aes256gcm://` cipher: a keyring held in process. */
|
|
91
|
+
declare function keyringCipher(construct: string, url: string, onStaleKey?: StaleKeyReporter): Cipher;
|
|
92
|
+
/**
|
|
93
|
+
* A cipher for a URL scheme other than `aes256gcm:` — `kms:` on AWS.
|
|
94
|
+
*
|
|
95
|
+
* Registered by the entry point a build generates, and only for a target
|
|
96
|
+
* whose keys live there, so a process that never holds such a key never
|
|
97
|
+
* imports the SDK it would need.
|
|
98
|
+
*/
|
|
99
|
+
interface CipherDriver {
|
|
100
|
+
/** The scheme it opens, with its colon: `'kms:'`. */
|
|
101
|
+
readonly scheme: string;
|
|
102
|
+
open(construct: string, url: string): Cipher;
|
|
103
|
+
}
|
|
104
|
+
//#endregion
|
|
105
|
+
export { CurrentKeyCannotRetire as a, Keyring as c, generateKeyring as d, keyringCipher as f, rotateKeyring as h, CiphertextNotAuthentic as i, UnknownEncryptionScheme as l, retireKey as m, CipherDriver as n, EncryptionKeyRetired as o, parseKeyring as p, CiphertextMalformed as r, EncryptionUrlInvalid as s, Cipher as t, formatKeyring as u };
|
|
106
|
+
//# sourceMappingURL=cipher-DSDxPRIp.d.mts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"cipher-DSDxPRIp.d.mts","names":[],"sources":["../src/encryption/cipher.ts"],"mappings":";;;;;;;;;;;;;;;;;;UAyBiB;;EAEhB,QAAQ,oBAAoB;;EAE5B,QAAQ,qBAAqB;;;;;;;;EAQ7B,MAAM,gBAAgB;;;;;EAKtB,UAAU,qBAAqB;;cAanB,6BAA6B;WAE/B;WACA;EAFV,YACU,mBACA;;cASE,gCAAgC;WAElC;WACA;EAFV,YACU,mBACA;;cASE,4BAA4B;WACnB;EAArB,YAAqB;;cAQT,6BAA6B;WAE/B;WACA;EAFV,YACU,mBACA;;cASE,+BAA+B;WACtB;EAArB,YAAqB;;cAQT,+BAA+B;WAEjC;WACA;EAFV,YACU,mBACA;;;UAuEM;EAChB;IAAQ;IAAY,KAAK;;EACzB,OAAO;;;iBAIQ,aAAa,mBAAmB,cAAc;iBAqC9C,cAAc,SAAS;;iBAQvB;;;;;;;iBAaA,cAAc,mBAAmB;;iBAYjC,UACf,mBACA,aACA;;KAiBW,oBAAoB,eAAe;;iBAG/B,cACf,mBACA,aACA,aAAY,mBACV;;;;;;;;UAsDc;;WAEP;EACT,KAAK,mBAAmB,cAAc"}
|
package/dist/database/kysely.cjs
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
1
|
Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
|
|
2
|
-
const require_kysely = require("../kysely-
|
|
2
|
+
const require_kysely = require("../kysely-CukArHJ0.cjs");
|
|
3
3
|
exports.KyselyDatabase = require_kysely.KyselyDatabase;
|
|
4
4
|
exports.closeDatabasePools = require_kysely.closeDatabasePools;
|
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
import {
|
|
2
|
-
export { KyselyDatabase, KyselyDatabaseOptions, ReadOnlyDatabase, closeDatabasePools };
|
|
1
|
+
import { a as closeDatabasePools, i as ReadOnlyDatabase, n as KyselyDatabaseOptions, r as PostgresRuntime, t as KyselyDatabase } from "../kysely-BHYV-WC-.cjs";
|
|
2
|
+
export { KyselyDatabase, KyselyDatabaseOptions, PostgresRuntime, ReadOnlyDatabase, closeDatabasePools };
|
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
import {
|
|
2
|
-
export { KyselyDatabase, KyselyDatabaseOptions, ReadOnlyDatabase, closeDatabasePools };
|
|
1
|
+
import { a as closeDatabasePools, i as ReadOnlyDatabase, n as KyselyDatabaseOptions, r as PostgresRuntime, t as KyselyDatabase } from "../kysely-CAW3QSqp.mjs";
|
|
2
|
+
export { KyselyDatabase, KyselyDatabaseOptions, PostgresRuntime, ReadOnlyDatabase, closeDatabasePools };
|
package/dist/database/kysely.mjs
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
import { n as closeDatabasePools, t as KyselyDatabase } from "../kysely-
|
|
1
|
+
import { n as closeDatabasePools, t as KyselyDatabase } from "../kysely-Cs4uPO8k.mjs";
|
|
2
2
|
export { KyselyDatabase, closeDatabasePools };
|
|
@@ -1,4 +1,6 @@
|
|
|
1
|
-
|
|
1
|
+
Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
|
|
2
|
+
const require_cipher = require("../cipher-CVxfwLjk.cjs");
|
|
3
|
+
let _geekmidas_manifest = require("@geekmidas/manifest");
|
|
2
4
|
let _aws_sdk_client_kms = require("@aws-sdk/client-kms");
|
|
3
5
|
//#region src/encryption/kms.ts
|
|
4
6
|
/**
|
|
@@ -17,8 +19,10 @@ let _aws_sdk_client_kms = require("@aws-sdk/client-kms");
|
|
|
17
19
|
* The blind index is a KMS HMAC key's `GenerateMac`: deterministic, and the
|
|
18
20
|
* key is as unextractable as the encryption key.
|
|
19
21
|
*
|
|
20
|
-
* In its own
|
|
21
|
-
*
|
|
22
|
+
* In its own entry, `@geekmidas/constructs/encryption/kms`, which only an
|
|
23
|
+
* AWS build's generated entry imports — to register {@link kmsCipherDriver}
|
|
24
|
+
* — so a project that never deploys to AWS never needs
|
|
25
|
+
* `@aws-sdk/client-kms` installed, and its bundle never mentions it.
|
|
22
26
|
*/
|
|
23
27
|
/** The ciphertext's key field: the version lives inside KMS's own blob. */
|
|
24
28
|
const KMS_KEY_ID = "kms";
|
|
@@ -79,7 +83,13 @@ function kmsCipher(construct, url) {
|
|
|
79
83
|
}
|
|
80
84
|
};
|
|
81
85
|
}
|
|
86
|
+
/** The `kms:` cipher, as an AWS build's entry registers it. */
|
|
87
|
+
const kmsCipherDriver = {
|
|
88
|
+
scheme: _geekmidas_manifest.KMS_SCHEME,
|
|
89
|
+
open: kmsCipher
|
|
90
|
+
};
|
|
82
91
|
//#endregion
|
|
83
92
|
exports.kmsCipher = kmsCipher;
|
|
93
|
+
exports.kmsCipherDriver = kmsCipherDriver;
|
|
84
94
|
|
|
85
|
-
//# sourceMappingURL=kms
|
|
95
|
+
//# sourceMappingURL=kms.cjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"kms.cjs","names":["EncryptionUrlInvalid","KMSClient","associatedData","GenerateDataKeyCommand","seal","formatCiphertext","parseCiphertext","CiphertextNotAuthentic","DecryptCommand","open","GenerateMacCommand","KMS_SCHEME"],"sources":["../../src/encryption/kms.ts"],"sourcesContent":["/**\n * The `kms://` cipher: envelope encryption under an AWS KMS key.\n *\n * Each `encrypt` asks KMS for a fresh data key, seals the plaintext with it\n * locally, and stores the data key *as KMS encrypted it* beside the data. The\n * plaintext data key is used once and dropped, and the master key never leaves\n * KMS — every decrypt is a KMS call IAM can refuse and CloudTrail records.\n *\n * Rotation is KMS's: it keeps every backing version of the key, and the\n * encrypted data key names the one that wrapped it, so nothing here tracks a\n * version and `reencrypt` has nothing to move. The ciphertext's key field is\n * `kms` for that reason.\n *\n * The blind index is a KMS HMAC key's `GenerateMac`: deterministic, and the\n * key is as unextractable as the encryption key.\n *\n * In its own entry, `@geekmidas/constructs/encryption/kms`, which only an\n * AWS build's generated entry imports — to register {@link kmsCipherDriver}\n * — so a project that never deploys to AWS never needs\n * `@aws-sdk/client-kms` installed, and its bundle never mentions it.\n */\n\nimport {\n\tDecryptCommand,\n\tGenerateDataKeyCommand,\n\tGenerateMacCommand,\n\tKMSClient,\n} from '@aws-sdk/client-kms';\nimport { KMS_SCHEME } from '@geekmidas/manifest';\nimport {\n\tassociatedData,\n\ttype Cipher,\n\ttype CipherDriver,\n\tCiphertextNotAuthentic,\n\tEncryptionUrlInvalid,\n\tformatCiphertext,\n\topen,\n\tparseCiphertext,\n\tseal,\n} from './cipher';\n\n/** The ciphertext's key field: the version lives inside KMS's own blob. */\nconst KMS_KEY_ID = 'kms';\n\nexport function kmsCipher(construct: string, url: string): Cipher {\n\tconst parsed = new URL(url);\n\tconst region = parsed.hostname;\n\tconst keyId = parsed.searchParams.get('key');\n\tconst indexKeyId = parsed.searchParams.get('index');\n\tif (!region || !keyId || !indexKeyId) {\n\t\tthrow new EncryptionUrlInvalid(\n\t\t\tconstruct,\n\t\t\t'a kms:// URL names a region, a key and an index key',\n\t\t);\n\t}\n\n\tconst endpoint = parsed.searchParams.get('endpoint');\n\t// Credentials are not part of the address: the default chain — a Lambda's\n\t// role deployed, the environment against an emulator.\n\tconst kms = new KMSClient({ region, ...(endpoint ? { endpoint } : {}) });\n\t// What KMS checks on every decrypt, and what CloudTrail shows each call was\n\t// for — a data key minted for one construct will not open for another.\n\tconst context = { construct };\n\tconst aad = associatedData(construct, KMS_KEY_ID);\n\n\treturn {\n\t\tasync encrypt(plaintext) {\n\t\t\tconst dataKey = await kms.send(\n\t\t\t\tnew GenerateDataKeyCommand({\n\t\t\t\t\tKeyId: keyId,\n\t\t\t\t\tKeySpec: 'AES_256',\n\t\t\t\t\tEncryptionContext: context,\n\t\t\t\t}),\n\t\t\t);\n\t\t\tconst wrapped = Buffer.from(dataKey.CiphertextBlob!);\n\t\t\tconst sealed = seal(\n\t\t\t\tBuffer.from(dataKey.Plaintext!),\n\t\t\t\tBuffer.from(plaintext, 'utf8'),\n\t\t\t\taad,\n\t\t\t);\n\n\t\t\tconst length = Buffer.alloc(2);\n\t\t\tlength.writeUInt16BE(wrapped.length);\n\t\t\treturn formatCiphertext(\n\t\t\t\tKMS_KEY_ID,\n\t\t\t\tBuffer.concat([length, wrapped, sealed]),\n\t\t\t);\n\t\t},\n\n\t\tasync decrypt(ciphertext) {\n\t\t\tconst { payload } = parseCiphertext(construct, ciphertext);\n\t\t\tconst length = payload.length >= 2 ? payload.readUInt16BE(0) : 0;\n\t\t\tconst wrapped = payload.subarray(2, 2 + length);\n\t\t\tif (!length || wrapped.length !== length) {\n\t\t\t\tthrow new CiphertextNotAuthentic(construct);\n\t\t\t}\n\n\t\t\tconst dataKey = await kms.send(\n\t\t\t\tnew DecryptCommand({\n\t\t\t\t\tCiphertextBlob: wrapped,\n\t\t\t\t\tEncryptionContext: context,\n\t\t\t\t}),\n\t\t\t);\n\t\t\tconst plaintext = open(\n\t\t\t\tBuffer.from(dataKey.Plaintext!),\n\t\t\t\tpayload.subarray(2 + length),\n\t\t\t\taad,\n\t\t\t);\n\t\t\tif (!plaintext) throw new CiphertextNotAuthentic(construct);\n\n\t\t\treturn plaintext.toString('utf8');\n\t\t},\n\n\t\tasync index(value) {\n\t\t\tconst { Mac } = await kms.send(\n\t\t\t\tnew GenerateMacCommand({\n\t\t\t\t\tKeyId: indexKeyId,\n\t\t\t\t\tMacAlgorithm: 'HMAC_SHA_256',\n\t\t\t\t\tMessage: Buffer.from(value, 'utf8'),\n\t\t\t\t}),\n\t\t\t);\n\t\t\treturn Buffer.from(Mac!).toString('base64url');\n\t\t},\n\n\t\t// KMS keeps every version it rotated through, so a value is never under\n\t\t// a key that could be retired from under it.\n\t\tasync reencrypt(ciphertext) {\n\t\t\tparseCiphertext(construct, ciphertext);\n\t\t\treturn ciphertext;\n\t\t},\n\t};\n}\n\n/** The `kms:` cipher, as an AWS build's entry registers it. */\nexport const kmsCipherDriver: CipherDriver = {\n\tscheme: KMS_SCHEME,\n\topen: kmsCipher,\n};\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;AA0CA,MAAM,aAAa;AAEnB,SAAgB,UAAU,WAAmB,KAAqB;CACjE,MAAM,SAAS,IAAI,IAAI,GAAG;CAC1B,MAAM,SAAS,OAAO;CACtB,MAAM,QAAQ,OAAO,aAAa,IAAI,KAAK;CAC3C,MAAM,aAAa,OAAO,aAAa,IAAI,OAAO;CAClD,IAAI,CAAC,UAAU,CAAC,SAAS,CAAC,YACzB,MAAM,IAAIA,eAAAA,qBACT,WACA,qDACD;CAGD,MAAM,WAAW,OAAO,aAAa,IAAI,UAAU;CAGnD,MAAM,MAAM,IAAIC,oBAAAA,UAAU;EAAE;EAAQ,GAAI,WAAW,EAAE,SAAS,IAAI,CAAC;CAAG,CAAC;CAGvE,MAAM,UAAU,EAAE,UAAU;CAC5B,MAAM,MAAMC,eAAAA,eAAe,WAAW,UAAU;CAEhD,OAAO;EACN,MAAM,QAAQ,WAAW;GACxB,MAAM,UAAU,MAAM,IAAI,KACzB,IAAIC,oBAAAA,uBAAuB;IAC1B,OAAO;IACP,SAAS;IACT,mBAAmB;GACpB,CAAC,CACF;GACA,MAAM,UAAU,OAAO,KAAK,QAAQ,cAAe;GACnD,MAAM,SAASC,eAAAA,KACd,OAAO,KAAK,QAAQ,SAAU,GAC9B,OAAO,KAAK,WAAW,MAAM,GAC7B,GACD;GAEA,MAAM,SAAS,OAAO,MAAM,CAAC;GAC7B,OAAO,cAAc,QAAQ,MAAM;GACnC,OAAOC,eAAAA,iBACN,YACA,OAAO,OAAO;IAAC;IAAQ;IAAS;GAAM,CAAC,CACxC;EACD;EAEA,MAAM,QAAQ,YAAY;GACzB,MAAM,EAAE,YAAYC,eAAAA,gBAAgB,WAAW,UAAU;GACzD,MAAM,SAAS,QAAQ,UAAU,IAAI,QAAQ,aAAa,CAAC,IAAI;GAC/D,MAAM,UAAU,QAAQ,SAAS,GAAG,IAAI,MAAM;GAC9C,IAAI,CAAC,UAAU,QAAQ,WAAW,QACjC,MAAM,IAAIC,eAAAA,uBAAuB,SAAS;GAG3C,MAAM,UAAU,MAAM,IAAI,KACzB,IAAIC,oBAAAA,eAAe;IAClB,gBAAgB;IAChB,mBAAmB;GACpB,CAAC,CACF;GACA,MAAM,YAAYC,eAAAA,KACjB,OAAO,KAAK,QAAQ,SAAU,GAC9B,QAAQ,SAAS,IAAI,MAAM,GAC3B,GACD;GACA,IAAI,CAAC,WAAW,MAAM,IAAIF,eAAAA,uBAAuB,SAAS;GAE1D,OAAO,UAAU,SAAS,MAAM;EACjC;EAEA,MAAM,MAAM,OAAO;GAClB,MAAM,EAAE,QAAQ,MAAM,IAAI,KACzB,IAAIG,oBAAAA,mBAAmB;IACtB,OAAO;IACP,cAAc;IACd,SAAS,OAAO,KAAK,OAAO,MAAM;GACnC,CAAC,CACF;GACA,OAAO,OAAO,KAAK,GAAI,CAAC,CAAC,SAAS,WAAW;EAC9C;EAIA,MAAM,UAAU,YAAY;GAC3B,eAAA,gBAAgB,WAAW,UAAU;GACrC,OAAO;EACR;CACD;AACD;;AAGA,MAAa,kBAAgC;CAC5C,QAAQC,oBAAAA;CACR,MAAM;AACP"}
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import { n as CipherDriver, t as Cipher } from "../cipher-DSDxPRIp.cjs";
|
|
2
|
+
//#region src/encryption/kms.d.ts
|
|
3
|
+
export declare function kmsCipher(construct: string, url: string): Cipher;
|
|
4
|
+
/** The `kms:` cipher, as an AWS build's entry registers it. */
|
|
5
|
+
export declare const kmsCipherDriver: CipherDriver;
|
|
6
|
+
//#endregion
|
|
7
|
+
//# sourceMappingURL=kms.d.cts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"kms.d.cts","names":[],"sources":["../../src/encryption/kms.ts"],"mappings":";;wBA4CgB,UAAU,mBAAmB,cAAc;;qBA0F9C,iBAAiB"}
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import { n as CipherDriver, t as Cipher } from "../cipher-DSDxPRIp.mjs";
|
|
2
|
+
//#region src/encryption/kms.d.ts
|
|
3
|
+
export declare function kmsCipher(construct: string, url: string): Cipher;
|
|
4
|
+
/** The `kms:` cipher, as an AWS build's entry registers it. */
|
|
5
|
+
export declare const kmsCipherDriver: CipherDriver;
|
|
6
|
+
//#endregion
|
|
7
|
+
//# sourceMappingURL=kms.d.mts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"kms.d.mts","names":[],"sources":["../../src/encryption/kms.ts"],"mappings":";;wBA4CgB,UAAU,mBAAmB,cAAc;;qBA0F9C,iBAAiB"}
|
|
@@ -1,4 +1,5 @@
|
|
|
1
|
-
import { a as EncryptionUrlInvalid, c as associatedData, l as formatCiphertext, m as parseCiphertext, n as CiphertextNotAuthentic, p as open, v as seal } from "
|
|
1
|
+
import { a as EncryptionUrlInvalid, c as associatedData, l as formatCiphertext, m as parseCiphertext, n as CiphertextNotAuthentic, p as open, v as seal } from "../cipher-C1lEFX63.mjs";
|
|
2
|
+
import { KMS_SCHEME } from "@geekmidas/manifest";
|
|
2
3
|
import { DecryptCommand, GenerateDataKeyCommand, GenerateMacCommand, KMSClient } from "@aws-sdk/client-kms";
|
|
3
4
|
//#region src/encryption/kms.ts
|
|
4
5
|
/**
|
|
@@ -17,8 +18,10 @@ import { DecryptCommand, GenerateDataKeyCommand, GenerateMacCommand, KMSClient }
|
|
|
17
18
|
* The blind index is a KMS HMAC key's `GenerateMac`: deterministic, and the
|
|
18
19
|
* key is as unextractable as the encryption key.
|
|
19
20
|
*
|
|
20
|
-
* In its own
|
|
21
|
-
*
|
|
21
|
+
* In its own entry, `@geekmidas/constructs/encryption/kms`, which only an
|
|
22
|
+
* AWS build's generated entry imports — to register {@link kmsCipherDriver}
|
|
23
|
+
* — so a project that never deploys to AWS never needs
|
|
24
|
+
* `@aws-sdk/client-kms` installed, and its bundle never mentions it.
|
|
22
25
|
*/
|
|
23
26
|
/** The ciphertext's key field: the version lives inside KMS's own blob. */
|
|
24
27
|
const KMS_KEY_ID = "kms";
|
|
@@ -79,7 +82,12 @@ function kmsCipher(construct, url) {
|
|
|
79
82
|
}
|
|
80
83
|
};
|
|
81
84
|
}
|
|
85
|
+
/** The `kms:` cipher, as an AWS build's entry registers it. */
|
|
86
|
+
const kmsCipherDriver = {
|
|
87
|
+
scheme: KMS_SCHEME,
|
|
88
|
+
open: kmsCipher
|
|
89
|
+
};
|
|
82
90
|
//#endregion
|
|
83
|
-
export { kmsCipher };
|
|
91
|
+
export { kmsCipher, kmsCipherDriver };
|
|
84
92
|
|
|
85
|
-
//# sourceMappingURL=kms
|
|
93
|
+
//# sourceMappingURL=kms.mjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"kms.mjs","names":[],"sources":["../../src/encryption/kms.ts"],"sourcesContent":["/**\n * The `kms://` cipher: envelope encryption under an AWS KMS key.\n *\n * Each `encrypt` asks KMS for a fresh data key, seals the plaintext with it\n * locally, and stores the data key *as KMS encrypted it* beside the data. The\n * plaintext data key is used once and dropped, and the master key never leaves\n * KMS — every decrypt is a KMS call IAM can refuse and CloudTrail records.\n *\n * Rotation is KMS's: it keeps every backing version of the key, and the\n * encrypted data key names the one that wrapped it, so nothing here tracks a\n * version and `reencrypt` has nothing to move. The ciphertext's key field is\n * `kms` for that reason.\n *\n * The blind index is a KMS HMAC key's `GenerateMac`: deterministic, and the\n * key is as unextractable as the encryption key.\n *\n * In its own entry, `@geekmidas/constructs/encryption/kms`, which only an\n * AWS build's generated entry imports — to register {@link kmsCipherDriver}\n * — so a project that never deploys to AWS never needs\n * `@aws-sdk/client-kms` installed, and its bundle never mentions it.\n */\n\nimport {\n\tDecryptCommand,\n\tGenerateDataKeyCommand,\n\tGenerateMacCommand,\n\tKMSClient,\n} from '@aws-sdk/client-kms';\nimport { KMS_SCHEME } from '@geekmidas/manifest';\nimport {\n\tassociatedData,\n\ttype Cipher,\n\ttype CipherDriver,\n\tCiphertextNotAuthentic,\n\tEncryptionUrlInvalid,\n\tformatCiphertext,\n\topen,\n\tparseCiphertext,\n\tseal,\n} from './cipher';\n\n/** The ciphertext's key field: the version lives inside KMS's own blob. */\nconst KMS_KEY_ID = 'kms';\n\nexport function kmsCipher(construct: string, url: string): Cipher {\n\tconst parsed = new URL(url);\n\tconst region = parsed.hostname;\n\tconst keyId = parsed.searchParams.get('key');\n\tconst indexKeyId = parsed.searchParams.get('index');\n\tif (!region || !keyId || !indexKeyId) {\n\t\tthrow new EncryptionUrlInvalid(\n\t\t\tconstruct,\n\t\t\t'a kms:// URL names a region, a key and an index key',\n\t\t);\n\t}\n\n\tconst endpoint = parsed.searchParams.get('endpoint');\n\t// Credentials are not part of the address: the default chain — a Lambda's\n\t// role deployed, the environment against an emulator.\n\tconst kms = new KMSClient({ region, ...(endpoint ? { endpoint } : {}) });\n\t// What KMS checks on every decrypt, and what CloudTrail shows each call was\n\t// for — a data key minted for one construct will not open for another.\n\tconst context = { construct };\n\tconst aad = associatedData(construct, KMS_KEY_ID);\n\n\treturn {\n\t\tasync encrypt(plaintext) {\n\t\t\tconst dataKey = await kms.send(\n\t\t\t\tnew GenerateDataKeyCommand({\n\t\t\t\t\tKeyId: keyId,\n\t\t\t\t\tKeySpec: 'AES_256',\n\t\t\t\t\tEncryptionContext: context,\n\t\t\t\t}),\n\t\t\t);\n\t\t\tconst wrapped = Buffer.from(dataKey.CiphertextBlob!);\n\t\t\tconst sealed = seal(\n\t\t\t\tBuffer.from(dataKey.Plaintext!),\n\t\t\t\tBuffer.from(plaintext, 'utf8'),\n\t\t\t\taad,\n\t\t\t);\n\n\t\t\tconst length = Buffer.alloc(2);\n\t\t\tlength.writeUInt16BE(wrapped.length);\n\t\t\treturn formatCiphertext(\n\t\t\t\tKMS_KEY_ID,\n\t\t\t\tBuffer.concat([length, wrapped, sealed]),\n\t\t\t);\n\t\t},\n\n\t\tasync decrypt(ciphertext) {\n\t\t\tconst { payload } = parseCiphertext(construct, ciphertext);\n\t\t\tconst length = payload.length >= 2 ? payload.readUInt16BE(0) : 0;\n\t\t\tconst wrapped = payload.subarray(2, 2 + length);\n\t\t\tif (!length || wrapped.length !== length) {\n\t\t\t\tthrow new CiphertextNotAuthentic(construct);\n\t\t\t}\n\n\t\t\tconst dataKey = await kms.send(\n\t\t\t\tnew DecryptCommand({\n\t\t\t\t\tCiphertextBlob: wrapped,\n\t\t\t\t\tEncryptionContext: context,\n\t\t\t\t}),\n\t\t\t);\n\t\t\tconst plaintext = open(\n\t\t\t\tBuffer.from(dataKey.Plaintext!),\n\t\t\t\tpayload.subarray(2 + length),\n\t\t\t\taad,\n\t\t\t);\n\t\t\tif (!plaintext) throw new CiphertextNotAuthentic(construct);\n\n\t\t\treturn plaintext.toString('utf8');\n\t\t},\n\n\t\tasync index(value) {\n\t\t\tconst { Mac } = await kms.send(\n\t\t\t\tnew GenerateMacCommand({\n\t\t\t\t\tKeyId: indexKeyId,\n\t\t\t\t\tMacAlgorithm: 'HMAC_SHA_256',\n\t\t\t\t\tMessage: Buffer.from(value, 'utf8'),\n\t\t\t\t}),\n\t\t\t);\n\t\t\treturn Buffer.from(Mac!).toString('base64url');\n\t\t},\n\n\t\t// KMS keeps every version it rotated through, so a value is never under\n\t\t// a key that could be retired from under it.\n\t\tasync reencrypt(ciphertext) {\n\t\t\tparseCiphertext(construct, ciphertext);\n\t\t\treturn ciphertext;\n\t\t},\n\t};\n}\n\n/** The `kms:` cipher, as an AWS build's entry registers it. */\nexport const kmsCipherDriver: CipherDriver = {\n\tscheme: KMS_SCHEME,\n\topen: kmsCipher,\n};\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;AA0CA,MAAM,aAAa;AAEnB,SAAgB,UAAU,WAAmB,KAAqB;CACjE,MAAM,SAAS,IAAI,IAAI,GAAG;CAC1B,MAAM,SAAS,OAAO;CACtB,MAAM,QAAQ,OAAO,aAAa,IAAI,KAAK;CAC3C,MAAM,aAAa,OAAO,aAAa,IAAI,OAAO;CAClD,IAAI,CAAC,UAAU,CAAC,SAAS,CAAC,YACzB,MAAM,IAAI,qBACT,WACA,qDACD;CAGD,MAAM,WAAW,OAAO,aAAa,IAAI,UAAU;CAGnD,MAAM,MAAM,IAAI,UAAU;EAAE;EAAQ,GAAI,WAAW,EAAE,SAAS,IAAI,CAAC;CAAG,CAAC;CAGvE,MAAM,UAAU,EAAE,UAAU;CAC5B,MAAM,MAAM,eAAe,WAAW,UAAU;CAEhD,OAAO;EACN,MAAM,QAAQ,WAAW;GACxB,MAAM,UAAU,MAAM,IAAI,KACzB,IAAI,uBAAuB;IAC1B,OAAO;IACP,SAAS;IACT,mBAAmB;GACpB,CAAC,CACF;GACA,MAAM,UAAU,OAAO,KAAK,QAAQ,cAAe;GACnD,MAAM,SAAS,KACd,OAAO,KAAK,QAAQ,SAAU,GAC9B,OAAO,KAAK,WAAW,MAAM,GAC7B,GACD;GAEA,MAAM,SAAS,OAAO,MAAM,CAAC;GAC7B,OAAO,cAAc,QAAQ,MAAM;GACnC,OAAO,iBACN,YACA,OAAO,OAAO;IAAC;IAAQ;IAAS;GAAM,CAAC,CACxC;EACD;EAEA,MAAM,QAAQ,YAAY;GACzB,MAAM,EAAE,YAAY,gBAAgB,WAAW,UAAU;GACzD,MAAM,SAAS,QAAQ,UAAU,IAAI,QAAQ,aAAa,CAAC,IAAI;GAC/D,MAAM,UAAU,QAAQ,SAAS,GAAG,IAAI,MAAM;GAC9C,IAAI,CAAC,UAAU,QAAQ,WAAW,QACjC,MAAM,IAAI,uBAAuB,SAAS;GAG3C,MAAM,UAAU,MAAM,IAAI,KACzB,IAAI,eAAe;IAClB,gBAAgB;IAChB,mBAAmB;GACpB,CAAC,CACF;GACA,MAAM,YAAY,KACjB,OAAO,KAAK,QAAQ,SAAU,GAC9B,QAAQ,SAAS,IAAI,MAAM,GAC3B,GACD;GACA,IAAI,CAAC,WAAW,MAAM,IAAI,uBAAuB,SAAS;GAE1D,OAAO,UAAU,SAAS,MAAM;EACjC;EAEA,MAAM,MAAM,OAAO;GAClB,MAAM,EAAE,QAAQ,MAAM,IAAI,KACzB,IAAI,mBAAmB;IACtB,OAAO;IACP,cAAc;IACd,SAAS,OAAO,KAAK,OAAO,MAAM;GACnC,CAAC,CACF;GACA,OAAO,OAAO,KAAK,GAAI,CAAC,CAAC,SAAS,WAAW;EAC9C;EAIA,MAAM,UAAU,YAAY;GAC3B,gBAAgB,WAAW,UAAU;GACrC,OAAO;EACR;CACD;AACD;;AAGA,MAAa,kBAAgC;CAC5C,QAAQ;CACR,MAAM;AACP"}
|