fix(cli): no base64 fallback on direct-message decrypt failure

The push handler previously fell through to base64-decoding the
raw ciphertext whenever decryptDirect() returned null. For direct
(crypto_box) messages that produces garbage binary which surfaces
as garbled bytes in Claude's <channel> reminder. Limit the base64
fallback to legacy broadcast/channel messages (no senderPubkey),
and emit a clearer "⚠ message from <pubkey> failed to decrypt"
warning when direct decryption fails.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
Alejandro Gutiérrez
2026-04-05 22:22:33 +01:00
parent f8369a0e9b
commit f144e0485a
2 changed files with 16 additions and 7 deletions

View File

@@ -73,8 +73,13 @@ function resolveClient(to: string): {
};
}
function decryptFailedWarning(senderPubkey: string): string {
const who = senderPubkey ? senderPubkey.slice(0, 12) + "…" : "unknown sender";
return `⚠ message from ${who} failed to decrypt (tampered or wrong keypair)`;
}
function formatPush(p: InboundPush, meshSlug: string): string {
const body = p.plaintext ?? "(decryption failed)";
const body = p.plaintext ?? decryptFailedWarning(p.senderPubkey);
return `[${meshSlug}] from ${p.senderPubkey.slice(0, 12)}… (${p.priority}, ${p.createdAt}):\n${body}`;
}
@@ -82,7 +87,7 @@ export async function startMcpServer(): Promise<void> {
const config = loadConfig();
const server = new Server(
{ name: "claudemesh", version: "0.1.1" },
{ name: "claudemesh", version: "0.1.2" },
{
capabilities: {
experimental: { "claude/channel": {} },
@@ -215,7 +220,7 @@ If you have multiple joined meshes, prefix the \`to\` argument of send_message w
const fromName = fromPubkey
? `peer-${fromPubkey.slice(0, 8)}`
: "unknown";
const content = msg.plaintext ?? "(decryption failed)";
const content = msg.plaintext ?? decryptFailedWarning(fromPubkey);
try {
await server.notification({
method: "notifications/claude/channel",

View File

@@ -312,10 +312,14 @@ export class BrokerClient {
this.mesh.secretKey,
);
}
// If decryption failed, fall back to base64 UTF-8 unwrap —
// this covers the legacy plaintext path for broadcasts/channels
// until channel crypto lands.
if (plaintext === null && ciphertext) {
// Legacy/broadcast path: no senderPubkey means the message
// was not crypto_box'd, so base64 UTF-8 unwrap is correct.
// For direct messages (senderPubkey present) we MUST NOT
// base64-decode the ciphertext on decrypt failure — that
// produces garbage binary that surfaces as garbled bytes
// to Claude. Leave plaintext=null and let consumers emit
// a clear "failed to decrypt" warning.
if (plaintext === null && ciphertext && !senderPubkey) {
try {
plaintext = Buffer.from(ciphertext, "base64").toString("utf-8");
} catch {