diff --git a/apps/cli/package.json b/apps/cli/package.json index 8d7fa87..fd4b905 100644 --- a/apps/cli/package.json +++ b/apps/cli/package.json @@ -1,6 +1,6 @@ { "name": "claudemesh-cli", - "version": "0.8.3", + "version": "0.8.4", "description": "Claude Code MCP client for claudemesh — peer mesh messaging between Claude sessions.", "keywords": [ "claude-code", diff --git a/apps/cli/src/mcp/server.ts b/apps/cli/src/mcp/server.ts index 9fbda77..016326b 100644 --- a/apps/cli/src/mcp/server.ts +++ b/apps/cli/src/mcp/server.ts @@ -1354,16 +1354,30 @@ Your message mode is "${messageMode}". const client = allClients()[0]; if (!client) return text("vault_set: not connected", true); const entryType = vType ?? "env"; - let plaintext = value; + + // Read plaintext + let plaintextBytes: Uint8Array; if (entryType === "file") { const { existsSync, readFileSync } = await import("node:fs"); if (!existsSync(value)) return text(`vault_set: file not found: ${value}`, true); - plaintext = readFileSync(value, "base64"); + plaintextBytes = new Uint8Array(readFileSync(value)); + } else { + plaintextBytes = new TextEncoder().encode(value); } - const encoded = Buffer.from(plaintext).toString("base64"); - const ok = await client.vaultSet(key, encoded, "placeholder-nonce", "placeholder-sealed", entryType, mount_path, description); + + // E2E encrypt: crypto_secretbox with random Kf, then seal Kf with mesh pubkey + const { encryptFile, sealKeyForPeer } = await import("../crypto/file-crypto"); + const { ciphertext, nonce, key: kf } = await encryptFile(plaintextBytes); + const sealedKey = await sealKeyForPeer(kf, client.getMeshPubkey()); + + // Convert ciphertext to base64 for storage + const { ensureSodium } = await import("../crypto/keypair"); + const sodium = await ensureSodium(); + const ciphertextB64 = sodium.to_base64(ciphertext, sodium.base64_variants.ORIGINAL); + + const ok = await client.vaultSet(key, ciphertextB64, nonce, sealedKey, entryType, mount_path, description); if (!ok) return text("vault_set: broker did not acknowledge", true); - return text(`Vault entry "${key}" stored (${entryType}).`); + return text(`Vault entry "${key}" stored (${entryType}, E2E encrypted).`); } case "vault_list": { const client = allClients()[0]; diff --git a/apps/cli/src/ws/client.ts b/apps/cli/src/ws/client.ts index f34d588..e95254d 100644 --- a/apps/cli/src/ws/client.ts +++ b/apps/cli/src/ws/client.ts @@ -194,6 +194,10 @@ export class BrokerClient { getSessionPubkey(): string | null { return this.sessionPubkey; } /** Session secret key hex (null before first connection). */ getSessionSecretKey(): string | null { return this.sessionSecretKey; } + /** Mesh member public key hex (stable across sessions). */ + getMeshPubkey(): string { return this.mesh.pubkey; } + /** Mesh member secret key hex (stable across sessions). */ + getMeshSecretKey(): string { return this.mesh.secretKey; } private makeReqId(): string { return Math.random().toString(36).slice(2) + Date.now().toString(36);