@noy-db/at-aws-kms 0.2.0-pre.16 → 0.2.0-pre.18
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/index.cjs +66 -0
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +41 -2
- package/dist/index.d.ts +41 -2
- package/dist/index.js +67 -1
- package/dist/index.js.map +1 -1
- package/package.json +5 -5
package/dist/index.cjs
CHANGED
|
@@ -20,9 +20,11 @@ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: tru
|
|
|
20
20
|
// src/index.ts
|
|
21
21
|
var index_exports = {};
|
|
22
22
|
__export(index_exports, {
|
|
23
|
+
awsKmsRecipientSealer: () => awsKmsRecipientSealer,
|
|
23
24
|
awsKmsSealingProvider: () => awsKmsSealingProvider
|
|
24
25
|
});
|
|
25
26
|
module.exports = __toCommonJS(index_exports);
|
|
27
|
+
var import_hub = require("@noy-db/hub");
|
|
26
28
|
var import_client_kms = require("@aws-sdk/client-kms");
|
|
27
29
|
function awsKmsSealingProvider(opts) {
|
|
28
30
|
const client = opts.client ?? new import_client_kms.KMSClient({});
|
|
@@ -46,8 +48,72 @@ function awsKmsSealingProvider(opts) {
|
|
|
46
48
|
}
|
|
47
49
|
};
|
|
48
50
|
}
|
|
51
|
+
var SUPPORTED_RSA_KEY_SPECS = /* @__PURE__ */ new Set(["RSA_2048", "RSA_3072", "RSA_4096"]);
|
|
52
|
+
function derSpkiToPem(der) {
|
|
53
|
+
let binary = "";
|
|
54
|
+
for (let i = 0; i < der.length; i++) binary += String.fromCharCode(der[i]);
|
|
55
|
+
const b64 = btoa(binary);
|
|
56
|
+
return "-----BEGIN PUBLIC KEY-----\n" + (b64.match(/.{1,64}/g) ?? []).join("\n") + "\n-----END PUBLIC KEY-----\n";
|
|
57
|
+
}
|
|
58
|
+
function awsKmsRecipientSealer(opts) {
|
|
59
|
+
const client = opts.client ?? new import_client_kms.KMSClient(opts.region ? { region: opts.region } : {});
|
|
60
|
+
const id = `aws-kms-recipient:${opts.keyId}`;
|
|
61
|
+
return {
|
|
62
|
+
id,
|
|
63
|
+
async publishRecipientHint() {
|
|
64
|
+
const out = await client.send(
|
|
65
|
+
new import_client_kms.GetPublicKeyCommand({ KeyId: opts.keyId })
|
|
66
|
+
);
|
|
67
|
+
if (out.KeyUsage !== "ENCRYPT_DECRYPT") {
|
|
68
|
+
throw new Error(
|
|
69
|
+
`@noy-db/at-aws-kms: awsKmsRecipientSealer requires an ENCRYPT_DECRYPT key, got KeyUsage='${String(out.KeyUsage)}' for ${opts.keyId}`
|
|
70
|
+
);
|
|
71
|
+
}
|
|
72
|
+
if (!out.KeySpec || !SUPPORTED_RSA_KEY_SPECS.has(out.KeySpec)) {
|
|
73
|
+
throw new Error(
|
|
74
|
+
`@noy-db/at-aws-kms: awsKmsRecipientSealer requires an RSA key (RSA_2048/3072/4096), got KeySpec='${String(out.KeySpec)}' for ${opts.keyId}`
|
|
75
|
+
);
|
|
76
|
+
}
|
|
77
|
+
const der = out.PublicKey;
|
|
78
|
+
if (!der) throw new Error("@noy-db/at-aws-kms: KMS GetPublicKey returned no PublicKey");
|
|
79
|
+
const derBytes = der instanceof Uint8Array ? der : new Uint8Array(der);
|
|
80
|
+
const publicKeyPem = derSpkiToPem(derBytes);
|
|
81
|
+
return { v: 1, pid: id, alg: "rsa-oaep-sha256", material: { publicKeyPem } };
|
|
82
|
+
},
|
|
83
|
+
async sealForRecipient(plaintext, hint) {
|
|
84
|
+
if (hint.v !== 1) {
|
|
85
|
+
throw new Error(`@noy-db/at-aws-kms: awsKmsRecipientSealer.sealForRecipient: unsupported hint.v ${String(hint.v)} (expected 1)`);
|
|
86
|
+
}
|
|
87
|
+
if (hint.alg !== "rsa-oaep-sha256") {
|
|
88
|
+
throw new Error(`@noy-db/at-aws-kms: awsKmsRecipientSealer.sealForRecipient: unsupported hint.alg '${String(hint.alg)}' (expected 'rsa-oaep-sha256')`);
|
|
89
|
+
}
|
|
90
|
+
const pem = hint.material["publicKeyPem"];
|
|
91
|
+
if (typeof pem !== "string") {
|
|
92
|
+
throw new Error("@noy-db/at-aws-kms: awsKmsRecipientSealer.sealForRecipient: hint.material.publicKeyPem missing or not a string");
|
|
93
|
+
}
|
|
94
|
+
return (0, import_hub.sealRsaOaepTlv)(plaintext, pem);
|
|
95
|
+
},
|
|
96
|
+
async unseal(sealed) {
|
|
97
|
+
const { wrapped, iv, ct } = (0, import_hub.parseRsaOaepTlv)(sealed);
|
|
98
|
+
const out = await client.send(
|
|
99
|
+
new import_client_kms.DecryptCommand({
|
|
100
|
+
KeyId: opts.keyId,
|
|
101
|
+
CiphertextBlob: wrapped,
|
|
102
|
+
EncryptionAlgorithm: "RSAES_OAEP_SHA_256"
|
|
103
|
+
})
|
|
104
|
+
);
|
|
105
|
+
const cek = out.Plaintext;
|
|
106
|
+
if (!cek) throw new Error("@noy-db/at-aws-kms: KMS Decrypt returned no Plaintext (CEK)");
|
|
107
|
+
const cekBytes = cek instanceof Uint8Array ? cek : new Uint8Array(cek);
|
|
108
|
+
const pt = await (0, import_hub.aesGcmOpen)(cekBytes, iv, ct);
|
|
109
|
+
cekBytes.fill(0);
|
|
110
|
+
return pt;
|
|
111
|
+
}
|
|
112
|
+
};
|
|
113
|
+
}
|
|
49
114
|
// Annotate the CommonJS export names for ESM import in node:
|
|
50
115
|
0 && (module.exports = {
|
|
116
|
+
awsKmsRecipientSealer,
|
|
51
117
|
awsKmsSealingProvider
|
|
52
118
|
});
|
|
53
119
|
//# sourceMappingURL=index.cjs.map
|
package/dist/index.cjs.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/index.ts"],"sourcesContent":["/**\n * **@noy-db/at-aws-kms** — AWS KMS sealing key provider for noy-db\n * managed-passphrase mode.\n *\n * An `at-*` provider that seals and unseals the hub-generated random\n * passphrase via AWS KMS Encrypt / Decrypt. Every seal and unseal is an\n * authenticated KMS API call, giving you a CloudTrail-backed access log of\n * every time a user's vault is opened — no additional instrumentation\n * required.\n *\n * ## When to use\n *\n * - Compliance regimes requiring auditable key access logs (FedRAMP, HIPAA\n * with managed-encryption requirements, SOC 2 Type II).\n * - Workloads already running on AWS where a KMS key costs less than\n * engineering an equivalent audit trail.\n * - Any case where you want automatic CMK rotation without rotating your\n * app's sealing key material manually.\n *\n * ## Setup\n *\n * ```bash\n * # 1. Create a KMS key (one-time, in your AWS console or CLI):\n * aws kms create-key --description \"noy-db sealing key\"\n * # Note the KeyId/ARN from the output.\n *\n * # 2. Grant the host's IAM role kms:Encrypt + kms:Decrypt on that key.\n * # Credentials are picked up automatically from the SDK's ambient chain\n * # (IAM role, ~/.aws/credentials, env vars — see AWS SDK docs).\n * ```\n *\n * ```ts\n * // 3. In your app:\n * import { createNoydb } from '@noy-db/hub'\n * import { awsKmsSealingProvider } from '@noy-db/at-aws-kms'\n * import { shamirRecoveryProvider } from '@noy-db/on-shamir'\n *\n * const db = await createNoydb({\n * store,\n * user: 'alice',\n * passphraseMode: 'managed',\n * sealingKey: awsKmsSealingProvider({ keyId: 'arn:aws:kms:us-east-1:123:key/abc' }),\n * shamirRecovery: shamirRecoveryProvider(),\n * })\n * ```\n *\n * @packageDocumentation\n */\n\nimport type { SealingKeyProvider } from '@noy-db/hub'\nimport {\n KMSClient,\n EncryptCommand,\n DecryptCommand,\n type EncryptCommandOutput,\n type DecryptCommandOutput,\n} from '@aws-sdk/client-kms'\n\n/** Options for {@link awsKmsSealingProvider}. */\nexport interface AwsKmsSealingProviderOptions {\n /** KMS key id or ARN (e.g. `arn:aws:kms:us-east-1:123:key/abc`). */\n readonly keyId: string\n /** Optional pre-built client (DI for tests). Default `new KMSClient({})` (ambient creds). */\n readonly client?: Pick<KMSClient, 'send'>\n}\n\n/**\n * Build a {@link SealingKeyProvider} backed by AWS KMS Encrypt / Decrypt.\n *\n * Credentials are resolved by the SDK's ambient chain — IAM instance roles,\n * environment variables, or `~/.aws/credentials`. Never pass raw credentials\n * in the options; inject a pre-configured client for non-default auth instead.\n *\n * @throws Error when KMS returns no ciphertext or no plaintext (guards\n * against unexpected SDK-response shapes).\n * Any KMS API error (AccessDenied, InvalidKeyUsage, etc.) propagates as-is.\n */\nexport function awsKmsSealingProvider(opts: AwsKmsSealingProviderOptions): SealingKeyProvider {\n const client = opts.client ?? new KMSClient({})\n return {\n id: `aws-kms:${opts.keyId}`,\n\n async seal(passphrase) {\n const out: EncryptCommandOutput = await client.send(\n new EncryptCommand({ KeyId: opts.keyId, Plaintext: passphrase }),\n )\n const blob = out.CiphertextBlob\n if (!blob) throw new Error('@noy-db/at-aws-kms: KMS Encrypt returned no CiphertextBlob')\n return blob instanceof Uint8Array ? blob : new Uint8Array(blob)\n },\n\n async unseal(sealed) {\n const out: DecryptCommandOutput = await client.send(\n new DecryptCommand({ CiphertextBlob: sealed, KeyId: opts.keyId }),\n )\n const pt = out.Plaintext\n if (!pt) throw new Error('@noy-db/at-aws-kms: KMS Decrypt returned no Plaintext')\n return pt instanceof Uint8Array ? pt : new Uint8Array(pt)\n },\n }\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;;;AAAA;AAAA;AAAA;AAAA;AAAA;AAkDA,wBAMO;AAqBA,SAAS,sBAAsB,MAAwD;AAC5F,QAAM,SAAS,KAAK,UAAU,IAAI,4BAAU,CAAC,CAAC;AAC9C,SAAO;AAAA,IACL,IAAI,WAAW,KAAK,KAAK;AAAA,IAEzB,MAAM,KAAK,YAAY;AACrB,YAAM,MAA4B,MAAM,OAAO;AAAA,QAC7C,IAAI,iCAAe,EAAE,OAAO,KAAK,OAAO,WAAW,WAAW,CAAC;AAAA,MACjE;AACA,YAAM,OAAO,IAAI;AACjB,UAAI,CAAC,KAAM,OAAM,IAAI,MAAM,4DAA4D;AACvF,aAAO,gBAAgB,aAAa,OAAO,IAAI,WAAW,IAAI;AAAA,IAChE;AAAA,IAEA,MAAM,OAAO,QAAQ;AACnB,YAAM,MAA4B,MAAM,OAAO;AAAA,QAC7C,IAAI,iCAAe,EAAE,gBAAgB,QAAQ,OAAO,KAAK,MAAM,CAAC;AAAA,MAClE;AACA,YAAM,KAAK,IAAI;AACf,UAAI,CAAC,GAAI,OAAM,IAAI,MAAM,uDAAuD;AAChF,aAAO,cAAc,aAAa,KAAK,IAAI,WAAW,EAAE;AAAA,IAC1D;AAAA,EACF;AACF;","names":[]}
|
|
1
|
+
{"version":3,"sources":["../src/index.ts"],"sourcesContent":["/**\n * **@noy-db/at-aws-kms** — AWS KMS sealing key provider for noy-db\n * managed-passphrase mode.\n *\n * An `at-*` provider that seals and unseals the hub-generated random\n * passphrase via AWS KMS Encrypt / Decrypt. Every seal and unseal is an\n * authenticated KMS API call, giving you a CloudTrail-backed access log of\n * every time a user's vault is opened — no additional instrumentation\n * required.\n *\n * ## When to use\n *\n * - Compliance regimes requiring auditable key access logs (FedRAMP, HIPAA\n * with managed-encryption requirements, SOC 2 Type II).\n * - Workloads already running on AWS where a KMS key costs less than\n * engineering an equivalent audit trail.\n * - Any case where you want automatic CMK rotation without rotating your\n * app's sealing key material manually.\n *\n * ## Setup\n *\n * ```bash\n * # 1. Create a KMS key (one-time, in your AWS console or CLI):\n * aws kms create-key --description \"noy-db sealing key\"\n * # Note the KeyId/ARN from the output.\n *\n * # 2. Grant the host's IAM role kms:Encrypt + kms:Decrypt on that key.\n * # Credentials are picked up automatically from the SDK's ambient chain\n * # (IAM role, ~/.aws/credentials, env vars — see AWS SDK docs).\n * ```\n *\n * ```ts\n * // 3. In your app:\n * import { createNoydb } from '@noy-db/hub'\n * import { awsKmsSealingProvider } from '@noy-db/at-aws-kms'\n * import { shamirRecoveryProvider } from '@noy-db/on-shamir'\n *\n * const db = await createNoydb({\n * store,\n * user: 'alice',\n * passphraseMode: 'managed',\n * sealingKey: awsKmsSealingProvider({ keyId: 'arn:aws:kms:us-east-1:123:key/abc' }),\n * shamirRecovery: shamirRecoveryProvider(),\n * })\n * ```\n *\n * @packageDocumentation\n */\n\nimport type { SealingKeyProvider, RecipientSealer, RecipientHint } from '@noy-db/hub'\nimport { sealRsaOaepTlv, parseRsaOaepTlv, aesGcmOpen } from '@noy-db/hub'\nimport {\n KMSClient,\n EncryptCommand,\n DecryptCommand,\n GetPublicKeyCommand,\n type EncryptCommandOutput,\n type DecryptCommandOutput,\n type GetPublicKeyCommandOutput,\n} from '@aws-sdk/client-kms'\n\n/** Options for {@link awsKmsSealingProvider}. */\nexport interface AwsKmsSealingProviderOptions {\n /** KMS key id or ARN (e.g. `arn:aws:kms:us-east-1:123:key/abc`). */\n readonly keyId: string\n /** Optional pre-built client (DI for tests). Default `new KMSClient({})` (ambient creds). */\n readonly client?: Pick<KMSClient, 'send'>\n}\n\n/**\n * Build a {@link SealingKeyProvider} backed by AWS KMS Encrypt / Decrypt.\n *\n * Credentials are resolved by the SDK's ambient chain — IAM instance roles,\n * environment variables, or `~/.aws/credentials`. Never pass raw credentials\n * in the options; inject a pre-configured client for non-default auth instead.\n *\n * @throws Error when KMS returns no ciphertext or no plaintext (guards\n * against unexpected SDK-response shapes).\n * Any KMS API error (AccessDenied, InvalidKeyUsage, etc.) propagates as-is.\n */\nexport function awsKmsSealingProvider(opts: AwsKmsSealingProviderOptions): SealingKeyProvider {\n const client = opts.client ?? new KMSClient({})\n return {\n id: `aws-kms:${opts.keyId}`,\n\n async seal(passphrase) {\n const out: EncryptCommandOutput = await client.send(\n new EncryptCommand({ KeyId: opts.keyId, Plaintext: passphrase }),\n )\n const blob = out.CiphertextBlob\n if (!blob) throw new Error('@noy-db/at-aws-kms: KMS Encrypt returned no CiphertextBlob')\n return blob instanceof Uint8Array ? blob : new Uint8Array(blob)\n },\n\n async unseal(sealed) {\n const out: DecryptCommandOutput = await client.send(\n new DecryptCommand({ CiphertextBlob: sealed, KeyId: opts.keyId }),\n )\n const pt = out.Plaintext\n if (!pt) throw new Error('@noy-db/at-aws-kms: KMS Decrypt returned no Plaintext')\n return pt instanceof Uint8Array ? pt : new Uint8Array(pt)\n },\n }\n}\n\n/** Options for {@link awsKmsRecipientSealer}. */\nexport interface AwsKmsRecipientSealerOptions {\n /**\n * KMS key id or ARN of an **asymmetric RSA** key with `KeyUsage:\n * ENCRYPT_DECRYPT` and `KeySpec` one of `RSA_2048` / `RSA_3072` /\n * `RSA_4096`. The private key never leaves KMS — unseal runs `Decrypt`.\n */\n readonly keyId: string\n /** Optional region passed to the default `KMSClient` when no `client` is given. */\n readonly region?: string\n /** Optional pre-built client (DI for tests). Default `new KMSClient({ region? })` (ambient creds). */\n readonly client?: Pick<KMSClient, 'send'>\n}\n\n/** KMS asymmetric key specs this recipient sealer accepts (RSA-OAEP-SHA256 wrap). */\nconst SUPPORTED_RSA_KEY_SPECS = new Set(['RSA_2048', 'RSA_3072', 'RSA_4096'])\n\n/** DER SPKI bytes (from KMS `GetPublicKey`) → PEM SubjectPublicKeyInfo. */\nfunction derSpkiToPem(der: Uint8Array): string {\n let binary = ''\n for (let i = 0; i < der.length; i++) binary += String.fromCharCode(der[i]!)\n const b64 = btoa(binary)\n return '-----BEGIN PUBLIC KEY-----\\n'\n + (b64.match(/.{1,64}/g) ?? []).join('\\n')\n + '\\n-----END PUBLIC KEY-----\\n'\n}\n\n/**\n * A {@link RecipientSealer} (plus `unseal`) backed by an **asymmetric RSA**\n * AWS KMS key. Lets an `at-aws-kms` host act as a recipient target: a grantor\n * obtains the host's published {@link RecipientHint} (the KMS public key as\n * PEM) and seals bytes to it *locally* — no KMS call on the seal path. The\n * host unseals via KMS `Decrypt` with `EncryptionAlgorithm:\n * RSAES_OAEP_SHA_256`; the private key never leaves KMS.\n *\n * Wire format is the canonical recipient-target TLV\n * ({@link sealRsaOaepTlv}/{@link parseRsaOaepTlv}) shared with hub's\n * `MemoryRecipientSealer`, so a blob sealed by either side unseals on the\n * other. WebCrypto RSA-OAEP/SHA-256 and KMS `RSAES_OAEP_SHA_256` are\n * wire-compatible (RSAES-OAEP, SHA-256, MGF1-SHA256, empty label). KMS\n * asymmetric keys do not support an encryption context, so none is used.\n *\n * Separate from {@link awsKmsSealingProvider} (symmetric self-seal for the\n * managed passphrase) — this factory targets an asymmetric key.\n *\n * @throws Error from `publishRecipientHint` when the key is not an RSA\n * `ENCRYPT_DECRYPT` key, or `GetPublicKey` returns no public key.\n * @throws Error from `sealForRecipient` on an unsupported `hint.v`/`hint.alg`.\n * Any KMS API error (AccessDenied, etc.) propagates as-is.\n */\nexport function awsKmsRecipientSealer(\n opts: AwsKmsRecipientSealerOptions,\n): RecipientSealer & { unseal(sealed: Uint8Array): Promise<Uint8Array> } {\n const client = opts.client ?? new KMSClient(opts.region ? { region: opts.region } : {})\n const id = `aws-kms-recipient:${opts.keyId}`\n return {\n id,\n\n async publishRecipientHint(): Promise<RecipientHint> {\n const out: GetPublicKeyCommandOutput = await client.send(\n new GetPublicKeyCommand({ KeyId: opts.keyId }),\n )\n if (out.KeyUsage !== 'ENCRYPT_DECRYPT') {\n throw new Error(\n `@noy-db/at-aws-kms: awsKmsRecipientSealer requires an ENCRYPT_DECRYPT key, got KeyUsage='${String(out.KeyUsage)}' for ${opts.keyId}`,\n )\n }\n if (!out.KeySpec || !SUPPORTED_RSA_KEY_SPECS.has(out.KeySpec)) {\n throw new Error(\n `@noy-db/at-aws-kms: awsKmsRecipientSealer requires an RSA key (RSA_2048/3072/4096), got KeySpec='${String(out.KeySpec)}' for ${opts.keyId}`,\n )\n }\n const der = out.PublicKey\n if (!der) throw new Error('@noy-db/at-aws-kms: KMS GetPublicKey returned no PublicKey')\n const derBytes = der instanceof Uint8Array ? der : new Uint8Array(der)\n const publicKeyPem = derSpkiToPem(derBytes)\n return { v: 1, pid: id, alg: 'rsa-oaep-sha256', material: { publicKeyPem } }\n },\n\n async sealForRecipient(plaintext: Uint8Array, hint: RecipientHint): Promise<Uint8Array> {\n if (hint.v !== 1) {\n throw new Error(`@noy-db/at-aws-kms: awsKmsRecipientSealer.sealForRecipient: unsupported hint.v ${String(hint.v)} (expected 1)`)\n }\n if (hint.alg !== 'rsa-oaep-sha256') {\n throw new Error(`@noy-db/at-aws-kms: awsKmsRecipientSealer.sealForRecipient: unsupported hint.alg '${String(hint.alg)}' (expected 'rsa-oaep-sha256')`)\n }\n const pem = hint.material['publicKeyPem']\n if (typeof pem !== 'string') {\n throw new Error('@noy-db/at-aws-kms: awsKmsRecipientSealer.sealForRecipient: hint.material.publicKeyPem missing or not a string')\n }\n // Grantor seals LOCALLY — no KMS call.\n return sealRsaOaepTlv(plaintext, pem)\n },\n\n async unseal(sealed: Uint8Array): Promise<Uint8Array> {\n const { wrapped, iv, ct } = parseRsaOaepTlv(sealed)\n // RSA-unwrap the CEK via KMS — the private key never leaves KMS.\n const out: DecryptCommandOutput = await client.send(\n new DecryptCommand({\n KeyId: opts.keyId,\n CiphertextBlob: wrapped,\n EncryptionAlgorithm: 'RSAES_OAEP_SHA_256',\n }),\n )\n const cek = out.Plaintext\n if (!cek) throw new Error('@noy-db/at-aws-kms: KMS Decrypt returned no Plaintext (CEK)')\n const cekBytes = cek instanceof Uint8Array ? cek : new Uint8Array(cek)\n const pt = await aesGcmOpen(cekBytes, iv, ct)\n cekBytes.fill(0)\n return pt\n },\n }\n}\n"],"mappings":"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","names":[]}
|
package/dist/index.d.cts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { SealingKeyProvider } from '@noy-db/hub';
|
|
1
|
+
import { RecipientSealer, SealingKeyProvider } from '@noy-db/hub';
|
|
2
2
|
import { KMSClient } from '@aws-sdk/client-kms';
|
|
3
3
|
|
|
4
4
|
/**
|
|
@@ -69,5 +69,44 @@ interface AwsKmsSealingProviderOptions {
|
|
|
69
69
|
* Any KMS API error (AccessDenied, InvalidKeyUsage, etc.) propagates as-is.
|
|
70
70
|
*/
|
|
71
71
|
declare function awsKmsSealingProvider(opts: AwsKmsSealingProviderOptions): SealingKeyProvider;
|
|
72
|
+
/** Options for {@link awsKmsRecipientSealer}. */
|
|
73
|
+
interface AwsKmsRecipientSealerOptions {
|
|
74
|
+
/**
|
|
75
|
+
* KMS key id or ARN of an **asymmetric RSA** key with `KeyUsage:
|
|
76
|
+
* ENCRYPT_DECRYPT` and `KeySpec` one of `RSA_2048` / `RSA_3072` /
|
|
77
|
+
* `RSA_4096`. The private key never leaves KMS — unseal runs `Decrypt`.
|
|
78
|
+
*/
|
|
79
|
+
readonly keyId: string;
|
|
80
|
+
/** Optional region passed to the default `KMSClient` when no `client` is given. */
|
|
81
|
+
readonly region?: string;
|
|
82
|
+
/** Optional pre-built client (DI for tests). Default `new KMSClient({ region? })` (ambient creds). */
|
|
83
|
+
readonly client?: Pick<KMSClient, 'send'>;
|
|
84
|
+
}
|
|
85
|
+
/**
|
|
86
|
+
* A {@link RecipientSealer} (plus `unseal`) backed by an **asymmetric RSA**
|
|
87
|
+
* AWS KMS key. Lets an `at-aws-kms` host act as a recipient target: a grantor
|
|
88
|
+
* obtains the host's published {@link RecipientHint} (the KMS public key as
|
|
89
|
+
* PEM) and seals bytes to it *locally* — no KMS call on the seal path. The
|
|
90
|
+
* host unseals via KMS `Decrypt` with `EncryptionAlgorithm:
|
|
91
|
+
* RSAES_OAEP_SHA_256`; the private key never leaves KMS.
|
|
92
|
+
*
|
|
93
|
+
* Wire format is the canonical recipient-target TLV
|
|
94
|
+
* ({@link sealRsaOaepTlv}/{@link parseRsaOaepTlv}) shared with hub's
|
|
95
|
+
* `MemoryRecipientSealer`, so a blob sealed by either side unseals on the
|
|
96
|
+
* other. WebCrypto RSA-OAEP/SHA-256 and KMS `RSAES_OAEP_SHA_256` are
|
|
97
|
+
* wire-compatible (RSAES-OAEP, SHA-256, MGF1-SHA256, empty label). KMS
|
|
98
|
+
* asymmetric keys do not support an encryption context, so none is used.
|
|
99
|
+
*
|
|
100
|
+
* Separate from {@link awsKmsSealingProvider} (symmetric self-seal for the
|
|
101
|
+
* managed passphrase) — this factory targets an asymmetric key.
|
|
102
|
+
*
|
|
103
|
+
* @throws Error from `publishRecipientHint` when the key is not an RSA
|
|
104
|
+
* `ENCRYPT_DECRYPT` key, or `GetPublicKey` returns no public key.
|
|
105
|
+
* @throws Error from `sealForRecipient` on an unsupported `hint.v`/`hint.alg`.
|
|
106
|
+
* Any KMS API error (AccessDenied, etc.) propagates as-is.
|
|
107
|
+
*/
|
|
108
|
+
declare function awsKmsRecipientSealer(opts: AwsKmsRecipientSealerOptions): RecipientSealer & {
|
|
109
|
+
unseal(sealed: Uint8Array): Promise<Uint8Array>;
|
|
110
|
+
};
|
|
72
111
|
|
|
73
|
-
export { type AwsKmsSealingProviderOptions, awsKmsSealingProvider };
|
|
112
|
+
export { type AwsKmsRecipientSealerOptions, type AwsKmsSealingProviderOptions, awsKmsRecipientSealer, awsKmsSealingProvider };
|
package/dist/index.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { SealingKeyProvider } from '@noy-db/hub';
|
|
1
|
+
import { RecipientSealer, SealingKeyProvider } from '@noy-db/hub';
|
|
2
2
|
import { KMSClient } from '@aws-sdk/client-kms';
|
|
3
3
|
|
|
4
4
|
/**
|
|
@@ -69,5 +69,44 @@ interface AwsKmsSealingProviderOptions {
|
|
|
69
69
|
* Any KMS API error (AccessDenied, InvalidKeyUsage, etc.) propagates as-is.
|
|
70
70
|
*/
|
|
71
71
|
declare function awsKmsSealingProvider(opts: AwsKmsSealingProviderOptions): SealingKeyProvider;
|
|
72
|
+
/** Options for {@link awsKmsRecipientSealer}. */
|
|
73
|
+
interface AwsKmsRecipientSealerOptions {
|
|
74
|
+
/**
|
|
75
|
+
* KMS key id or ARN of an **asymmetric RSA** key with `KeyUsage:
|
|
76
|
+
* ENCRYPT_DECRYPT` and `KeySpec` one of `RSA_2048` / `RSA_3072` /
|
|
77
|
+
* `RSA_4096`. The private key never leaves KMS — unseal runs `Decrypt`.
|
|
78
|
+
*/
|
|
79
|
+
readonly keyId: string;
|
|
80
|
+
/** Optional region passed to the default `KMSClient` when no `client` is given. */
|
|
81
|
+
readonly region?: string;
|
|
82
|
+
/** Optional pre-built client (DI for tests). Default `new KMSClient({ region? })` (ambient creds). */
|
|
83
|
+
readonly client?: Pick<KMSClient, 'send'>;
|
|
84
|
+
}
|
|
85
|
+
/**
|
|
86
|
+
* A {@link RecipientSealer} (plus `unseal`) backed by an **asymmetric RSA**
|
|
87
|
+
* AWS KMS key. Lets an `at-aws-kms` host act as a recipient target: a grantor
|
|
88
|
+
* obtains the host's published {@link RecipientHint} (the KMS public key as
|
|
89
|
+
* PEM) and seals bytes to it *locally* — no KMS call on the seal path. The
|
|
90
|
+
* host unseals via KMS `Decrypt` with `EncryptionAlgorithm:
|
|
91
|
+
* RSAES_OAEP_SHA_256`; the private key never leaves KMS.
|
|
92
|
+
*
|
|
93
|
+
* Wire format is the canonical recipient-target TLV
|
|
94
|
+
* ({@link sealRsaOaepTlv}/{@link parseRsaOaepTlv}) shared with hub's
|
|
95
|
+
* `MemoryRecipientSealer`, so a blob sealed by either side unseals on the
|
|
96
|
+
* other. WebCrypto RSA-OAEP/SHA-256 and KMS `RSAES_OAEP_SHA_256` are
|
|
97
|
+
* wire-compatible (RSAES-OAEP, SHA-256, MGF1-SHA256, empty label). KMS
|
|
98
|
+
* asymmetric keys do not support an encryption context, so none is used.
|
|
99
|
+
*
|
|
100
|
+
* Separate from {@link awsKmsSealingProvider} (symmetric self-seal for the
|
|
101
|
+
* managed passphrase) — this factory targets an asymmetric key.
|
|
102
|
+
*
|
|
103
|
+
* @throws Error from `publishRecipientHint` when the key is not an RSA
|
|
104
|
+
* `ENCRYPT_DECRYPT` key, or `GetPublicKey` returns no public key.
|
|
105
|
+
* @throws Error from `sealForRecipient` on an unsupported `hint.v`/`hint.alg`.
|
|
106
|
+
* Any KMS API error (AccessDenied, etc.) propagates as-is.
|
|
107
|
+
*/
|
|
108
|
+
declare function awsKmsRecipientSealer(opts: AwsKmsRecipientSealerOptions): RecipientSealer & {
|
|
109
|
+
unseal(sealed: Uint8Array): Promise<Uint8Array>;
|
|
110
|
+
};
|
|
72
111
|
|
|
73
|
-
export { type AwsKmsSealingProviderOptions, awsKmsSealingProvider };
|
|
112
|
+
export { type AwsKmsRecipientSealerOptions, type AwsKmsSealingProviderOptions, awsKmsRecipientSealer, awsKmsSealingProvider };
|
package/dist/index.js
CHANGED
|
@@ -1,8 +1,10 @@
|
|
|
1
1
|
// src/index.ts
|
|
2
|
+
import { sealRsaOaepTlv, parseRsaOaepTlv, aesGcmOpen } from "@noy-db/hub";
|
|
2
3
|
import {
|
|
3
4
|
KMSClient,
|
|
4
5
|
EncryptCommand,
|
|
5
|
-
DecryptCommand
|
|
6
|
+
DecryptCommand,
|
|
7
|
+
GetPublicKeyCommand
|
|
6
8
|
} from "@aws-sdk/client-kms";
|
|
7
9
|
function awsKmsSealingProvider(opts) {
|
|
8
10
|
const client = opts.client ?? new KMSClient({});
|
|
@@ -26,7 +28,71 @@ function awsKmsSealingProvider(opts) {
|
|
|
26
28
|
}
|
|
27
29
|
};
|
|
28
30
|
}
|
|
31
|
+
var SUPPORTED_RSA_KEY_SPECS = /* @__PURE__ */ new Set(["RSA_2048", "RSA_3072", "RSA_4096"]);
|
|
32
|
+
function derSpkiToPem(der) {
|
|
33
|
+
let binary = "";
|
|
34
|
+
for (let i = 0; i < der.length; i++) binary += String.fromCharCode(der[i]);
|
|
35
|
+
const b64 = btoa(binary);
|
|
36
|
+
return "-----BEGIN PUBLIC KEY-----\n" + (b64.match(/.{1,64}/g) ?? []).join("\n") + "\n-----END PUBLIC KEY-----\n";
|
|
37
|
+
}
|
|
38
|
+
function awsKmsRecipientSealer(opts) {
|
|
39
|
+
const client = opts.client ?? new KMSClient(opts.region ? { region: opts.region } : {});
|
|
40
|
+
const id = `aws-kms-recipient:${opts.keyId}`;
|
|
41
|
+
return {
|
|
42
|
+
id,
|
|
43
|
+
async publishRecipientHint() {
|
|
44
|
+
const out = await client.send(
|
|
45
|
+
new GetPublicKeyCommand({ KeyId: opts.keyId })
|
|
46
|
+
);
|
|
47
|
+
if (out.KeyUsage !== "ENCRYPT_DECRYPT") {
|
|
48
|
+
throw new Error(
|
|
49
|
+
`@noy-db/at-aws-kms: awsKmsRecipientSealer requires an ENCRYPT_DECRYPT key, got KeyUsage='${String(out.KeyUsage)}' for ${opts.keyId}`
|
|
50
|
+
);
|
|
51
|
+
}
|
|
52
|
+
if (!out.KeySpec || !SUPPORTED_RSA_KEY_SPECS.has(out.KeySpec)) {
|
|
53
|
+
throw new Error(
|
|
54
|
+
`@noy-db/at-aws-kms: awsKmsRecipientSealer requires an RSA key (RSA_2048/3072/4096), got KeySpec='${String(out.KeySpec)}' for ${opts.keyId}`
|
|
55
|
+
);
|
|
56
|
+
}
|
|
57
|
+
const der = out.PublicKey;
|
|
58
|
+
if (!der) throw new Error("@noy-db/at-aws-kms: KMS GetPublicKey returned no PublicKey");
|
|
59
|
+
const derBytes = der instanceof Uint8Array ? der : new Uint8Array(der);
|
|
60
|
+
const publicKeyPem = derSpkiToPem(derBytes);
|
|
61
|
+
return { v: 1, pid: id, alg: "rsa-oaep-sha256", material: { publicKeyPem } };
|
|
62
|
+
},
|
|
63
|
+
async sealForRecipient(plaintext, hint) {
|
|
64
|
+
if (hint.v !== 1) {
|
|
65
|
+
throw new Error(`@noy-db/at-aws-kms: awsKmsRecipientSealer.sealForRecipient: unsupported hint.v ${String(hint.v)} (expected 1)`);
|
|
66
|
+
}
|
|
67
|
+
if (hint.alg !== "rsa-oaep-sha256") {
|
|
68
|
+
throw new Error(`@noy-db/at-aws-kms: awsKmsRecipientSealer.sealForRecipient: unsupported hint.alg '${String(hint.alg)}' (expected 'rsa-oaep-sha256')`);
|
|
69
|
+
}
|
|
70
|
+
const pem = hint.material["publicKeyPem"];
|
|
71
|
+
if (typeof pem !== "string") {
|
|
72
|
+
throw new Error("@noy-db/at-aws-kms: awsKmsRecipientSealer.sealForRecipient: hint.material.publicKeyPem missing or not a string");
|
|
73
|
+
}
|
|
74
|
+
return sealRsaOaepTlv(plaintext, pem);
|
|
75
|
+
},
|
|
76
|
+
async unseal(sealed) {
|
|
77
|
+
const { wrapped, iv, ct } = parseRsaOaepTlv(sealed);
|
|
78
|
+
const out = await client.send(
|
|
79
|
+
new DecryptCommand({
|
|
80
|
+
KeyId: opts.keyId,
|
|
81
|
+
CiphertextBlob: wrapped,
|
|
82
|
+
EncryptionAlgorithm: "RSAES_OAEP_SHA_256"
|
|
83
|
+
})
|
|
84
|
+
);
|
|
85
|
+
const cek = out.Plaintext;
|
|
86
|
+
if (!cek) throw new Error("@noy-db/at-aws-kms: KMS Decrypt returned no Plaintext (CEK)");
|
|
87
|
+
const cekBytes = cek instanceof Uint8Array ? cek : new Uint8Array(cek);
|
|
88
|
+
const pt = await aesGcmOpen(cekBytes, iv, ct);
|
|
89
|
+
cekBytes.fill(0);
|
|
90
|
+
return pt;
|
|
91
|
+
}
|
|
92
|
+
};
|
|
93
|
+
}
|
|
29
94
|
export {
|
|
95
|
+
awsKmsRecipientSealer,
|
|
30
96
|
awsKmsSealingProvider
|
|
31
97
|
};
|
|
32
98
|
//# sourceMappingURL=index.js.map
|
package/dist/index.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/index.ts"],"sourcesContent":["/**\n * **@noy-db/at-aws-kms** — AWS KMS sealing key provider for noy-db\n * managed-passphrase mode.\n *\n * An `at-*` provider that seals and unseals the hub-generated random\n * passphrase via AWS KMS Encrypt / Decrypt. Every seal and unseal is an\n * authenticated KMS API call, giving you a CloudTrail-backed access log of\n * every time a user's vault is opened — no additional instrumentation\n * required.\n *\n * ## When to use\n *\n * - Compliance regimes requiring auditable key access logs (FedRAMP, HIPAA\n * with managed-encryption requirements, SOC 2 Type II).\n * - Workloads already running on AWS where a KMS key costs less than\n * engineering an equivalent audit trail.\n * - Any case where you want automatic CMK rotation without rotating your\n * app's sealing key material manually.\n *\n * ## Setup\n *\n * ```bash\n * # 1. Create a KMS key (one-time, in your AWS console or CLI):\n * aws kms create-key --description \"noy-db sealing key\"\n * # Note the KeyId/ARN from the output.\n *\n * # 2. Grant the host's IAM role kms:Encrypt + kms:Decrypt on that key.\n * # Credentials are picked up automatically from the SDK's ambient chain\n * # (IAM role, ~/.aws/credentials, env vars — see AWS SDK docs).\n * ```\n *\n * ```ts\n * // 3. In your app:\n * import { createNoydb } from '@noy-db/hub'\n * import { awsKmsSealingProvider } from '@noy-db/at-aws-kms'\n * import { shamirRecoveryProvider } from '@noy-db/on-shamir'\n *\n * const db = await createNoydb({\n * store,\n * user: 'alice',\n * passphraseMode: 'managed',\n * sealingKey: awsKmsSealingProvider({ keyId: 'arn:aws:kms:us-east-1:123:key/abc' }),\n * shamirRecovery: shamirRecoveryProvider(),\n * })\n * ```\n *\n * @packageDocumentation\n */\n\nimport type { SealingKeyProvider } from '@noy-db/hub'\nimport {\n KMSClient,\n EncryptCommand,\n DecryptCommand,\n type EncryptCommandOutput,\n type DecryptCommandOutput,\n} from '@aws-sdk/client-kms'\n\n/** Options for {@link awsKmsSealingProvider}. */\nexport interface AwsKmsSealingProviderOptions {\n /** KMS key id or ARN (e.g. `arn:aws:kms:us-east-1:123:key/abc`). */\n readonly keyId: string\n /** Optional pre-built client (DI for tests). Default `new KMSClient({})` (ambient creds). */\n readonly client?: Pick<KMSClient, 'send'>\n}\n\n/**\n * Build a {@link SealingKeyProvider} backed by AWS KMS Encrypt / Decrypt.\n *\n * Credentials are resolved by the SDK's ambient chain — IAM instance roles,\n * environment variables, or `~/.aws/credentials`. Never pass raw credentials\n * in the options; inject a pre-configured client for non-default auth instead.\n *\n * @throws Error when KMS returns no ciphertext or no plaintext (guards\n * against unexpected SDK-response shapes).\n * Any KMS API error (AccessDenied, InvalidKeyUsage, etc.) propagates as-is.\n */\nexport function awsKmsSealingProvider(opts: AwsKmsSealingProviderOptions): SealingKeyProvider {\n const client = opts.client ?? new KMSClient({})\n return {\n id: `aws-kms:${opts.keyId}`,\n\n async seal(passphrase) {\n const out: EncryptCommandOutput = await client.send(\n new EncryptCommand({ KeyId: opts.keyId, Plaintext: passphrase }),\n )\n const blob = out.CiphertextBlob\n if (!blob) throw new Error('@noy-db/at-aws-kms: KMS Encrypt returned no CiphertextBlob')\n return blob instanceof Uint8Array ? blob : new Uint8Array(blob)\n },\n\n async unseal(sealed) {\n const out: DecryptCommandOutput = await client.send(\n new DecryptCommand({ CiphertextBlob: sealed, KeyId: opts.keyId }),\n )\n const pt = out.Plaintext\n if (!pt) throw new Error('@noy-db/at-aws-kms: KMS Decrypt returned no Plaintext')\n return pt instanceof Uint8Array ? pt : new Uint8Array(pt)\n },\n }\n}\n"],"mappings":";AAkDA;AAAA,EACE;AAAA,EACA;AAAA,EACA;AAAA,OAGK;AAqBA,SAAS,sBAAsB,MAAwD;AAC5F,QAAM,SAAS,KAAK,UAAU,IAAI,UAAU,CAAC,CAAC;AAC9C,SAAO;AAAA,IACL,IAAI,WAAW,KAAK,KAAK;AAAA,IAEzB,MAAM,KAAK,YAAY;AACrB,YAAM,MAA4B,MAAM,OAAO;AAAA,QAC7C,IAAI,eAAe,EAAE,OAAO,KAAK,OAAO,WAAW,WAAW,CAAC;AAAA,MACjE;AACA,YAAM,OAAO,IAAI;AACjB,UAAI,CAAC,KAAM,OAAM,IAAI,MAAM,4DAA4D;AACvF,aAAO,gBAAgB,aAAa,OAAO,IAAI,WAAW,IAAI;AAAA,IAChE;AAAA,IAEA,MAAM,OAAO,QAAQ;AACnB,YAAM,MAA4B,MAAM,OAAO;AAAA,QAC7C,IAAI,eAAe,EAAE,gBAAgB,QAAQ,OAAO,KAAK,MAAM,CAAC;AAAA,MAClE;AACA,YAAM,KAAK,IAAI;AACf,UAAI,CAAC,GAAI,OAAM,IAAI,MAAM,uDAAuD;AAChF,aAAO,cAAc,aAAa,KAAK,IAAI,WAAW,EAAE;AAAA,IAC1D;AAAA,EACF;AACF;","names":[]}
|
|
1
|
+
{"version":3,"sources":["../src/index.ts"],"sourcesContent":["/**\n * **@noy-db/at-aws-kms** — AWS KMS sealing key provider for noy-db\n * managed-passphrase mode.\n *\n * An `at-*` provider that seals and unseals the hub-generated random\n * passphrase via AWS KMS Encrypt / Decrypt. Every seal and unseal is an\n * authenticated KMS API call, giving you a CloudTrail-backed access log of\n * every time a user's vault is opened — no additional instrumentation\n * required.\n *\n * ## When to use\n *\n * - Compliance regimes requiring auditable key access logs (FedRAMP, HIPAA\n * with managed-encryption requirements, SOC 2 Type II).\n * - Workloads already running on AWS where a KMS key costs less than\n * engineering an equivalent audit trail.\n * - Any case where you want automatic CMK rotation without rotating your\n * app's sealing key material manually.\n *\n * ## Setup\n *\n * ```bash\n * # 1. Create a KMS key (one-time, in your AWS console or CLI):\n * aws kms create-key --description \"noy-db sealing key\"\n * # Note the KeyId/ARN from the output.\n *\n * # 2. Grant the host's IAM role kms:Encrypt + kms:Decrypt on that key.\n * # Credentials are picked up automatically from the SDK's ambient chain\n * # (IAM role, ~/.aws/credentials, env vars — see AWS SDK docs).\n * ```\n *\n * ```ts\n * // 3. In your app:\n * import { createNoydb } from '@noy-db/hub'\n * import { awsKmsSealingProvider } from '@noy-db/at-aws-kms'\n * import { shamirRecoveryProvider } from '@noy-db/on-shamir'\n *\n * const db = await createNoydb({\n * store,\n * user: 'alice',\n * passphraseMode: 'managed',\n * sealingKey: awsKmsSealingProvider({ keyId: 'arn:aws:kms:us-east-1:123:key/abc' }),\n * shamirRecovery: shamirRecoveryProvider(),\n * })\n * ```\n *\n * @packageDocumentation\n */\n\nimport type { SealingKeyProvider, RecipientSealer, RecipientHint } from '@noy-db/hub'\nimport { sealRsaOaepTlv, parseRsaOaepTlv, aesGcmOpen } from '@noy-db/hub'\nimport {\n KMSClient,\n EncryptCommand,\n DecryptCommand,\n GetPublicKeyCommand,\n type EncryptCommandOutput,\n type DecryptCommandOutput,\n type GetPublicKeyCommandOutput,\n} from '@aws-sdk/client-kms'\n\n/** Options for {@link awsKmsSealingProvider}. */\nexport interface AwsKmsSealingProviderOptions {\n /** KMS key id or ARN (e.g. `arn:aws:kms:us-east-1:123:key/abc`). */\n readonly keyId: string\n /** Optional pre-built client (DI for tests). Default `new KMSClient({})` (ambient creds). */\n readonly client?: Pick<KMSClient, 'send'>\n}\n\n/**\n * Build a {@link SealingKeyProvider} backed by AWS KMS Encrypt / Decrypt.\n *\n * Credentials are resolved by the SDK's ambient chain — IAM instance roles,\n * environment variables, or `~/.aws/credentials`. Never pass raw credentials\n * in the options; inject a pre-configured client for non-default auth instead.\n *\n * @throws Error when KMS returns no ciphertext or no plaintext (guards\n * against unexpected SDK-response shapes).\n * Any KMS API error (AccessDenied, InvalidKeyUsage, etc.) propagates as-is.\n */\nexport function awsKmsSealingProvider(opts: AwsKmsSealingProviderOptions): SealingKeyProvider {\n const client = opts.client ?? new KMSClient({})\n return {\n id: `aws-kms:${opts.keyId}`,\n\n async seal(passphrase) {\n const out: EncryptCommandOutput = await client.send(\n new EncryptCommand({ KeyId: opts.keyId, Plaintext: passphrase }),\n )\n const blob = out.CiphertextBlob\n if (!blob) throw new Error('@noy-db/at-aws-kms: KMS Encrypt returned no CiphertextBlob')\n return blob instanceof Uint8Array ? blob : new Uint8Array(blob)\n },\n\n async unseal(sealed) {\n const out: DecryptCommandOutput = await client.send(\n new DecryptCommand({ CiphertextBlob: sealed, KeyId: opts.keyId }),\n )\n const pt = out.Plaintext\n if (!pt) throw new Error('@noy-db/at-aws-kms: KMS Decrypt returned no Plaintext')\n return pt instanceof Uint8Array ? pt : new Uint8Array(pt)\n },\n }\n}\n\n/** Options for {@link awsKmsRecipientSealer}. */\nexport interface AwsKmsRecipientSealerOptions {\n /**\n * KMS key id or ARN of an **asymmetric RSA** key with `KeyUsage:\n * ENCRYPT_DECRYPT` and `KeySpec` one of `RSA_2048` / `RSA_3072` /\n * `RSA_4096`. The private key never leaves KMS — unseal runs `Decrypt`.\n */\n readonly keyId: string\n /** Optional region passed to the default `KMSClient` when no `client` is given. */\n readonly region?: string\n /** Optional pre-built client (DI for tests). Default `new KMSClient({ region? })` (ambient creds). */\n readonly client?: Pick<KMSClient, 'send'>\n}\n\n/** KMS asymmetric key specs this recipient sealer accepts (RSA-OAEP-SHA256 wrap). */\nconst SUPPORTED_RSA_KEY_SPECS = new Set(['RSA_2048', 'RSA_3072', 'RSA_4096'])\n\n/** DER SPKI bytes (from KMS `GetPublicKey`) → PEM SubjectPublicKeyInfo. */\nfunction derSpkiToPem(der: Uint8Array): string {\n let binary = ''\n for (let i = 0; i < der.length; i++) binary += String.fromCharCode(der[i]!)\n const b64 = btoa(binary)\n return '-----BEGIN PUBLIC KEY-----\\n'\n + (b64.match(/.{1,64}/g) ?? []).join('\\n')\n + '\\n-----END PUBLIC KEY-----\\n'\n}\n\n/**\n * A {@link RecipientSealer} (plus `unseal`) backed by an **asymmetric RSA**\n * AWS KMS key. Lets an `at-aws-kms` host act as a recipient target: a grantor\n * obtains the host's published {@link RecipientHint} (the KMS public key as\n * PEM) and seals bytes to it *locally* — no KMS call on the seal path. The\n * host unseals via KMS `Decrypt` with `EncryptionAlgorithm:\n * RSAES_OAEP_SHA_256`; the private key never leaves KMS.\n *\n * Wire format is the canonical recipient-target TLV\n * ({@link sealRsaOaepTlv}/{@link parseRsaOaepTlv}) shared with hub's\n * `MemoryRecipientSealer`, so a blob sealed by either side unseals on the\n * other. WebCrypto RSA-OAEP/SHA-256 and KMS `RSAES_OAEP_SHA_256` are\n * wire-compatible (RSAES-OAEP, SHA-256, MGF1-SHA256, empty label). KMS\n * asymmetric keys do not support an encryption context, so none is used.\n *\n * Separate from {@link awsKmsSealingProvider} (symmetric self-seal for the\n * managed passphrase) — this factory targets an asymmetric key.\n *\n * @throws Error from `publishRecipientHint` when the key is not an RSA\n * `ENCRYPT_DECRYPT` key, or `GetPublicKey` returns no public key.\n * @throws Error from `sealForRecipient` on an unsupported `hint.v`/`hint.alg`.\n * Any KMS API error (AccessDenied, etc.) propagates as-is.\n */\nexport function awsKmsRecipientSealer(\n opts: AwsKmsRecipientSealerOptions,\n): RecipientSealer & { unseal(sealed: Uint8Array): Promise<Uint8Array> } {\n const client = opts.client ?? new KMSClient(opts.region ? { region: opts.region } : {})\n const id = `aws-kms-recipient:${opts.keyId}`\n return {\n id,\n\n async publishRecipientHint(): Promise<RecipientHint> {\n const out: GetPublicKeyCommandOutput = await client.send(\n new GetPublicKeyCommand({ KeyId: opts.keyId }),\n )\n if (out.KeyUsage !== 'ENCRYPT_DECRYPT') {\n throw new Error(\n `@noy-db/at-aws-kms: awsKmsRecipientSealer requires an ENCRYPT_DECRYPT key, got KeyUsage='${String(out.KeyUsage)}' for ${opts.keyId}`,\n )\n }\n if (!out.KeySpec || !SUPPORTED_RSA_KEY_SPECS.has(out.KeySpec)) {\n throw new Error(\n `@noy-db/at-aws-kms: awsKmsRecipientSealer requires an RSA key (RSA_2048/3072/4096), got KeySpec='${String(out.KeySpec)}' for ${opts.keyId}`,\n )\n }\n const der = out.PublicKey\n if (!der) throw new Error('@noy-db/at-aws-kms: KMS GetPublicKey returned no PublicKey')\n const derBytes = der instanceof Uint8Array ? der : new Uint8Array(der)\n const publicKeyPem = derSpkiToPem(derBytes)\n return { v: 1, pid: id, alg: 'rsa-oaep-sha256', material: { publicKeyPem } }\n },\n\n async sealForRecipient(plaintext: Uint8Array, hint: RecipientHint): Promise<Uint8Array> {\n if (hint.v !== 1) {\n throw new Error(`@noy-db/at-aws-kms: awsKmsRecipientSealer.sealForRecipient: unsupported hint.v ${String(hint.v)} (expected 1)`)\n }\n if (hint.alg !== 'rsa-oaep-sha256') {\n throw new Error(`@noy-db/at-aws-kms: awsKmsRecipientSealer.sealForRecipient: unsupported hint.alg '${String(hint.alg)}' (expected 'rsa-oaep-sha256')`)\n }\n const pem = hint.material['publicKeyPem']\n if (typeof pem !== 'string') {\n throw new Error('@noy-db/at-aws-kms: awsKmsRecipientSealer.sealForRecipient: hint.material.publicKeyPem missing or not a string')\n }\n // Grantor seals LOCALLY — no KMS call.\n return sealRsaOaepTlv(plaintext, pem)\n },\n\n async unseal(sealed: Uint8Array): Promise<Uint8Array> {\n const { wrapped, iv, ct } = parseRsaOaepTlv(sealed)\n // RSA-unwrap the CEK via KMS — the private key never leaves KMS.\n const out: DecryptCommandOutput = await client.send(\n new DecryptCommand({\n KeyId: opts.keyId,\n CiphertextBlob: wrapped,\n EncryptionAlgorithm: 'RSAES_OAEP_SHA_256',\n }),\n )\n const cek = out.Plaintext\n if (!cek) throw new Error('@noy-db/at-aws-kms: KMS Decrypt returned no Plaintext (CEK)')\n const cekBytes = cek instanceof Uint8Array ? cek : new Uint8Array(cek)\n const pt = await aesGcmOpen(cekBytes, iv, ct)\n cekBytes.fill(0)\n return pt\n },\n }\n}\n"],"mappings":"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","names":[]}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@noy-db/at-aws-kms",
|
|
3
|
-
"version": "0.2.0-pre.
|
|
3
|
+
"version": "0.2.0-pre.18",
|
|
4
4
|
"description": "AWS KMS sealing key provider for noy-db managed-passphrase mode — seal/unseal via KMS Encrypt/Decrypt, with KMS access logs.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"author": "vLannaAi <vicio@lanna.ai>",
|
|
@@ -40,14 +40,14 @@
|
|
|
40
40
|
},
|
|
41
41
|
"peerDependencies": {
|
|
42
42
|
"@aws-sdk/client-kms": "^3.0.0",
|
|
43
|
-
"@noy-db/hub": "0.2.0-pre.
|
|
43
|
+
"@noy-db/hub": "0.2.0-pre.18"
|
|
44
44
|
},
|
|
45
45
|
"devDependencies": {
|
|
46
46
|
"@types/node": "^22.0.0",
|
|
47
47
|
"@aws-sdk/client-kms": "^3.0.0",
|
|
48
|
-
"@noy-db/hub": "0.2.0-pre.
|
|
49
|
-
"@noy-db/
|
|
50
|
-
"@noy-db/
|
|
48
|
+
"@noy-db/hub": "0.2.0-pre.18",
|
|
49
|
+
"@noy-db/to-memory": "0.2.0-pre.18",
|
|
50
|
+
"@noy-db/on-shamir": "0.2.0-pre.18"
|
|
51
51
|
},
|
|
52
52
|
"keywords": [
|
|
53
53
|
"noy-db",
|