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)
This commit is contained in:
NW
2026-08-09 01:23:34 +01:00
parent d1ce76a14c
commit d827a18ee9
5 changed files with 97 additions and 15 deletions

View File

@@ -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');
}