@seekrit/cli 0.16.0 → 0.17.0

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.js CHANGED
@@ -595,6 +595,44 @@ z.object({
595
595
  environmentId: z.string().min(1).nullish(),
596
596
  expiresAt: z.iso.datetime().nullish()
597
597
  });
598
+ const kmsKeyPurposeSchema = z.enum(["encrypt", "sign"]);
599
+ const kmsKeySpecSchema = z.enum(["aes-256-gcm", "ecdsa-p256"]);
600
+ /** A wrapped key grant supplied by the client (server never sees plaintext material). */
601
+ const kmsGrantInputSchema = z.object({
602
+ principalType: principalTypeSchema,
603
+ principalId: z.string().min(1),
604
+ /** Key material wrapped to the principal's public key (`wd1.` blob). */
605
+ wrappedKey: z.string().min(1)
606
+ });
607
+ z.object({
608
+ name: nameSchema,
609
+ purpose: kmsKeyPurposeSchema,
610
+ spec: kmsKeySpecSchema,
611
+ applicationId: z.string().min(1).nullish(),
612
+ groupId: z.string().min(1).nullish(),
613
+ /** ECDSA P-256 public key (JWK) for `sign` keys; omit for `encrypt` keys. */
614
+ publicKeyJwk: z.string().min(1).nullish(),
615
+ grants: z.array(kmsGrantInputSchema).min(1)
616
+ }).refine((v) => !(v.applicationId && v.groupId), {
617
+ message: "a key may be scoped to an application or a group, not both",
618
+ path: ["groupId"]
619
+ }).refine((v) => v.purpose === "sign" === (v.spec === "ecdsa-p256"), {
620
+ message: "sign keys require spec ecdsa-p256; encrypt keys require aes-256-gcm",
621
+ path: ["spec"]
622
+ }).refine((v) => v.purpose === "sign" === (v.publicKeyJwk != null), {
623
+ message: "sign keys require a publicKeyJwk; encrypt keys must omit it",
624
+ path: ["publicKeyJwk"]
625
+ });
626
+ z.object({
627
+ principalType: principalTypeSchema,
628
+ principalId: z.string().min(1),
629
+ /** Current-version key material wrapped to the principal's public key. */
630
+ wrappedKey: z.string().min(1)
631
+ });
632
+ z.object({
633
+ publicKeyJwk: z.string().min(1).nullish(),
634
+ grants: z.array(kmsGrantInputSchema).min(1)
635
+ });
598
636
  z.object({ prefs: z.partialRecord(z.enum(NOTIFICATION_TYPES), z.boolean()) });
