669 lines
19 KiB
TypeScript
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;
|
|
}
|
|
}
|
|
}
|