@noy-db/at-aws-kms 0.6.0 → 0.7.0-pre.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +6 -6
- package/dist/index.d.ts +9 -9
- package/dist/index.js +3 -3
- package/dist/index.js.map +1 -1
- package/package.json +7 -6
package/README.md
CHANGED
|
@@ -28,14 +28,14 @@ aws kms create-key --description "noy-db sealing key"
|
|
|
28
28
|
```ts
|
|
29
29
|
// 3. In your app:
|
|
30
30
|
import { createNoydb } from '@noy-db/hub'
|
|
31
|
-
import {
|
|
31
|
+
import { atAwsKms } from '@noy-db/at-aws-kms'
|
|
32
32
|
import { shamirRecoveryProvider } from '@noy-db/on-shamir'
|
|
33
33
|
|
|
34
34
|
const db = await createNoydb({
|
|
35
35
|
store,
|
|
36
36
|
user: 'alice',
|
|
37
37
|
secretMode: 'managed',
|
|
38
|
-
sealingKey:
|
|
38
|
+
sealingKey: atAwsKms({ keyId: 'arn:aws:kms:us-east-1:123456789012:key/abc' }),
|
|
39
39
|
shamirRecovery: shamirRecoveryProvider(),
|
|
40
40
|
})
|
|
41
41
|
|
|
@@ -55,7 +55,7 @@ const vault = await db.openVault('acme')
|
|
|
55
55
|
## When NOT to use this provider
|
|
56
56
|
|
|
57
57
|
- ❌ Non-AWS or multi-cloud deployments where adding an AWS dependency is undesirable. Use [`@noy-db/at-env`](../at-env) for a zero-extra-dependency option.
|
|
58
|
-
- ❌ Local dev / CI where you don't want real KMS calls or AWS credentials in CI. Use [`@noy-db/at-env`](../at-env) or `
|
|
58
|
+
- ❌ Local dev / CI where you don't want real KMS calls or AWS credentials in CI. Use [`@noy-db/at-env`](../at-env) or `MemorySealer` from `@noy-db/hub` instead.
|
|
59
59
|
|
|
60
60
|
## Key rotation
|
|
61
61
|
|
|
@@ -64,15 +64,15 @@ KMS supports automatic key rotation for symmetric keys. Enable it on the CMK and
|
|
|
64
64
|
## API
|
|
65
65
|
|
|
66
66
|
```ts
|
|
67
|
-
function
|
|
67
|
+
function atAwsKms(opts: {
|
|
68
68
|
keyId: string // KMS key id or full ARN
|
|
69
69
|
client?: Pick<KMSClient, 'send'> // optional pre-built client (useful for tests)
|
|
70
|
-
}):
|
|
70
|
+
}): NoydbSealer
|
|
71
71
|
```
|
|
72
72
|
|
|
73
73
|
Never pass raw AWS credentials in the options — inject a pre-configured `KMSClient` for non-default auth. The default `new KMSClient({})` resolves credentials via the SDK's ambient chain.
|
|
74
74
|
|
|
75
|
-
Returns a [`
|
|
75
|
+
Returns a [`NoydbSealer`](../hub/src/port/at/index.ts) — importable as `@noy-db/hub/at` — the contract `@noy-db/hub`'s managed-secret mode consumes.
|
|
76
76
|
|
|
77
77
|
## License
|
|
78
78
|
|
package/dist/index.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { NoydbSealer, RecipientSealer } from '@noy-db/hub/at';
|
|
2
2
|
import { KMSClient } from '@aws-sdk/client-kms';
|
|
3
3
|
|
|
4
4
|
/**
|
|
@@ -35,14 +35,14 @@ import { KMSClient } from '@aws-sdk/client-kms';
|
|
|
35
35
|
* ```ts
|
|
36
36
|
* // 3. In your app:
|
|
37
37
|
* import { createNoydb } from '@noy-db/hub'
|
|
38
|
-
* import {
|
|
38
|
+
* import { atAwsKms } from '@noy-db/at-aws-kms'
|
|
39
39
|
* import { shamirRecoveryProvider } from '@noy-db/on-shamir'
|
|
40
40
|
*
|
|
41
41
|
* const db = await createNoydb({
|
|
42
42
|
* store,
|
|
43
43
|
* user: 'alice',
|
|
44
44
|
* secretMode: 'managed',
|
|
45
|
-
* sealingKey:
|
|
45
|
+
* sealingKey: atAwsKms({ keyId: 'arn:aws:kms:us-east-1:123:key/abc' }),
|
|
46
46
|
* shamirRecovery: shamirRecoveryProvider(),
|
|
47
47
|
* })
|
|
48
48
|
* ```
|
|
@@ -50,15 +50,15 @@ import { KMSClient } from '@aws-sdk/client-kms';
|
|
|
50
50
|
* @packageDocumentation
|
|
51
51
|
*/
|
|
52
52
|
|
|
53
|
-
/** Options for {@link
|
|
54
|
-
interface
|
|
53
|
+
/** Options for {@link atAwsKms}. */
|
|
54
|
+
interface AtAwsKmsOptions {
|
|
55
55
|
/** KMS key id or ARN (e.g. `arn:aws:kms:us-east-1:123:key/abc`). */
|
|
56
56
|
readonly keyId: string;
|
|
57
57
|
/** Optional pre-built client (DI for tests). Default `new KMSClient({})` (ambient creds). */
|
|
58
58
|
readonly client?: Pick<KMSClient, 'send'>;
|
|
59
59
|
}
|
|
60
60
|
/**
|
|
61
|
-
* Build a {@link
|
|
61
|
+
* Build a {@link NoydbSealer} backed by AWS KMS Encrypt / Decrypt.
|
|
62
62
|
*
|
|
63
63
|
* Credentials are resolved by the SDK's ambient chain — IAM instance roles,
|
|
64
64
|
* environment variables, or `~/.aws/credentials`. Never pass raw credentials
|
|
@@ -68,7 +68,7 @@ interface AwsKmsSealingProviderOptions {
|
|
|
68
68
|
* against unexpected SDK-response shapes).
|
|
69
69
|
* Any KMS API error (AccessDenied, InvalidKeyUsage, etc.) propagates as-is.
|
|
70
70
|
*/
|
|
71
|
-
declare function
|
|
71
|
+
declare function atAwsKms(opts: AtAwsKmsOptions): NoydbSealer;
|
|
72
72
|
/** Options for {@link awsKmsRecipientSealer}. */
|
|
73
73
|
interface AwsKmsRecipientSealerOptions {
|
|
74
74
|
/**
|
|
@@ -97,7 +97,7 @@ interface AwsKmsRecipientSealerOptions {
|
|
|
97
97
|
* wire-compatible (RSAES-OAEP, SHA-256, MGF1-SHA256, empty label). KMS
|
|
98
98
|
* asymmetric keys do not support an encryption context, so none is used.
|
|
99
99
|
*
|
|
100
|
-
* Separate from {@link
|
|
100
|
+
* Separate from {@link atAwsKms} (symmetric self-seal for the
|
|
101
101
|
* managed secret) — this factory targets an asymmetric key.
|
|
102
102
|
*
|
|
103
103
|
* @throws Error from `publishRecipientHint` when the key is not an RSA
|
|
@@ -109,4 +109,4 @@ declare function awsKmsRecipientSealer(opts: AwsKmsRecipientSealerOptions): Reci
|
|
|
109
109
|
unseal(sealed: Uint8Array): Promise<Uint8Array>;
|
|
110
110
|
};
|
|
111
111
|
|
|
112
|
-
export { type
|
|
112
|
+
export { type AtAwsKmsOptions, type AwsKmsRecipientSealerOptions, atAwsKms, awsKmsRecipientSealer };
|
package/dist/index.js
CHANGED
|
@@ -6,7 +6,7 @@ import {
|
|
|
6
6
|
DecryptCommand,
|
|
7
7
|
GetPublicKeyCommand
|
|
8
8
|
} from "@aws-sdk/client-kms";
|
|
9
|
-
function
|
|
9
|
+
function atAwsKms(opts) {
|
|
10
10
|
const client = opts.client ?? new KMSClient({});
|
|
11
11
|
return {
|
|
12
12
|
id: `aws-kms:${opts.keyId}`,
|
|
@@ -92,7 +92,7 @@ function awsKmsRecipientSealer(opts) {
|
|
|
92
92
|
};
|
|
93
93
|
}
|
|
94
94
|
export {
|
|
95
|
-
|
|
96
|
-
|
|
95
|
+
atAwsKms,
|
|
96
|
+
awsKmsRecipientSealer
|
|
97
97
|
};
|
|
98
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-secret mode.\n *\n * An `at-*` provider that seals and unseals the hub-generated random\n * secret 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 * secretMode: '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(secret) {\n const out: EncryptCommandOutput = await client.send(\n new EncryptCommand({ KeyId: opts.keyId, Plaintext: secret }),\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 secret) — 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":[]}
|
|
1
|
+
{"version":3,"sources":["../src/index.ts"],"sourcesContent":["/**\n * **@noy-db/at-aws-kms** — AWS KMS sealing key provider for noy-db\n * managed-secret mode.\n *\n * An `at-*` provider that seals and unseals the hub-generated random\n * secret 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 { atAwsKms } 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 * secretMode: 'managed',\n * sealingKey: atAwsKms({ keyId: 'arn:aws:kms:us-east-1:123:key/abc' }),\n * shamirRecovery: shamirRecoveryProvider(),\n * })\n * ```\n *\n * @packageDocumentation\n */\n\nimport type { NoydbSealer, RecipientSealer, RecipientHint } from '@noy-db/hub/at'\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 atAwsKms}. */\nexport interface AtAwsKmsOptions {\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 NoydbSealer} 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 atAwsKms(opts: AtAwsKmsOptions): NoydbSealer {\n const client = opts.client ?? new KMSClient({})\n return {\n id: `aws-kms:${opts.keyId}`,\n\n async seal(secret) {\n const out: EncryptCommandOutput = await client.send(\n new EncryptCommand({ KeyId: opts.keyId, Plaintext: secret }),\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 atAwsKms} (symmetric self-seal for the\n * managed secret) — 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.
|
|
3
|
+
"version": "0.7.0-pre.1",
|
|
4
4
|
"description": "AWS KMS sealing key provider for noy-db managed-secret mode — seal/unseal via KMS Encrypt/Decrypt, with KMS access logs.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"author": "vLannaAi <vicio@lanna.ai>",
|
|
@@ -33,14 +33,15 @@
|
|
|
33
33
|
},
|
|
34
34
|
"peerDependencies": {
|
|
35
35
|
"@aws-sdk/client-kms": "^3.0.0",
|
|
36
|
-
"@noy-db/hub": "0.
|
|
36
|
+
"@noy-db/hub": "0.7.0-pre.1"
|
|
37
37
|
},
|
|
38
38
|
"devDependencies": {
|
|
39
|
-
"@types/node": "^22.0.0",
|
|
40
39
|
"@aws-sdk/client-kms": "^3.0.0",
|
|
41
|
-
"@
|
|
42
|
-
"@noy-db/
|
|
43
|
-
"@noy-db/on-shamir": "0.
|
|
40
|
+
"@types/node": "^22.0.0",
|
|
41
|
+
"@noy-db/hub": "0.7.0-pre.1",
|
|
42
|
+
"@noy-db/on-shamir": "0.7.0-pre.1",
|
|
43
|
+
"@noy-db/test-sealer-conformance": "0.7.0-pre.1",
|
|
44
|
+
"@noy-db/to-memory": "0.7.0-pre.1"
|
|
44
45
|
},
|
|
45
46
|
"keywords": [
|
|
46
47
|
"noy-db",
|