From 946b81384c1f4f7cb3bc6f594ff09a72cd61a816 Mon Sep 17 00:00:00 2001 From: hobokenchicken Date: Tue, 21 Apr 2026 14:53:55 -0400 Subject: [PATCH] fix(crypto): SHA-256 hash ENCRYPTION_KEY for AES-256-GCM ENCRYPTION_KEY was 64 hex chars = 64 bytes. AES-256 needs exactly 32 bytes. Now hashing with SHA-256 to derive proper 32-byte key regardless of input length. Fixed in auth.ts backfill encrypt and wallet.ts encrypt/decrypt. --- backend/src/routes/auth.ts | 9 +- backend/src/routes/wallet.ts | 314 +++++++++++++++++++---------------- 2 files changed, 170 insertions(+), 153 deletions(-) diff --git a/backend/src/routes/auth.ts b/backend/src/routes/auth.ts index c425076..298ffd3 100644 --- a/backend/src/routes/auth.ts +++ b/backend/src/routes/auth.ts @@ -13,15 +13,12 @@ const TAUTULLI_API_KEY = process.env.TAUTULLI_API_KEY || ""; function encrypt(text: string): string { const iv = crypto.randomBytes(16); - const cipher = crypto.createCipheriv( - "aes-256-gcm", - Buffer.from(ENCRYPTION_KEY), - iv, - ); + const key = crypto.createHash("sha256").update(ENCRYPTION_KEY).digest(); + const cipher = crypto.createCipheriv("aes-256-gcm", key, iv); let encrypted = cipher.update(text, "utf8", "hex"); encrypted += cipher.final("hex"); const authTag = cipher.getAuthTag(); - return iv.toString("hex") + ":" + authTag.toString("hex") + ":" + encrypted; + return `${iv.toString("hex")}:${authTag.toString("hex")}:${encrypted}`; } const router = Router(); diff --git a/backend/src/routes/wallet.ts b/backend/src/routes/wallet.ts index d3377aa..63bcebf 100644 --- a/backend/src/routes/wallet.ts +++ b/backend/src/routes/wallet.ts @@ -1,193 +1,213 @@ -import { Router } from 'express'; -import { authenticate, AuthenticatedRequest, requireAdmin } from '../middleware/auth'; -import { prisma } from '../utils/prisma'; -import { createWallet, getTokenBalance, requestAirdrop } from '../services/solana'; -import { asyncHandler } from '../middleware/errorHandler'; -import crypto from 'crypto'; +import crypto from "crypto"; +import { Router } from "express"; +import { + type AuthenticatedRequest, + authenticate, + requireAdmin, +} from "../middleware/auth"; +import { asyncHandler } from "../middleware/errorHandler"; +import { + createWallet, + getTokenBalance, + requestAirdrop, +} from "../services/solana"; +import { prisma } from "../utils/prisma"; const router = Router(); -const ENCRYPTION_KEY = process.env.ENCRYPTION_KEY || 'default-key-32-chars-long!!!!!'; +const ENCRYPTION_KEY = + process.env.ENCRYPTION_KEY || "default-key-32-chars-long!!!!!"; // Encrypt private key function encrypt(text: string): string { - const iv = crypto.randomBytes(16); - const cipher = crypto.createCipheriv('aes-256-gcm', Buffer.from(ENCRYPTION_KEY), iv); - let encrypted = cipher.update(text, 'utf8', 'hex'); - encrypted += cipher.final('hex'); - const authTag = cipher.getAuthTag(); - return iv.toString('hex') + ':' + authTag.toString('hex') + ':' + encrypted; + const iv = crypto.randomBytes(16); + const key = crypto.createHash("sha256").update(ENCRYPTION_KEY).digest(); + const cipher = crypto.createCipheriv("aes-256-gcm", key, iv); + let encrypted = cipher.update(text, "utf8", "hex"); + encrypted += cipher.final("hex"); + const authTag = cipher.getAuthTag(); + return `${iv.toString("hex")}:${authTag.toString("hex")}:${encrypted}`; } // Decrypt private key function decrypt(encryptedData: string): string { - const parts = encryptedData.split(':'); - const iv = Buffer.from(parts[0], 'hex'); - const authTag = Buffer.from(parts[1], 'hex'); - const encrypted = parts[2]; - const decipher = crypto.createDecipheriv('aes-256-gcm', Buffer.from(ENCRYPTION_KEY), iv); - decipher.setAuthTag(authTag); - let decrypted = decipher.update(encrypted, 'hex', 'utf8'); - decrypted += decipher.final('utf8'); - return decrypted; + const parts = encryptedData.split(":"); + const iv = Buffer.from(parts[0], "hex"); + const authTag = Buffer.from(parts[1], "hex"); + const encrypted = parts[2]; + const key = crypto.createHash("sha256").update(ENCRYPTION_KEY).digest(); + const decipher = crypto.createDecipheriv("aes-256-gcm", key, iv); + decipher.setAuthTag(authTag); + let decrypted = decipher.update(encrypted, "hex", "utf8"); + decrypted += decipher.final("utf8"); + return decrypted; } // Get user's wallet info -router.get('/', - authenticate, - asyncHandler(async (req: AuthenticatedRequest, res) => { - const user = await prisma.user.findUnique({ - where: { id: req.user!.id }, - select: { - walletAddress: true, - totalEarned: true, - totalSpent: true - } - }); +router.get( + "/", + authenticate, + asyncHandler(async (req: AuthenticatedRequest, res) => { + const user = await prisma.user.findUnique({ + where: { id: req.user!.id }, + select: { + walletAddress: true, + totalEarned: true, + totalSpent: true, + }, + }); - if (!user?.walletAddress) { - return res.json({ - hasWallet: false, - balance: 0, - totalEarned: user?.totalEarned || 0, - totalSpent: user?.totalSpent || 0 - }); - } + if (!user?.walletAddress) { + return res.json({ + hasWallet: false, + balance: 0, + totalEarned: user?.totalEarned || 0, + totalSpent: user?.totalSpent || 0, + }); + } - // Get on-chain balance - const balance = await getTokenBalance(user.walletAddress); + // Get on-chain balance + const balance = await getTokenBalance(user.walletAddress); - res.json({ - hasWallet: true, - address: user.walletAddress, - balance, - totalEarned: user.totalEarned, - totalSpent: user.totalSpent, - explorerUrl: `https://explorer.solana.com/address/${user.walletAddress}?cluster=devnet` - }); - }) + res.json({ + hasWallet: true, + address: user.walletAddress, + balance, + totalEarned: user.totalEarned, + totalSpent: user.totalSpent, + explorerUrl: `https://explorer.solana.com/address/${user.walletAddress}?cluster=devnet`, + }); + }), ); // Create new wallet -router.post('/create', - authenticate, - asyncHandler(async (req: AuthenticatedRequest, res) => { - const user = await prisma.user.findUnique({ - where: { id: req.user!.id } - }); +router.post( + "/create", + authenticate, + asyncHandler(async (req: AuthenticatedRequest, res) => { + const user = await prisma.user.findUnique({ + where: { id: req.user!.id }, + }); - if (user?.walletAddress) { - return res.status(400).json({ error: 'Wallet already exists' }); - } + if (user?.walletAddress) { + return res.status(400).json({ error: "Wallet already exists" }); + } - // Create new Solana wallet - const wallet = createWallet(); - const encryptedKey = encrypt(wallet.secretKey); + // Create new Solana wallet + const wallet = createWallet(); + const encryptedKey = encrypt(wallet.secretKey); - await prisma.user.update({ - where: { id: req.user!.id }, - data: { - walletAddress: wallet.publicKey, - encryptedPrivateKey: encryptedKey - } - }); + await prisma.user.update({ + where: { id: req.user!.id }, + data: { + walletAddress: wallet.publicKey, + encryptedPrivateKey: encryptedKey, + }, + }); - // Request airdrop for testing - await requestAirdrop(wallet.publicKey); + // Request airdrop for testing + await requestAirdrop(wallet.publicKey); - res.json({ - address: wallet.publicKey, - message: 'Wallet created successfully. Funded with 2 SOL for transaction fees.', - warning: 'Please backup your recovery phrase if shown. This is the only time it will be displayed.' - }); - }) + res.json({ + address: wallet.publicKey, + message: + "Wallet created successfully. Funded with 2 SOL for transaction fees.", + warning: + "Please backup your recovery phrase if shown. This is the only time it will be displayed.", + }); + }), ); // Connect existing wallet -router.post('/connect', - authenticate, - asyncHandler(async (req: AuthenticatedRequest, res) => { - const { address } = req.body; +router.post( + "/connect", + authenticate, + asyncHandler(async (req: AuthenticatedRequest, res) => { + const { address } = req.body; - if (!address) { - return res.status(400).json({ error: 'Wallet address required' }); - } + if (!address) { + return res.status(400).json({ error: "Wallet address required" }); + } - // Check if address is already connected to another user - const existing = await prisma.user.findFirst({ - where: { - walletAddress: address, - NOT: { id: req.user!.id } - } - }); + // Check if address is already connected to another user + const existing = await prisma.user.findFirst({ + where: { + walletAddress: address, + NOT: { id: req.user!.id }, + }, + }); - if (existing) { - return res.status(400).json({ error: 'Wallet already connected to another account' }); - } + if (existing) { + return res + .status(400) + .json({ error: "Wallet already connected to another account" }); + } - await prisma.user.update({ - where: { id: req.user!.id }, - data: { walletAddress: address } - }); + await prisma.user.update({ + where: { id: req.user!.id }, + data: { walletAddress: address }, + }); - res.json({ - address, - message: 'Wallet connected successfully' - }); - }) + res.json({ + address, + message: "Wallet connected successfully", + }); + }), ); // Get recovery phrase (only shown once at creation) -router.post('/backup', - authenticate, - asyncHandler(async (req: AuthenticatedRequest, res) => { - const user = await prisma.user.findUnique({ - where: { id: req.user!.id } - }); +router.post( + "/backup", + authenticate, + asyncHandler(async (req: AuthenticatedRequest, res) => { + const user = await prisma.user.findUnique({ + where: { id: req.user!.id }, + }); - if (!user?.encryptedPrivateKey) { - return res.status(400).json({ error: 'No wallet found' }); - } + if (!user?.encryptedPrivateKey) { + return res.status(400).json({ error: "No wallet found" }); + } - // Decrypt and return private key for backup - const privateKey = decrypt(user.encryptedPrivateKey); + // Decrypt and return private key for backup + const privateKey = decrypt(user.encryptedPrivateKey); - res.json({ - privateKey, - warning: 'Store this securely. Never share it with anyone.' - }); - }) + res.json({ + privateKey, + warning: "Store this securely. Never share it with anyone.", + }); + }), ); // Admin: Get user's wallet -router.get('/admin/:userId', - authenticate, - requireAdmin, - asyncHandler(async (req: AuthenticatedRequest, res) => { - const user = await prisma.user.findUnique({ - where: { id: req.params.userId }, - select: { - id: true, - plexUsername: true, - walletAddress: true, - totalEarned: true, - totalSpent: true - } - }); +router.get( + "/admin/:userId", + authenticate, + requireAdmin, + asyncHandler(async (req: AuthenticatedRequest, res) => { + const user = await prisma.user.findUnique({ + where: { id: req.params.userId }, + select: { + id: true, + plexUsername: true, + walletAddress: true, + totalEarned: true, + totalSpent: true, + }, + }); - if (!user) { - return res.status(404).json({ error: 'User not found' }); - } + if (!user) { + return res.status(404).json({ error: "User not found" }); + } - let balance = 0; - if (user.walletAddress) { - balance = await getTokenBalance(user.walletAddress); - } + let balance = 0; + if (user.walletAddress) { + balance = await getTokenBalance(user.walletAddress); + } - res.json({ - ...user, - balance - }); - }) + res.json({ + ...user, + balance, + }); + }), ); export { router as walletRouter };