599
637
  z.object({
600
638
  endpoint: z.url().max(2048),
@@ -663,7 +701,7 @@ function splitBlob(blob, prefix, segments) {
663
701
  //#endregion
664
702
  //#region ../../packages/crypto/src/aes.ts
665
703
  const SECRET_PREFIX = "sc1";
666
- const IV_LENGTH = 12;
704
+ const IV_LENGTH$1 = 12;
667
705
  /** Generate a fresh 256-bit data encryption key for an environment. */
668
706
  function generateDek() {
669
707
  return crypto.getRandomValues(/* @__PURE__ */ new Uint8Array(32));
@@ -680,7 +718,7 @@ async function importDek(dek, usage) {
680
718
  */
681
719
  async function encryptSecret(dek, plaintext, aad) {
682
720
  const key = await importDek(dek, "encrypt");
683
- const iv = crypto.getRandomValues(new Uint8Array(IV_LENGTH));
721
+ const iv = crypto.getRandomValues(new Uint8Array(IV_LENGTH$1));
684
722
  const ciphertext = await crypto.subtle.encrypt({
685
723
  name: "AES-GCM",
686
724
  iv,
@@ -865,6 +903,137 @@ async function unwrapGcpCredential(wrapped, privateKeyJwk) {
865
903
  };
866
904
  }
867
905
  //#endregion
906
+ //#region ../../packages/crypto/src/kms.ts
907
+ /**
908
+ * Client-side KMS envelope encryption. A managed `encrypt` key is a 256-bit
909
+ * AES-GCM key whose material reaches the client only as a `wd1.` grant (wrapped
910
+ * to the principal's public key). These helpers operate on that material
911
+ * directly — the server never sees plaintext or key material, exactly as for
912
+ * environment DEKs.
913
+ *
914
+ * Blob formats:
915
+ * `ce1.<keyId>.<version>.<iv>.<ciphertext>` — Encrypt output
916
+ * `dk1.<keyId>.<version>.<iv>.<ciphertext>` — GenerateDataKey wrapped key
917
+ *
918
+ * The keyId + version travel in the blob (so Decrypt can select the right key
919
+ * version) and are folded into the AAD (so a blob can't be replayed under a
920
+ * different key/version). For `ce1` the caller's optional encryption *context*
921
+ * is also bound — Decrypt must supply the same context, mirroring AWS KMS.
922
+ */
923
+ const ENCRYPT_PREFIX = "ce1";
924
+ const DATAKEY_PREFIX = "dk1";
925
+ const IV_LENGTH = 12;
926
+ const KEY_LENGTH = 32;
927
+ /** Generate fresh 256-bit material for an `encrypt` KMS key. */
928
+ function generateEncryptKeyMaterial() {
929
+ return crypto.getRandomValues(new Uint8Array(KEY_LENGTH));
930
+ }
931
+ async function importAesKey(material, usage) {
932
+ return crypto.subtle.importKey("raw", material, { name: "AES-GCM" }, false, [usage]);
933
+ }
934
+ function encryptAad(ref, context) {
935
+ return `${ref.keyId}/${ref.version}/${context}`;
936
+ }
937
+ function dataKeyAad(keyId, version) {
938
+ return `${keyId}/${version}`;
939
+ }
940
+ function parseVersion(versionStr) {
941
+ const version = Number(versionStr);
942
+ if (!Number.isInteger(version) || version < 1) throw new SeekritCryptoError("MALFORMED_BLOB", "invalid key version in KMS blob");
943
+ return version;
944
+ }
945
+ /** Read the key id + version a `ce1`/`dk1` blob was produced under. */
946
+ function kmsBlobKeyRef(blob) {
947
+ const prefix = blob.split(".")[0];
948
+ if (prefix !== ENCRYPT_PREFIX && prefix !== DATAKEY_PREFIX) throw new SeekritCryptoError("UNSUPPORTED_VERSION", `not a KMS blob: "${prefix ?? ""}"`);
949
+ const [keyId, versionStr] = splitBlob(blob, prefix, 4);
950
+ return {
951
+ keyId,
952
+ version: parseVersion(versionStr)
953
+ };
954
+ }
955
+ /** Encrypt a value under a managed key. `context` (bound as AAD) defaults to empty. */
956
+ async function kmsEncrypt(material, ref, plaintext, context = "") {
957
+ const key = await importAesKey(material, "encrypt");
958
+ const iv = crypto.getRandomValues(new Uint8Array(IV_LENGTH));
959
+ const ciphertext = await crypto.subtle.encrypt({
960
+ name: "AES-GCM",
961
+ iv,
962
+ additionalData: utf8Encode(encryptAad(ref, context))
963
+ }, key, utf8Encode(plaintext));
964
+ return [
965
+ ENCRYPT_PREFIX,
966
+ ref.keyId,
967
+ String(ref.version),
968
+ toBase64Url(iv),
969
+ toBase64Url(new Uint8Array(ciphertext))
970
+ ].join(".");
971
+ }
972
+ /**
973
+ * Decrypt a `ce1` blob. The caller supplies the material for the key version
974
+ * named in the blob (see `kmsBlobKeyRef`) and the same `context` used to
975
+ * encrypt.
976
+ */
977
+ async function kmsDecrypt(material, blob, context = "") {
978
+ const [keyId, versionStr, ivB64, ctB64] = splitBlob(blob, ENCRYPT_PREFIX, 4);
979
+ const ref = {
980
+ keyId,
981
+ version: parseVersion(versionStr)
982
+ };
983
+ const key = await importAesKey(material, "decrypt");
984
+ try {
985
+ const plaintext = await crypto.subtle.decrypt({
986
+ name: "AES-GCM",
987
+ iv: fromBase64Url(ivB64),
988
+ additionalData: utf8Encode(encryptAad(ref, context))
989
+ }, key, fromBase64Url(ctB64));
990
+ return utf8Decode(new Uint8Array(plaintext));
991
+ } catch {
992
+ throw new SeekritCryptoError("DECRYPT_FAILED", "KMS decryption failed: wrong key/version, tampered data, or mismatched context");
993
+ }
994
+ }
995
+ /**
996
+ * Generate a fresh data key wrapped under a managed key — the envelope pattern
997
+ * for large payloads (AWS KMS GenerateDataKey). Encrypt bulk data with
998
+ * `plaintext`, store `wrapped` alongside it, and recover the key later with
999
+ * `decryptDataKey`.
1000
+ */
1001
+ async function generateDataKey(material, ref) {
1002
+ const plaintext = crypto.getRandomValues(new Uint8Array(KEY_LENGTH));
1003
+ const key = await importAesKey(material, "encrypt");
1004
+ const iv = crypto.getRandomValues(new Uint8Array(IV_LENGTH));
1005
+ const ciphertext = await crypto.subtle.encrypt({
1006
+ name: "AES-GCM",
1007
+ iv,
1008
+ additionalData: utf8Encode(dataKeyAad(ref.keyId, String(ref.version)))
1009
+ }, key, plaintext);
1010
+ return {
1011
+ plaintext,
1012
+ wrapped: [
1013
+ DATAKEY_PREFIX,
1014
+ ref.keyId,
1015
+ String(ref.version),
1016
+ toBase64Url(iv),
1017
+ toBase64Url(new Uint8Array(ciphertext))
1018
+ ].join(".")
1019
+ };
1020
+ }
1021
+ /** Recover a data key previously produced by `generateDataKey`. */
1022
+ async function decryptDataKey(material, wrapped) {
1023
+ const [keyId, versionStr, ivB64, ctB64] = splitBlob(wrapped, DATAKEY_PREFIX, 4);
1024
+ const key = await importAesKey(material, "decrypt");
1025
+ try {
1026
+ const dk = await crypto.subtle.decrypt({
1027
+ name: "AES-GCM",
1028
+ iv: fromBase64Url(ivB64),
1029
+ additionalData: utf8Encode(dataKeyAad(keyId, versionStr))
1030
+ }, key, fromBase64Url(ctB64));
1031
+ return new Uint8Array(dk);
1032
+ } catch {
1033
+ throw new SeekritCryptoError("DECRYPT_FAILED", "data key unwrap failed: wrong key or tampered blob");
1034
+ }
1035
+ }
1036
+ //#endregion
868
1037
  //#region ../../packages/crypto/src/mongodb.ts
869
1038
  /** Generate the ephemeral P-256 keypair a client uses to receive one MongoDB lease. */
870
1039
  async function generateMongoRecipientKeyPair() {
@@ -1155,6 +1324,69 @@ async function generatePostgresCredential(options = {}) {
1155
1324
  };
1156
1325
  }
1157
1326
  //#endregion
1327
+ //#region ../../packages/crypto/src/sign.ts
1328
+ /**
1329
+ * Managed signing keys for the client-side KMS — ECDSA over P-256 (the curve
1330
+ * already used for principal keypairs; universal in WebCrypto). A `sign` key's
1331
+ * private half reaches the client only as a `wd1.` grant wrapping its PKCS8
1332
+ * bytes; the public half is published per version so verification needs no
1333
+ * grant. Signatures are `sg1.<keyId>.<version>.<signature>` — the keyId +
1334
+ * version let a verifier fetch the matching version's public key.
1335
+ */
1336
+ const ECDSA_PARAMS = {
1337
+ name: "ECDSA",
1338
+ namedCurve: "P-256"
1339
+ };
1340
+ const ECDSA_SIGN = {
1341
+ name: "ECDSA",
1342
+ hash: "SHA-256"
1343
+ };
1344
+ const SIGN_PREFIX = "sg1";
1345
+ /** Generate a fresh signing keypair for a `sign` KMS key (or a new version). */
1346
+ async function generateSigningKeyMaterial() {
1347
+ const pair = await crypto.subtle.generateKey(ECDSA_PARAMS, true, ["sign", "verify"]);
1348
+ const [publicJwk, pkcs8] = await Promise.all([crypto.subtle.exportKey("jwk", pair.publicKey), crypto.subtle.exportKey("pkcs8", pair.privateKey)]);
1349
+ return {
1350
+ publicKeyJwk: JSON.stringify(publicJwk),
1351
+ privateKeyPkcs8: new Uint8Array(pkcs8)
1352
+ };
1353
+ }
1354
+ /** Import the wrapped-then-unwrapped PKCS8 private key for signing. */
1355
+ async function importSigningKey(pkcs8) {
1356
+ return crypto.subtle.importKey("pkcs8", pkcs8, ECDSA_PARAMS, false, ["sign"]);
1357
+ }
1358
+ /** Import a published public key (JWK) for verification. */
1359
+ async function importVerifyingKey(publicKeyJwk) {
1360
+ return crypto.subtle.importKey("jwk", JSON.parse(publicKeyJwk), ECDSA_PARAMS, false, ["verify"]);
1361
+ }
1362
+ /** Sign a message with a managed signing key; returns an `sg1.` blob. */
1363
+ async function signMessage(privateKey, ref, message) {
1364
+ const data = typeof message === "string" ? utf8Encode(message) : message;
1365
+ const sig = new Uint8Array(await crypto.subtle.sign(ECDSA_SIGN, privateKey, data));
1366
+ return [
1367
+ SIGN_PREFIX,
1368
+ ref.keyId,
1369
+ String(ref.version),
1370
+ toBase64Url(sig)
1371
+ ].join(".");
1372
+ }
1373
+ /** Verify an `sg1.` signature over a message with the version's public key. */
1374
+ async function verifyMessage(publicKey, signature, message) {
1375
+ const [, , sigB64] = splitBlob(signature, SIGN_PREFIX, 3);
1376
+ const data = typeof message === "string" ? utf8Encode(message) : message;
1377
+ return crypto.subtle.verify(ECDSA_SIGN, publicKey, fromBase64Url(sigB64), data);
1378
+ }
1379
+ /** Read the key id + version an `sg1.` signature was produced under. */
1380
+ function signatureKeyRef(signature) {
1381
+ const [keyId, versionStr] = splitBlob(signature, SIGN_PREFIX, 3);
1382
+ const version = Number(versionStr);
1383
+ if (!Number.isInteger(version) || version < 1) throw new SeekritCryptoError("MALFORMED_BLOB", "invalid key version in signature");
1384
+ return {
1385
+ keyId,
1386
+ version
1387
+ };
1388
+ }
1389
+ //#endregion
1158
1390
  //#region ../../packages/crypto/src/ssh.ts
1159
1391
  /**
1160
1392
  * Client-side SSH certificate authority for minting *temporary SSH access*
@@ -1344,7 +1576,7 @@ function isServiceToken(value) {
1344
1576
  }
1345
1577
  //#endregion
1346
1578
  //#region package.json
1347
- var version = "0.16.0";
1579
+ var version = "0.17.0";
1348
1580
  //#endregion
1349
1581
  //#region ../../packages/api-client/src/index.ts
1350
1582
  var SeekritApiError = class extends Error {
@@ -1516,6 +1748,42 @@ var SeekritClient = class {
1516
1748
  revokeToken(orgId, tokenId) {
1517
1749
  return this.request("DELETE", `/v1/orgs/${orgId}/tokens/${tokenId}`);
1518
1750
  }
1751
+ /** Keys the caller can see: all org keys for admins, granted keys otherwise. */
1752
+ listKmsKeys(orgId) {
1753
+ return this.request("GET", `/v1/orgs/${orgId}/kms/keys`);
1754
+ }
1755
+ createKmsKey(orgId, input) {
1756
+ return this.request("POST", `/v1/orgs/${orgId}/kms/keys`, input);
1757
+ }
1758
+ /** Key metadata + every version (admin). */
1759
+ getKmsKey(orgId, keyId) {
1760
+ return this.request("GET", `/v1/orgs/${orgId}/kms/keys/${keyId}`);
1761
+ }
1762
+ /** The caller's wrapped key material for a key, across granted versions. */
1763
+ getMyKmsKey(orgId, keyId) {
1764
+ return this.request("GET", `/v1/orgs/${orgId}/kms/keys/${keyId}/key`);
1765
+ }
1766
+ /** Published public keys of a `sign` key (grant-free within the org). */
1767
+ getKmsPublicKeys(orgId, keyId) {
1768
+ return this.request("GET", `/v1/orgs/${orgId}/kms/keys/${keyId}/public`);
1769
+ }
1770
+ listKmsGrants(orgId, keyId) {
1771
+ return this.request("GET", `/v1/orgs/${orgId}/kms/keys/${keyId}/grants`);
1772
+ }
1773
+ grantKmsKey(orgId, keyId, input) {
1774
+ return this.request("POST", `/v1/orgs/${orgId}/kms/keys/${keyId}/grants`, input);
1775
+ }
1776
+ /** Revoke a principal entirely (all versions). */
1777
+ revokeKmsKey(orgId, keyId, principal) {
1778
+ const qs = new URLSearchParams(principal).toString();
1779
+ return this.request("DELETE", `/v1/orgs/${orgId}/kms/keys/${keyId}/grants?${qs}`);
1780
+ }
1781
+ rotateKmsKey(orgId, keyId, input) {
1782
+ return this.request("POST", `/v1/orgs/${orgId}/kms/keys/${keyId}/rotate`, input);
1783
+ }
1784
+ disableKmsKey(orgId, keyId) {
1785
+ return this.request("POST", `/v1/orgs/${orgId}/kms/keys/${keyId}/disable`);
1786
+ }
1519
1787
  /** The broker's public key — wrap the admin credential to it before registering a target. */
1520
1788
  getLeaseBrokerKey(orgId) {
1521
1789
  return this.request("GET", `/v1/orgs/${orgId}/leases/broker-key`);
@@ -2121,6 +2389,286 @@ function registerGcpCommands(program) {
2121
2389
  });
2122
2390
  }
2123
2391
  //#endregion
2392
+ //#region src/kms.ts
2393
+ /** Collect a repeatable option into a list. */
2394
+ function collect$5(value, acc = []) {
2395
+ acc.push(value);
2396
+ return acc;
2397
+ }
2398
+ /** The calling principal's identity + public key (for a self-grant). */
2399
+ async function kmsCallerIdentity(ctx) {
2400
+ if (ctx.auth.type === "bearer" && isServiceToken(ctx.auth.token)) {
2401
+ const { tokenId, privateKey } = await parseServiceToken(ctx.auth.token);
2402
+ const { d: _d, key_ops: _ops, ext: _ext, ...pub } = await crypto.subtle.exportKey("jwk", privateKey);
2403
+ return {
2404
+ principalType: "service_token",
2405
+ principalId: tokenId,
2406
+ publicKeyJwk: JSON.stringify(pub)
2407
+ };
2408
+ }
2409
+ const { user } = await ctx.client.me();
2410
+ if (!user.publicKeyJwk) fail("run `seekrit keys setup` first");
2411
+ return {
2412
+ principalType: "user",
2413
+ principalId: user.id,
2414
+ publicKeyJwk: user.publicKeyJwk
2415
+ };
2416
+ }
2417
+ /** Look up an org member (by email) or service token (by id) as a grant recipient. */
2418
+ async function kmsResolveRecipient(ctx, orgId, who) {
2419
+ if (who.user) {
2420
+ const { members } = await ctx.client.listMembers(orgId);
2421
+ const m = members.find((x) => x.email === who.user);
2422
+ if (!m) fail(`no member ${who.user}`);
2423
+ if (!m.publicKeyJwk) fail(`${who.user} has not completed key setup`);
2424
+ return {
2425
+ principalType: "user",
2426
+ principalId: m.userId,
2427
+ publicKeyJwk: m.publicKeyJwk
2428
+ };
2429
+ }
2430
+ if (who.token) {
2431
+ const { tokens } = await ctx.client.listTokens(orgId);
2432
+ const t = tokens.find((x) => x.id === who.token);
2433
+ if (!t) fail(`no service token ${who.token}`);
2434
+ return {
2435
+ principalType: "service_token",
2436
+ principalId: t.id,
2437
+ publicKeyJwk: t.publicKeyJwk
2438
+ };
2439
+ }
2440
+ fail("specify --user <email> or --token <id>");
2441
+ }
2442
+ async function kmsResolveKey(ctx, orgId, ref) {
2443
+ const { keys } = await ctx.client.listKmsKeys(orgId);
2444
+ const key = keys.find((k) => k.id === ref || k.name === ref);
2445
+ if (!key) fail(`no KMS key "${ref}"`);
2446
+ return key;
2447
+ }
2448
+ /** Recover a key's material for one version (default: current), for the caller. */
2449
+ async function kmsRecoverMaterial(ctx, orgId, keyId, version) {
2450
+ const mat = await ctx.client.getMyKmsKey(orgId, keyId);
2451
+ const v = version ?? mat.currentVersion;
2452
+ const grant = mat.grants.find((g) => g.version === v);
2453
+ if (!grant) fail(`no grant for version ${v} of this key`);
2454
+ return {
2455
+ material: await unwrapDek(grant.wrappedKey, await getPrivateKey(ctx)),
2456
+ version: v,
2457
+ currentVersion: mat.currentVersion
2458
+ };
2459
+ }
2460
+ function registerKmsCommands(program) {
2461
+ const kms = program.command("kms").description("managed keys for application-layer encryption & signing (client-side)");
2462
+ kms.command("create").description("create a managed key (material is generated locally and wrapped, never sent)").requiredOption("--name <name>", "org-unique key name").requiredOption("--purpose <purpose>", "encrypt | sign").option("--org <slug>").option("--app <slug>", "scope the key to an application").option("--group <slug>", "scope the key to a group").option("--grant-user <email>", "also grant an org member (repeatable)", collect$5, []).option("--grant-token <tokenId>", "also grant a service token (repeatable)", collect$5, []).action(async (options) => {
2463
+ if (options.purpose !== "encrypt" && options.purpose !== "sign") fail("--purpose must be encrypt or sign");
2464
+ if (options.app && options.group) fail("pass at most one of --app or --group");
2465
+ const ctx = buildContext();
2466
+ const org = await resolveOrg(ctx, options.org);
2467
+ let toWrap;
2468
+ let publicKeyJwk;
2469
+ if (options.purpose === "encrypt") toWrap = generateEncryptKeyMaterial();
2470
+ else {
2471
+ const km = await generateSigningKeyMaterial();
2472
+ toWrap = km.privateKeyPkcs8;
2473
+ publicKeyJwk = km.publicKeyJwk;
2474
+ }
2475
+ let applicationId;
2476
+ let groupId;
2477
+ if (options.app) {
2478
+ const { apps } = await ctx.client.listApps(org.id);
2479
+ const app = apps.find((a) => a.slug === options.app || a.id === options.app);
2480
+ if (!app) fail(`no app "${options.app}" in ${org.slug}`);
2481
+ applicationId = app.id;
2482
+ } else if (options.group) {
2483
+ const { groups } = await ctx.client.listGroups(org.id);
2484
+ const group = groups.find((g) => g.slug === options.group || g.id === options.group);
2485
+ if (!group) fail(`no group "${options.group}" in ${org.slug}`);
2486
+ groupId = group.id;
2487
+ }
2488
+ const recipients = [await kmsCallerIdentity(ctx)];
2489
+ for (const email of options.grantUser) recipients.push(await kmsResolveRecipient(ctx, org.id, { user: email }));
2490
+ for (const tokenId of options.grantToken) recipients.push(await kmsResolveRecipient(ctx, org.id, { token: tokenId }));
2491
+ const seen = /* @__PURE__ */ new Set();
2492
+ const grants = [];
2493
+ for (const r of recipients) {
2494
+ const dedupeKey = `${r.principalType}:${r.principalId}`;
2495
+ if (seen.has(dedupeKey)) continue;
2496
+ seen.add(dedupeKey);
2497
+ grants.push({
2498
+ principalType: r.principalType,
2499
+ principalId: r.principalId,
2500
+ wrappedKey: await wrapDek(toWrap, r.publicKeyJwk)
2501
+ });
2502
+ }
2503
+ const input = {
2504
+ name: options.name,
2505
+ purpose: options.purpose,
2506
+ spec: options.purpose === "sign" ? "ecdsa-p256" : "aes-256-gcm",
2507
+ ...applicationId ? { applicationId } : {},
2508
+ ...groupId ? { groupId } : {},
2509
+ ...publicKeyJwk ? { publicKeyJwk } : {},
2510
+ grants
2511
+ };
2512
+ const { key } = await ctx.client.createKmsKey(org.id, input);
2513
+ console.error(`created ${key.purpose} key ${key.name} (${key.id}), ${grants.length} grant(s)`);
2514
+ });
2515
+ kms.command("ls").description("list keys you can see").option("--org <slug>").action(async (options) => {
2516
+ const ctx = buildContext();
2517
+ const org = await resolveOrg(ctx, options.org);
2518
+ const { keys } = await ctx.client.listKmsKeys(org.id);
2519
+ if (keys.length === 0) {
2520
+ console.error("(no keys)");
2521
+ return;
2522
+ }
2523
+ for (const k of keys) {
2524
+ const scope = k.applicationId ? `app:${k.applicationId}` : k.groupId ? `group:${k.groupId}` : "org";
2525
+ const state = k.disabledAt ? " [disabled]" : "";
2526
+ console.log(`${k.name}\t${k.purpose}\tv${k.currentVersion}\t${scope}\t${k.id}${state}`);
2527
+ }
2528
+ });
2529
+ kms.command("grant").description("grant a principal use of a key's current version").requiredOption("--key <name>", "key name or id").option("--org <slug>").option("--user <email>", "grant an org member").option("--token <tokenId>", "grant a service token").action(async (options) => {
2530
+ if (!options.user === !options.token) fail("pass exactly one of --user or --token");
2531
+ const ctx = buildContext();
2532
+ const org = await resolveOrg(ctx, options.org);
2533
+ const key = await kmsResolveKey(ctx, org.id, options.key);
2534
+ const recipient = await kmsResolveRecipient(ctx, org.id, options);
2535
+ const { material } = await kmsRecoverMaterial(ctx, org.id, key.id);
2536
+ await ctx.client.grantKmsKey(org.id, key.id, {
2537
+ principalType: recipient.principalType,
2538
+ principalId: recipient.principalId,
2539
+ wrappedKey: await wrapDek(material, recipient.publicKeyJwk)
2540
+ });
2541
+ console.error(`granted ${key.name} to ${recipient.principalId}`);
2542
+ });
2543
+ kms.command("revoke").description("revoke a principal from a key (all versions)").requiredOption("--key <name>", "key name or id").option("--org <slug>").option("--user <email>").option("--token <tokenId>").action(async (options) => {
2544
+ if (!options.user === !options.token) fail("pass exactly one of --user or --token");
2545
+ const ctx = buildContext();
2546
+ const org = await resolveOrg(ctx, options.org);
2547
+ const key = await kmsResolveKey(ctx, org.id, options.key);
2548
+ const recipient = await kmsResolveRecipient(ctx, org.id, options);
2549
+ await ctx.client.revokeKmsKey(org.id, key.id, {
2550
+ principalType: recipient.principalType,
2551
+ principalId: recipient.principalId
2552
+ });
2553
+ console.error(`revoked ${recipient.principalId} from ${key.name}`);
2554
+ });
2555
+ kms.command("rotate").description("add a new key version and re-wrap it for every current grantee").requiredOption("--key <name>", "key name or id").option("--org <slug>").action(async (options) => {
2556
+ const ctx = buildContext();
2557
+ const org = await resolveOrg(ctx, options.org);
2558
+ const key = await kmsResolveKey(ctx, org.id, options.key);
2559
+ let toWrap;
2560
+ let publicKeyJwk;
2561
+ if (key.purpose === "encrypt") toWrap = generateEncryptKeyMaterial();
2562
+ else {
2563
+ const km = await generateSigningKeyMaterial();
2564
+ toWrap = km.privateKeyPkcs8;
2565
+ publicKeyJwk = km.publicKeyJwk;
2566
+ }
2567
+ const { grants: current } = await ctx.client.listKmsGrants(org.id, key.id);
2568
+ const [{ members }, { tokens }] = await Promise.all([ctx.client.listMembers(org.id), ctx.client.listTokens(org.id)]);
2569
+ const grants = [];
2570
+ for (const g of current) {
2571
+ const pub = g.principalType === "user" ? members.find((m) => m.userId === g.principalId)?.publicKeyJwk : tokens.find((t) => t.id === g.principalId)?.publicKeyJwk;
2572
+ if (!pub) {
2573
+ console.error(`skipping ${g.principalType} ${g.principalId} (no public key)`);
2574
+ continue;
2575
+ }
2576
+ grants.push({
2577
+ principalType: g.principalType,
2578
+ principalId: g.principalId,
2579
+ wrappedKey: await wrapDek(toWrap, pub)
2580
+ });
2581
+ }
2582
+ if (grants.length === 0) fail("no grantees with public keys to re-wrap for");
2583
+ const { key: rotated } = await ctx.client.rotateKmsKey(org.id, key.id, {
2584
+ ...publicKeyJwk ? { publicKeyJwk } : {},
2585
+ grants
2586
+ });
2587
+ console.error(`rotated ${rotated.name} to v${rotated.currentVersion} (${grants.length} grantees)`);
2588
+ });
2589
+ kms.command("disable").description("disable a key (blocks new grants/rotations; existing data still decrypts)").requiredOption("--key <name>", "key name or id").option("--org <slug>").action(async (options) => {
2590
+ const ctx = buildContext();
2591
+ const org = await resolveOrg(ctx, options.org);
2592
+ const key = await kmsResolveKey(ctx, org.id, options.key);
2593
+ await ctx.client.disableKmsKey(org.id, key.id);
2594
+ console.error(`disabled ${key.name}`);
2595
+ });
2596
+ kms.command("encrypt").description("encrypt stdin under a key (prints a ce1 ciphertext blob)").requiredOption("--key <name>", "key name or id").option("--org <slug>").option("--context <ctx>", "encryption context bound as AAD (required identically to decrypt)").action(async (options) => {
2597
+ const ctx = buildContext();
2598
+ const org = await resolveOrg(ctx, options.org);
2599
+ const key = await kmsResolveKey(ctx, org.id, options.key);
2600
+ if (key.purpose !== "encrypt") fail(`${key.name} is a ${key.purpose} key`);
2601
+ const { material, currentVersion } = await kmsRecoverMaterial(ctx, org.id, key.id);
2602
+ const plaintext = (await readStdin()).replace(/\n$/, "");
2603
+ const blob = await kmsEncrypt(material, {
2604
+ keyId: key.id,
2605
+ version: currentVersion
2606
+ }, plaintext, options.context ?? "");
2607
+ console.log(blob);
2608
+ });
2609
+ kms.command("decrypt").description("decrypt a ce1 blob from stdin").requiredOption("--key <name>", "key name or id").option("--org <slug>").option("--context <ctx>", "the same encryption context used to encrypt").action(async (options) => {
2610
+ const ctx = buildContext();
2611
+ const org = await resolveOrg(ctx, options.org);
2612
+ const key = await kmsResolveKey(ctx, org.id, options.key);
2613
+ const blob = (await readStdin()).trim();
2614
+ const ref = kmsBlobKeyRef(blob);
2615
+ const { material } = await kmsRecoverMaterial(ctx, org.id, key.id, ref.version);
2616
+ console.log(await kmsDecrypt(material, blob, options.context ?? ""));
2617
+ });
2618
+ kms.command("generate-data-key").description("generate a data key: prints JSON {plaintextBase64, wrapped}").requiredOption("--key <name>", "key name or id").option("--org <slug>").action(async (options) => {
2619
+ const ctx = buildContext();
2620
+ const org = await resolveOrg(ctx, options.org);
2621
+ const key = await kmsResolveKey(ctx, org.id, options.key);
2622
+ if (key.purpose !== "encrypt") fail(`${key.name} is a ${key.purpose} key`);
2623
+ const { material, currentVersion } = await kmsRecoverMaterial(ctx, org.id, key.id);
2624
+ const dk = await generateDataKey(material, {
2625
+ keyId: key.id,
2626
+ version: currentVersion
2627
+ });
2628
+ console.log(JSON.stringify({
2629
+ plaintextBase64: toBase64(dk.plaintext),
2630
+ wrapped: dk.wrapped
2631
+ }));
2632
+ });
2633
+ kms.command("open-data-key").description("recover a data key from a dk1 blob on stdin (prints plaintext base64)").requiredOption("--key <name>", "key name or id").option("--org <slug>").action(async (options) => {
2634
+ const ctx = buildContext();
2635
+ const org = await resolveOrg(ctx, options.org);
2636
+ const key = await kmsResolveKey(ctx, org.id, options.key);
2637
+ const wrapped = (await readStdin()).trim();
2638
+ const ref = kmsBlobKeyRef(wrapped);
2639
+ const { material } = await kmsRecoverMaterial(ctx, org.id, key.id, ref.version);
2640
+ console.log(toBase64(await decryptDataKey(material, wrapped)));
2641
+ });
2642
+ kms.command("sign").description("sign stdin with a signing key (prints an sg1 signature)").requiredOption("--key <name>", "key name or id").option("--org <slug>").action(async (options) => {
2643
+ const ctx = buildContext();
2644
+ const org = await resolveOrg(ctx, options.org);
2645
+ const key = await kmsResolveKey(ctx, org.id, options.key);
2646
+ if (key.purpose !== "sign") fail(`${key.name} is a ${key.purpose} key`);
2647
+ const { material, currentVersion } = await kmsRecoverMaterial(ctx, org.id, key.id);
2648
+ const message = await readStdin();
2649
+ const privateKey = await importSigningKey(material);
2650
+ console.log(await signMessage(privateKey, {
2651
+ keyId: key.id,
2652
+ version: currentVersion
2653
+ }, message));
2654
+ });
2655
+ kms.command("verify").description("verify an sg1 signature over stdin (exit 0 = valid)").requiredOption("--key <name>", "key name or id").requiredOption("--signature <sg1>", "the signature blob").option("--org <slug>").action(async (options) => {
2656
+ const ctx = buildContext();
2657
+ const org = await resolveOrg(ctx, options.org);
2658
+ const key = await kmsResolveKey(ctx, org.id, options.key);
2659
+ const ref = signatureKeyRef(options.signature);
2660
+ const { versions } = await ctx.client.getKmsPublicKeys(org.id, key.id);
2661
+ const pub = versions.find((v) => v.version === ref.version)?.publicKeyJwk;
2662
+ if (!pub) fail(`no published public key for version ${ref.version}`);
2663
+ const message = await readStdin();
2664
+ if (await verifyMessage(await importVerifyingKey(pub), options.signature, message)) console.error("valid");
2665
+ else {
2666
+ console.error("INVALID");
2667
+ process.exitCode = 1;
2668
+ }
2669
+ });
2670
+ }
2671
+ //#endregion
2124
2672
  //#region src/mongodb.ts
2125
2673
  /**
2126
2674
  * `seekrit mongodb` — temporary MongoDB credentials (Vault-style dynamic
@@ -3346,8 +3894,9 @@ registerSshCommands(program);
3346
3894
  registerAwsCommands(program);
3347
3895
  registerGcpCommands(program);
3348
3896
  registerMongoCommands(program);
3897
+ registerKmsCommands(program);
3349
3898
  program.command("mcp").description("run an MCP server over stdio so AI agents can drive seekrit").action(async () => {
3350
- const { runMcpServer } = await import("./mcp-ARBuneH3.js");
3899
+ const { runMcpServer } = await import("./mcp-CVhEQDfd.js");
3351
3900
  await runMcpServer();
3352
3901
  });
3353
3902
  program.command("audit").description("show the org audit trail").option("--org <slug>").option("--limit <n>", "entries to fetch", "50").action(async (options) => {
@@ -3361,4 +3910,4 @@ program.parseAsync(argv).catch((err) => {
3361
3910
  fail(err instanceof Error ? err.message : String(err));
3362
3911
  });
3363
3912
  //#endregion
3364
- export { generatePostgresCredential as _, resolveEnvTarget as a, generateDek as b, getDek as c, setFailThrows as d, writeProjectConfig as f, parseServiceToken as g, isServiceToken as h, resolveAppEnv as i, isTokenAuth as l, createServiceToken as m, fetchDecryptedSecrets as n, resolveGroup as o, version as p, materializeEnv as r, resolveOrg as s, encryptAndSetSecret as t, tryBuildContext as u, generateMysqlCredential as v, wrapDek as y };
3913
+ export { generateEncryptKeyMaterial as A, importVerifyingKey as C, generatePostgresCredential as D, verifyMessage as E, generateDek as F, toBase64 as I, kmsDecrypt as M, kmsEncrypt as N, generateMysqlCredential as O, wrapDek as P, importSigningKey as S, signatureKeyRef as T, version as _, kmsRecoverMaterial as a, parseServiceToken as b, resolveAppEnv as c, resolveOrg as d, getDek as f, writeProjectConfig as g, setFailThrows as h, kmsCallerIdentity as i, kmsBlobKeyRef as j, generateDataKey as k, resolveEnvTarget as l, tryBuildContext as m, fetchDecryptedSecrets as n, kmsResolveKey as o, isTokenAuth as p, materializeEnv as r, kmsResolveRecipient as s, encryptAndSetSecret as t, resolveGroup as u, createServiceToken as v, signMessage as w, generateSigningKeyMaterial as x, isServiceToken as y };
@@ -1,4 +1,4 @@
1
- import { _ as generatePostgresCredential, a as resolveEnvTarget, b as generateDek, c as getDek, d as setFailThrows, f as writeProjectConfig, g as parseServiceToken, h as isServiceToken, i as resolveAppEnv, l as isTokenAuth, m as createServiceToken, n as fetchDecryptedSecrets, o as resolveGroup, p as version, r as materializeEnv, s as resolveOrg, t as encryptAndSetSecret, u as tryBuildContext, v as generateMysqlCredential, y as wrapDek } from "./index.js";
1
+ import { A as generateEncryptKeyMaterial, C as importVerifyingKey, D as generatePostgresCredential, E as verifyMessage, F as generateDek, I as toBase64, M as kmsDecrypt, N as kmsEncrypt, O as generateMysqlCredential, P as wrapDek, S as importSigningKey, T as signatureKeyRef, _ as version, a as kmsRecoverMaterial, b as parseServiceToken, c as resolveAppEnv, d as resolveOrg, f as getDek, g as writeProjectConfig, h as setFailThrows, i as kmsCallerIdentity, j as kmsBlobKeyRef, k as generateDataKey, l as resolveEnvTarget, m as tryBuildContext, n as fetchDecryptedSecrets, o as kmsResolveKey, p as isTokenAuth, r as materializeEnv, s as kmsResolveRecipient, t as encryptAndSetSecret, u as resolveGroup, v as createServiceToken, w as signMessage, x as generateSigningKeyMaterial, y as isServiceToken } from "./index.js";
2
2
  import { spawn } from "node:child_process";
3
3
  import { z } from "zod";
4
4
  import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
@@ -206,6 +206,158 @@ async function runMcpServer() {
206
206
  const orgRef = await resolveOrg(ctx, org);
207
207
  return (await ctx.client.listMembers(orgRef.id)).members;
208
208
  });
209
+ tool("kms_list_keys", "List managed KMS keys the caller can see (metadata only).", { org: z.string().optional() }, async ({ org }) => {
210
+ const ctx = getCtx();
211
+ const orgRef = await resolveOrg(ctx, org);
212
+ return (await ctx.client.listKmsKeys(orgRef.id)).keys;
213
+ });
214
+ tool("kms_create_key", "Create an org-scoped managed key. Material is generated locally and wrapped to each grantee (self plus any listed users/tokens); the server never sees it. Use the CLI for app/group-scoped keys.", {
215
+ org: z.string().optional(),
216
+ name: z.string(),
217
+ purpose: z.enum(["encrypt", "sign"]),
218
+ grantUsers: z.array(z.string()).optional(),
219
+ grantTokens: z.array(z.string()).optional()
220
+ }, async ({ org, name, purpose, grantUsers, grantTokens }) => {
221
+ const ctx = getCtx();
222
+ const orgRef = await resolveOrg(ctx, org);
223
+ let toWrap;
224
+ let publicKeyJwk;
225
+ if (purpose === "encrypt") toWrap = generateEncryptKeyMaterial();
226
+ else {
227
+ const km = await generateSigningKeyMaterial();
228
+ toWrap = km.privateKeyPkcs8;
229
+ publicKeyJwk = km.publicKeyJwk;
230
+ }
231
+ const recipients = [await kmsCallerIdentity(ctx)];
232
+ for (const u of grantUsers ?? []) recipients.push(await kmsResolveRecipient(ctx, orgRef.id, { user: u }));
233
+ for (const t of grantTokens ?? []) recipients.push(await kmsResolveRecipient(ctx, orgRef.id, { token: t }));
234
+ const seen = /* @__PURE__ */ new Set();
235
+ const grants = [];
236
+ for (const r of recipients) {
237
+ const dedupeKey = `${r.principalType}:${r.principalId}`;
238
+ if (seen.has(dedupeKey)) continue;
239
+ seen.add(dedupeKey);
240
+ grants.push({
241
+ principalType: r.principalType,
242
+ principalId: r.principalId,
243
+ wrappedKey: await wrapDek(toWrap, r.publicKeyJwk)
244
+ });
245
+ }
246
+ const { key } = await ctx.client.createKmsKey(orgRef.id, {
247
+ name,
248
+ purpose,
249
+ spec: purpose === "sign" ? "ecdsa-p256" : "aes-256-gcm",
250
+ ...publicKeyJwk ? { publicKeyJwk } : {},
251
+ grants
252
+ });
253
+ return key;
254
+ });
255
+ tool("kms_grant", "Grant a principal (user email or token id) use of a key's current version.", {
256
+ org: z.string().optional(),
257
+ key: z.string(),
258
+ user: z.string().optional(),
259
+ token: z.string().optional()
260
+ }, async ({ org, key, user, token }) => {
261
+ const ctx = getCtx();
262
+ ensureDecryptable(ctx);
263
+ const orgRef = await resolveOrg(ctx, org);
264
+ const k = await kmsResolveKey(ctx, orgRef.id, key);
265
+ const recipient = await kmsResolveRecipient(ctx, orgRef.id, {
266
+ user,
267
+ token
268
+ });
269
+ const { material } = await kmsRecoverMaterial(ctx, orgRef.id, k.id);
270
+ await ctx.client.grantKmsKey(orgRef.id, k.id, {
271
+ principalType: recipient.principalType,
272
+ principalId: recipient.principalId,
273
+ wrappedKey: await wrapDek(material, recipient.publicKeyJwk)
274
+ });
275
+ return {
276
+ granted: recipient.principalId,
277
+ key: k.name
278
+ };
279
+ });
280
+ tool("kms_encrypt", "Encrypt a value under a managed encrypt key; returns a ce1 ciphertext blob. `context` (if given) is bound as AAD and must be supplied identically to decrypt.", {
281
+ org: z.string().optional(),
282
+ key: z.string(),
283
+ plaintext: z.string(),
284
+ context: z.string().optional()
285
+ }, async ({ org, key, plaintext, context }) => {
286
+ const ctx = getCtx();
287
+ ensureDecryptable(ctx);
288
+ const orgRef = await resolveOrg(ctx, org);
289
+ const k = await kmsResolveKey(ctx, orgRef.id, key);
290
+ if (k.purpose !== "encrypt") throw new Error(`${k.name} is a ${k.purpose} key`);
291
+ const { material, currentVersion } = await kmsRecoverMaterial(ctx, orgRef.id, k.id);
292
+ return { ciphertext: await kmsEncrypt(material, {
293
+ keyId: k.id,
294
+ version: currentVersion
295
+ }, plaintext, context ?? "") };
296
+ });
297
+ tool("kms_decrypt", "Decrypt a ce1 blob. Supply the same `context` used to encrypt.", {
298
+ org: z.string().optional(),
299
+ key: z.string(),
300
+ ciphertext: z.string(),
301
+ context: z.string().optional()
302
+ }, async ({ org, key, ciphertext, context }) => {
303
+ const ctx = getCtx();
304
+ ensureDecryptable(ctx);
305
+ const orgRef = await resolveOrg(ctx, org);
306
+ const k = await kmsResolveKey(ctx, orgRef.id, key);
307
+ const ref = kmsBlobKeyRef(ciphertext);
308
+ const { material } = await kmsRecoverMaterial(ctx, orgRef.id, k.id, ref.version);
309
+ return { plaintext: await kmsDecrypt(material, ciphertext, context ?? "") };
310
+ });
311
+ tool("kms_generate_data_key", "Generate a data key under a managed encrypt key (envelope encryption). Returns the plaintext key (base64) and its wrapped form to store.", {
312
+ org: z.string().optional(),
313
+ key: z.string()
314
+ }, async ({ org, key }) => {
315
+ const ctx = getCtx();
316
+ ensureDecryptable(ctx);
317
+ const orgRef = await resolveOrg(ctx, org);
318
+ const k = await kmsResolveKey(ctx, orgRef.id, key);
319
+ if (k.purpose !== "encrypt") throw new Error(`${k.name} is a ${k.purpose} key`);
320
+ const { material, currentVersion } = await kmsRecoverMaterial(ctx, orgRef.id, k.id);
321
+ const dk = await generateDataKey(material, {
322
+ keyId: k.id,
323
+ version: currentVersion
324
+ });
325
+ return {
326
+ plaintextBase64: toBase64(dk.plaintext),
327
+ wrapped: dk.wrapped
328
+ };
329
+ });
330
+ tool("kms_sign", "Sign a message with a managed signing key; returns an sg1 signature blob.", {
331
+ org: z.string().optional(),
332
+ key: z.string(),
333
+ message: z.string()
334
+ }, async ({ org, key, message }) => {
335
+ const ctx = getCtx();
336
+ ensureDecryptable(ctx);
337
+ const orgRef = await resolveOrg(ctx, org);
338
+ const k = await kmsResolveKey(ctx, orgRef.id, key);
339
+ if (k.purpose !== "sign") throw new Error(`${k.name} is a ${k.purpose} key`);
340
+ const { material, currentVersion } = await kmsRecoverMaterial(ctx, orgRef.id, k.id);
341
+ return { signature: await signMessage(await importSigningKey(material), {
342
+ keyId: k.id,
343
+ version: currentVersion
344
+ }, message) };
345
+ });
346
+ tool("kms_verify", "Verify an sg1 signature over a message using a signing key's published public key (no grant needed).", {
347
+ org: z.string().optional(),
348
+ key: z.string(),
349
+ signature: z.string(),
350
+ message: z.string()
351
+ }, async ({ org, key, signature, message }) => {
352
+ const ctx = getCtx();
353
+ const orgRef = await resolveOrg(ctx, org);
354
+ const k = await kmsResolveKey(ctx, orgRef.id, key);
355
+ const ref = signatureKeyRef(signature);
356
+ const { versions } = await ctx.client.getKmsPublicKeys(orgRef.id, k.id);
357
+ const pub = versions.find((v) => v.version === ref.version)?.publicKeyJwk;
358
+ if (!pub) throw new Error(`no published public key for version ${ref.version}`);
359
+ return { valid: await verifyMessage(await importVerifyingKey(pub), signature, message) };
360
+ });
209
361
  tool("list_secrets", "List secret names + versions in an environment (never values).", targetShape, async (o) => {
210
362
  const ctx = getCtx();
211
363
  const { orgId, envId } = await resolveTargetEnv(ctx, o);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@seekrit/cli",
3
- "version": "0.16.0",
3
+ "version": "0.17.0",
4
4
  "description": "End-to-end encrypted secrets manager CLI — inject decrypted secrets into any command.",
5
5
  "type": "module",
6
6
  "publishConfig": {
@@ -23,9 +23,9 @@
23
23
  "devDependencies": {
24
24
  "@types/node": "^26.1.0",
25
25
  "tsdown": "^0.22.3",
26
+ "@seekrit/api-client": "0.0.1",
26
27
  "@seekrit/core": "0.0.1",
27
- "@seekrit/crypto": "0.0.1",
28
- "@seekrit/api-client": "0.0.1"
28
+ "@seekrit/crypto": "0.0.1"
29
29
  },
30
30
  "scripts": {
31
31
  "build": "tsdown",