@sebastienrousseau/crypto-mcp 0.0.8 → 0.0.9
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +75 -22
- package/dist/protocol.d.ts +18 -0
- package/dist/protocol.d.ts.map +1 -0
- package/dist/protocol.js +63 -0
- package/dist/server.d.ts +1 -0
- package/dist/server.d.ts.map +1 -1
- package/dist/server.js +16 -14
- package/dist/tools/annotations.d.ts +6 -0
- package/dist/tools/annotations.d.ts.map +1 -0
- package/dist/tools/annotations.js +48 -0
- package/dist/tools/definitions.d.ts +4 -0
- package/dist/tools/definitions.d.ts.map +1 -1
- package/dist/tools/definitions.js +190 -132
- package/dist/tools/index.d.ts +1 -0
- package/dist/tools/index.d.ts.map +1 -1
- package/dist/tools/index.js +24 -6
- package/dist/tools/kem.d.ts +4 -0
- package/dist/tools/kem.d.ts.map +1 -0
- package/dist/tools/kem.js +20 -0
- package/dist/tools/keyimport.d.ts +5 -0
- package/dist/tools/keyimport.d.ts.map +1 -0
- package/dist/tools/keyimport.js +206 -0
- package/dist/tools/keys.d.ts +9 -0
- package/dist/tools/keys.d.ts.map +1 -1
- package/dist/tools/keys.js +56 -41
- package/dist/tools/keystore.d.ts +25 -0
- package/dist/tools/keystore.d.ts.map +1 -0
- package/dist/tools/keystore.js +81 -0
- package/dist/tools/kms.d.ts +1 -0
- package/dist/tools/kms.d.ts.map +1 -1
- package/dist/tools/kms.js +27 -18
- package/dist/tools/result.d.ts +0 -1
- package/dist/tools/result.d.ts.map +1 -1
- package/dist/tools/result.js +0 -8
- package/dist/tools/signing.d.ts.map +1 -1
- package/dist/tools/signing.js +81 -34
- package/dist/tools/symmetric.d.ts +1 -0
- package/dist/tools/symmetric.d.ts.map +1 -1
- package/dist/tools/symmetric.js +18 -7
- package/dist/tools/validate.d.ts +3 -0
- package/dist/tools/validate.d.ts.map +1 -0
- package/dist/tools/validate.js +76 -0
- package/dist/types.d.ts +19 -2
- package/dist/types.d.ts.map +1 -1
- package/package.json +19 -17
|
@@ -0,0 +1,206 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
3
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
|
+
};
|
|
5
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
+
exports.importKey = void 0;
|
|
7
|
+
exports.readAtMost = readAtMost;
|
|
8
|
+
const node_crypto_1 = __importDefault(require("node:crypto"));
|
|
9
|
+
const node_fs_1 = __importDefault(require("node:fs"));
|
|
10
|
+
const node_path_1 = __importDefault(require("node:path"));
|
|
11
|
+
const definitions_1 = require("./definitions");
|
|
12
|
+
const keys_1 = require("./keys");
|
|
13
|
+
const keystore_1 = require("./keystore");
|
|
14
|
+
const result_1 = require("./result");
|
|
15
|
+
const RAW_KEY_BYTES = 32;
|
|
16
|
+
const SOURCE = "crypto_key_import";
|
|
17
|
+
const UNREADABLE = `Key file not found or not readable in ${definitions_1.KEY_DIR_ENV}`;
|
|
18
|
+
const OPEN_FLAGS = node_fs_1.default.constants.O_RDONLY | (node_fs_1.default.constants.O_NOFOLLOW | 0);
|
|
19
|
+
async function keyDirectory() {
|
|
20
|
+
const dir = process.env[definitions_1.KEY_DIR_ENV];
|
|
21
|
+
if (!dir) {
|
|
22
|
+
throw new Error(`Key import is disabled: set ${definitions_1.KEY_DIR_ENV} to the directory holding the key files`);
|
|
23
|
+
}
|
|
24
|
+
try {
|
|
25
|
+
return await node_fs_1.default.promises.realpath(dir);
|
|
26
|
+
}
|
|
27
|
+
catch {
|
|
28
|
+
throw new Error(`${definitions_1.KEY_DIR_ENV} does not exist or cannot be read`);
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
function checkRelative(file) {
|
|
32
|
+
if (node_path_1.default.isAbsolute(file)) {
|
|
33
|
+
throw new Error(`path must be relative to ${definitions_1.KEY_DIR_ENV}`);
|
|
34
|
+
}
|
|
35
|
+
if (Array.from(file).some((char) => char < " ")) {
|
|
36
|
+
throw new Error("path must not contain control characters");
|
|
37
|
+
}
|
|
38
|
+
if (file.split(/[\\/]/).includes("..")) {
|
|
39
|
+
throw new Error("path must not contain '..' segments");
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
async function resolveKeyFile(dir, file) {
|
|
43
|
+
checkRelative(file);
|
|
44
|
+
const resolved = await fsStep(node_fs_1.default.promises.realpath(node_path_1.default.resolve(dir, file)));
|
|
45
|
+
if (!resolved.startsWith(dir + node_path_1.default.sep)) {
|
|
46
|
+
throw new Error(`path resolves outside ${definitions_1.KEY_DIR_ENV}`);
|
|
47
|
+
}
|
|
48
|
+
return resolved;
|
|
49
|
+
}
|
|
50
|
+
async function fsStep(operation) {
|
|
51
|
+
try {
|
|
52
|
+
return await operation;
|
|
53
|
+
}
|
|
54
|
+
catch {
|
|
55
|
+
throw new Error(UNREADABLE);
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
function checkFile(stats) {
|
|
59
|
+
if (!stats.isFile())
|
|
60
|
+
throw new Error("path is not a regular file");
|
|
61
|
+
if (stats.size > definitions_1.MAX_KEY_FILE_BYTES) {
|
|
62
|
+
throw new Error(`Key file is larger than ${definitions_1.MAX_KEY_FILE_BYTES} bytes`);
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
async function readAtMost(handle, max) {
|
|
66
|
+
const buf = Buffer.alloc(max + 1);
|
|
67
|
+
let length = 0;
|
|
68
|
+
for (;;) {
|
|
69
|
+
const { bytesRead } = await handle.read(buf, length, buf.length - length);
|
|
70
|
+
if (bytesRead === 0)
|
|
71
|
+
break;
|
|
72
|
+
length += bytesRead;
|
|
73
|
+
if (length > max) {
|
|
74
|
+
buf.fill(0);
|
|
75
|
+
throw new Error(`Key file is larger than ${max} bytes`);
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
return buf.subarray(0, length);
|
|
79
|
+
}
|
|
80
|
+
async function readKeyFile(file) {
|
|
81
|
+
checkFile(await fsStep(node_fs_1.default.promises.stat(file)));
|
|
82
|
+
const handle = await fsStep(node_fs_1.default.promises.open(file, OPEN_FLAGS));
|
|
83
|
+
try {
|
|
84
|
+
checkFile(await fsStep(handle.stat()));
|
|
85
|
+
return await readAtMost(handle, definitions_1.MAX_KEY_FILE_BYTES);
|
|
86
|
+
}
|
|
87
|
+
finally {
|
|
88
|
+
await handle.close();
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
function hexDigit(byte) {
|
|
92
|
+
if (byte >= 0x30 && byte <= 0x39)
|
|
93
|
+
return byte - 0x30;
|
|
94
|
+
const lower = byte | 0x20;
|
|
95
|
+
if (lower >= 0x61 && lower <= 0x66)
|
|
96
|
+
return lower - 0x57;
|
|
97
|
+
return -1;
|
|
98
|
+
}
|
|
99
|
+
function decodeHex(text) {
|
|
100
|
+
const out = Buffer.alloc(text.length / 2);
|
|
101
|
+
for (let i = 0; i < out.length; i++) {
|
|
102
|
+
const high = hexDigit(text[2 * i]);
|
|
103
|
+
const low = hexDigit(text[2 * i + 1]);
|
|
104
|
+
if (high < 0 || low < 0) {
|
|
105
|
+
out.fill(0);
|
|
106
|
+
return undefined;
|
|
107
|
+
}
|
|
108
|
+
out[i] = high * 16 + low;
|
|
109
|
+
}
|
|
110
|
+
return out;
|
|
111
|
+
}
|
|
112
|
+
function trimEnd(bytes) {
|
|
113
|
+
let end = bytes.length;
|
|
114
|
+
while (end > 0 && " \t\r\n".includes(String.fromCharCode(bytes[end - 1]))) {
|
|
115
|
+
end--;
|
|
116
|
+
}
|
|
117
|
+
return bytes.subarray(0, end);
|
|
118
|
+
}
|
|
119
|
+
function rawSecret(bytes) {
|
|
120
|
+
if (bytes.length === RAW_KEY_BYTES)
|
|
121
|
+
return Buffer.from(bytes);
|
|
122
|
+
const text = trimEnd(bytes);
|
|
123
|
+
const secret = text.length === 2 * RAW_KEY_BYTES ? decodeHex(text) : undefined;
|
|
124
|
+
if (!secret) {
|
|
125
|
+
throw new Error("Unrecognised key file: expected a PKCS#8 PEM private key, 32 raw bytes, or 64 hex digits");
|
|
126
|
+
}
|
|
127
|
+
return secret;
|
|
128
|
+
}
|
|
129
|
+
function rawKey(bytes, kind) {
|
|
130
|
+
const secret = rawSecret(bytes);
|
|
131
|
+
if (kind === "hmac-sha256") {
|
|
132
|
+
return {
|
|
133
|
+
kind: "hmac-sha256",
|
|
134
|
+
secret,
|
|
135
|
+
info: { bits: 256, source: SOURCE, usage: "crypto_sign, crypto_verify" },
|
|
136
|
+
};
|
|
137
|
+
}
|
|
138
|
+
return (0, keystore_1.symmetricKey)(secret, SOURCE);
|
|
139
|
+
}
|
|
140
|
+
function privateKeyKind(key) {
|
|
141
|
+
const details = key.asymmetricKeyDetails;
|
|
142
|
+
switch (key.asymmetricKeyType) {
|
|
143
|
+
case "ed25519":
|
|
144
|
+
return { kind: "ed25519", info: {} };
|
|
145
|
+
case "rsa":
|
|
146
|
+
if (!definitions_1.RSA_MODULUS_LENGTHS.includes(Number(details.modulusLength))) {
|
|
147
|
+
throw new Error(`Unsupported RSA modulus length (allowed: ${definitions_1.RSA_MODULUS_LENGTHS.join(", ")})`);
|
|
148
|
+
}
|
|
149
|
+
return { kind: "rsa", info: { bits: details.modulusLength } };
|
|
150
|
+
case "ec":
|
|
151
|
+
if (!definitions_1.EC_CURVES.includes(String(details.namedCurve))) {
|
|
152
|
+
throw new Error(`Unsupported EC curve (allowed: ${definitions_1.EC_CURVES.join(", ")})`);
|
|
153
|
+
}
|
|
154
|
+
return { kind: "ecc", info: { curve: details.namedCurve } };
|
|
155
|
+
default:
|
|
156
|
+
throw new Error(`Unsupported private key type: ${key.asymmetricKeyType} (allowed: ed25519, rsa, ec)`);
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
function parsePrivateKey(bytes) {
|
|
160
|
+
try {
|
|
161
|
+
return node_crypto_1.default.createPrivateKey({ key: bytes, format: "pem" });
|
|
162
|
+
}
|
|
163
|
+
catch {
|
|
164
|
+
throw new Error("Key file is not a valid PKCS#8 PEM private key");
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
function pkcs8Key(bytes) {
|
|
168
|
+
const privateKey = parsePrivateKey(bytes);
|
|
169
|
+
const { kind, info } = privateKeyKind(privateKey);
|
|
170
|
+
const publicKey = node_crypto_1.default.createPublicKey(privateKey);
|
|
171
|
+
return (0, keys_1.asymmetric)(kind, { privateKey, publicKey }, { ...info, source: SOURCE });
|
|
172
|
+
}
|
|
173
|
+
function pemKey(bytes, kind) {
|
|
174
|
+
if (kind !== undefined) {
|
|
175
|
+
throw new Error("kind applies only to raw keys; a PEM key's type comes from the key");
|
|
176
|
+
}
|
|
177
|
+
if (bytes.includes("-----BEGIN ENCRYPTED PRIVATE KEY-----")) {
|
|
178
|
+
throw new Error("Encrypted PKCS#8 keys are not supported: decrypt the key file first (openssl pkcs8 -nocrypt)");
|
|
179
|
+
}
|
|
180
|
+
if (bytes.includes("-----BEGIN PRIVATE KEY-----"))
|
|
181
|
+
return pkcs8Key(bytes);
|
|
182
|
+
if (bytes.includes("PUBLIC KEY-----")) {
|
|
183
|
+
throw new Error("Public keys are not imported: pass the PEM to crypto_verify as publicKey instead");
|
|
184
|
+
}
|
|
185
|
+
throw new Error("Unsupported PEM block: only unencrypted PKCS#8 private keys ('BEGIN PRIVATE KEY') are imported");
|
|
186
|
+
}
|
|
187
|
+
function parseKey(bytes, kind) {
|
|
188
|
+
if (bytes.length !== RAW_KEY_BYTES && bytes.includes("-----BEGIN ")) {
|
|
189
|
+
return pemKey(bytes, kind);
|
|
190
|
+
}
|
|
191
|
+
return rawKey(bytes, kind);
|
|
192
|
+
}
|
|
193
|
+
const importKey = async (args) => {
|
|
194
|
+
const dir = await keyDirectory();
|
|
195
|
+
const file = await resolveKeyFile(dir, String(args.path));
|
|
196
|
+
const bytes = await readKeyFile(file);
|
|
197
|
+
try {
|
|
198
|
+
const material = parseKey(bytes, args.kind);
|
|
199
|
+
return (0, result_1.jsonResult)(keystore_1.keyStore.describe(keystore_1.keyStore.add(material)));
|
|
200
|
+
}
|
|
201
|
+
finally {
|
|
202
|
+
bytes.fill(0);
|
|
203
|
+
}
|
|
204
|
+
};
|
|
205
|
+
exports.importKey = importKey;
|
|
206
|
+
//# sourceMappingURL=keyimport.js.map
|
package/dist/tools/keys.d.ts
CHANGED
|
@@ -1,4 +1,13 @@
|
|
|
1
|
+
import crypto from "node:crypto";
|
|
2
|
+
import { KeyMaterial } from "./keystore";
|
|
1
3
|
import { ToolHandler } from "./result";
|
|
4
|
+
export declare function publicPem(key: crypto.KeyObject): string;
|
|
5
|
+
export declare function asymmetric(kind: KeyMaterial["kind"], pair: {
|
|
6
|
+
privateKey: crypto.KeyObject;
|
|
7
|
+
publicKey: crypto.KeyObject;
|
|
8
|
+
}, info: Record<string, unknown>): KeyMaterial;
|
|
2
9
|
export declare const generateKey: ToolHandler;
|
|
10
|
+
export declare const listKeys: ToolHandler;
|
|
11
|
+
export declare const destroyKey: ToolHandler;
|
|
3
12
|
export declare const inspectKey: ToolHandler;
|
|
4
13
|
//# sourceMappingURL=keys.d.ts.map
|
package/dist/tools/keys.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"keys.d.ts","sourceRoot":"","sources":["../../src/tools/keys.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"keys.d.ts","sourceRoot":"","sources":["../../src/tools/keys.ts"],"names":[],"mappings":"AAEA,OAAO,MAAM,MAAM,aAAa,CAAC;AAGjC,OAAO,EAAE,WAAW,EAA0B,MAAM,YAAY,CAAC;AACjE,OAAO,EAAY,WAAW,EAA2B,MAAM,UAAU,CAAC;AAG1E,wBAAgB,SAAS,CAAC,GAAG,EAAE,MAAM,CAAC,SAAS,GAAG,MAAM,CAEvD;AAGD,wBAAgB,UAAU,CACxB,IAAI,EAAE,WAAW,CAAC,MAAM,CAAC,EACzB,IAAI,EAAE;IAAE,UAAU,EAAE,MAAM,CAAC,SAAS,CAAC;IAAC,SAAS,EAAE,MAAM,CAAC,SAAS,CAAA;CAAE,EACnE,IAAI,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAC5B,WAAW,CAOb;AAqED,eAAO,MAAM,WAAW,EAAE,WAOzB,CAAC;AAGF,eAAO,MAAM,QAAQ,EAAE,WAC6C,CAAC;AAGrE,eAAO,MAAM,UAAU,EAAE,WAMxB,CAAC;AAmBF,eAAO,MAAM,UAAU,EAAE,WAaxB,CAAC"}
|
package/dist/tools/keys.js
CHANGED
|
@@ -3,63 +3,66 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
|
3
3
|
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
4
|
};
|
|
5
5
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
-
exports.inspectKey = exports.generateKey = void 0;
|
|
6
|
+
exports.inspectKey = exports.destroyKey = exports.listKeys = exports.generateKey = void 0;
|
|
7
|
+
exports.publicPem = publicPem;
|
|
8
|
+
exports.asymmetric = asymmetric;
|
|
7
9
|
const node_crypto_1 = __importDefault(require("node:crypto"));
|
|
8
10
|
const crypto_lib_1 = require("@sebastienrousseau/crypto-lib");
|
|
9
11
|
const definitions_1 = require("./definitions");
|
|
12
|
+
const keystore_1 = require("./keystore");
|
|
10
13
|
const result_1 = require("./result");
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
14
|
+
function publicPem(key) {
|
|
15
|
+
return key.export({ type: "spki", format: "pem" });
|
|
16
|
+
}
|
|
17
|
+
function asymmetric(kind, pair, info) {
|
|
18
|
+
return {
|
|
19
|
+
kind,
|
|
20
|
+
privateKey: pair.privateKey,
|
|
21
|
+
publicKey: pair.publicKey,
|
|
22
|
+
info: { ...info, quantumSafe: false, publicKey: publicPem(pair.publicKey) },
|
|
23
|
+
};
|
|
24
|
+
}
|
|
15
25
|
function generateEd25519() {
|
|
16
|
-
|
|
17
|
-
return { type: "ed25519", quantumSafe: false, publicKey, privateKey };
|
|
26
|
+
return asymmetric("ed25519", node_crypto_1.default.generateKeyPairSync("ed25519"), {});
|
|
18
27
|
}
|
|
19
28
|
function generateRsa(args) {
|
|
20
|
-
const modulusLength = Number(args.modulusLength
|
|
29
|
+
const modulusLength = Number(args.modulusLength ?? 2048);
|
|
21
30
|
if (!definitions_1.RSA_MODULUS_LENGTHS.includes(modulusLength)) {
|
|
22
31
|
throw new Error(`Unsupported RSA modulus length: ${modulusLength} (allowed: ${definitions_1.RSA_MODULUS_LENGTHS.join(", ")})`);
|
|
23
32
|
}
|
|
24
|
-
const
|
|
25
|
-
|
|
26
|
-
...PEM,
|
|
27
|
-
});
|
|
28
|
-
return {
|
|
29
|
-
type: "rsa",
|
|
30
|
-
bits: modulusLength,
|
|
31
|
-
quantumSafe: false,
|
|
32
|
-
publicKey,
|
|
33
|
-
privateKey,
|
|
34
|
-
};
|
|
33
|
+
const pair = node_crypto_1.default.generateKeyPairSync("rsa", { modulusLength });
|
|
34
|
+
return asymmetric("rsa", pair, { bits: modulusLength });
|
|
35
35
|
}
|
|
36
36
|
function generateEcc(args) {
|
|
37
|
-
const namedCurve = args.curve
|
|
37
|
+
const namedCurve = String(args.curve ?? "prime256v1");
|
|
38
38
|
if (!definitions_1.EC_CURVES.includes(namedCurve)) {
|
|
39
39
|
throw new Error(`Unsupported curve: ${namedCurve} (allowed: ${definitions_1.EC_CURVES.join(", ")})`);
|
|
40
40
|
}
|
|
41
|
-
const
|
|
42
|
-
|
|
43
|
-
...PEM,
|
|
44
|
-
});
|
|
45
|
-
return {
|
|
46
|
-
type: "ecc",
|
|
47
|
-
curve: namedCurve,
|
|
48
|
-
quantumSafe: false,
|
|
49
|
-
publicKey,
|
|
50
|
-
privateKey,
|
|
51
|
-
};
|
|
41
|
+
const pair = node_crypto_1.default.generateKeyPairSync("ec", { namedCurve });
|
|
42
|
+
return asymmetric("ecc", pair, { curve: namedCurve });
|
|
52
43
|
}
|
|
53
44
|
function generateMlKem768() {
|
|
54
45
|
const { publicKey, secretKey } = (0, crypto_lib_1.mlKemKeygen)(768);
|
|
55
46
|
return {
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
47
|
+
kind: "ml-kem-768",
|
|
48
|
+
secret: Buffer.from(secretKey, "hex"),
|
|
49
|
+
info: {
|
|
50
|
+
standard: "NIST FIPS 203",
|
|
51
|
+
securityCategory: 3,
|
|
52
|
+
quantumSafe: true,
|
|
53
|
+
encoding: "hex",
|
|
54
|
+
publicKey,
|
|
55
|
+
},
|
|
56
|
+
};
|
|
57
|
+
}
|
|
58
|
+
function generateSymmetric() {
|
|
59
|
+
return (0, keystore_1.symmetricKey)(node_crypto_1.default.randomBytes(32), "generated");
|
|
60
|
+
}
|
|
61
|
+
function generateHmac() {
|
|
62
|
+
return {
|
|
63
|
+
kind: "hmac-sha256",
|
|
64
|
+
secret: node_crypto_1.default.randomBytes(32),
|
|
65
|
+
info: { bits: 256, usage: "crypto_sign, crypto_verify" },
|
|
63
66
|
};
|
|
64
67
|
}
|
|
65
68
|
const GENERATORS = {
|
|
@@ -67,17 +70,29 @@ const GENERATORS = {
|
|
|
67
70
|
rsa: generateRsa,
|
|
68
71
|
ecc: generateEcc,
|
|
69
72
|
"ml-kem-768": generateMlKem768,
|
|
73
|
+
"symmetric-256": generateSymmetric,
|
|
74
|
+
"hmac-sha256": generateHmac,
|
|
70
75
|
};
|
|
71
76
|
const generateKey = async (args) => {
|
|
72
|
-
const type = args.type
|
|
77
|
+
const type = String(args.type);
|
|
73
78
|
const generator = Object.hasOwn(GENERATORS, type)
|
|
74
79
|
? GENERATORS[type]
|
|
75
80
|
: undefined;
|
|
76
81
|
if (!generator)
|
|
77
82
|
return (0, result_1.errorResult)(`Unsupported key type: ${type}`);
|
|
78
|
-
return (0, result_1.jsonResult)(generator(args));
|
|
83
|
+
return (0, result_1.jsonResult)(keystore_1.keyStore.describe(keystore_1.keyStore.add(generator(args))));
|
|
79
84
|
};
|
|
80
85
|
exports.generateKey = generateKey;
|
|
86
|
+
const listKeys = async () => (0, result_1.jsonResult)({ keys: keystore_1.keyStore.list(), capacity: keystore_1.keyStore.capacity });
|
|
87
|
+
exports.listKeys = listKeys;
|
|
88
|
+
const destroyKey = async (args) => {
|
|
89
|
+
const keyHandle = String(args.keyHandle);
|
|
90
|
+
if (!keystore_1.keyStore.remove(keyHandle)) {
|
|
91
|
+
return (0, result_1.errorResult)("Unknown key handle: nothing was destroyed");
|
|
92
|
+
}
|
|
93
|
+
return (0, result_1.jsonResult)({ keyHandle, destroyed: true });
|
|
94
|
+
};
|
|
95
|
+
exports.destroyKey = destroyKey;
|
|
81
96
|
function classifyKey(keyData) {
|
|
82
97
|
if (keyData.includes("-----BEGIN PGP"))
|
|
83
98
|
return "OpenPGP Key Block";
|
|
@@ -97,7 +112,7 @@ function keyFormat(keyData) {
|
|
|
97
112
|
return keyData.includes("-----BEGIN") ? "PEM" : "Raw";
|
|
98
113
|
}
|
|
99
114
|
const inspectKey = async (args) => {
|
|
100
|
-
const keyData =
|
|
115
|
+
const keyData = String(args.keyData).trim();
|
|
101
116
|
const fingerprint = node_crypto_1.default
|
|
102
117
|
.createHash("sha256")
|
|
103
118
|
.update(keyData)
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import crypto from "node:crypto";
|
|
2
|
+
export type KeyKind = "ed25519" | "rsa" | "ecc" | "ml-kem-768" | "symmetric-256" | "hmac-sha256";
|
|
3
|
+
export interface KeyMaterial {
|
|
4
|
+
kind: KeyKind;
|
|
5
|
+
info: Record<string, unknown>;
|
|
6
|
+
privateKey?: crypto.KeyObject;
|
|
7
|
+
publicKey?: crypto.KeyObject;
|
|
8
|
+
secret?: Buffer;
|
|
9
|
+
}
|
|
10
|
+
export declare const MAX_KEYS = 64;
|
|
11
|
+
export declare const KEY_HANDLE_PATTERN = "^kh_[0-9a-f]{32}$";
|
|
12
|
+
export declare function symmetricKey(secret: Buffer, source: string): KeyMaterial;
|
|
13
|
+
export declare class KeyStore {
|
|
14
|
+
readonly capacity: number;
|
|
15
|
+
private readonly keys;
|
|
16
|
+
constructor(capacity?: number);
|
|
17
|
+
get size(): number;
|
|
18
|
+
add(material: KeyMaterial): string;
|
|
19
|
+
use(handle: string, kinds: readonly KeyKind[]): KeyMaterial;
|
|
20
|
+
describe(handle: string): Record<string, unknown>;
|
|
21
|
+
list(): Array<Record<string, unknown>>;
|
|
22
|
+
remove(handle: string): boolean;
|
|
23
|
+
}
|
|
24
|
+
export declare const keyStore: KeyStore;
|
|
25
|
+
//# sourceMappingURL=keystore.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"keystore.d.ts","sourceRoot":"","sources":["../../src/tools/keystore.ts"],"names":[],"mappings":"AAEA,OAAO,MAAM,MAAM,aAAa,CAAC;AAYjC,MAAM,MAAM,OAAO,GACjB,SAAS,GAAG,KAAK,GAAG,KAAK,GAAG,YAAY,GAAG,eAAe,GAAG,aAAa,CAAC;AAG7E,MAAM,WAAW,WAAW;IAC1B,IAAI,EAAE,OAAO,CAAC;IAEd,IAAI,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;IAE9B,UAAU,CAAC,EAAE,MAAM,CAAC,SAAS,CAAC;IAE9B,SAAS,CAAC,EAAE,MAAM,CAAC,SAAS,CAAC;IAE7B,MAAM,CAAC,EAAE,MAAM,CAAC;CACjB;AAOD,eAAO,MAAM,QAAQ,KAAK,CAAC;AAG3B,eAAO,MAAM,kBAAkB,sBAAsB,CAAC;AAMtD,wBAAgB,YAAY,CAAC,MAAM,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,GAAG,WAAW,CAUxE;AAQD,qBAAa,QAAQ;aAIgB,QAAQ,EAAE,MAAM;IAHnD,OAAO,CAAC,QAAQ,CAAC,IAAI,CAAgC;gBAGlB,QAAQ,GAAE,MAAiB;IAG9D,IAAW,IAAI,IAAI,MAAM,CAExB;IAGM,GAAG,CAAC,QAAQ,EAAE,WAAW,GAAG,MAAM;IAiBlC,GAAG,CAAC,MAAM,EAAE,MAAM,EAAE,KAAK,EAAE,SAAS,OAAO,EAAE,GAAG,WAAW;IAkB3D,QAAQ,CAAC,MAAM,EAAE,MAAM,GAAG,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC;IAWjD,IAAI,IAAI,KAAK,CAAC,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;IAKtC,MAAM,CAAC,MAAM,EAAE,MAAM,GAAG,OAAO;CAOvC;AAGD,eAAO,MAAM,QAAQ,UAAiB,CAAC"}
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
3
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
|
+
};
|
|
5
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
+
exports.keyStore = exports.KeyStore = exports.KEY_HANDLE_PATTERN = exports.MAX_KEYS = void 0;
|
|
7
|
+
exports.symmetricKey = symmetricKey;
|
|
8
|
+
const node_crypto_1 = __importDefault(require("node:crypto"));
|
|
9
|
+
exports.MAX_KEYS = 64;
|
|
10
|
+
exports.KEY_HANDLE_PATTERN = "^kh_[0-9a-f]{32}$";
|
|
11
|
+
function symmetricKey(secret, source) {
|
|
12
|
+
return {
|
|
13
|
+
kind: "symmetric-256",
|
|
14
|
+
secret,
|
|
15
|
+
info: {
|
|
16
|
+
bits: 256,
|
|
17
|
+
source,
|
|
18
|
+
usage: "crypto_encrypt, crypto_decrypt, crypto_kms_wrap",
|
|
19
|
+
},
|
|
20
|
+
};
|
|
21
|
+
}
|
|
22
|
+
function wipe(key) {
|
|
23
|
+
key.secret?.fill(0);
|
|
24
|
+
}
|
|
25
|
+
class KeyStore {
|
|
26
|
+
capacity;
|
|
27
|
+
keys = new Map();
|
|
28
|
+
constructor(capacity = exports.MAX_KEYS) {
|
|
29
|
+
this.capacity = capacity;
|
|
30
|
+
}
|
|
31
|
+
get size() {
|
|
32
|
+
return this.keys.size;
|
|
33
|
+
}
|
|
34
|
+
add(material) {
|
|
35
|
+
const handle = `kh_${node_crypto_1.default.randomBytes(16).toString("hex")}`;
|
|
36
|
+
this.keys.set(handle, {
|
|
37
|
+
...material,
|
|
38
|
+
createdAt: new Date().toISOString(),
|
|
39
|
+
});
|
|
40
|
+
while (this.keys.size > this.capacity) {
|
|
41
|
+
const oldest = this.keys.keys().next().value;
|
|
42
|
+
this.remove(oldest);
|
|
43
|
+
}
|
|
44
|
+
return handle;
|
|
45
|
+
}
|
|
46
|
+
use(handle, kinds) {
|
|
47
|
+
const key = this.keys.get(handle);
|
|
48
|
+
if (!key) {
|
|
49
|
+
throw new Error("Unknown key handle: it was never issued, or the key was destroyed or evicted");
|
|
50
|
+
}
|
|
51
|
+
if (!kinds.includes(key.kind)) {
|
|
52
|
+
throw new Error(`Key handle refers to a ${key.kind} key; this tool needs ${kinds.join(" or ")}`);
|
|
53
|
+
}
|
|
54
|
+
this.keys.delete(handle);
|
|
55
|
+
this.keys.set(handle, key);
|
|
56
|
+
return key;
|
|
57
|
+
}
|
|
58
|
+
describe(handle) {
|
|
59
|
+
const key = this.keys.get(handle);
|
|
60
|
+
return {
|
|
61
|
+
keyHandle: handle,
|
|
62
|
+
type: key.kind,
|
|
63
|
+
createdAt: key.createdAt,
|
|
64
|
+
...key.info,
|
|
65
|
+
};
|
|
66
|
+
}
|
|
67
|
+
list() {
|
|
68
|
+
return [...this.keys.keys()].map((handle) => this.describe(handle));
|
|
69
|
+
}
|
|
70
|
+
remove(handle) {
|
|
71
|
+
const key = this.keys.get(handle);
|
|
72
|
+
if (!key)
|
|
73
|
+
return false;
|
|
74
|
+
wipe(key);
|
|
75
|
+
this.keys.delete(handle);
|
|
76
|
+
return true;
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
exports.KeyStore = KeyStore;
|
|
80
|
+
exports.keyStore = new KeyStore();
|
|
81
|
+
//# sourceMappingURL=keystore.js.map
|
package/dist/tools/kms.d.ts
CHANGED
package/dist/tools/kms.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"kms.d.ts","sourceRoot":"","sources":["../../src/tools/kms.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"kms.d.ts","sourceRoot":"","sources":["../../src/tools/kms.ts"],"names":[],"mappings":"AAIA,OAAO,EAAY,WAAW,EAAc,MAAM,UAAU,CAAC;AAe7D,eAAO,MAAM,cAAc,KAAK,CAAC;AA2CjC,eAAO,MAAM,OAAO,EAAE,WAmBrB,CAAC;AAMF,eAAO,MAAM,SAAS,EAAE,WAgBvB,CAAC"}
|
package/dist/tools/kms.js
CHANGED
|
@@ -1,16 +1,19 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
-
exports.kmsUnwrap = exports.kmsWrap = void 0;
|
|
3
|
+
exports.kmsUnwrap = exports.kmsWrap = exports.MAX_KEK_LABELS = void 0;
|
|
4
4
|
const crypto_kms_1 = require("@sebastienrousseau/crypto-kms");
|
|
5
|
+
const keystore_1 = require("./keystore");
|
|
5
6
|
const result_1 = require("./result");
|
|
7
|
+
const symmetric_1 = require("./symmetric");
|
|
6
8
|
const localKms = new crypto_kms_1.LocalKmsProvider();
|
|
7
9
|
const localKeyIds = new Map();
|
|
10
|
+
exports.MAX_KEK_LABELS = 64;
|
|
8
11
|
function kmsRequest(args) {
|
|
9
|
-
const provider = String(args.provider
|
|
12
|
+
const provider = String(args.provider);
|
|
10
13
|
if (provider !== "local") {
|
|
11
14
|
throw new Error(`KMS provider '${provider}' is not configured in this server; only 'local' is available`);
|
|
12
15
|
}
|
|
13
|
-
return { provider, keyId: String(args.keyId
|
|
16
|
+
return { provider, keyId: String(args.keyId) };
|
|
14
17
|
}
|
|
15
18
|
function wrapContext(keyId) {
|
|
16
19
|
return { purpose: "crypto-mcp-dek-wrap", keyId };
|
|
@@ -19,6 +22,9 @@ async function localKeyFor(label) {
|
|
|
19
22
|
const existing = localKeyIds.get(label);
|
|
20
23
|
if (existing)
|
|
21
24
|
return existing;
|
|
25
|
+
if (localKeyIds.size >= exports.MAX_KEK_LABELS) {
|
|
26
|
+
throw new Error(`At most ${exports.MAX_KEK_LABELS} KEK labels can be created; reuse an existing keyId`);
|
|
27
|
+
}
|
|
22
28
|
const created = await localKms.createKey("aes-256-gcm", "wrap", {
|
|
23
29
|
label,
|
|
24
30
|
});
|
|
@@ -27,15 +33,20 @@ async function localKeyFor(label) {
|
|
|
27
33
|
}
|
|
28
34
|
const kmsWrap = async (args) => {
|
|
29
35
|
const { provider, keyId } = kmsRequest(args);
|
|
30
|
-
const dek = (0,
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
36
|
+
const dek = Buffer.from((0, symmetric_1.symmetricSecret)(args.keyHandle));
|
|
37
|
+
try {
|
|
38
|
+
const kmsKeyId = await localKeyFor(keyId);
|
|
39
|
+
const { ciphertext } = await localKms.encrypt(kmsKeyId, dek, wrapContext(keyId));
|
|
40
|
+
return (0, result_1.jsonResult)({
|
|
41
|
+
provider,
|
|
42
|
+
keyId,
|
|
43
|
+
status: "wrapped",
|
|
44
|
+
wrappedKey: Buffer.from(ciphertext, "base64").toString("hex"),
|
|
45
|
+
});
|
|
46
|
+
}
|
|
47
|
+
finally {
|
|
48
|
+
dek.fill(0);
|
|
49
|
+
}
|
|
39
50
|
};
|
|
40
51
|
exports.kmsWrap = kmsWrap;
|
|
41
52
|
const kmsUnwrap = async (args) => {
|
|
@@ -46,12 +57,10 @@ const kmsUnwrap = async (args) => {
|
|
|
46
57
|
}
|
|
47
58
|
const wrapped = Buffer.from(String(args.wrappedKey), "hex");
|
|
48
59
|
const { plaintext } = await localKms.decrypt(kmsKeyId, wrapped.toString("base64"), wrapContext(keyId));
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
dek: Buffer.from(plaintext).toString("hex"),
|
|
54
|
-
});
|
|
60
|
+
const dek = Buffer.from(plaintext);
|
|
61
|
+
plaintext.fill(0);
|
|
62
|
+
const keyHandle = keystore_1.keyStore.add((0, keystore_1.symmetricKey)(dek, "crypto_kms_unwrap"));
|
|
63
|
+
return (0, result_1.jsonResult)({ provider, keyId, status: "unwrapped", keyHandle });
|
|
55
64
|
};
|
|
56
65
|
exports.kmsUnwrap = kmsUnwrap;
|
|
57
66
|
//# sourceMappingURL=kms.js.map
|
package/dist/tools/result.d.ts
CHANGED
|
@@ -3,5 +3,4 @@ export type ToolArgs = Record<string, unknown>;
|
|
|
3
3
|
export type ToolHandler = (args: ToolArgs) => Promise<MCPCallToolResult>;
|
|
4
4
|
export declare function jsonResult(value: unknown): MCPCallToolResult;
|
|
5
5
|
export declare function errorResult(text: string): MCPCallToolResult;
|
|
6
|
-
export declare function parseKey256(value: unknown, label?: string): Buffer;
|
|
7
6
|
//# sourceMappingURL=result.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"result.d.ts","sourceRoot":"","sources":["../../src/tools/result.ts"],"names":[],"mappings":"AAEA,OAAO,EAAE,iBAAiB,EAAE,MAAM,UAAU,CAAC;AAG7C,MAAM,MAAM,QAAQ,GAAG,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;AAG/C,MAAM,MAAM,WAAW,GAAG,CAAC,IAAI,EAAE,QAAQ,KAAK,OAAO,CAAC,iBAAiB,CAAC,CAAC;AAGzE,wBAAgB,UAAU,CAAC,KAAK,EAAE,OAAO,GAAG,iBAAiB,CAI5D;AAGD,wBAAgB,WAAW,CAAC,IAAI,EAAE,MAAM,GAAG,iBAAiB,CAE3D
|
|
1
|
+
{"version":3,"file":"result.d.ts","sourceRoot":"","sources":["../../src/tools/result.ts"],"names":[],"mappings":"AAEA,OAAO,EAAE,iBAAiB,EAAE,MAAM,UAAU,CAAC;AAG7C,MAAM,MAAM,QAAQ,GAAG,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;AAG/C,MAAM,MAAM,WAAW,GAAG,CAAC,IAAI,EAAE,QAAQ,KAAK,OAAO,CAAC,iBAAiB,CAAC,CAAC;AAGzE,wBAAgB,UAAU,CAAC,KAAK,EAAE,OAAO,GAAG,iBAAiB,CAI5D;AAGD,wBAAgB,WAAW,CAAC,IAAI,EAAE,MAAM,GAAG,iBAAiB,CAE3D"}
|
package/dist/tools/result.js
CHANGED
|
@@ -2,7 +2,6 @@
|
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
3
|
exports.jsonResult = jsonResult;
|
|
4
4
|
exports.errorResult = errorResult;
|
|
5
|
-
exports.parseKey256 = parseKey256;
|
|
6
5
|
function jsonResult(value) {
|
|
7
6
|
return {
|
|
8
7
|
content: [{ type: "text", text: JSON.stringify(value, null, 2) }],
|
|
@@ -11,11 +10,4 @@ function jsonResult(value) {
|
|
|
11
10
|
function errorResult(text) {
|
|
12
11
|
return { isError: true, content: [{ type: "text", text }] };
|
|
13
12
|
}
|
|
14
|
-
function parseKey256(value, label = "key") {
|
|
15
|
-
const hex = String(value ?? "");
|
|
16
|
-
if (!/^[0-9a-fA-F]{64}$/.test(hex)) {
|
|
17
|
-
throw new Error(`${label} must be a 256-bit key encoded as 64 hex characters`);
|
|
18
|
-
}
|
|
19
|
-
return Buffer.from(hex, "hex");
|
|
20
|
-
}
|
|
21
13
|
//# sourceMappingURL=result.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"signing.d.ts","sourceRoot":"","sources":["../../src/tools/signing.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"signing.d.ts","sourceRoot":"","sources":["../../src/tools/signing.ts"],"names":[],"mappings":"AAKA,OAAO,EAAY,WAAW,EAAc,MAAM,UAAU,CAAC;AA0D7D,eAAO,MAAM,IAAI,EAAE,WAalB,CAAC;AAwDF,eAAO,MAAM,MAAM,EAAE,WASpB,CAAC;AAGF,eAAO,MAAM,IAAI,EAAE,WAUlB,CAAC"}
|