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:
@@ -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();
|
||||||
|
|||||||
@@ -1,38 +1,50 @@
|
|||||||
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");
|
||||||
|
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}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
// 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();
|
||||||
|
const decipher = crypto.createDecipheriv("aes-256-gcm", key, iv);
|
||||||
decipher.setAuthTag(authTag);
|
decipher.setAuthTag(authTag);
|
||||||
let decrypted = decipher.update(encrypted, 'hex', 'utf8');
|
let decrypted = decipher.update(encrypted, "hex", "utf8");
|
||||||
decrypted += decipher.final('utf8');
|
decrypted += decipher.final("utf8");
|
||||||
return decrypted;
|
return decrypted;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Get user's wallet info
|
// Get user's wallet info
|
||||||
router.get('/',
|
router.get(
|
||||||
|
"/",
|
||||||
authenticate,
|
authenticate,
|
||||||
asyncHandler(async (req: AuthenticatedRequest, res) => {
|
asyncHandler(async (req: AuthenticatedRequest, res) => {
|
||||||
const user = await prisma.user.findUnique({
|
const user = await prisma.user.findUnique({
|
||||||
@@ -40,8 +52,8 @@ router.get('/',
|
|||||||
select: {
|
select: {
|
||||||
walletAddress: true,
|
walletAddress: true,
|
||||||
totalEarned: true,
|
totalEarned: true,
|
||||||
totalSpent: true
|
totalSpent: true,
|
||||||
}
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
if (!user?.walletAddress) {
|
if (!user?.walletAddress) {
|
||||||
@@ -49,7 +61,7 @@ router.get('/',
|
|||||||
hasWallet: false,
|
hasWallet: false,
|
||||||
balance: 0,
|
balance: 0,
|
||||||
totalEarned: user?.totalEarned || 0,
|
totalEarned: user?.totalEarned || 0,
|
||||||
totalSpent: user?.totalSpent || 0
|
totalSpent: user?.totalSpent || 0,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -62,21 +74,22 @@ router.get('/',
|
|||||||
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(
|
||||||
|
"/create",
|
||||||
authenticate,
|
authenticate,
|
||||||
asyncHandler(async (req: AuthenticatedRequest, res) => {
|
asyncHandler(async (req: AuthenticatedRequest, res) => {
|
||||||
const user = await prisma.user.findUnique({
|
const user = await prisma.user.findUnique({
|
||||||
where: { id: req.user!.id }
|
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
|
||||||
@@ -87,8 +100,8 @@ router.post('/create',
|
|||||||
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
|
||||||
@@ -96,56 +109,62 @@ router.post('/create',
|
|||||||
|
|
||||||
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(
|
||||||
|
"/connect",
|
||||||
authenticate,
|
authenticate,
|
||||||
asyncHandler(async (req: AuthenticatedRequest, res) => {
|
asyncHandler(async (req: AuthenticatedRequest, res) => {
|
||||||
const { address } = req.body;
|
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(
|
||||||
|
"/backup",
|
||||||
authenticate,
|
authenticate,
|
||||||
asyncHandler(async (req: AuthenticatedRequest, res) => {
|
asyncHandler(async (req: AuthenticatedRequest, res) => {
|
||||||
const user = await prisma.user.findUnique({
|
const user = await prisma.user.findUnique({
|
||||||
where: { id: req.user!.id }
|
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
|
||||||
@@ -153,13 +172,14 @@ router.post('/backup',
|
|||||||
|
|
||||||
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(
|
||||||
|
"/admin/:userId",
|
||||||
authenticate,
|
authenticate,
|
||||||
requireAdmin,
|
requireAdmin,
|
||||||
asyncHandler(async (req: AuthenticatedRequest, res) => {
|
asyncHandler(async (req: AuthenticatedRequest, res) => {
|
||||||
@@ -170,12 +190,12 @@ router.get('/admin/:userId',
|
|||||||
plexUsername: true,
|
plexUsername: true,
|
||||||
walletAddress: true,
|
walletAddress: true,
|
||||||
totalEarned: true,
|
totalEarned: true,
|
||||||
totalSpent: 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;
|
||||||
@@ -185,9 +205,9 @@ router.get('/admin/:userId',
|
|||||||
|
|
||||||
res.json({
|
res.json({
|
||||||
...user,
|
...user,
|
||||||
balance
|
balance,
|
||||||
});
|
});
|
||||||
})
|
}),
|
||||||
);
|
);
|
||||||
|
|
||||||
export { router as walletRouter };
|
export { router as walletRouter };
|
||||||
|
|||||||
Reference in New Issue
Block a user