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

267 lines
6.9 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.

import Store from 'electron-store';
import * as crypto from 'crypto';
import { v4 as uuidv4 } from 'uuid';
// Share 数据结构
export interface ShareEntry {
id: string;
sessionId: string;
walletName: string;
partyId: string;
partyIndex: number;
threshold: {
t: number;
n: number;
};
publicKey: string;
encryptedShare: string;
createdAt: string;
lastUsedAt?: string;
metadata: {
participants: Array<{
partyId: string;
name: string;
}>;
};
}
// 存储的数据结构
interface StoreSchema {
version: string;
shares: ShareEntry[];
settings: {
messageRouterHost: string;
messageRouterPort: number;
autoBackup: boolean;
};
}
// 加密配置
const ALGORITHM = 'aes-256-gcm';
const KEY_LENGTH = 32;
const IV_LENGTH = 16;
const SALT_LENGTH = 32;
const TAG_LENGTH = 16;
const ITERATIONS = 100000;
/**
* 安全存储类 - 本地加密存储 share
*/
export class SecureStorage {
private store: Store<StoreSchema>;
constructor() {
this.store = new Store<StoreSchema>({
name: 'service-party-data',
defaults: {
version: '1.0.0',
shares: [],
settings: {
messageRouterHost: 'localhost',
messageRouterPort: 9092,
autoBackup: false,
},
},
encryptionKey: 'service-party-app-encryption-key', // 基础加密share 数据还有额外加密
});
}
/**
* 从密码派生密钥
*/
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;
}
/**
* 保存 share
*/
saveShare(share: Omit<ShareEntry, 'id' | 'createdAt' | 'encryptedShare'> & { rawShare: string }, password: string): ShareEntry {
const encryptedShare = this.encrypt(share.rawShare, password);
const entry: ShareEntry = {
id: uuidv4(),
sessionId: share.sessionId,
walletName: share.walletName,
partyId: share.partyId,
partyIndex: share.partyIndex,
threshold: share.threshold,
publicKey: share.publicKey,
encryptedShare,
createdAt: new Date().toISOString(),
metadata: share.metadata,
};
const shares = this.store.get('shares', []);
shares.push(entry);
this.store.set('shares', shares);
return entry;
}
/**
* 获取 share 列表 (不含加密数据)
*/
listShares(): Omit<ShareEntry, 'encryptedShare'>[] {
const shares = this.store.get('shares', []);
return shares.map(({ encryptedShare: _, ...rest }) => rest);
}
/**
* 获取单个 share (解密)
*/
getShare(id: string, password: string): ShareEntry & { rawShare: string } {
const shares = this.store.get('shares', []);
const share = shares.find((s) => s.id === id);
if (!share) {
throw new Error('Share not found');
}
const rawShare = this.decrypt(share.encryptedShare, password);
return {
...share,
rawShare,
};
}
/**
* 更新 share 使用时间
*/
updateLastUsed(id: string): void {
const shares = this.store.get('shares', []);
const index = shares.findIndex((s) => s.id === id);
if (index !== -1) {
shares[index].lastUsedAt = new Date().toISOString();
this.store.set('shares', shares);
}
}
/**
* 删除 share
*/
deleteShare(id: string): void {
const shares = this.store.get('shares', []);
const filtered = shares.filter((s) => s.id !== id);
this.store.set('shares', filtered);
}
/**
* 导出 share (加密后的备份文件)
*/
exportShare(id: string, password: string): Buffer {
const share = this.getShare(id, password);
const exportData = {
version: '1.0.0',
exportedAt: new Date().toISOString(),
share: {
...share,
// 导出时使用新密码重新加密
encryptedShare: this.encrypt(share.rawShare, password),
},
};
// 再次加密整个导出数据
const encryptedExport = this.encrypt(JSON.stringify(exportData), password);
return Buffer.from(encryptedExport, 'utf8');
}
/**
* 导入 share
*/
importShare(data: Buffer, password: string): ShareEntry {
const encryptedExport = data.toString('utf8');
// 解密导出数据
const decrypted = this.decrypt(encryptedExport, password);
const exportData = JSON.parse(decrypted);
if (!exportData.version || !exportData.share) {
throw new Error('Invalid export file format');
}
// 解密 share 数据
const rawShare = this.decrypt(exportData.share.encryptedShare, password);
// 检查是否已存在相同的 share
const shares = this.store.get('shares', []);
const existing = shares.find(
(s) => s.sessionId === exportData.share.sessionId && s.partyId === exportData.share.partyId
);
if (existing) {
throw new Error('Share already exists');
}
// 保存导入的 share
return this.saveShare(
{
sessionId: exportData.share.sessionId,
walletName: exportData.share.walletName,
partyId: exportData.share.partyId,
partyIndex: exportData.share.partyIndex,
threshold: exportData.share.threshold,
publicKey: exportData.share.publicKey,
metadata: exportData.share.metadata,
rawShare,
},
password
);
}
/**
* 获取设置
*/
getSettings() {
return this.store.get('settings');
}
/**
* 更新设置
*/
updateSettings(settings: Partial<StoreSchema['settings']>): void {
const current = this.store.get('settings');
this.store.set('settings', { ...current, ...settings });
}
}