mirror of
https://github.com/NoFxAiOS/nofx.git
synced 2026-07-12 15:26:55 +08:00
feat: crypto for key
This commit is contained in:
@@ -11,7 +11,8 @@ import type {
|
||||
UpdateModelConfigRequest,
|
||||
UpdateExchangeConfigRequest,
|
||||
CompetitionData,
|
||||
} from '../types'
|
||||
} from '../types';
|
||||
import { CryptoService } from './crypto';
|
||||
|
||||
const API_BASE = '/api'
|
||||
|
||||
@@ -165,6 +166,40 @@ export const api = {
|
||||
if (!res.ok) throw new Error('更新交易所配置失败')
|
||||
},
|
||||
|
||||
// 使用加密传输更新交易所配置
|
||||
async updateExchangeConfigsEncrypted(request: UpdateExchangeConfigRequest): Promise<void> {
|
||||
// 从系统配置获取公钥
|
||||
const configRes = await fetch(`${API_BASE}/config`);
|
||||
if (!configRes.ok) throw new Error('获取系统配置失败');
|
||||
const config = await configRes.json();
|
||||
|
||||
if (!config.rsa_public_key) {
|
||||
throw new Error('系统未配置RSA公钥,无法使用加密传输');
|
||||
}
|
||||
|
||||
// 初始化加密服务
|
||||
await CryptoService.initialize(config.rsa_public_key);
|
||||
|
||||
// 获取用户信息(从localStorage或其他地方)
|
||||
const userId = localStorage.getItem('user_id') || '';
|
||||
const sessionId = sessionStorage.getItem('session_id') || '';
|
||||
|
||||
// 加密敏感数据
|
||||
const encryptedPayload = await CryptoService.encryptSensitiveData(
|
||||
JSON.stringify(request),
|
||||
userId,
|
||||
sessionId
|
||||
);
|
||||
|
||||
// 发送加密数据
|
||||
const res = await fetch(`${API_BASE}/exchanges/encrypted`, {
|
||||
method: 'PUT',
|
||||
headers: getAuthHeaders(),
|
||||
body: JSON.stringify(encryptedPayload),
|
||||
});
|
||||
if (!res.ok) throw new Error('更新交易所配置失败');
|
||||
},
|
||||
|
||||
// 获取系统状态(支持trader_id)
|
||||
async getStatus(traderId?: string): Promise<SystemStatus> {
|
||||
const url = traderId
|
||||
|
||||
@@ -3,6 +3,8 @@ export interface SystemConfig {
|
||||
default_coins?: string[]
|
||||
btc_eth_leverage?: number
|
||||
altcoin_leverage?: number
|
||||
rsa_public_key?: string
|
||||
rsa_key_id?: string
|
||||
}
|
||||
|
||||
let configPromise: Promise<SystemConfig> | null = null
|
||||
|
||||
142
web/src/lib/crypto.ts
Normal file
142
web/src/lib/crypto.ts
Normal file
@@ -0,0 +1,142 @@
|
||||
export interface EncryptedPayload {
|
||||
wrappedKey: string; // RSA-OAEP(K)
|
||||
iv: string; // 12 bytes
|
||||
ciphertext: string; // AES-GCM 输出(含 tag)
|
||||
aad?: string; // 可选:额外认证数据
|
||||
kid?: string; // 可选:服务端公钥标识
|
||||
ts?: number; // 可选:unix 秒,用于重放保护
|
||||
}
|
||||
|
||||
export class CryptoService {
|
||||
private static publicKey: CryptoKey | null = null;
|
||||
private static publicKeyPEM: string | null = null;
|
||||
|
||||
static async initialize(publicKeyPEM: string) {
|
||||
if (this.publicKey && this.publicKeyPEM === publicKeyPEM) {
|
||||
return;
|
||||
}
|
||||
this.publicKeyPEM = publicKeyPEM;
|
||||
this.publicKey = await this.importPublicKey(publicKeyPEM);
|
||||
}
|
||||
|
||||
private static async importPublicKey(pem: string): Promise<CryptoKey> {
|
||||
const pemHeader = '-----BEGIN PUBLIC KEY-----';
|
||||
const pemFooter = '-----END PUBLIC KEY-----';
|
||||
const headerIndex = pem.indexOf(pemHeader);
|
||||
const footerIndex = pem.indexOf(pemFooter);
|
||||
|
||||
if (headerIndex === -1 || footerIndex === -1 || headerIndex >= footerIndex) {
|
||||
throw new Error('Invalid PEM formatted public key');
|
||||
}
|
||||
|
||||
const pemContents = pem
|
||||
.substring(headerIndex + pemHeader.length, footerIndex)
|
||||
.replace(/\s+/g, ''); // 移除所有空白字符(包括换行符、空格等)
|
||||
|
||||
const binaryDerString = atob(pemContents);
|
||||
const binaryDer = new Uint8Array(binaryDerString.length);
|
||||
for (let i = 0; i < binaryDerString.length; i++) {
|
||||
binaryDer[i] = binaryDerString.charCodeAt(i);
|
||||
}
|
||||
|
||||
return crypto.subtle.importKey(
|
||||
'spki',
|
||||
binaryDer,
|
||||
{
|
||||
name: 'RSA-OAEP',
|
||||
hash: 'SHA-256',
|
||||
},
|
||||
false,
|
||||
['encrypt']
|
||||
);
|
||||
}
|
||||
|
||||
static async encryptSensitiveData(
|
||||
plaintext: string,
|
||||
userId?: string,
|
||||
sessionId?: string
|
||||
): Promise<EncryptedPayload> {
|
||||
if (!this.publicKey) {
|
||||
throw new Error('Crypto service not initialized. Call initialize() first.');
|
||||
}
|
||||
|
||||
// 1. 生成 256-bit AES 密钥
|
||||
const aesKey = await crypto.subtle.generateKey(
|
||||
{
|
||||
name: 'AES-GCM',
|
||||
length: 256,
|
||||
},
|
||||
true,
|
||||
['encrypt']
|
||||
);
|
||||
|
||||
// 2. 生成 12 字节随机 IV
|
||||
const iv = crypto.getRandomValues(new Uint8Array(12));
|
||||
|
||||
// 3. 准备 AAD (额外认证数据)
|
||||
const ts = Math.floor(Date.now() / 1000);
|
||||
const aadObject = {
|
||||
userId: userId || '',
|
||||
sessionId: sessionId || '',
|
||||
ts: ts,
|
||||
purpose: 'sensitive_data_encryption'
|
||||
};
|
||||
const aadString = JSON.stringify(aadObject);
|
||||
const aadBytes = new TextEncoder().encode(aadString);
|
||||
|
||||
// 4. 使用 AES-GCM 加密数据
|
||||
const plaintextBytes = new TextEncoder().encode(plaintext);
|
||||
const ciphertext = await crypto.subtle.encrypt(
|
||||
{
|
||||
name: 'AES-GCM',
|
||||
iv: iv,
|
||||
additionalData: aadBytes,
|
||||
tagLength: 128, // 16 bytes tag
|
||||
},
|
||||
aesKey,
|
||||
plaintextBytes
|
||||
);
|
||||
|
||||
// 5. 导出 AES 密钥
|
||||
const aesKeyRaw = await crypto.subtle.exportKey('raw', aesKey);
|
||||
|
||||
// 6. 使用 RSA-OAEP 加密 AES 密钥
|
||||
const wrappedKey = await crypto.subtle.encrypt(
|
||||
{
|
||||
name: 'RSA-OAEP',
|
||||
},
|
||||
this.publicKey,
|
||||
aesKeyRaw
|
||||
);
|
||||
|
||||
// 7. 转换为 base64url 格式
|
||||
return {
|
||||
wrappedKey: this.arrayBufferToBase64Url(wrappedKey),
|
||||
iv: this.arrayBufferToBase64Url(iv),
|
||||
ciphertext: this.arrayBufferToBase64Url(ciphertext),
|
||||
aad: this.arrayBufferToBase64Url(aadBytes),
|
||||
kid: 'rsa-key-2025-11-05',
|
||||
ts: ts,
|
||||
};
|
||||
}
|
||||
|
||||
private static arrayBufferToBase64Url(buffer: ArrayBuffer | Uint8Array): string {
|
||||
const bytes = buffer instanceof Uint8Array ? buffer : new Uint8Array(buffer);
|
||||
let binary = '';
|
||||
for (let i = 0; i < bytes.byteLength; i++) {
|
||||
binary += String.fromCharCode(bytes[i]);
|
||||
}
|
||||
return btoa(binary)
|
||||
.replace(/\+/g, '-')
|
||||
.replace(/\//g, '_')
|
||||
.replace(/=/g, '');
|
||||
}
|
||||
|
||||
static async encryptWalletPrivateKey(privateKey: string, userId?: string, sessionId?: string): Promise<EncryptedPayload> {
|
||||
return this.encryptSensitiveData(privateKey, userId, sessionId);
|
||||
}
|
||||
|
||||
static async encryptExchangeSecret(secretKey: string, userId?: string, sessionId?: string): Promise<EncryptedPayload> {
|
||||
return this.encryptSensitiveData(secretKey, userId, sessionId);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user