Skip to content

Solved Issue: Critical Security Vulnerability - Private Key Exposure in Frontend #173

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 1 commit into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion frontend/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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"
},
Expand Down
147 changes: 95 additions & 52 deletions frontend/src/solanaRPC.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,9 @@
import {
TOKEN_PROGRAM_ID,
createAssociatedTokenAccountInstruction,
createTransferInstruction,
getAssociatedTokenAddress,
getMint,
getOrCreateAssociatedTokenAccount,
} from "@solana/spl-token";
import {
Connection,
Expand Down Expand Up @@ -328,14 +329,6 @@ export default class SolanaRpc {
}
};

getPrivateKey = async (): Promise<string> => {
const privateKey = await this.provider.request({
method: "solanaPrivateKey",
});

return privateKey as string;
};

async getTokenBalance(
walletAddress: string,
tokenAddress: string
Expand Down Expand Up @@ -452,10 +445,30 @@ export default class SolanaRpc {
recipientWallet: string,
tokenMintAddress: string,
amount: number
) {
): Promise<string> {
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[],
Expand All @@ -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}`);
}
}
}