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.
This commit is contained in:
2026-04-21 14:53:55 -04:00
parent 338e2a1fc1
commit 946b81384c
2 changed files with 170 additions and 153 deletions
+3 -6
View File
@@ -13,15 +13,12 @@ const TAUTULLI_API_KEY = process.env.TAUTULLI_API_KEY || "";
function encrypt(text: string): string { function encrypt(text: string): string {
const iv = crypto.randomBytes(16); const iv = crypto.randomBytes(16);
const cipher = crypto.createCipheriv( const key = crypto.createHash("sha256").update(ENCRYPTION_KEY).digest();
"aes-256-gcm", const cipher = crypto.createCipheriv("aes-256-gcm", key, iv);
Buffer.from(ENCRYPTION_KEY),
iv,
);
let encrypted = cipher.update(text, "utf8", "hex"); let encrypted = cipher.update(text, "utf8", "hex");
encrypted += cipher.final("hex"); encrypted += cipher.final("hex");
const authTag = cipher.getAuthTag(); const authTag = cipher.getAuthTag();
return iv.toString("hex") + ":" + authTag.toString("hex") + ":" + encrypted; return `${iv.toString("hex")}:${authTag.toString("hex")}:${encrypted}`;
} }
const router = Router(); const router = Router();
+167 -147
View File
@@ -1,193 +1,213 @@
import { Router } from 'express'; import crypto from "crypto";
import { authenticate, AuthenticatedRequest, requireAdmin } from '../middleware/auth'; import { Router } from "express";
import { prisma } from '../utils/prisma'; import {
import { createWallet, getTokenBalance, requestAirdrop } from '../services/solana'; type AuthenticatedRequest,
import { asyncHandler } from '../middleware/errorHandler'; authenticate,
import crypto from 'crypto'; 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 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 // Encrypt private key
function encrypt(text: string): string { function encrypt(text: string): string {
const iv = crypto.randomBytes(16); 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();
let encrypted = cipher.update(text, 'utf8', 'hex'); const cipher = crypto.createCipheriv("aes-256-gcm", key, iv);
encrypted += cipher.final('hex'); let encrypted = cipher.update(text, "utf8", "hex");
const authTag = cipher.getAuthTag(); encrypted += cipher.final("hex");
return iv.toString('hex') + ':' + authTag.toString('hex') + ':' + encrypted; const authTag = cipher.getAuthTag();
return `${iv.toString("hex")}:${authTag.toString("hex")}:${encrypted}`;
} }
// Decrypt private key // Decrypt private key
function decrypt(encryptedData: string): string { function decrypt(encryptedData: string): string {
const parts = encryptedData.split(':'); const parts = encryptedData.split(":");
const iv = Buffer.from(parts[0], 'hex'); const iv = Buffer.from(parts[0], "hex");
const authTag = Buffer.from(parts[1], 'hex'); const authTag = Buffer.from(parts[1], "hex");
const encrypted = parts[2]; const encrypted = parts[2];
const decipher = crypto.createDecipheriv('aes-256-gcm', Buffer.from(ENCRYPTION_KEY), iv); const key = crypto.createHash("sha256").update(ENCRYPTION_KEY).digest();
decipher.setAuthTag(authTag); const decipher = crypto.createDecipheriv("aes-256-gcm", key, iv);
let decrypted = decipher.update(encrypted, 'hex', 'utf8'); decipher.setAuthTag(authTag);
decrypted += decipher.final('utf8'); let decrypted = decipher.update(encrypted, "hex", "utf8");
return decrypted; decrypted += decipher.final("utf8");
return decrypted;
} }
// Get user's wallet info // Get user's wallet info
router.get('/', router.get(
authenticate, "/",
asyncHandler(async (req: AuthenticatedRequest, res) => { authenticate,
const user = await prisma.user.findUnique({ asyncHandler(async (req: AuthenticatedRequest, res) => {
where: { id: req.user!.id }, const user = await prisma.user.findUnique({
select: { where: { id: req.user!.id },
walletAddress: true, select: {
totalEarned: true, walletAddress: true,
totalSpent: true totalEarned: true,
} totalSpent: true,
}); },
});
if (!user?.walletAddress) { if (!user?.walletAddress) {
return res.json({ return res.json({
hasWallet: false, hasWallet: false,
balance: 0, balance: 0,
totalEarned: user?.totalEarned || 0, totalEarned: user?.totalEarned || 0,
totalSpent: user?.totalSpent || 0 totalSpent: user?.totalSpent || 0,
}); });
} }
// Get on-chain balance // Get on-chain balance
const balance = await getTokenBalance(user.walletAddress); const balance = await getTokenBalance(user.walletAddress);
res.json({ res.json({
hasWallet: true, hasWallet: true,
address: user.walletAddress, address: user.walletAddress,
balance, balance,
totalEarned: user.totalEarned, totalEarned: user.totalEarned,
totalSpent: user.totalSpent, totalSpent: user.totalSpent,
explorerUrl: `https://explorer.solana.com/address/${user.walletAddress}?cluster=devnet` explorerUrl: `https://explorer.solana.com/address/${user.walletAddress}?cluster=devnet`,
}); });
}) }),
); );
// Create new wallet // Create new wallet
router.post('/create', router.post(
authenticate, "/create",
asyncHandler(async (req: AuthenticatedRequest, res) => { authenticate,
const user = await prisma.user.findUnique({ asyncHandler(async (req: AuthenticatedRequest, res) => {
where: { id: req.user!.id } const user = await prisma.user.findUnique({
}); where: { id: req.user!.id },
});
if (user?.walletAddress) { if (user?.walletAddress) {
return res.status(400).json({ error: 'Wallet already exists' }); return res.status(400).json({ error: "Wallet already exists" });
} }
// Create new Solana wallet // Create new Solana wallet
const wallet = createWallet(); const wallet = createWallet();
const encryptedKey = encrypt(wallet.secretKey); const encryptedKey = encrypt(wallet.secretKey);
await prisma.user.update({ await prisma.user.update({
where: { id: req.user!.id }, where: { id: req.user!.id },
data: { data: {
walletAddress: wallet.publicKey, walletAddress: wallet.publicKey,
encryptedPrivateKey: encryptedKey encryptedPrivateKey: encryptedKey,
} },
}); });
// Request airdrop for testing // Request airdrop for testing
await requestAirdrop(wallet.publicKey); await requestAirdrop(wallet.publicKey);
res.json({ res.json({
address: wallet.publicKey, address: wallet.publicKey,
message: 'Wallet created successfully. Funded with 2 SOL for transaction fees.', message:
warning: 'Please backup your recovery phrase if shown. This is the only time it will be displayed.' "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 // Connect existing wallet
router.post('/connect', router.post(
authenticate, "/connect",
asyncHandler(async (req: AuthenticatedRequest, res) => { authenticate,
const { address } = req.body; asyncHandler(async (req: AuthenticatedRequest, res) => {
const { address } = req.body;
if (!address) { if (!address) {
return res.status(400).json({ error: 'Wallet address required' }); return res.status(400).json({ error: "Wallet address required" });
} }
// Check if address is already connected to another user // Check if address is already connected to another user
const existing = await prisma.user.findFirst({ const existing = await prisma.user.findFirst({
where: { where: {
walletAddress: address, walletAddress: address,
NOT: { id: req.user!.id } NOT: { id: req.user!.id },
} },
}); });
if (existing) { if (existing) {
return res.status(400).json({ error: 'Wallet already connected to another account' }); return res
} .status(400)
.json({ error: "Wallet already connected to another account" });
}
await prisma.user.update({ await prisma.user.update({
where: { id: req.user!.id }, where: { id: req.user!.id },
data: { walletAddress: address } data: { walletAddress: address },
}); });
res.json({ res.json({
address, address,
message: 'Wallet connected successfully' message: "Wallet connected successfully",
}); });
}) }),
); );
// Get recovery phrase (only shown once at creation) // Get recovery phrase (only shown once at creation)
router.post('/backup', router.post(
authenticate, "/backup",
asyncHandler(async (req: AuthenticatedRequest, res) => { authenticate,
const user = await prisma.user.findUnique({ asyncHandler(async (req: AuthenticatedRequest, res) => {
where: { id: req.user!.id } const user = await prisma.user.findUnique({
}); where: { id: req.user!.id },
});
if (!user?.encryptedPrivateKey) { if (!user?.encryptedPrivateKey) {
return res.status(400).json({ error: 'No wallet found' }); return res.status(400).json({ error: "No wallet found" });
} }
// Decrypt and return private key for backup // Decrypt and return private key for backup
const privateKey = decrypt(user.encryptedPrivateKey); const privateKey = decrypt(user.encryptedPrivateKey);
res.json({ res.json({
privateKey, privateKey,
warning: 'Store this securely. Never share it with anyone.' warning: "Store this securely. Never share it with anyone.",
}); });
}) }),
); );
// Admin: Get user's wallet // Admin: Get user's wallet
router.get('/admin/:userId', router.get(
authenticate, "/admin/:userId",
requireAdmin, authenticate,
asyncHandler(async (req: AuthenticatedRequest, res) => { requireAdmin,
const user = await prisma.user.findUnique({ asyncHandler(async (req: AuthenticatedRequest, res) => {
where: { id: req.params.userId }, const user = await prisma.user.findUnique({
select: { where: { id: req.params.userId },
id: true, select: {
plexUsername: true, id: true,
walletAddress: true, plexUsername: true,
totalEarned: true, walletAddress: true,
totalSpent: true totalEarned: true,
} totalSpent: true,
}); },
});
if (!user) { if (!user) {
return res.status(404).json({ error: 'User not found' }); return res.status(404).json({ error: "User not found" });
} }
let balance = 0; let balance = 0;
if (user.walletAddress) { if (user.walletAddress) {
balance = await getTokenBalance(user.walletAddress); balance = await getTokenBalance(user.walletAddress);
} }
res.json({ res.json({
...user, ...user,
balance balance,
}); });
}) }),
); );
export { router as walletRouter }; export { router as walletRouter };