/** * Recovery Mnemonic Adapter * * 生成与钱包公钥关联的恢复助记词 * 支持挂失和更换功能 */ import { Injectable, Logger } from '@nestjs/common'; import { validateMnemonic, entropyToMnemonic } from '@scure/bip39'; import { wordlist } from '@scure/bip39/wordlists/english'; import { createHash, createCipheriv, createDecipheriv, randomBytes } from 'crypto'; import { ConfigService } from '@nestjs/config'; import * as bcrypt from 'bcrypt'; export interface GenerateMnemonicParams { userId: string; publicKey: string; // 钱包公钥 (hex) } export interface GenerateMnemonicResult { mnemonic: string; // 12词助记词 (明文,仅首次返回) encryptedMnemonic: string; // 加密的助记词 mnemonicHash: string; // 助记词哈希 (用于验证) publicKey: string; // 关联的公钥 } export interface VerifyMnemonicResult { valid: boolean; message?: string; } @Injectable() export class RecoveryMnemonicAdapter { private readonly logger = new Logger(RecoveryMnemonicAdapter.name); private readonly encryptionKey: Buffer; constructor(private readonly configService: ConfigService) { // 从环境变量获取加密密钥 const key = this.configService.get('MNEMONIC_ENCRYPTION_KEY'); if (key) { this.encryptionKey = createHash('sha256').update(key).digest(); } else { this.logger.warn('MNEMONIC_ENCRYPTION_KEY not set, using development key'); this.encryptionKey = createHash('sha256').update('dev-mnemonic-key-do-not-use-in-production').digest(); } } /** * 生成与钱包公钥关联的恢复助记词 * * 生成逻辑: * 1. 用公钥 + 随机数生成熵 * 2. 从熵生成 12 词 BIP39 助记词 * 3. 加密存储 */ async generateMnemonic(params: GenerateMnemonicParams): Promise { const { userId, publicKey } = params; this.logger.log(`Generating recovery mnemonic for user=${userId}, publicKey=${publicKey.slice(0, 16)}...`); // 生成随机熵 (128 bits = 16 bytes for 12 words) const randomEntropy = randomBytes(16); const publicKeyBytes = Buffer.from(publicKey.replace('0x', ''), 'hex'); // 混合熵: SHA256(randomEntropy + publicKey + timestamp) const timestampBuffer = Buffer.alloc(8); timestampBuffer.writeBigInt64BE(BigInt(Date.now())); const mixedEntropyFull = createHash('sha256') .update(randomEntropy) .update(publicKeyBytes) .update(timestampBuffer) .digest(); // 取前 16 bytes (128 bits) 作为 BIP39 熵 const entropy = mixedEntropyFull.slice(0, 16); // 生成 12 词助记词 const mnemonic = entropyToMnemonic(entropy, wordlist); if (!validateMnemonic(mnemonic, wordlist)) { throw new Error('Generated mnemonic validation failed'); } // 加密助记词 const encryptedMnemonic = this.encryptMnemonic(mnemonic); // 计算助记词哈希 (用于验证,不可逆,使用 bcrypt) const mnemonicHash = await this.hashMnemonic(mnemonic); this.logger.log(`Recovery mnemonic generated: hash=${mnemonicHash.slice(0, 16)}...`); return { mnemonic, encryptedMnemonic, mnemonicHash, publicKey, }; } /** * 验证助记词是否正确 (异步,使用 bcrypt) */ async verifyMnemonic(mnemonic: string, expectedHash: string): Promise { if (!validateMnemonic(mnemonic, wordlist)) { return { valid: false, message: 'Invalid mnemonic format' }; } // 兼容旧的 SHA256 hash 和新的 bcrypt hash if (expectedHash.startsWith('$2')) { // bcrypt hash const isValid = await bcrypt.compare(mnemonic, expectedHash); if (!isValid) { return { valid: false, message: 'Mnemonic does not match' }; } } else { // 旧的 SHA256 hash (兼容性) const hash = this.hashMnemonicSha256(mnemonic); if (hash !== expectedHash) { return { valid: false, message: 'Mnemonic does not match' }; } } return { valid: true }; } /** * 解密助记词 */ decryptMnemonic(encryptedMnemonic: string): string { try { const data = Buffer.from(encryptedMnemonic, 'base64'); const iv = data.slice(0, 16); const encrypted = data.slice(16); const decipher = createDecipheriv('aes-256-cbc', this.encryptionKey, iv); let decrypted = decipher.update(encrypted); decrypted = Buffer.concat([decrypted, decipher.final()]); return decrypted.toString('utf8'); } catch (error) { this.logger.error(`Failed to decrypt mnemonic: ${error.message}`); throw new Error('Failed to decrypt mnemonic'); } } /** * 加密助记词 */ private encryptMnemonic(mnemonic: string): string { const iv = randomBytes(16); const cipher = createCipheriv('aes-256-cbc', this.encryptionKey, iv); let encrypted = cipher.update(mnemonic, 'utf8'); encrypted = Buffer.concat([encrypted, cipher.final()]); return Buffer.concat([iv, encrypted]).toString('base64'); } /** * 计算助记词哈希 (使用 bcrypt,抗暴力破解) */ private async hashMnemonic(mnemonic: string): Promise { // bcrypt rounds = 12, 足够安全且不会太慢 const hash = await bcrypt.hash(mnemonic, 12); return hash; } /** * SHA256 哈希 (兼容旧数据) */ private hashMnemonicSha256(mnemonic: string): string { const hash1 = createHash('sha256').update(mnemonic).digest(); const hash2 = createHash('sha256').update(hash1).digest(); return hash2.toString('hex'); } }