@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.32.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
 
@@ -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
- return JSON.parse(readFileSync(path, "utf-8")) as NodeIdentity;
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
+ }