@cryptiklemur/lattice 1.32.1 → 1.33.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/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@cryptiklemur/lattice",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.33.0",
|
|
4
4
|
"description": "Multi-machine agentic dashboard for Claude Code. Monitor sessions, manage MCP servers and skills, orchestrate across mesh-networked nodes.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"author": "Aaron Scherer <me@aaronscherer.me>",
|
|
@@ -111,6 +111,7 @@ registerHandler("mesh", function (clientId: string, message: ClientMessage) {
|
|
|
111
111
|
type: "mesh:hello",
|
|
112
112
|
nodeId: identity.id,
|
|
113
113
|
name: pairConfig.name,
|
|
114
|
+
publicKey: identity.publicKey,
|
|
114
115
|
token: parsed!.token,
|
|
115
116
|
port: pairConfig.port,
|
|
116
117
|
addresses: getAllAddresses().map(function (a) { return a.address + ":" + pairConfig.port; }),
|
|
@@ -120,7 +121,7 @@ registerHandler("mesh", function (clientId: string, message: ClientMessage) {
|
|
|
120
121
|
|
|
121
122
|
pairWs.addEventListener("message", function (event: MessageEvent) {
|
|
122
123
|
try {
|
|
123
|
-
var data = JSON.parse(event.data as string) as { type: string; nodeId?: string; name?: string; error?: string };
|
|
124
|
+
var data = JSON.parse(event.data as string) as { type: string; nodeId?: string; name?: string; publicKey?: string; error?: string };
|
|
124
125
|
|
|
125
126
|
if (data.type === "mesh:hello_rejected") {
|
|
126
127
|
clearTimeout(pairTimeout);
|
|
@@ -136,7 +137,7 @@ registerHandler("mesh", function (clientId: string, message: ClientMessage) {
|
|
|
136
137
|
id: data.nodeId,
|
|
137
138
|
name: data.name,
|
|
138
139
|
addresses: [peerAddr],
|
|
139
|
-
publicKey: "",
|
|
140
|
+
publicKey: data.publicKey ?? "",
|
|
140
141
|
pairedAt: Date.now(),
|
|
141
142
|
};
|
|
142
143
|
addPeer(peer);
|
|
@@ -170,16 +171,21 @@ registerHandler("mesh", function (clientId: string, message: ClientMessage) {
|
|
|
170
171
|
}
|
|
171
172
|
|
|
172
173
|
if ((message as any).type === "mesh:hello") {
|
|
173
|
-
var hello = message as any as { type: "mesh:hello"; nodeId: string; name: string; token?: string; port?: number; addresses?: string[]; projects: Array<{ slug: string; title: string }> };
|
|
174
|
+
var hello = message as any as { type: "mesh:hello"; nodeId: string; name: string; publicKey?: string; token?: string; port?: number; addresses?: string[]; projects: Array<{ slug: string; title: string }> };
|
|
174
175
|
|
|
175
176
|
var knownPeer = hello.nodeId ? getPeer(hello.nodeId) : undefined;
|
|
176
177
|
|
|
177
178
|
if (knownPeer) {
|
|
179
|
+
if (knownPeer.publicKey && hello.publicKey && knownPeer.publicKey !== hello.publicKey) {
|
|
180
|
+
sendTo(clientId, { type: "mesh:hello_rejected" as any, error: "Public key mismatch — possible impersonation" });
|
|
181
|
+
return;
|
|
182
|
+
}
|
|
178
183
|
var identity = loadOrCreateIdentity();
|
|
179
184
|
sendTo(clientId, {
|
|
180
185
|
type: "mesh:hello" as any,
|
|
181
186
|
nodeId: identity.id,
|
|
182
187
|
name: loadConfig().name,
|
|
188
|
+
publicKey: identity.publicKey,
|
|
183
189
|
projects: [],
|
|
184
190
|
});
|
|
185
191
|
broadcast({ type: "mesh:nodes", nodes: buildNodesMessage() });
|
|
@@ -198,7 +204,7 @@ registerHandler("mesh", function (clientId: string, message: ClientMessage) {
|
|
|
198
204
|
id: hello.nodeId,
|
|
199
205
|
name: hello.name,
|
|
200
206
|
addresses: peerAddresses,
|
|
201
|
-
publicKey: "",
|
|
207
|
+
publicKey: hello.publicKey ?? "",
|
|
202
208
|
pairedAt: Date.now(),
|
|
203
209
|
};
|
|
204
210
|
addPeer(peer);
|
|
@@ -212,6 +218,7 @@ registerHandler("mesh", function (clientId: string, message: ClientMessage) {
|
|
|
212
218
|
type: "mesh:hello" as any,
|
|
213
219
|
nodeId: identity2.id,
|
|
214
220
|
name: loadConfig().name,
|
|
221
|
+
publicKey: identity2.publicKey,
|
|
215
222
|
projects: [],
|
|
216
223
|
});
|
|
217
224
|
|
package/server/src/identity.ts
CHANGED
|
@@ -1,10 +1,12 @@
|
|
|
1
1
|
import { existsSync, readFileSync, writeFileSync } from "node:fs";
|
|
2
2
|
import { join } from "node:path";
|
|
3
|
-
import { randomUUID } from "node:crypto";
|
|
3
|
+
import { randomUUID, generateKeyPairSync } from "node:crypto";
|
|
4
4
|
import { getLatticeHome } from "./config";
|
|
5
5
|
|
|
6
6
|
interface NodeIdentity {
|
|
7
7
|
id: string;
|
|
8
|
+
publicKey: string;
|
|
9
|
+
privateKey: string;
|
|
8
10
|
createdAt: number;
|
|
9
11
|
}
|
|
10
12
|
|
|
@@ -15,12 +17,38 @@ export function getIdentityPath(): string {
|
|
|
15
17
|
export function loadOrCreateIdentity(): NodeIdentity {
|
|
16
18
|
var path = getIdentityPath();
|
|
17
19
|
if (existsSync(path)) {
|
|
18
|
-
|
|
20
|
+
var stored = JSON.parse(readFileSync(path, "utf-8")) as NodeIdentity;
|
|
21
|
+
if (stored.publicKey && stored.privateKey) {
|
|
22
|
+
return stored;
|
|
23
|
+
}
|
|
24
|
+
var keys = generateEd25519Keypair();
|
|
25
|
+
stored.publicKey = keys.publicKey;
|
|
26
|
+
stored.privateKey = keys.privateKey;
|
|
27
|
+
writeFileSync(path, JSON.stringify(stored, null, 2), "utf-8");
|
|
28
|
+
return stored;
|
|
19
29
|
}
|
|
30
|
+
var keys = generateEd25519Keypair();
|
|
20
31
|
var identity: NodeIdentity = {
|
|
21
32
|
id: randomUUID(),
|
|
33
|
+
publicKey: keys.publicKey,
|
|
34
|
+
privateKey: keys.privateKey,
|
|
22
35
|
createdAt: Date.now(),
|
|
23
36
|
};
|
|
24
37
|
writeFileSync(path, JSON.stringify(identity, null, 2), "utf-8");
|
|
25
38
|
return identity;
|
|
26
39
|
}
|
|
40
|
+
|
|
41
|
+
function generateEd25519Keypair(): { publicKey: string; privateKey: string } {
|
|
42
|
+
var pair = generateKeyPairSync("ed25519", {
|
|
43
|
+
publicKeyEncoding: { type: "spki", format: "der" },
|
|
44
|
+
privateKeyEncoding: { type: "pkcs8", format: "der" },
|
|
45
|
+
});
|
|
46
|
+
return {
|
|
47
|
+
publicKey: Buffer.from(pair.publicKey).toString("base64"),
|
|
48
|
+
privateKey: Buffer.from(pair.privateKey).toString("base64"),
|
|
49
|
+
};
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
export function getPublicKey(): string {
|
|
53
|
+
return loadOrCreateIdentity().publicKey;
|
|
54
|
+
}
|
|
@@ -138,10 +138,11 @@ function openConnection(conn: PeerConnection, url: string): void {
|
|
|
138
138
|
var config = loadConfig();
|
|
139
139
|
var projects = listProjects(identity.id);
|
|
140
140
|
|
|
141
|
-
var hello: MeshHelloMessage = {
|
|
141
|
+
var hello: MeshHelloMessage & { publicKey?: string } = {
|
|
142
142
|
type: "mesh:hello",
|
|
143
143
|
nodeId: identity.id,
|
|
144
144
|
name: config.name,
|
|
145
|
+
publicKey: identity.publicKey,
|
|
145
146
|
projects: projects.map(function (p) {
|
|
146
147
|
return { slug: p.slug, title: p.title };
|
|
147
148
|
}),
|
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
import { createSign, createVerify, randomBytes, createCipheriv, createDecipheriv, createHash, diffieHellman, generateKeyPairSync, createPublicKey, createPrivateKey } from "node:crypto";
|
|
2
|
+
|
|
3
|
+
export function sign(privateKeyBase64: string, data: Buffer): string {
|
|
4
|
+
var privateKeyDer = Buffer.from(privateKeyBase64, "base64");
|
|
5
|
+
var key = createPrivateKey({ key: privateKeyDer, format: "der", type: "pkcs8" });
|
|
6
|
+
var signer = createSign("ed25519");
|
|
7
|
+
signer.update(data);
|
|
8
|
+
return signer.sign(key).toString("base64");
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export function verify(publicKeyBase64: string, data: Buffer, signatureBase64: string): boolean {
|
|
12
|
+
try {
|
|
13
|
+
var publicKeyDer = Buffer.from(publicKeyBase64, "base64");
|
|
14
|
+
var key = createPublicKey({ key: publicKeyDer, format: "der", type: "spki" });
|
|
15
|
+
var verifier = createVerify("ed25519");
|
|
16
|
+
verifier.update(data);
|
|
17
|
+
return verifier.verify(key, Buffer.from(signatureBase64, "base64"));
|
|
18
|
+
} catch {
|
|
19
|
+
return false;
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export function generateChallenge(): string {
|
|
24
|
+
return randomBytes(32).toString("base64");
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export interface EphemeralKeys {
|
|
28
|
+
publicKey: string;
|
|
29
|
+
privateKey: Buffer;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export function generateEphemeralKeys(): EphemeralKeys {
|
|
33
|
+
var pair = generateKeyPairSync("x25519", {
|
|
34
|
+
publicKeyEncoding: { type: "spki", format: "der" },
|
|
35
|
+
privateKeyEncoding: { type: "pkcs8", format: "der" },
|
|
36
|
+
});
|
|
37
|
+
return {
|
|
38
|
+
publicKey: Buffer.from(pair.publicKey).toString("base64"),
|
|
39
|
+
privateKey: Buffer.from(pair.privateKey),
|
|
40
|
+
};
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
export function deriveSharedSecret(myPrivateKeyDer: Buffer, theirPublicKeyBase64: string, nodeIdA: string, nodeIdB: string): Buffer {
|
|
44
|
+
var myKey = createPrivateKey({ key: myPrivateKeyDer, format: "der", type: "pkcs8" });
|
|
45
|
+
var theirKey = createPublicKey({ key: Buffer.from(theirPublicKeyBase64, "base64"), format: "der", type: "spki" });
|
|
46
|
+
|
|
47
|
+
var shared = diffieHellman({ privateKey: myKey, publicKey: theirKey });
|
|
48
|
+
|
|
49
|
+
var sortedIds = [nodeIdA, nodeIdB].sort().join(":");
|
|
50
|
+
var hash = createHash("sha256");
|
|
51
|
+
hash.update(shared);
|
|
52
|
+
hash.update(Buffer.from("lattice-mesh-v1:" + sortedIds));
|
|
53
|
+
return hash.digest();
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
export function encrypt(key: Buffer, nonce: number, plaintext: string): { ciphertext: string; tag: string } {
|
|
57
|
+
var iv = Buffer.alloc(12);
|
|
58
|
+
iv.writeBigUInt64BE(BigInt(nonce), 4);
|
|
59
|
+
var cipher = createCipheriv("aes-256-gcm", key, iv);
|
|
60
|
+
var encrypted = Buffer.concat([cipher.update(plaintext, "utf-8"), cipher.final()]);
|
|
61
|
+
return {
|
|
62
|
+
ciphertext: encrypted.toString("base64"),
|
|
63
|
+
tag: cipher.getAuthTag().toString("base64"),
|
|
64
|
+
};
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
export function decrypt(key: Buffer, nonce: number, ciphertext: string, tag: string): string | null {
|
|
68
|
+
try {
|
|
69
|
+
var iv = Buffer.alloc(12);
|
|
70
|
+
iv.writeBigUInt64BE(BigInt(nonce), 4);
|
|
71
|
+
var decipher = createDecipheriv("aes-256-gcm", key, iv);
|
|
72
|
+
decipher.setAuthTag(Buffer.from(tag, "base64"));
|
|
73
|
+
var decrypted = Buffer.concat([decipher.update(Buffer.from(ciphertext, "base64")), decipher.final()]);
|
|
74
|
+
return decrypted.toString("utf-8");
|
|
75
|
+
} catch {
|
|
76
|
+
return null;
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
export interface SecureSession {
|
|
81
|
+
sharedKey: Buffer;
|
|
82
|
+
sendNonce: number;
|
|
83
|
+
recvNonce: number;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
export function createSecureSession(sharedKey: Buffer): SecureSession {
|
|
87
|
+
return { sharedKey, sendNonce: 0, recvNonce: 0 };
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
export function encryptMessage(session: SecureSession, message: object): { type: "mesh:encrypted"; nonce: number; ciphertext: string; tag: string } {
|
|
91
|
+
session.sendNonce++;
|
|
92
|
+
var result = encrypt(session.sharedKey, session.sendNonce, JSON.stringify(message));
|
|
93
|
+
return { type: "mesh:encrypted", nonce: session.sendNonce, ...result };
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
export function decryptMessage(session: SecureSession, nonce: number, ciphertext: string, tag: string): object | null {
|
|
97
|
+
if (nonce <= session.recvNonce) return null;
|
|
98
|
+
var plaintext = decrypt(session.sharedKey, nonce, ciphertext, tag);
|
|
99
|
+
if (!plaintext) return null;
|
|
100
|
+
session.recvNonce = nonce;
|
|
101
|
+
try {
|
|
102
|
+
return JSON.parse(plaintext);
|
|
103
|
+
} catch {
|
|
104
|
+
return null;
|
|
105
|
+
}
|
|
106
|
+
}
|