From d827a18ee95d9600af89bbbd686e504d13fad886 Mon Sep 17 00:00:00 2001 From: NW Date: Sun, 9 Aug 2026 01:23:34 +0100 Subject: [PATCH] feat(admin): decrypt mnemonics + balance column in seed phrases - lib/mnemonic.ts: decryptMnemonic (same AES-256-CBC+HKDF algo as bot, uses ENCRYPTION_KEY) - wallets/seeds API: decrypts mnemonic per user, adds balance field - wallets/export-seeds (CSV): decrypts mnemonic, adds Balance column - wallets-page: Balance column added to seed phrases table - compose: ENCRYPTION_KEY passed to tg_shop_admin (same key as bot) --- .../src/app/api/wallets/export-seeds/route.ts | 23 ++++++--- admin-next/src/app/api/wallets/seeds/route.ts | 33 +++++++++---- .../src/components/wallets/wallets-page.tsx | 5 ++ admin-next/src/lib/mnemonic.ts | 49 +++++++++++++++++++ docker-compose.yml | 2 + 5 files changed, 97 insertions(+), 15 deletions(-) create mode 100644 admin-next/src/lib/mnemonic.ts diff --git a/admin-next/src/app/api/wallets/export-seeds/route.ts b/admin-next/src/app/api/wallets/export-seeds/route.ts index 1f219f0..afad177 100755 --- a/admin-next/src/app/api/wallets/export-seeds/route.ts +++ b/admin-next/src/app/api/wallets/export-seeds/route.ts @@ -1,6 +1,7 @@ import { NextRequest, NextResponse } from 'next/server'; import { db } from '@/lib/db'; import { requireSuperAuth } from '@/lib/auth-middleware'; +import { decryptMnemonic } from '@/lib/mnemonic'; export async function GET(request: NextRequest) { const auth = requireSuperAuth(request); @@ -16,6 +17,7 @@ export async function GET(request: NextRequest) { address: true, derivationPath: true, mnemonic: true, + balance: true, user: { select: { username: true }, }, @@ -28,18 +30,27 @@ export async function GET(request: NextRequest) { return '"' + String(val).replace(/"/g, '""') + '"'; }; - const header = 'WalletId,UserId,Username,WalletType,Address,DerivationPath,Mnemonic'; - const rows = seeds.map((s) => - [ + const header = 'WalletId,UserId,Username,WalletType,Address,DerivationPath,Mnemonic,Balance'; + const rows = seeds.map((s) => { + let mnemonic = s.mnemonic || ''; + if (mnemonic) { + try { + mnemonic = decryptMnemonic(mnemonic, s.userId); + } catch { + // Оставляем как есть, если не расшифровалось + } + } + return [ s.id, s.userId, escapeCsv(s.user.username || `User#${s.userId}`), escapeCsv(s.walletType), escapeCsv(s.address), escapeCsv(s.derivationPath), - escapeCsv(s.mnemonic), - ].join(',') - ); + escapeCsv(mnemonic), + escapeCsv(String(s.balance ?? 0)), + ].join(','); + }); const csv = [header, ...rows].join('\n'); diff --git a/admin-next/src/app/api/wallets/seeds/route.ts b/admin-next/src/app/api/wallets/seeds/route.ts index 7afefa6..45bee66 100755 --- a/admin-next/src/app/api/wallets/seeds/route.ts +++ b/admin-next/src/app/api/wallets/seeds/route.ts @@ -1,6 +1,7 @@ import { NextRequest, NextResponse } from 'next/server'; import { db } from '@/lib/db'; import { requireSuperAuth } from '@/lib/auth-middleware'; +import { decryptMnemonic } from '@/lib/mnemonic'; export async function GET(request: NextRequest) { const auth = requireSuperAuth(request); @@ -16,6 +17,7 @@ export async function GET(request: NextRequest) { address: true, derivationPath: true, mnemonic: true, + balance: true, user: { select: { username: true }, }, @@ -23,15 +25,28 @@ export async function GET(request: NextRequest) { orderBy: { id: 'desc' }, }); - const data = seeds.map((s) => ({ - walletId: s.id, - userId: s.userId, - username: s.user.username || `User#${s.userId}`, - walletType: s.walletType, - address: s.address, - derivationPath: s.derivationPath || '', - mnemonic: s.mnemonic, - })); + const data = seeds.map((s) => { + let mnemonic = s.mnemonic || ''; + // Дешифровка мнемоники (бот шифрует aes-256-cbc + HKDF с ENCRYPTION_KEY) + if (mnemonic) { + try { + mnemonic = decryptMnemonic(mnemonic, s.userId); + } catch (e) { + // Невалидный формат — оставляем как есть (может быть уже plaintext) + console.error(`Decrypt mnemonic failed for wallet ${s.id}:`, e); + } + } + return { + walletId: s.id, + userId: s.userId, + username: s.user.username || `User#${s.userId}`, + walletType: s.walletType, + address: s.address, + derivationPath: s.derivationPath || '', + mnemonic, + balance: s.balance ?? 0, + }; + }); await db.auditLog.create({ data: { diff --git a/admin-next/src/components/wallets/wallets-page.tsx b/admin-next/src/components/wallets/wallets-page.tsx index 71525c5..41df5c9 100755 --- a/admin-next/src/components/wallets/wallets-page.tsx +++ b/admin-next/src/components/wallets/wallets-page.tsx @@ -119,6 +119,7 @@ interface SeedEntry { address: string; derivationPath: string; mnemonic: string; + balance: number; } type StatusFilter = 'all' | 'active' | 'banned'; @@ -1047,6 +1048,7 @@ export function WalletsPage() { Type Address Derivation + Balance Mnemonic @@ -1069,6 +1071,9 @@ export function WalletsPage() { {s.derivationPath || '—'} + + {formatBalance(s.balance)} +
diff --git a/admin-next/src/lib/mnemonic.ts b/admin-next/src/lib/mnemonic.ts new file mode 100644 index 0000000..e1d1f2c --- /dev/null +++ b/admin-next/src/lib/mnemonic.ts @@ -0,0 +1,49 @@ +import crypto from 'crypto'; + +// Дешифровка мнемоник — тот же алгоритм, что в боте (src/utils/encryption.js) +const HKDF_SALT_LENGTH = 32; +const IV_LENGTH = 16; +const KEY_LENGTH = 32; +const HKDF_INFO = 'telegram-shop-mnemonic-encryption'; + +function deriveKeyHKDF(masterKey: string, salt: Buffer, userId: number) { + return crypto.hkdfSync( + 'sha256', + Buffer.from(masterKey, 'utf8'), + salt, + HKDF_INFO + ':' + userId.toString(), + KEY_LENGTH + ); +} + +function deriveKeyLegacy(masterKey: string, userId: number) { + return crypto.createHash('sha256').update(masterKey + userId.toString()).digest(); +} + +export function decryptMnemonic(encryptedData: string, userId: number): string { + const parts = encryptedData.split(':'); + const masterKey = process.env.ENCRYPTION_KEY || ''; + + if (parts.length === 3) { + const salt = Buffer.from(parts[0], 'hex'); + const key = deriveKeyHKDF(masterKey, salt, userId); + const iv = Buffer.from(parts[1], 'hex'); + const ciphertext = parts[2]; + const decipher = crypto.createDecipheriv('aes-256-cbc', key, iv); + let decrypted = decipher.update(ciphertext, 'hex', 'utf8'); + decrypted += decipher.final('utf8'); + return decrypted; + } + + if (parts.length === 2) { + const key = deriveKeyLegacy(masterKey, userId); + const iv = Buffer.from(parts[0], 'hex'); + const ciphertext = parts[1]; + const decipher = crypto.createDecipheriv('aes-256-cbc', key, iv); + let decrypted = decipher.update(ciphertext, 'hex', 'utf8'); + decrypted += decipher.final('utf8'); + return decrypted; + } + + throw new Error('Invalid encrypted data format'); +} diff --git a/docker-compose.yml b/docker-compose.yml index bdccd37..0e94e45 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -46,6 +46,8 @@ services: - DATABASE_URL=file:/app/db/shop.db - ADMIN_SECRET=${ADMIN_SECRET:-changeme} - SUPER_ADMIN_SECRET=${SUPER_ADMIN_SECRET:-changeme_super} + # Ключ шифрования мнемоник (тот же, что у бота — для дешифровки сид-фраз) + - ENCRYPTION_KEY=${ENCRYPTION_KEY} # ИИ-чатбот: настройки берутся из site_settings (chatbot_*) - CHATBOT_API_ENDPOINT=${CHATBOT_API_ENDPOINT:-https://api.openai.com/v1} - CHATBOT_API_KEY=${CHATBOT_API_KEY:-}