From 39fe296aaaf6600d62ac79c9e7ff05695cddab8e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Alejandro=20Guti=C3=A9rrez?= <35082514+alezmad@users.noreply.github.com> Date: Wed, 15 Apr 2026 14:37:36 +0100 Subject: [PATCH] fix(cli): decrypt falls back to member secret key when session key fails MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit When Alice's session-A encrypts a direct message to Bob (target = Bob's stable member pubkey) and Bob's session-B receives it, Bob has BOTH an ephemeral session secret key and the member secret key. The old code only tried session_sk, then silently failed with '⚠ message from failed to decrypt' even though the message was valid — just encrypted to the member key. Now: try session first, fall back to member on null. Matches the sender side's choice freedom (encrypt using either key). Repros when: user opens multiple Claude Code sessions (all use the same member key but each generates its own session key), and one session sends to another by display-name resolution which returns the member pubkey. Co-Authored-By: Claude Opus 4.6 (1M context) --- apps/cli/package.json | 2 +- apps/cli/src/services/broker/ws-client.ts | 16 +++++++++++----- 2 files changed, 12 insertions(+), 6 deletions(-) diff --git a/apps/cli/package.json b/apps/cli/package.json index e6e577d..d2bb72e 100644 --- a/apps/cli/package.json +++ b/apps/cli/package.json @@ -1,6 +1,6 @@ { "name": "claudemesh-cli", - "version": "1.0.0-alpha.33", + "version": "1.0.0-alpha.34", "description": "Peer mesh for Claude Code sessions — CLI + MCP server.", "keywords": [ "claude-code", diff --git a/apps/cli/src/services/broker/ws-client.ts b/apps/cli/src/services/broker/ws-client.ts index c17bedd..c6dadde 100644 --- a/apps/cli/src/services/broker/ws-client.ts +++ b/apps/cli/src/services/broker/ws-client.ts @@ -1670,11 +1670,17 @@ export class BrokerClient { plaintext = `[${event}]`; } } else if (senderPubkey && nonce && ciphertext) { - plaintext = await decryptDirect( - { nonce, ciphertext }, - senderPubkey, - this.sessionSecretKey ?? this.mesh.secretKey, - ); + // Try the session secret first (per-connection ephemeral key), then + // fall back to the mesh member secret (stable identity). Senders + // may encrypt to either our session pubkey OR our member pubkey + // depending on how the target was resolved; we must match both. + const envelope = { nonce, ciphertext }; + if (this.sessionSecretKey) { + plaintext = await decryptDirect(envelope, senderPubkey, this.sessionSecretKey); + } + if (plaintext === null) { + plaintext = await decryptDirect(envelope, senderPubkey, this.mesh.secretKey); + } } // Legacy/broadcast path: no senderPubkey means the message // was not crypto_box'd, so base64 UTF-8 unwrap is correct.