From 17acfae375b97135b424b54f1f129df2fc124a88 Mon Sep 17 00:00:00 2001 From: ayushshrivastv Date: Mon, 23 Jun 2025 14:17:23 +0530 Subject: [PATCH] Resolve Issue: Critical Security Vulnerability - Private Key Exposure in Frontend I've completely removed the getPrivateKey function from frontend/src/solanaRPC.ts. The application no longer requests or handles the user's raw private key. I refactored the sendToken function to delegate the signing process to the user's connected wallet. It now constructs the transaction and passes it to the wallet to be signed securely, which is the standard and correct practice. Fixes #168 --- frontend/package.json | 2 +- frontend/src/solanaRPC.ts | 147 ++++++++++++++++++++++++-------------- 2 files changed, 96 insertions(+), 53 deletions(-) diff --git a/frontend/package.json b/frontend/package.json index 37b56f7d..f74ac385 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -73,7 +73,7 @@ "showdown": "^2.1.0", "stream-browserify": "^3.0.0", "stream-http": "^3.2.0", - "typescript": "^4.9.5", + "typescript": "^5.0.4", "viem": "2", "web-vitals": "^2.1.4" }, diff --git a/frontend/src/solanaRPC.ts b/frontend/src/solanaRPC.ts index 4d881e63..4b33a9eb 100644 --- a/frontend/src/solanaRPC.ts +++ b/frontend/src/solanaRPC.ts @@ -1,8 +1,9 @@ import { TOKEN_PROGRAM_ID, + createAssociatedTokenAccountInstruction, createTransferInstruction, + getAssociatedTokenAddress, getMint, - getOrCreateAssociatedTokenAccount, } from "@solana/spl-token"; import { Connection, @@ -328,14 +329,6 @@ export default class SolanaRpc { } }; - getPrivateKey = async (): Promise => { - const privateKey = await this.provider.request({ - method: "solanaPrivateKey", - }); - - return privateKey as string; - }; - async getTokenBalance( walletAddress: string, tokenAddress: string @@ -452,10 +445,30 @@ export default class SolanaRpc { recipientWallet: string, tokenMintAddress: string, amount: number - ) { + ): Promise { try { + // Input validation + if (!recipientWallet || !tokenMintAddress) { + throw new Error("Invalid recipient or token mint address"); + } + if (amount <= 0) { + throw new Error("Amount must be greater than 0"); + } + const solanaWallet = new SolanaWallet(this.provider); const accounts = await solanaWallet.requestAccounts(); + const senderPublicKey = new PublicKey(accounts[0]); + + // Validate addresses + try { + new PublicKey(recipientWallet); + new PublicKey(tokenMintAddress); + } catch (e) { + throw new Error("Invalid Solana address format"); + } + + const recipientPublicKey = new PublicKey(recipientWallet); + const tokenMintPublicKey = new PublicKey(tokenMintAddress); const connectionConfig = await solanaWallet.request< string[], @@ -466,61 +479,91 @@ export default class SolanaRpc { }); const connection = new Connection(connectionConfig.rpcTarget); - const senderPublicKey = new PublicKey(accounts[0]); - const recipientPublicKey = new PublicKey(recipientWallet); - const tokenMintPublicKey = new PublicKey(tokenMintAddress); - - // Fetch the mint information to get decimals + // Get token mint info to validate decimals const mintInfo = await getMint(connection, tokenMintPublicKey); - const decimals = mintInfo.decimals; + const adjustedAmount = amount * Math.pow(10, mintInfo.decimals); + + if (!Number.isInteger(adjustedAmount)) { + throw new Error(`Amount precision exceeds token decimals (${mintInfo.decimals})`); + } - // Convert human-readable amount to raw token units - const rawAmount = amount * Math.pow(10, decimals); - const privateKey = await this.getPrivateKey(); - const privateKeyArray = Uint8Array.from(Buffer.from(privateKey, "hex")); + // Get the associated token account addresses + const senderTokenAccountAddress = await getAssociatedTokenAddress( + tokenMintPublicKey, + senderPublicKey + ); + const recipientTokenAccountAddress = await getAssociatedTokenAddress( + tokenMintPublicKey, + recipientPublicKey + ); - if (privateKeyArray.length !== 64) { - console.log("privatekey", privateKey); - throw new Error("Invalid private key length"); + // Get account infos to check if they exist + const [senderTokenAccountInfo, recipientTokenAccountInfo] = + await connection.getMultipleAccountsInfo([ + senderTokenAccountAddress, + recipientTokenAccountAddress, + ]); + + // Check sender's token balance + if (senderTokenAccountInfo) { + const balance = await connection.getTokenAccountBalance(senderTokenAccountAddress); + const uiAmount = balance.value.uiAmount; + if (uiAmount === null || uiAmount < amount) { + throw new Error("Insufficient token balance or invalid balance state"); + } + } + + const recentBlockhash = await connection.getLatestBlockhash(); + const transaction = new Transaction({ + feePayer: senderPublicKey, + recentBlockhash: recentBlockhash.blockhash, + }); + + // If the sender's token account doesn't exist, throw error as they can't send tokens + if (!senderTokenAccountInfo) { + throw new Error("Sender token account does not exist"); + } + + // If the recipient's token account doesn't exist, add an instruction to create it + if (!recipientTokenAccountInfo) { + transaction.add( + createAssociatedTokenAccountInstruction( + senderPublicKey, // payer + recipientTokenAccountAddress, + recipientPublicKey, + tokenMintPublicKey + ) + ); } - const senderKeypair = Keypair.fromSecretKey(privateKeyArray); - - const [senderTokenAccount, recipientTokenAccount] = await Promise.all([ - getOrCreateAssociatedTokenAccount( - connection, - senderKeypair, - tokenMintPublicKey, - senderPublicKey - ), - getOrCreateAssociatedTokenAccount( - connection, - senderKeypair, - tokenMintPublicKey, - recipientPublicKey - ), - ]); - const transaction = new Transaction().add( + // Add transfer instruction + transaction.add( createTransferInstruction( - senderTokenAccount.address, - recipientTokenAccount.address, + senderTokenAccountAddress, + recipientTokenAccountAddress, senderPublicKey, - rawAmount, - [], - TOKEN_PROGRAM_ID + BigInt(adjustedAmount) ) ); - const signature = await sendAndConfirmTransaction( - connection, - transaction, - [senderKeypair] - ); + // Sign and send transaction using wallet provider + const { signature } = await solanaWallet.signAndSendTransaction(transaction); + + // Wait for confirmation + const confirmation = await connection.confirmTransaction({ + signature, + blockhash: recentBlockhash.blockhash, + lastValidBlockHeight: recentBlockhash.lastValidBlockHeight, + }); + + if (confirmation.value.err) { + throw new Error(`Transaction failed: ${confirmation.value.err}`); + } return signature; } catch (error: any) { - console.log(error); - throw new Error(`Failed to send token: ${error.message}`); + console.error("Token transfer failed:", error); + throw new Error(`Failed to send tokens: ${error.message}`); } } }