rwadurian/backend/mpc-system/services/service-party-app/electron/modules/database.ts

669 lines
19 KiB
TypeScript

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';
// =============================================================================
// 数据库路径
// =============================================================================
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<void>;
constructor() {
this.dbPath = getDatabasePath();
this.initPromise = this.initialize();
}
/**
* 初始化数据库
*/
private async initialize(): Promise<void> {
// 初始化 sql.js (加载 WASM)
this.SQL = await initSqlJs();
// 如果数据库文件存在,加载它
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<void> {
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 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(`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<T>(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<string, unknown> = {};
columns.forEach((col: string, i: number) => {
obj[col] = row[i];
});
return obj as T;
});
}
/**
* 查询单个对象
*/
private queryOne<T>(sql: string, params: unknown[] = []): T | undefined {
const results = this.queryToObjects<T>(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<ShareRecord, 'encrypted_share'>[] {
return this.queryToObjects<Omit<ShareRecord, 'encrypted_share'>>(`
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<ShareRecord>(`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<DerivedAddressRecord>(`
SELECT * FROM derived_addresses
WHERE share_id = ?
ORDER BY chain, derivation_path
`, [shareId]);
}
/**
* 根据链获取地址
*/
getAddressByChain(shareId: string, chain: string): DerivedAddressRecord | undefined {
return this.queryOne<DerivedAddressRecord>(`
SELECT * FROM derived_addresses
WHERE share_id = ? AND chain = ?
LIMIT 1
`, [shareId, chain]);
}
/**
* 获取所有指定链的地址
*/
getAllAddressesByChain(chain: string): DerivedAddressRecord[] {
return this.queryToObjects<DerivedAddressRecord>(`
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<SigningHistoryRecord>(`
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<string, string> {
const rows = this.queryToObjects<SettingsRecord>(`SELECT key, value FROM settings`);
const settings: Record<string, string> = {};
for (const row of rows) {
settings[row.key] = row.value;
}
return settings;
}
// ===========================================================================
// 导入导出
// ===========================================================================
/**
* 导出 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;
}
}
}