177 lines
5.5 KiB
TypeScript
177 lines
5.5 KiB
TypeScript
/**
|
||
* 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');
|
||
}
|
||
}
|