@seekrit/cli 0.15.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 +822 -14
- package/dist/{mcp-ARBuneH3.js → mcp-CVhEQDfd.js} +153 -1
- package/package.json +3 -3
package/dist/index.js
CHANGED
|
@@ -13,7 +13,8 @@ z.enum([
|
|
|
13
13
|
"ssh",
|
|
14
14
|
"redis",
|
|
15
15
|
"aws",
|
|
16
|
-
"gcp"
|
|
16
|
+
"gcp",
|
|
17
|
+
"mongodb"
|
|
17
18
|
]);
|
|
18
19
|
const executorModeSchema = z.enum(["in_do", "remote"]);
|
|
19
20
|
/**
|
|
@@ -113,6 +114,22 @@ const p256PublicKeyJwkSchema = z.string().max(2048).refine((s) => {
|
|
|
113
114
|
return false;
|
|
114
115
|
}
|
|
115
116
|
}, "must be a JWK-serialized P-256 public key");
|
|
117
|
+
z.string().regex(/^[A-Za-z0-9_-]{1,64}$/, "must be 1–64 chars of [A-Za-z0-9_-]");
|
|
118
|
+
/**
|
|
119
|
+
* A MongoDB database name — the db a preset role is granted on, or the
|
|
120
|
+
* authentication database a leased user is created in. MongoDB forbids
|
|
121
|
+
* `/\. "$*<>:|?` and the empty string in db names; we keep to a safe subset.
|
|
122
|
+
*/
|
|
123
|
+
const mongoDatabaseNameSchema = z.string().regex(/^[A-Za-z0-9_-]{1,63}$/, "must be 1–63 chars of [A-Za-z0-9_-]");
|
|
124
|
+
/**
|
|
125
|
+
* A single MongoDB role grant `{ role, db }` for a `custom` target — e.g.
|
|
126
|
+
* `{ role: "readWrite", db: "app" }` or a user-defined role. Admin-supplied
|
|
127
|
+
* trusted input, set once at registration; still bounded structurally.
|
|
128
|
+
*/
|
|
129
|
+
const mongoRoleSchema = z.object({
|
|
130
|
+
role: z.string().regex(/^[A-Za-z0-9_-]{1,64}$/, "must be a role name"),
|
|
131
|
+
db: mongoDatabaseNameSchema
|
|
132
|
+
});
|
|
116
133
|
const postgresAccessLevelSchema = z.enum([
|
|
117
134
|
"readonly",
|
|
118
135
|
"readwrite",
|
|
@@ -133,6 +150,11 @@ const redisAccessLevelSchema = z.enum([
|
|
|
133
150
|
"readwrite",
|
|
134
151
|
"custom"
|
|
135
152
|
]);
|
|
153
|
+
const mongoAccessLevelSchema = z.enum([
|
|
154
|
+
"readonly",
|
|
155
|
+
"readwrite",
|
|
156
|
+
"custom"
|
|
157
|
+
]);
|
|
136
158
|
const connectionSchema = z.object({
|
|
137
159
|
host: z.string().min(1),
|
|
138
160
|
port: z.number().int().min(1).max(65535),
|
|
@@ -210,13 +232,24 @@ const gcpTargetConfigSchema = z.object({
|
|
|
210
232
|
delegates: z.array(gcpServiceAccountEmailSchema).max(8).optional(),
|
|
211
233
|
maxTtlSeconds: z.number().int().min(60).max(GCP_MAX_TTL_SECONDS).optional()
|
|
212
234
|
});
|
|
235
|
+
const mongoTargetConfigSchema = z.object({
|
|
236
|
+
provider: z.literal("mongodb"),
|
|
237
|
+
executor: z.literal("in_do"),
|
|
238
|
+
accessLevel: mongoAccessLevelSchema.optional(),
|
|
239
|
+
connection: connectionSchema,
|
|
240
|
+
authSource: mongoDatabaseNameSchema.optional(),
|
|
241
|
+
roles: z.array(mongoRoleSchema).min(1).max(32).optional(),
|
|
242
|
+
tls: z.boolean().optional(),
|
|
243
|
+
maxTtlSeconds: z.number().int().min(60).max(3600 * 24 * 7).optional()
|
|
244
|
+
});
|
|
213
245
|
const leaseTargetConfigSchema = z.discriminatedUnion("provider", [
|
|
214
246
|
postgresTargetConfigSchema,
|
|
215
247
|
mysqlTargetConfigSchema,
|
|
216
248
|
redisTargetConfigSchema,
|
|
217
249
|
sshTargetConfigSchema,
|
|
218
250
|
awsTargetConfigSchema,
|
|
219
|
-
gcpTargetConfigSchema
|
|
251
|
+
gcpTargetConfigSchema,
|
|
252
|
+
mongoTargetConfigSchema
|
|
220
253
|
]);
|
|
221
254
|
z.object({
|
|
222
255
|
name: z.string().trim().min(1).max(128),
|
|
@@ -309,13 +342,27 @@ const mintGcpLeaseSchema = z.object({
|
|
|
309
342
|
recipientPublicKey: p256PublicKeyJwkSchema,
|
|
310
343
|
ttlSeconds: z.number().int().min(60).max(GCP_MAX_TTL_SECONDS)
|
|
311
344
|
});
|
|
345
|
+
/**
|
|
346
|
+
* Client → API: mint a MongoDB lease. Like AWS (tier 2), the client generates
|
|
347
|
+
* an ephemeral P-256 keypair locally and sends only the public key; the broker
|
|
348
|
+
* generates the password, runs `createUser`, and returns the credential wrapped
|
|
349
|
+
* to that key. The private key never leaves the requesting machine, so the
|
|
350
|
+
* plaintext credential is only decryptable there.
|
|
351
|
+
*/
|
|
352
|
+
const mintMongoLeaseSchema = z.object({
|
|
353
|
+
provider: z.literal("mongodb"),
|
|
354
|
+
targetId: z.string().min(1),
|
|
355
|
+
recipientPublicKey: p256PublicKeyJwkSchema,
|
|
356
|
+
ttlSeconds: ttlSecondsSchema
|
|
357
|
+
});
|
|
312
358
|
z.discriminatedUnion("provider", [
|
|
313
359
|
mintPostgresLeaseSchema,
|
|
314
360
|
mintMysqlLeaseSchema,
|
|
315
361
|
mintRedisLeaseSchema,
|
|
316
362
|
mintSshLeaseSchema,
|
|
317
363
|
mintAwsLeaseSchema,
|
|
318
|
-
mintGcpLeaseSchema
|
|
364
|
+
mintGcpLeaseSchema,
|
|
365
|
+
mintMongoLeaseSchema
|
|
319
366
|
]);
|
|
320
367
|
//#endregion
|
|
321
368
|
//#region ../../packages/core/src/providers/aws.ts
|
|
@@ -369,6 +416,32 @@ function gcpSetupInstructions(config) {
|
|
|
369
416
|
].join("\n");
|
|
370
417
|
}
|
|
371
418
|
//#endregion
|
|
419
|
+
//#region ../../packages/core/src/providers/mongodb.ts
|
|
420
|
+
/**
|
|
421
|
+
* The one-time setup an admin performs so seekrit can provision users: create a
|
|
422
|
+
* dedicated provisioning user with `userAdmin` on the target database (or
|
|
423
|
+
* `userAdminAnyDatabase` if leases span databases). Printed for the admin to run
|
|
424
|
+
* in `mongosh`, never executed by seekrit — the analogue of
|
|
425
|
+
* `awsTrustPolicyInstructions` / `sshHostSetupInstructions`.
|
|
426
|
+
*/
|
|
427
|
+
function mongoAdminSetupInstructions(config) {
|
|
428
|
+
const authSource = config.authSource ?? "admin";
|
|
429
|
+
const db = config.connection.database;
|
|
430
|
+
return [
|
|
431
|
+
`// Run once in mongosh against the "${authSource}" database. Creates the`,
|
|
432
|
+
"// provisioning user whose connection string you register as the admin secret.",
|
|
433
|
+
`use ${authSource}`,
|
|
434
|
+
"db.createUser({",
|
|
435
|
+
" user: \"seekrit_provisioner\",",
|
|
436
|
+
" pwd: passwordPrompt(),",
|
|
437
|
+
` roles: [{ role: "userAdmin", db: "${db}" }],`,
|
|
438
|
+
"})",
|
|
439
|
+
"",
|
|
440
|
+
`// Register it: seekrit mongodb target add --name ${db} \\`,
|
|
441
|
+
`// --uri "mongodb://seekrit_provisioner:<pwd>@${config.connection.host}:${config.connection.port}/?authSource=${authSource}"`
|
|
442
|
+
].join("\n");
|
|
443
|
+
}
|
|
444
|
+
//#endregion
|
|
372
445
|
//#region ../../packages/core/src/providers/postgres.ts
|
|
373
446
|
/**
|
|
374
447
|
* The one-time setup SQL an admin runs to create the shared group role that a
|
|
@@ -445,6 +518,8 @@ const NOTIFICATION_TYPES = [
|
|
|
445
518
|
/** URL-safe identifier segment: `my-app`, `production`, … */
|
|
446
519
|
const slugSchema = z.string().min(1).max(64).regex(/^[a-z0-9]+(?:-[a-z0-9]+)*$/, "must be lowercase alphanumeric with hyphens");
|
|
447
520
|
const nameSchema = z.string().trim().min(1).max(128);
|
|
521
|
+
/** An email address, normalized to trimmed lowercase before validation. */
|
|
522
|
+
const emailSchema = z.string().trim().toLowerCase().pipe(z.email().max(320));
|
|
448
523
|
/** Env-var style secret name: FOO, DATABASE_URL, apiKey2 … */
|
|
449
524
|
const secretNameSchema = z.string().min(1).max(256).regex(/^[A-Za-z_][A-Za-z0-9_]*$/, "must be a valid environment variable name");
|
|
450
525
|
z.enum([
|
|
@@ -452,6 +527,8 @@ z.enum([
|
|
|
452
527
|
"admin",
|
|
453
528
|
"member"
|
|
454
529
|
]);
|
|
530
|
+
/** Role a person may be invited at — never `owner` (ownership isn't invitable). */
|
|
531
|
+
const inviteRoleSchema = z.enum(["admin", "member"]);
|
|
455
532
|
const principalTypeSchema = z.enum(["user", "service_token"]);
|
|
456
533
|
/** Org-level capability a service token can hold (never `owner`). */
|
|
457
534
|
const serviceTokenRoleSchema = z.enum(["admin", "member"]);
|
|
@@ -463,6 +540,10 @@ z.object({
|
|
|
463
540
|
name: nameSchema,
|
|
464
541
|
slug: slugSchema
|
|
465
542
|
});
|
|
543
|
+
z.object({
|
|
544
|
+
email: emailSchema,
|
|
545
|
+
role: inviteRoleSchema.default("member")
|
|
546
|
+
});
|
|
466
547
|
z.object({
|
|
467
548
|
name: nameSchema,
|
|
468
549
|
slug: slugSchema
|
|
@@ -514,6 +595,44 @@ z.object({
|
|
|
514
595
|
environmentId: z.string().min(1).nullish(),
|
|
515
596
|
expiresAt: z.iso.datetime().nullish()
|
|
516
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
|
+
});
|
|
517
636
|
z.object({ prefs: z.partialRecord(z.enum(NOTIFICATION_TYPES), z.boolean()) });
|
|
518
637
|
z.object({
|
|
519
638
|
endpoint: z.url().max(2048),
|
|
@@ -582,7 +701,7 @@ function splitBlob(blob, prefix, segments) {
|
|
|
582
701
|
//#endregion
|
|
583
702
|
//#region ../../packages/crypto/src/aes.ts
|
|
584
703
|
const SECRET_PREFIX = "sc1";
|
|
585
|
-
const IV_LENGTH = 12;
|
|
704
|
+
const IV_LENGTH$1 = 12;
|
|
586
705
|
/** Generate a fresh 256-bit data encryption key for an environment. */
|
|
587
706
|
function generateDek() {
|
|
588
707
|
return crypto.getRandomValues(/* @__PURE__ */ new Uint8Array(32));
|
|
@@ -599,7 +718,7 @@ async function importDek(dek, usage) {
|
|
|
599
718
|
*/
|
|
600
719
|
async function encryptSecret(dek, plaintext, aad) {
|
|
601
720
|
const key = await importDek(dek, "encrypt");
|
|
602
|
-
const iv = crypto.getRandomValues(new Uint8Array(IV_LENGTH));
|
|
721
|
+
const iv = crypto.getRandomValues(new Uint8Array(IV_LENGTH$1));
|
|
603
722
|
const ciphertext = await crypto.subtle.encrypt({
|
|
604
723
|
name: "AES-GCM",
|
|
605
724
|
iv,
|
|
@@ -784,6 +903,172 @@ async function unwrapGcpCredential(wrapped, privateKeyJwk) {
|
|
|
784
903
|
};
|
|
785
904
|
}
|
|
786
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
|
|
1037
|
+
//#region ../../packages/crypto/src/mongodb.ts
|
|
1038
|
+
/** Generate the ephemeral P-256 keypair a client uses to receive one MongoDB lease. */
|
|
1039
|
+
async function generateMongoRecipientKeyPair() {
|
|
1040
|
+
return generateKeyPair();
|
|
1041
|
+
}
|
|
1042
|
+
/**
|
|
1043
|
+
* Unwrap the `wd1.` blob returned by a MongoDB lease mint using the ephemeral
|
|
1044
|
+
* private key generated for that lease, yielding the plaintext credential.
|
|
1045
|
+
*/
|
|
1046
|
+
async function unwrapMongoCredential(wrapped, privateKeyJwk) {
|
|
1047
|
+
const bytes = await unwrapDek(wrapped, await importPrivateKey(privateKeyJwk));
|
|
1048
|
+
let parsed;
|
|
1049
|
+
try {
|
|
1050
|
+
parsed = JSON.parse(utf8Decode(bytes));
|
|
1051
|
+
} catch {
|
|
1052
|
+
throw new SeekritCryptoError("DECRYPT_FAILED", "unwrapped MongoDB credential was not valid JSON");
|
|
1053
|
+
}
|
|
1054
|
+
const c = parsed;
|
|
1055
|
+
if (typeof c.username !== "string" || typeof c.password !== "string" || typeof c.database !== "string" || typeof c.authSource !== "string" || typeof c.host !== "string" || typeof c.port !== "number" || typeof c.expiration !== "string") throw new SeekritCryptoError("DECRYPT_FAILED", "unwrapped MongoDB credential is missing fields");
|
|
1056
|
+
return {
|
|
1057
|
+
username: c.username,
|
|
1058
|
+
password: c.password,
|
|
1059
|
+
database: c.database,
|
|
1060
|
+
authSource: c.authSource,
|
|
1061
|
+
host: c.host,
|
|
1062
|
+
port: c.port,
|
|
1063
|
+
expiration: c.expiration,
|
|
1064
|
+
uri: mongoConnectionUri(c)
|
|
1065
|
+
};
|
|
1066
|
+
}
|
|
1067
|
+
/** Build a `mongodb://user:pass@host:port/db?authSource=…` URI from a credential. */
|
|
1068
|
+
function mongoConnectionUri(cred) {
|
|
1069
|
+
return `mongodb://${encodeURIComponent(cred.username)}:${encodeURIComponent(cred.password)}@${cred.host}:${cred.port}/${cred.database}?authSource=${cred.authSource}`;
|
|
1070
|
+
}
|
|
1071
|
+
//#endregion
|
|
787
1072
|
//#region ../../packages/crypto/src/mysql.ts
|
|
788
1073
|
/**
|
|
789
1074
|
* Client-side construction of a MySQL/MariaDB `mysql_native_password`
|
|
@@ -1039,6 +1324,69 @@ async function generatePostgresCredential(options = {}) {
|
|
|
1039
1324
|
};
|
|
1040
1325
|
}
|
|
1041
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
|
|
1042
1390
|
//#region ../../packages/crypto/src/ssh.ts
|
|
1043
1391
|
/**
|
|
1044
1392
|
* Client-side SSH certificate authority for minting *temporary SSH access*
|
|
@@ -1228,7 +1576,7 @@ function isServiceToken(value) {
|
|
|
1228
1576
|
}
|
|
1229
1577
|
//#endregion
|
|
1230
1578
|
//#region package.json
|
|
1231
|
-
var version = "0.
|
|
1579
|
+
var version = "0.17.0";
|
|
1232
1580
|
//#endregion
|
|
1233
1581
|
//#region ../../packages/api-client/src/index.ts
|
|
1234
1582
|
var SeekritApiError = class extends Error {
|
|
@@ -1301,6 +1649,15 @@ var SeekritClient = class {
|
|
|
1301
1649
|
listMembers(orgId) {
|
|
1302
1650
|
return this.request("GET", `/v1/orgs/${orgId}/members`);
|
|
1303
1651
|
}
|
|
1652
|
+
listInvites(orgId) {
|
|
1653
|
+
return this.request("GET", `/v1/orgs/${orgId}/invites`);
|
|
1654
|
+
}
|
|
1655
|
+
createInvite(orgId, input) {
|
|
1656
|
+
return this.request("POST", `/v1/orgs/${orgId}/invites`, input);
|
|
1657
|
+
}
|
|
1658
|
+
revokeInvite(orgId, inviteId) {
|
|
1659
|
+
return this.request("DELETE", `/v1/orgs/${orgId}/invites/${inviteId}`);
|
|
1660
|
+
}
|
|
1304
1661
|
listApps(orgId) {
|
|
1305
1662
|
return this.request("GET", `/v1/orgs/${orgId}/apps`);
|
|
1306
1663
|
}
|
|
@@ -1391,6 +1748,42 @@ var SeekritClient = class {
|
|
|
1391
1748
|
revokeToken(orgId, tokenId) {
|
|
1392
1749
|
return this.request("DELETE", `/v1/orgs/${orgId}/tokens/${tokenId}`);
|
|
1393
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
|
+
}
|
|
1394
1787
|
/** The broker's public key — wrap the admin credential to it before registering a target. */
|
|
1395
1788
|
getLeaseBrokerKey(orgId) {
|
|
1396
1789
|
return this.request("GET", `/v1/orgs/${orgId}/leases/broker-key`);
|
|
@@ -1677,7 +2070,7 @@ async function resolveGroup(ctx, opts) {
|
|
|
1677
2070
|
* `sts:AssumeRole` on the target role.
|
|
1678
2071
|
*/
|
|
1679
2072
|
/** Parse a duration like `30m`, `1h`, or a bare seconds count. */
|
|
1680
|
-
function parseTtlSeconds$
|
|
2073
|
+
function parseTtlSeconds$6(input) {
|
|
1681
2074
|
const m = /^(\d+)\s*([smhd]?)$/.exec(input.trim());
|
|
1682
2075
|
if (!m) fail(`invalid --ttl "${input}" (try 30m, 1h, 12h)`);
|
|
1683
2076
|
return Number(m[1]) * ({
|
|
@@ -1717,7 +2110,7 @@ function registerAwsCommands(program) {
|
|
|
1717
2110
|
region: options.region,
|
|
1718
2111
|
...options.externalId ? { externalId: options.externalId } : {},
|
|
1719
2112
|
...sessionPolicy ? { sessionPolicy } : {},
|
|
1720
|
-
...options.maxTtl ? { maxTtlSeconds: parseTtlSeconds$
|
|
2113
|
+
...options.maxTtl ? { maxTtlSeconds: parseTtlSeconds$6(options.maxTtl) } : {}
|
|
1721
2114
|
};
|
|
1722
2115
|
const baseCredential = resolveBaseCredential(options);
|
|
1723
2116
|
const { publicKeyJwk } = await ctx.client.getLeaseBrokerKey(org.id);
|
|
@@ -1765,7 +2158,7 @@ function registerAwsCommands(program) {
|
|
|
1765
2158
|
if (!t) fail(`no target "${targetRef}" in ${org.slug}`);
|
|
1766
2159
|
const cfg = t.config;
|
|
1767
2160
|
if (cfg.provider !== "aws") fail(`"${t.name}" is not an aws target (see \`seekrit pg\`)`);
|
|
1768
|
-
const ttlSeconds = parseTtlSeconds$
|
|
2161
|
+
const ttlSeconds = parseTtlSeconds$6(options.ttl);
|
|
1769
2162
|
if (ttlSeconds < 900) fail(`--ttl must be at least ${900 / 60}m (STS minimum)`);
|
|
1770
2163
|
const recipient = await generateAwsRecipientKeyPair();
|
|
1771
2164
|
const { aws: leased } = await ctx.client.mintLease(org.id, {
|
|
@@ -1870,7 +2263,7 @@ function formatSecrets(values, format) {
|
|
|
1870
2263
|
* `roles/iam.serviceAccountTokenCreator` on the target.
|
|
1871
2264
|
*/
|
|
1872
2265
|
/** Parse a duration like `30m`, `1h`, or a bare seconds count. */
|
|
1873
|
-
function parseTtlSeconds$
|
|
2266
|
+
function parseTtlSeconds$5(input) {
|
|
1874
2267
|
const m = /^(\d+)\s*([smhd]?)$/.exec(input.trim());
|
|
1875
2268
|
if (!m) fail(`invalid --ttl "${input}" (try 30m, 1h, 12h)`);
|
|
1876
2269
|
return Number(m[1]) * ({
|
|
@@ -1914,7 +2307,7 @@ function registerGcpCommands(program) {
|
|
|
1914
2307
|
serviceAccount: options.serviceAccount,
|
|
1915
2308
|
...options.scope?.length ? { scopes: options.scope } : {},
|
|
1916
2309
|
...options.delegate?.length ? { delegates: options.delegate } : {},
|
|
1917
|
-
...options.maxTtl ? { maxTtlSeconds: parseTtlSeconds$
|
|
2310
|
+
...options.maxTtl ? { maxTtlSeconds: parseTtlSeconds$5(options.maxTtl) } : {}
|
|
1918
2311
|
};
|
|
1919
2312
|
const keyJson = resolveServiceAccountKey(options);
|
|
1920
2313
|
const { publicKeyJwk } = await ctx.client.getLeaseBrokerKey(org.id);
|
|
@@ -1961,7 +2354,7 @@ function registerGcpCommands(program) {
|
|
|
1961
2354
|
const t = targets.find((x) => x.id === targetRef || x.name === targetRef);
|
|
1962
2355
|
if (!t) fail(`no target "${targetRef}" in ${org.slug}`);
|
|
1963
2356
|
if (t.config.provider !== "gcp") fail(`"${t.name}" is not a gcp target (see \`seekrit aws\`)`);
|
|
1964
|
-
const ttlSeconds = parseTtlSeconds$
|
|
2357
|
+
const ttlSeconds = parseTtlSeconds$5(options.ttl);
|
|
1965
2358
|
if (ttlSeconds < 60) fail(`--ttl must be at least 60s`);
|
|
1966
2359
|
if (ttlSeconds > 43200) fail(`--ttl must be at most ${GCP_MAX_TTL_SECONDS / 3600}h`);
|
|
1967
2360
|
const recipient = await generateGcpRecipientKeyPair();
|
|
@@ -1996,6 +2389,419 @@ function registerGcpCommands(program) {
|
|
|
1996
2389
|
});
|
|
1997
2390
|
}
|
|
1998
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
|
|
2672
|
+
//#region src/mongodb.ts
|
|
2673
|
+
/**
|
|
2674
|
+
* `seekrit mongodb` — temporary MongoDB credentials (Vault-style dynamic
|
|
2675
|
+
* secrets), the tier-2 sibling of `seekrit aws`.
|
|
2676
|
+
*
|
|
2677
|
+
* MongoDB hashes the password server-side (no verifier injection), so minting
|
|
2678
|
+
* generates an ephemeral P-256 keypair on THIS machine and sends only the public
|
|
2679
|
+
* key; the broker creates the user and returns the credential wrapped to that
|
|
2680
|
+
* key, so the control plane only ever relays ciphertext and only this machine
|
|
2681
|
+
* can unwrap it. Registering a target wraps the admin connection string to the
|
|
2682
|
+
* broker's public key locally, so the control plane never sees it either.
|
|
2683
|
+
*/
|
|
2684
|
+
/** Parse a duration like `30m`, `1h`, or a bare seconds count. */
|
|
2685
|
+
function parseTtlSeconds$4(input) {
|
|
2686
|
+
const m = /^(\d+)\s*([smhd]?)$/.exec(input.trim());
|
|
2687
|
+
if (!m) fail(`invalid --ttl "${input}" (try 30m, 1h, 8h)`);
|
|
2688
|
+
return Number(m[1]) * ({
|
|
2689
|
+
s: 1,
|
|
2690
|
+
m: 60,
|
|
2691
|
+
h: 3600,
|
|
2692
|
+
d: 86400
|
|
2693
|
+
}[m[2] || "s"] ?? 1);
|
|
2694
|
+
}
|
|
2695
|
+
/** Collect a repeatable option into an array. */
|
|
2696
|
+
function collect$4(value, previous) {
|
|
2697
|
+
return [...previous, value];
|
|
2698
|
+
}
|
|
2699
|
+
/** Parse `readWrite@app` → { role, db } for a custom target. */
|
|
2700
|
+
function parseRole(spec) {
|
|
2701
|
+
const [role, db] = spec.split("@");
|
|
2702
|
+
if (!role || !db) fail(`invalid --role "${spec}" (use role@database, e.g. readWrite@app)`);
|
|
2703
|
+
return {
|
|
2704
|
+
role,
|
|
2705
|
+
db
|
|
2706
|
+
};
|
|
2707
|
+
}
|
|
2708
|
+
/**
|
|
2709
|
+
* The admin connection string the broker provisions with. From `--uri` or
|
|
2710
|
+
* SEEKRIT_MONGODB_ADMIN_URL; never leaves this machine unwrapped — it is wrapped
|
|
2711
|
+
* to the broker key. Needs `userAdmin` on the target database.
|
|
2712
|
+
*/
|
|
2713
|
+
function resolveAdminUri(uri) {
|
|
2714
|
+
const value = uri ?? process.env.SEEKRIT_MONGODB_ADMIN_URL;
|
|
2715
|
+
if (!value) fail("provide the admin connection string via --uri or SEEKRIT_MONGODB_ADMIN_URL (mongodb://user:pass@host:port/?authSource=admin; needs userAdmin on the database)");
|
|
2716
|
+
return value;
|
|
2717
|
+
}
|
|
2718
|
+
function registerMongoCommands(program) {
|
|
2719
|
+
const mongo = program.command("mongodb").description("temporary MongoDB credentials (createUser, zero-knowledge delivery)");
|
|
2720
|
+
const target = mongo.command("target").description("manage MongoDB targets");
|
|
2721
|
+
target.command("add").description("register a MongoDB cluster to issue temporary credentials from").requiredOption("--name <name>", "display name, e.g. prod-app").requiredOption("--database <db>", "the database leased users get access to, e.g. app").option("--uri <uri>", "admin connection string (else SEEKRIT_MONGODB_ADMIN_URL)").option("--org <slug>").option("--access <level>", "readonly | readwrite | custom", "readonly").option("--role <role@db>", "grant for a custom target (repeatable)", collect$4, []).option("--auth-source <db>", "authentication database (default admin)").option("--max-ttl <duration>", "clamp requested credential lifetime, e.g. 8h").option("--no-tls", "disable TLS to the cluster (TLS is on by default)").action(async (options) => {
|
|
2722
|
+
const ctx = buildContext();
|
|
2723
|
+
const org = await resolveOrg(ctx, options.org);
|
|
2724
|
+
const adminUri = resolveAdminUri(options.uri);
|
|
2725
|
+
const url = new URL(adminUri);
|
|
2726
|
+
const accessLevel = options.access;
|
|
2727
|
+
if (accessLevel === "custom" && options.role.length === 0) fail("--access custom requires at least one --role role@db");
|
|
2728
|
+
const config = {
|
|
2729
|
+
provider: "mongodb",
|
|
2730
|
+
executor: "in_do",
|
|
2731
|
+
accessLevel,
|
|
2732
|
+
connection: {
|
|
2733
|
+
host: url.hostname,
|
|
2734
|
+
port: url.port ? Number(url.port) : 27017,
|
|
2735
|
+
database: options.database
|
|
2736
|
+
},
|
|
2737
|
+
...options.authSource ? { authSource: options.authSource } : {},
|
|
2738
|
+
...accessLevel === "custom" ? { roles: options.role.map(parseRole) } : {},
|
|
2739
|
+
...options.tls ? {} : { tls: false }
|
|
2740
|
+
};
|
|
2741
|
+
const { publicKeyJwk } = await ctx.client.getLeaseBrokerKey(org.id);
|
|
2742
|
+
const wrappedAdminSecret = await wrapDek(new TextEncoder().encode(adminUri), publicKeyJwk);
|
|
2743
|
+
const { target: created } = await ctx.client.registerLeaseTarget(org.id, {
|
|
2744
|
+
name: options.name,
|
|
2745
|
+
config,
|
|
2746
|
+
wrappedAdminSecret
|
|
2747
|
+
});
|
|
2748
|
+
console.error(`registered MongoDB target ${created.name} (${created.id})`);
|
|
2749
|
+
console.error("\nEnsure a provisioning user exists, then `seekrit mongodb lease`:\n");
|
|
2750
|
+
console.log(mongoAdminSetupInstructions(config));
|
|
2751
|
+
});
|
|
2752
|
+
target.command("list").description("list MongoDB targets").option("--org <slug>").action(async (options) => {
|
|
2753
|
+
const ctx = buildContext();
|
|
2754
|
+
const org = await resolveOrg(ctx, options.org);
|
|
2755
|
+
const { targets } = await ctx.client.listLeaseTargets(org.id);
|
|
2756
|
+
for (const t of targets) {
|
|
2757
|
+
const cfg = t.config;
|
|
2758
|
+
if (cfg.provider !== "mongodb") continue;
|
|
2759
|
+
const host = `${cfg.connection.host}:${cfg.connection.port}`;
|
|
2760
|
+
console.log(`${t.id}\t${t.name}\t${host}\t${cfg.connection.database}\t${cfg.accessLevel ?? "readonly"}`);
|
|
2761
|
+
}
|
|
2762
|
+
});
|
|
2763
|
+
target.command("rm <targetId>").description("delete a MongoDB target").option("--org <slug>").action(async (targetId, options) => {
|
|
2764
|
+
const ctx = buildContext();
|
|
2765
|
+
const org = await resolveOrg(ctx, options.org);
|
|
2766
|
+
await ctx.client.deleteLeaseTarget(org.id, targetId);
|
|
2767
|
+
console.error(`deleted ${targetId}`);
|
|
2768
|
+
});
|
|
2769
|
+
mongo.command("lease <target>").description("mint short-lived MongoDB credentials; prints a ready-to-use connection URI").option("--org <slug>").option("--ttl <duration>", "credential lifetime, e.g. 30m, 1h, 8h", "1h").option("--json", "print the full credential as JSON").action(async (targetRef, options) => {
|
|
2770
|
+
const ctx = buildContext();
|
|
2771
|
+
const org = await resolveOrg(ctx, options.org);
|
|
2772
|
+
const { targets } = await ctx.client.listLeaseTargets(org.id);
|
|
2773
|
+
const t = targets.find((x) => x.id === targetRef || x.name === targetRef);
|
|
2774
|
+
if (!t) fail(`no target "${targetRef}" in ${org.slug}`);
|
|
2775
|
+
if (t.config.provider !== "mongodb") fail(`"${t?.name}" is not a mongodb target (see \`seekrit aws\`)`);
|
|
2776
|
+
const recipient = await generateMongoRecipientKeyPair();
|
|
2777
|
+
const { mongodb: leased } = await ctx.client.mintLease(org.id, {
|
|
2778
|
+
provider: "mongodb",
|
|
2779
|
+
targetId: t.id,
|
|
2780
|
+
recipientPublicKey: recipient.publicKeyJwk,
|
|
2781
|
+
ttlSeconds: parseTtlSeconds$4(options.ttl)
|
|
2782
|
+
});
|
|
2783
|
+
const cred = await unwrapMongoCredential(leased.wrappedCredential, recipient.privateKeyJwk);
|
|
2784
|
+
console.error(`leased ${cred.username} on ${cred.host}:${cred.port}/${cred.database} — expires ${cred.expiration}`);
|
|
2785
|
+
if (options.json) console.log(JSON.stringify(cred, null, 2));
|
|
2786
|
+
else console.log(`export MONGODB_URI='${cred.uri}'`);
|
|
2787
|
+
});
|
|
2788
|
+
mongo.command("leases").description("list MongoDB leases (the ledger — never secret material)").option("--org <slug>").action(async (options) => {
|
|
2789
|
+
const ctx = buildContext();
|
|
2790
|
+
const org = await resolveOrg(ctx, options.org);
|
|
2791
|
+
const { leases } = await ctx.client.listLeases(org.id);
|
|
2792
|
+
for (const l of leases) {
|
|
2793
|
+
if (l.provider !== "mongodb") continue;
|
|
2794
|
+
console.log(`${l.id}\t${l.status}\texpires ${l.expiresAt}`);
|
|
2795
|
+
}
|
|
2796
|
+
});
|
|
2797
|
+
mongo.command("revoke <leaseId>").description("revoke a lease now (drops the MongoDB user immediately)").option("--org <slug>").action(async (leaseId, options) => {
|
|
2798
|
+
const ctx = buildContext();
|
|
2799
|
+
const org = await resolveOrg(ctx, options.org);
|
|
2800
|
+
await ctx.client.revokeLease(org.id, leaseId);
|
|
2801
|
+
console.error(`revoked ${leaseId} (the MongoDB user has been dropped)`);
|
|
2802
|
+
});
|
|
2803
|
+
}
|
|
2804
|
+
//#endregion
|
|
1999
2805
|
//#region src/provisioner.ts
|
|
2000
2806
|
/**
|
|
2001
2807
|
* `seekrit provisioner` — helpers for the self-hosted **remote executor**
|
|
@@ -3087,8 +3893,10 @@ registerProvisionerCommands(program);
|
|
|
3087
3893
|
registerSshCommands(program);
|
|
3088
3894
|
registerAwsCommands(program);
|
|
3089
3895
|
registerGcpCommands(program);
|
|
3896
|
+
registerMongoCommands(program);
|
|
3897
|
+
registerKmsCommands(program);
|
|
3090
3898
|
program.command("mcp").description("run an MCP server over stdio so AI agents can drive seekrit").action(async () => {
|
|
3091
|
-
const { runMcpServer } = await import("./mcp-
|
|
3899
|
+
const { runMcpServer } = await import("./mcp-CVhEQDfd.js");
|
|
3092
3900
|
await runMcpServer();
|
|
3093
3901
|
});
|
|
3094
3902
|
program.command("audit").description("show the org audit trail").option("--org <slug>").option("--limit <n>", "entries to fetch", "50").action(async (options) => {
|
|
@@ -3102,4 +3910,4 @@ program.parseAsync(argv).catch((err) => {
|
|
|
3102
3910
|
fail(err instanceof Error ? err.message : String(err));
|
|
3103
3911
|
});
|
|
3104
3912
|
//#endregion
|
|
3105
|
-
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",
|