267 lines
6.9 KiB
TypeScript
267 lines
6.9 KiB
TypeScript
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 });
|
||
}
|
||
}
|