rwadurian/backend/services/blockchain-service/src/infrastructure/blockchain/recovery-mnemonic.adapter.ts

177 lines
5.5 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

/**
* 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<string>('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<GenerateMnemonicResult> {
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<VerifyMnemonicResult> {
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<string> {
// 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');
}
}