From 467eaa270afcd58675ce4ff58458bdff25db29f8 Mon Sep 17 00:00:00 2001 From: Ashwin-3cS Date: Thu, 26 Mar 2026 19:32:25 +0530 Subject: [PATCH] fix(noter): implement server-side wallet signature verification MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Replace the TODO comment in connectWallet with actual verification using verifyPersonalMessageSignature from @mysten/sui/verify. Also fix a double base64 encoding bug in wallet-client.ts where signPersonalMessage was typed as returning Uint8Array but Slush wallet standard (v0.15+) returns a base64 string — Buffer.from(str) without encoding treats the string as UTF-8, producing garbled bytes that always fail scheme validation on the server. --- apps/noter/package/feature/auth/api/route.ts | 16 +++++++++++++--- .../package/feature/auth/lib/wallet-client.ts | 9 +++++---- 2 files changed, 18 insertions(+), 7 deletions(-) diff --git a/apps/noter/package/feature/auth/api/route.ts b/apps/noter/package/feature/auth/api/route.ts index 776d75da..12b2e948 100644 --- a/apps/noter/package/feature/auth/api/route.ts +++ b/apps/noter/package/feature/auth/api/route.ts @@ -5,6 +5,7 @@ import { router, procedure } from "@/shared/lib/trpc/init"; import { TRPCError } from "@trpc/server"; +import { verifyPersonalMessageSignature } from "@mysten/sui/verify"; import { uuidv7 } from "uuidv7"; import { initiateLoginInput, @@ -247,9 +248,18 @@ export const authRouter = router({ const { walletType, address, signature, message } = input; try { - // TODO: Verify signature on server-side - // For now, we trust the client signature - // In production, use @mysten/sui.js to verify the signature + // Verify the wallet signature before creating a session + const signerAddress = await verifyPersonalMessageSignature( + new TextEncoder().encode(message), + signature, + ).catch(() => { + throw new TRPCError({ code: "UNAUTHORIZED", message: "Invalid signature" }); + }); + + if (signerAddress.toSuiAddress() !== address) { + throw new TRPCError({ code: "UNAUTHORIZED", message: "Signature does not match address" }); + } + // Create or update user via service const user = await authService.upsertWalletUser(ctx.db, { address, diff --git a/apps/noter/package/feature/auth/lib/wallet-client.ts b/apps/noter/package/feature/auth/lib/wallet-client.ts index db5c998f..87e3e086 100644 --- a/apps/noter/package/feature/auth/lib/wallet-client.ts +++ b/apps/noter/package/feature/auth/lib/wallet-client.ts @@ -130,7 +130,7 @@ export async function signMessage( // Sign message using sui:signPersonalMessage feature const signFeature = wallet.features['sui:signPersonalMessage'] as { - signPersonalMessage: (params: { message: Uint8Array; account: any }) => Promise<{ signature: Uint8Array }> + signPersonalMessage: (params: { message: Uint8Array; account: any }) => Promise<{ signature: string | Uint8Array }> } | undefined; if (!signFeature) { @@ -144,9 +144,10 @@ export async function signMessage( account: walletAccount, }); - - // Convert Uint8Array signature to base64 - const signatureBase64 = Buffer.from(result.signature).toString("base64"); + // Wallet standard returns signature as a base64 string; older versions return Uint8Array + const signatureBase64 = typeof result.signature === 'string' + ? result.signature + : Buffer.from(result.signature).toString("base64"); return { signature: signatureBase64,