import * as crypto from 'crypto'; import * as path from 'path'; import * as fs from 'fs'; import { app } from 'electron'; import initSqlJs from 'sql.js'; import type { Database as SqlJsDatabase, SqlJsStatic } from 'sql.js'; import { v4 as uuidv4 } from 'uuid'; // ============================================================================= // sql.js WASM 文件路径 // ============================================================================= function getSqlJsWasmPath(): string { // 在开发环境中,WASM 文件在 node_modules 中 // 在生产环境中,WASM 文件被复制到 extraResources 目录 const isDev = !app.isPackaged; if (isDev) { // 开发环境: 使用 node_modules 中的文件 return path.join(__dirname, '../../node_modules/sql.js/dist/sql-wasm.wasm'); } else { // 生产环境: 使用 extraResources 中的文件 return path.join(process.resourcesPath, 'sql-wasm.wasm'); } } // ============================================================================= // 数据库路径 // ============================================================================= function getDatabasePath(): string { const userDataPath = app.getPath('userData'); return path.join(userDataPath, 'service-party.db'); } // ============================================================================= // 加密配置 // ============================================================================= const ALGORITHM = 'aes-256-gcm'; const KEY_LENGTH = 32; const IV_LENGTH = 16; const SALT_LENGTH = 32; const TAG_LENGTH = 16; const ITERATIONS = 100000; // ============================================================================= // 数据类型定义 // ============================================================================= export interface ShareRecord { id: string; session_id: string; wallet_name: string; party_id: string; party_index: number; threshold_t: number; threshold_n: number; public_key_hex: string; encrypted_share: string; created_at: string; last_used_at: string | null; participants_json: string; // JSON 存储参与者列表 } export interface DerivedAddressRecord { id: string; share_id: string; chain: string; derivation_path: string; address: string; public_key_hex: string; created_at: string; } export interface SigningHistoryRecord { id: string; share_id: string; session_id: string; message_hash: string; signature: string | null; status: 'pending' | 'in_progress' | 'completed' | 'failed'; error_message: string | null; created_at: string; completed_at: string | null; } export interface SettingsRecord { key: string; value: string; } // ============================================================================= // 数据库管理类 (使用 sql.js - 纯 JavaScript SQLite) // ============================================================================= export class DatabaseManager { private db: SqlJsDatabase | null = null; private SQL: SqlJsStatic | null = null; private dbPath: string; private initPromise: Promise; constructor() { this.dbPath = getDatabasePath(); this.initPromise = this.initialize(); } /** * 初始化数据库 */ private async initialize(): Promise { // 获取 WASM 文件路径 const wasmPath = getSqlJsWasmPath(); console.log('[Database] App packaged:', app.isPackaged); console.log('[Database] Resources path:', process.resourcesPath); console.log('[Database] WASM path:', wasmPath); console.log('[Database] WASM exists:', fs.existsSync(wasmPath)); // 初始化 sql.js (加载 WASM) // 使用 wasmBinary 直接加载 WASM 文件,这在打包环境中更可靠 let config: { wasmBinary?: ArrayBuffer; locateFile?: (file: string) => string } = {}; if (fs.existsSync(wasmPath)) { // 直接读取 WASM 文件作为 ArrayBuffer - 这种方式更可靠 const wasmBuffer = fs.readFileSync(wasmPath); config.wasmBinary = wasmBuffer.buffer.slice( wasmBuffer.byteOffset, wasmBuffer.byteOffset + wasmBuffer.byteLength ); console.log('[Database] WASM loaded as binary, size:', wasmBuffer.length); } else { console.warn('[Database] WASM file not found, sql.js will try to load from default location'); // 作为备用方案,使用 locateFile config.locateFile = (file: string) => { console.log('[Database] locateFile called for:', file); return file; }; } this.SQL = await initSqlJs(config); // 如果数据库文件存在,加载它 if (fs.existsSync(this.dbPath)) { const buffer = fs.readFileSync(this.dbPath); this.db = new this.SQL.Database(buffer); } else { this.db = new this.SQL.Database(); } // 创建表结构 this.createTables(); this.saveToFile(); } /** * 确保数据库已初始化 */ private async ensureReady(): Promise { await this.initPromise; } /** * 等待数据库初始化完成(公开方法) */ async waitForReady(): Promise { await this.initPromise; } /** * 创建表结构 */ private createTables(): void { if (!this.db) return; this.db.run(` CREATE TABLE IF NOT EXISTS shares ( id TEXT PRIMARY KEY, session_id TEXT NOT NULL, wallet_name TEXT NOT NULL, party_id TEXT NOT NULL, party_index INTEGER NOT NULL, threshold_t INTEGER NOT NULL, threshold_n INTEGER NOT NULL, public_key_hex TEXT NOT NULL, encrypted_share TEXT NOT NULL, created_at TEXT NOT NULL, last_used_at TEXT, participants_json TEXT NOT NULL DEFAULT '[]' ) `); this.db.run(` CREATE TABLE IF NOT EXISTS derived_addresses ( id TEXT PRIMARY KEY, share_id TEXT NOT NULL, chain TEXT NOT NULL, derivation_path TEXT NOT NULL, address TEXT NOT NULL, public_key_hex TEXT NOT NULL, created_at TEXT NOT NULL, UNIQUE(share_id, chain, derivation_path) ) `); this.db.run(` CREATE TABLE IF NOT EXISTS signing_history ( id TEXT PRIMARY KEY, share_id TEXT NOT NULL, session_id TEXT NOT NULL, message_hash TEXT NOT NULL, signature TEXT, status TEXT NOT NULL DEFAULT 'pending', error_message TEXT, created_at TEXT NOT NULL, completed_at TEXT ) `); this.db.run(` CREATE TABLE IF NOT EXISTS settings ( key TEXT PRIMARY KEY, value TEXT NOT NULL ) `); // 已处理消息表 - 用于消息去重,防止重连后重复处理消息 this.db.run(` CREATE TABLE IF NOT EXISTS processed_messages ( message_id TEXT PRIMARY KEY, session_id TEXT NOT NULL, processed_at TEXT NOT NULL ) `); // 创建索引 this.db.run(`CREATE INDEX IF NOT EXISTS idx_shares_session ON shares(session_id)`); this.db.run(`CREATE INDEX IF NOT EXISTS idx_addresses_share ON derived_addresses(share_id)`); this.db.run(`CREATE INDEX IF NOT EXISTS idx_addresses_chain ON derived_addresses(chain)`); this.db.run(`CREATE INDEX IF NOT EXISTS idx_history_share ON signing_history(share_id)`); this.db.run(`CREATE INDEX IF NOT EXISTS idx_history_status ON signing_history(status)`); this.db.run(`CREATE INDEX IF NOT EXISTS idx_processed_messages_session ON processed_messages(session_id)`); // 插入默认设置 this.db.run(`INSERT OR IGNORE INTO settings (key, value) VALUES (?, ?)`, ['message_router_url', 'mpc-grpc.szaiai.com:443']); this.db.run(`INSERT OR IGNORE INTO settings (key, value) VALUES (?, ?)`, ['auto_backup', 'false']); this.db.run(`INSERT OR IGNORE INTO settings (key, value) VALUES (?, ?)`, ['backup_path', '']); } /** * 保存数据库到文件 */ private saveToFile(): void { if (!this.db) return; const data = this.db.export(); const buffer = Buffer.from(data); fs.writeFileSync(this.dbPath, buffer); } /** * 从密码派生密钥 */ private deriveKey(password: string, salt: Buffer): Buffer { return crypto.pbkdf2Sync(password, salt, ITERATIONS, KEY_LENGTH, 'sha256'); } /** * 加密数据 */ private encrypt(data: string, password: string): string { const salt = crypto.randomBytes(SALT_LENGTH); const key = this.deriveKey(password, salt); const iv = crypto.randomBytes(IV_LENGTH); const cipher = crypto.createCipheriv(ALGORITHM, key, iv); let encrypted = cipher.update(data, 'utf8', 'hex'); encrypted += cipher.final('hex'); const tag = cipher.getAuthTag(); // 格式: salt(hex) + iv(hex) + tag(hex) + encrypted(hex) return salt.toString('hex') + iv.toString('hex') + tag.toString('hex') + encrypted; } /** * 解密数据 */ private decrypt(encryptedData: string, password: string): string { const salt = Buffer.from(encryptedData.slice(0, SALT_LENGTH * 2), 'hex'); const iv = Buffer.from(encryptedData.slice(SALT_LENGTH * 2, SALT_LENGTH * 2 + IV_LENGTH * 2), 'hex'); const tag = Buffer.from(encryptedData.slice(SALT_LENGTH * 2 + IV_LENGTH * 2, SALT_LENGTH * 2 + IV_LENGTH * 2 + TAG_LENGTH * 2), 'hex'); const encrypted = encryptedData.slice(SALT_LENGTH * 2 + IV_LENGTH * 2 + TAG_LENGTH * 2); const key = this.deriveKey(password, salt); const decipher = crypto.createDecipheriv(ALGORITHM, key, iv); decipher.setAuthTag(tag); let decrypted = decipher.update(encrypted, 'hex', 'utf8'); decrypted += decipher.final('utf8'); return decrypted; } /** * 将查询结果转换为对象数组 */ private queryToObjects(sql: string, params: unknown[] = []): T[] { if (!this.db) return []; const results = this.db.exec(sql, params); if (results.length === 0) return []; const columns = results[0].columns; return results[0].values.map((row: (number | string | Uint8Array | null)[]) => { const obj: Record = {}; columns.forEach((col: string, i: number) => { obj[col] = row[i]; }); return obj as T; }); } /** * 查询单个对象 */ private queryOne(sql: string, params: unknown[] = []): T | undefined { const results = this.queryToObjects(sql, params); return results[0]; } // =========================================================================== // Share 操作 // =========================================================================== /** * 保存 share */ saveShare(params: { sessionId: string; walletName: string; partyId: string; partyIndex: number; thresholdT: number; thresholdN: number; publicKeyHex: string; rawShare: string; participants: Array<{ partyId: string; name: string }>; }, password: string): ShareRecord { if (!this.db) throw new Error('Database not initialized'); const id = uuidv4(); const encryptedShare = this.encrypt(params.rawShare, password); const now = new Date().toISOString(); this.db.run(` INSERT INTO shares ( id, session_id, wallet_name, party_id, party_index, threshold_t, threshold_n, public_key_hex, encrypted_share, created_at, participants_json ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?) `, [ id, params.sessionId, params.walletName, params.partyId, params.partyIndex, params.thresholdT, params.thresholdN, params.publicKeyHex, encryptedShare, now, JSON.stringify(params.participants) ]); this.saveToFile(); return { id, session_id: params.sessionId, wallet_name: params.walletName, party_id: params.partyId, party_index: params.partyIndex, threshold_t: params.thresholdT, threshold_n: params.thresholdN, public_key_hex: params.publicKeyHex, encrypted_share: encryptedShare, created_at: now, last_used_at: null, participants_json: JSON.stringify(params.participants), }; } /** * 获取所有 share (不含加密数据) */ listShares(): Omit[] { return this.queryToObjects>(` SELECT id, session_id, wallet_name, party_id, party_index, threshold_t, threshold_n, public_key_hex, created_at, last_used_at, participants_json FROM shares ORDER BY created_at DESC `); } /** * 获取单个 share (解密) */ getShare(id: string, password: string): ShareRecord & { raw_share: string } { const share = this.queryOne(`SELECT * FROM shares WHERE id = ?`, [id]); if (!share) { throw new Error('Share not found'); } const rawShare = this.decrypt(share.encrypted_share, password); return { ...share, raw_share: rawShare, }; } /** * 更新 share 最后使用时间 */ updateShareLastUsed(id: string): void { if (!this.db) return; this.db.run(`UPDATE shares SET last_used_at = ? WHERE id = ?`, [new Date().toISOString(), id]); this.saveToFile(); } /** * 删除 share (级联删除派生地址和签名历史) */ deleteShare(id: string): void { if (!this.db) return; // 手动级联删除 this.db.run(`DELETE FROM derived_addresses WHERE share_id = ?`, [id]); this.db.run(`DELETE FROM signing_history WHERE share_id = ?`, [id]); this.db.run(`DELETE FROM shares WHERE id = ?`, [id]); this.saveToFile(); } // =========================================================================== // 派生地址操作 // =========================================================================== /** * 保存派生地址 */ saveDerivedAddress(params: { shareId: string; chain: string; derivationPath: string; address: string; publicKeyHex: string; }): DerivedAddressRecord { if (!this.db) throw new Error('Database not initialized'); const id = uuidv4(); const now = new Date().toISOString(); this.db.run(` INSERT OR REPLACE INTO derived_addresses ( id, share_id, chain, derivation_path, address, public_key_hex, created_at ) VALUES (?, ?, ?, ?, ?, ?, ?) `, [ id, params.shareId, params.chain, params.derivationPath, params.address, params.publicKeyHex, now ]); this.saveToFile(); return { id, share_id: params.shareId, chain: params.chain, derivation_path: params.derivationPath, address: params.address, public_key_hex: params.publicKeyHex, created_at: now, }; } /** * 获取 share 的所有派生地址 */ getAddressesByShare(shareId: string): DerivedAddressRecord[] { return this.queryToObjects(` SELECT * FROM derived_addresses WHERE share_id = ? ORDER BY chain, derivation_path `, [shareId]); } /** * 根据链获取地址 */ getAddressByChain(shareId: string, chain: string): DerivedAddressRecord | undefined { return this.queryOne(` SELECT * FROM derived_addresses WHERE share_id = ? AND chain = ? LIMIT 1 `, [shareId, chain]); } /** * 获取所有指定链的地址 */ getAllAddressesByChain(chain: string): DerivedAddressRecord[] { return this.queryToObjects(` SELECT * FROM derived_addresses WHERE chain = ? ORDER BY created_at DESC `, [chain]); } // =========================================================================== // 签名历史操作 // =========================================================================== /** * 创建签名历史记录 */ createSigningHistory(params: { shareId: string; sessionId: string; messageHash: string; }): SigningHistoryRecord { if (!this.db) throw new Error('Database not initialized'); const id = uuidv4(); const now = new Date().toISOString(); this.db.run(` INSERT INTO signing_history ( id, share_id, session_id, message_hash, status, created_at ) VALUES (?, ?, ?, ?, 'pending', ?) `, [id, params.shareId, params.sessionId, params.messageHash, now]); this.saveToFile(); return { id, share_id: params.shareId, session_id: params.sessionId, message_hash: params.messageHash, signature: null, status: 'pending', error_message: null, created_at: now, completed_at: null, }; } /** * 更新签名历史状态 */ updateSigningHistory(id: string, params: { status: SigningHistoryRecord['status']; signature?: string; errorMessage?: string; }): void { if (!this.db) return; const completedAt = params.status === 'completed' || params.status === 'failed' ? new Date().toISOString() : null; this.db.run(` UPDATE signing_history SET status = ?, signature = ?, error_message = ?, completed_at = ? WHERE id = ? `, [ params.status, params.signature || null, params.errorMessage || null, completedAt, id ]); this.saveToFile(); } /** * 获取 share 的签名历史 */ getSigningHistoryByShare(shareId: string): SigningHistoryRecord[] { return this.queryToObjects(` SELECT * FROM signing_history WHERE share_id = ? ORDER BY created_at DESC `, [shareId]); } // =========================================================================== // 设置操作 // =========================================================================== /** * 获取设置 */ getSetting(key: string): string | undefined { const row = this.queryOne<{ value: string }>(`SELECT value FROM settings WHERE key = ?`, [key]); return row?.value; } /** * 保存设置 */ setSetting(key: string, value: string): void { if (!this.db) return; this.db.run(`INSERT OR REPLACE INTO settings (key, value) VALUES (?, ?)`, [key, value]); this.saveToFile(); } /** * 获取所有设置 */ getAllSettings(): Record { const rows = this.queryToObjects(`SELECT key, value FROM settings`); const settings: Record = {}; for (const row of rows) { settings[row.key] = row.value; } return settings; } // =========================================================================== // 消息去重操作 // =========================================================================== /** * 检查消息是否已处理 */ isMessageProcessed(messageId: string): boolean { const row = this.queryOne<{ message_id: string }>( `SELECT message_id FROM processed_messages WHERE message_id = ?`, [messageId] ); return !!row; } /** * 标记消息为已处理 */ markMessageProcessed(messageId: string, sessionId: string): void { if (!this.db) return; const now = new Date().toISOString(); this.db.run( `INSERT OR IGNORE INTO processed_messages (message_id, session_id, processed_at) VALUES (?, ?, ?)`, [messageId, sessionId, now] ); this.saveToFile(); } /** * 清理指定会话的已处理消息记录 * 当会话完成后调用,释放空间 */ clearProcessedMessages(sessionId: string): void { if (!this.db) return; this.db.run(`DELETE FROM processed_messages WHERE session_id = ?`, [sessionId]); this.saveToFile(); } /** * 清理过期的已处理消息记录(超过24小时) * 可在应用启动时调用 */ cleanupOldProcessedMessages(): void { if (!this.db) return; const cutoff = new Date(Date.now() - 24 * 60 * 60 * 1000).toISOString(); this.db.run(`DELETE FROM processed_messages WHERE processed_at < ?`, [cutoff]); this.saveToFile(); } // =========================================================================== // 导入导出 // =========================================================================== /** * 导出 share (加密备份) */ exportShare(id: string, password: string): Buffer { const share = this.getShare(id, password); const addresses = this.getAddressesByShare(id); const exportData = { version: '1.0.0', exportedAt: new Date().toISOString(), share: { session_id: share.session_id, wallet_name: share.wallet_name, party_id: share.party_id, party_index: share.party_index, threshold_t: share.threshold_t, threshold_n: share.threshold_n, public_key_hex: share.public_key_hex, raw_share: share.raw_share, participants: JSON.parse(share.participants_json), }, addresses: addresses.map(addr => ({ chain: addr.chain, derivation_path: addr.derivation_path, address: addr.address, public_key_hex: addr.public_key_hex, })), }; const encrypted = this.encrypt(JSON.stringify(exportData), password); return Buffer.from(encrypted, 'utf8'); } /** * 导入 share */ importShare(data: Buffer, password: string): ShareRecord { if (!this.db) throw new Error('Database not initialized'); const encrypted = data.toString('utf8'); const decrypted = this.decrypt(encrypted, password); const exportData = JSON.parse(decrypted); if (!exportData.version || !exportData.share) { throw new Error('Invalid export file format'); } // 检查是否已存在 const existing = this.queryOne<{ id: string }>(` SELECT id FROM shares WHERE session_id = ? AND party_id = ? `, [exportData.share.session_id, exportData.share.party_id]); if (existing) { throw new Error('Share already exists'); } // 保存 share const share = this.saveShare({ sessionId: exportData.share.session_id, walletName: exportData.share.wallet_name, partyId: exportData.share.party_id, partyIndex: exportData.share.party_index, thresholdT: exportData.share.threshold_t, thresholdN: exportData.share.threshold_n, publicKeyHex: exportData.share.public_key_hex, rawShare: exportData.share.raw_share, participants: exportData.share.participants, }, password); // 恢复派生地址 if (exportData.addresses) { for (const addr of exportData.addresses) { this.saveDerivedAddress({ shareId: share.id, chain: addr.chain, derivationPath: addr.derivation_path, address: addr.address, publicKeyHex: addr.public_key_hex, }); } } return share; } /** * 关闭数据库连接 */ close(): void { if (this.db) { this.saveToFile(); this.db.close(); this.db = null; } } }