@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 +554 -5
- package/dist/{mcp-ARBuneH3.js → mcp-CVhEQDfd.js} +153 -1
- package/package.json +3 -3
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.
|
|
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-
|
|
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 _,
|
|
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 {
|
|
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.
|
|
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",
|