Dev Crypto (#730)

* feat: remove admin mode

* feat: bugfix

* feat(crypto): 添加RSA-OAEP + AES-GCM混合加密服务

- 实现CryptoService加密服务,支持RSA-OAEP-2048 + AES-256-GCM混合加密
- 集成数据库层加密,自动加密存储敏感字段(API密钥、私钥等)
- 支持环境变量DATA_ENCRYPTION_KEY配置数据加密密钥
- 适配SQLite数据库加密存储(从PostgreSQL移植)
- 保持Hyperliquid代理钱包处理兼容性
- 更新.gitignore以正确处理crypto模块代码

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-Authored-By: Claude <noreply@anthropic.com>

* feat(scripts): 添加加密环境一键设置脚本

- setup_encryption.sh: 一键生成RSA密钥对+数据加密密钥+JWT密钥
- generate_rsa_keys.sh: 专业的RSA-2048密钥对生成工具
- generate_data_key.sh: 生成AES-256数据加密密钥和JWT认证密钥
- ENCRYPTION_README.md: 详细的加密系统说明文档
- 支持自动检测现有密钥并只生成缺失的密钥
- 完善的权限管理和安全验证
- 兼容macOS和Linux的跨平台支持

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-Authored-By: Claude <noreply@anthropic.com>

* feat(api): 添加加密API端点和Gin框架集成

- 新增CryptoHandler处理加密相关API请求
- 提供/api/crypto/public-key端点获取RSA公钥
- 提供/api/crypto/decrypt端点解密敏感数据
- 适配Gin框架的HTTP处理器格式
- 集成CryptoService到API服务器
- 支持前端加密数据传输和解密

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-Authored-By: Claude <noreply@anthropic.com>

* feat(web): 添加前端加密服务和两阶段密钥输入组件

- CryptoService: Web Crypto API集成,支持RSA-OAEP加密
- TwoStageKeyModal: 安全的两阶段私钥输入组件,支持剪贴板混淆
- 完善国际化翻译支持加密相关UI文本
- 修复TypeScript类型错误和编译问题
- 支持前端敏感数据加密传输到后端
- 增强用户隐私保护和数据安全

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-Authored-By: Claude <noreply@anthropic.com>

* feat(auth): 增强JWT认证安全性

- 优先使用环境变量JWT_SECRET而不是数据库配置
- 支持通过.env文件安全配置JWT认证密钥
- 保留数据库配置作为回退机制
- 改进JWT密钥来源日志显示
- 增强系统启动时的安全配置检查
- 支持运行时动态JWT密钥切换

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-Authored-By: Claude <noreply@anthropic.com>

* feat(docker): 集成加密环境变量到Docker部署

- 添加DATA_ENCRYPTION_KEY环境变量传递到容器
- 添加JWT_SECRET环境变量支持
- 挂载secrets目录使容器可访问RSA密钥文件
- 确保容器内加密服务正常工作
- 解决容器启动失败和加密初始化问题
- 完善Docker Compose加密环境配置

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-Authored-By: Claude <noreply@anthropic.com>

* feat(start): 集成自动加密环境检测和设置

- 增强check_encryption()函数检测JWT_SECRET和DATA_ENCRYPTION_KEY
- 自动运行setup_encryption.sh当检测到缺失密钥时
- 改进加密状态显示,包含RSA+AES+JWT全套加密信息
- 优化用户体验,提供清晰的加密配置反馈
- 支持一键设置完整加密环境
- 确保容器启动前加密环境就绪

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-Authored-By: Claude <noreply@anthropic.com>

* feat: format fix

* fix(security): 修复前端模型和交易所配置敏感数据明文传输

- 在handleSaveModelConfig中对API密钥进行RSA-OAEP加密
- 在handleSaveExchangeConfig中对API密钥、Secret密钥和Aster私钥进行加密
- 只有非空敏感数据才进行加密处理
- 添加加密失败错误处理和用户友好提示
- 增加encryptionFailed翻译键的中英文支持
- 使用用户ID和会话ID作为加密上下文增强安全性

这修复了之前敏感数据在网络传输中以明文形式发送的安全漏洞。

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-Authored-By: Claude <noreply@anthropic.com>

* fix(crypto): 修复后端加密服务集成和缺失的加密端点

- 添加Server结构体缺少的cryptoService字段
- 实现handleUpdateModelConfigsEncrypted处理器用于模型配置加密传输
- 修复handleUpdateExchangeConfigsEncrypted中的函数调用
- 在前端API中添加updateModelConfigsEncrypted方法
- 统一RSA密钥路径从secrets/rsa_key改为keys/rsa_private.key
- 确保前端可以使用加密端点安全传输敏感数据
- 兼容原有加密通信模式和二段输入私钥功能

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-Authored-By: Claude <noreply@anthropic.com>

---------

Co-authored-by: icy <icyoung520@gmail.com>
Co-authored-by: Claude <noreply@anthropic.com>
This commit is contained in:
Icyoung
2025-11-08 02:03:09 +08:00
committed by GitHub
parent 5c2a2bd77d
commit 079995e458
21 changed files with 2478 additions and 458 deletions

View File

@@ -12,6 +12,7 @@ import type {
UpdateExchangeConfigRequest,
CompetitionData,
} from '../types'
import { CryptoService } from './crypto'
const API_BASE = '/api'
@@ -138,6 +139,36 @@ export const api = {
if (!res.ok) throw new Error('更新模型配置失败')
},
// 使用加密传输更新模型配置
async updateModelConfigsEncrypted(
request: UpdateModelConfigRequest
): Promise<void> {
// 获取RSA公钥
const publicKey = await CryptoService.fetchPublicKey()
// 初始化加密服务
await CryptoService.initialize(publicKey)
// 获取用户信息从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}/models/encrypted`, {
method: 'PUT',
headers: getAuthHeaders(),
body: JSON.stringify(encryptedPayload),
})
if (!res.ok) throw new Error('更新模型配置失败')
},
// 交易所配置接口
async getExchangeConfigs(): Promise<Exchange[]> {
const res = await fetch(`${API_BASE}/exchanges`, {
@@ -165,6 +196,36 @@ export const api = {
if (!res.ok) throw new Error('更新交易所配置失败')
},
// 使用加密传输更新交易所配置
async updateExchangeConfigsEncrypted(
request: UpdateExchangeConfigRequest
): Promise<void> {
// 获取RSA公钥
const publicKey = await CryptoService.fetchPublicKey()
// 初始化加密服务
await CryptoService.initialize(publicKey)
// 获取用户信息从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

View File

@@ -1,326 +1,188 @@
/**
* 端到端加密模組
* 使用混合加密: RSA-OAEP (密鑰交換) + AES-256-GCM (數據加密)
*/
// ==================== 核心加密函數 ====================
/**
* 生成隨機混淆字串 (用於剪貼簿混淆)
*/
export function generateObfuscation(): string {
const array = new Uint8Array(32)
crypto.getRandomValues(array)
return Array.from(array, (byte) => byte.toString(16).padStart(2, '0')).join(
''
)
export interface EncryptedPayload {
wrappedKey: string // RSA-OAEP(K)
iv: string // 12 bytes
ciphertext: string // AES-GCM 输出(含 tag)
aad?: string // 可选:额外认证数据
kid?: string // 可选:服务端公钥标识
ts?: number // 可选unix 秒,用于重放保护
}
/**
* 使用伺服器公鑰加密私鑰
* @param plaintext 明文私鑰
* @param serverPublicKeyPEM 伺服器 RSA 公鑰 (PEM 格式)
* @returns Base64 編碼的加密數據
*/
export async function encryptWithServerPublicKey(
plaintext: string,
serverPublicKeyPEM: string
): Promise<string> {
try {
// 1. 導入伺服器公鑰
const publicKey = await importRSAPublicKey(serverPublicKeyPEM)
export class CryptoService {
private static publicKey: CryptoKey | null = null
private static publicKeyPEM: string | null = null
// 2. 生成隨機 AES 密鑰 (256-bit)
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 },
{
name: 'AES-GCM',
length: 256,
},
true,
['encrypt']
)
// 3. 使用 AES-GCM 加密數據
const iv = crypto.getRandomValues(new Uint8Array(12)) // 96-bit nonce
const encodedText = new TextEncoder().encode(plaintext)
const encryptedData = await crypto.subtle.encrypt(
{ name: 'AES-GCM', iv },
// 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,
encodedText
plaintextBytes
)
// 4. 出 AES 密鑰並用 RSA 加密
const exportedAESKey = await crypto.subtle.exportKey('raw', aesKey)
const encryptedAESKey = await crypto.subtle.encrypt(
{ name: 'RSA-OAEP' },
publicKey,
exportedAESKey
// 5. 出 AES 密
const rawAesKey = await crypto.subtle.exportKey('raw', aesKey)
// 6. 使用 RSA-OAEP 加密 AES 密钥
const wrappedKey = await crypto.subtle.encrypt(
{
name: 'RSA-OAEP',
},
this.publicKey,
rawAesKey
)
// 5. 組合: [加密的 AES 密鑰長度(4字節)] + [加密的 AES 密鑰] + [IV] + [加密數據]
const result = new Uint8Array(
4 + encryptedAESKey.byteLength + iv.length + encryptedData.byteLength
)
const view = new DataView(result.buffer)
view.setUint32(0, encryptedAESKey.byteLength, false) // 大端序
result.set(new Uint8Array(encryptedAESKey), 4)
result.set(iv, 4 + encryptedAESKey.byteLength)
result.set(
new Uint8Array(encryptedData),
4 + encryptedAESKey.byteLength + iv.length
)
// 6. Base64 編碼
return arrayBufferToBase64(result)
} catch (error) {
console.error('加密失敗:', error)
throw new Error('加密過程中發生錯誤,請檢查伺服器公鑰是否有效')
}
}
/**
* 導入 PEM 格式的 RSA 公鑰
*/
async function importRSAPublicKey(pem: string): Promise<CryptoKey> {
// 移除 PEM header/footer 和換行符
const pemContents = pem
.replace(/-----BEGIN PUBLIC KEY-----/, '')
.replace(/-----END PUBLIC KEY-----/, '')
.replace(/\s/g, '')
// Base64 解碼
const binaryDer = base64ToArrayBuffer(pemContents)
// 導入為 CryptoKey
return crypto.subtle.importKey(
'spki',
binaryDer,
{
name: 'RSA-OAEP',
hash: 'SHA-256',
},
true,
['encrypt']
)
}
// ==================== 二階段輸入 UI ====================
export interface TwoStageInputResult {
encryptedKey: string
obfuscationLog: string[] // 混淆記錄(用於審計)
}
/**
* 二階段私鑰輸入流程
* @param serverPublicKey 伺服器公鑰
* @returns 加密後的私鑰 + 混淆記錄
*/
export async function twoStagePrivateKeyInput(
serverPublicKey: string
): Promise<TwoStageInputResult> {
const obfuscationLog: string[] = []
return new Promise((resolve, reject) => {
// 創建自定義 Modal
const modal = createTwoStageModal(async (part1: string, part2: string) => {
try {
const fullKey = part1 + part2
// 驗證私鑰格式
if (!validatePrivateKeyFormat(fullKey)) {
throw new Error('私鑰格式不正確(應為 64 位十六進制或 0x 開頭)')
}
// 加密
const encrypted = await encryptWithServerPublicKey(
fullKey,
serverPublicKey
)
// 清除敏感數據
part1 = ''
part2 = ''
resolve({ encryptedKey: encrypted, obfuscationLog })
} catch (error) {
reject(error)
}
}, obfuscationLog)
document.body.appendChild(modal)
})
}
/**
* 創建二階段輸入 Modal
*/
function createTwoStageModal(
onSubmit: (part1: string, part2: string) => void,
obfuscationLog: string[]
): HTMLElement {
const modal = document.createElement('div')
modal.style.cssText = `
position: fixed; top: 0; left: 0; right: 0; bottom: 0;
background: rgba(0,0,0,0.8); z-index: 10000;
display: flex; align-items: center; justify-content: center;
`
const content = document.createElement('div')
content.style.cssText = `
background: #1a1a2e; padding: 2rem; border-radius: 8px;
max-width: 500px; width: 90%; color: white;
`
let stage = 1
let part1 = ''
const render = () => {
if (stage === 1) {
content.innerHTML = `
<h2 style="margin-bottom: 1rem;">🔐 安全輸入 - 第一階段</h2>
<p style="margin-bottom: 1rem; color: #888;">請輸入私鑰的<strong>前 32 位</strong>字符</p>
<input
id="stage1-input"
type="password"
placeholder="0x1234..."
style="width: 100%; padding: 0.75rem; border-radius: 4px;
background: #0f0f1e; border: 1px solid #333; color: white;
font-family: monospace; font-size: 14px;"
maxlength="34"
/>
<button
id="stage1-next"
style="margin-top: 1rem; width: 100%; padding: 0.75rem;
background: #4CAF50; border: none; border-radius: 4px;
color: white; font-weight: bold; cursor: pointer;"
>下一步 →</button>
<button
id="cancel"
style="margin-top: 0.5rem; width: 100%; padding: 0.5rem;
background: transparent; border: 1px solid #555; border-radius: 4px;
color: #888; cursor: pointer;"
>取消</button>
`
const input = content.querySelector('#stage1-input') as HTMLInputElement
const nextBtn = content.querySelector('#stage1-next') as HTMLButtonElement
const cancelBtn = content.querySelector('#cancel') as HTMLButtonElement
input.focus()
input.addEventListener('input', () => {
nextBtn.disabled = input.value.length < 10
})
nextBtn.addEventListener('click', async () => {
part1 = input.value
input.value = '' // 立即清除
// 生成混淆字串並強制複製
const obfuscation = generateObfuscation()
await navigator.clipboard.writeText(obfuscation)
obfuscationLog.push(`Stage1: ${new Date().toISOString()}`)
alert(
'⚠️ 已複製混淆字串到剪貼簿\n\n請在任意地方貼上一次避免監控然後點擊確定繼續'
)
stage = 2
render()
})
cancelBtn.addEventListener('click', () => {
modal.remove()
})
} else if (stage === 2) {
content.innerHTML = `
<h2 style="margin-bottom: 1rem;">🔐 安全輸入 - 第二階段</h2>
<p style="margin-bottom: 1rem; color: #888;">請輸入私鑰的<strong>剩餘字符</strong></p>
<input
id="stage2-input"
type="password"
placeholder="...5678"
style="width: 100%; padding: 0.75rem; border-radius: 4px;
background: #0f0f1e; border: 1px solid #333; color: white;
font-family: monospace; font-size: 14px;"
maxlength="34"
/>
<button
id="stage2-submit"
style="margin-top: 1rem; width: 100%; padding: 0.75rem;
background: #2196F3; border: none; border-radius: 4px;
color: white; font-weight: bold; cursor: pointer;"
>🔒 加密並提交</button>
<button
id="back"
style="margin-top: 0.5rem; width: 100%; padding: 0.5rem;
background: transparent; border: 1px solid #555; border-radius: 4px;
color: #888; cursor: pointer;"
>← 返回上一步</button>
`
const input = content.querySelector('#stage2-input') as HTMLInputElement
const submitBtn = content.querySelector(
'#stage2-submit'
) as HTMLButtonElement
const backBtn = content.querySelector('#back') as HTMLButtonElement
input.focus()
submitBtn.addEventListener('click', async () => {
const part2 = input.value
input.value = '' // 立即清除
obfuscationLog.push(`Stage2: ${new Date().toISOString()}`)
modal.remove()
onSubmit(part1, part2)
})
backBtn.addEventListener('click', () => {
stage = 1
render()
})
// 7. 编码为 base64url
return {
wrappedKey: this.arrayBufferToBase64Url(wrappedKey),
iv: this.arrayBufferToBase64Url(iv.buffer),
ciphertext: this.arrayBufferToBase64Url(ciphertext),
aad: this.arrayBufferToBase64Url(aadBytes.buffer),
ts: ts,
}
}
render()
modal.appendChild(content)
return modal
}
/**
* 驗證私鑰格式
*/
function validatePrivateKeyFormat(key: string): boolean {
// EVM 私鑰: 64 位十六進制 (可選 0x 前綴)
const evmPattern = /^(0x)?[0-9a-fA-F]{64}$/
return evmPattern.test(key)
}
// ==================== 工具函數 ====================
function arrayBufferToBase64(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])
private static arrayBufferToBase64Url(buffer: ArrayBuffer): string {
const bytes = new Uint8Array(buffer)
let binary = ''
for (let i = 0; i < bytes.length; i++) {
binary += String.fromCharCode(bytes[i])
}
return btoa(binary)
.replace(/\+/g, '-')
.replace(/\//g, '_')
.replace(/=/g, '')
}
static async fetchPublicKey(): Promise<string> {
const response = await fetch('/api/crypto/public-key')
if (!response.ok) {
throw new Error(`Failed to fetch public key: ${response.statusText}`)
}
const data = await response.json()
return data.public_key
}
static async decryptSensitiveData(
payload: EncryptedPayload
): Promise<string> {
const response = await fetch('/api/crypto/decrypt', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify(payload),
})
if (!response.ok) {
throw new Error(`Decryption failed: ${response.statusText}`)
}
const result = await response.json()
return result.plaintext
}
return btoa(binary)
}
function base64ToArrayBuffer(base64: string): ArrayBuffer {
const binary = atob(base64)
const bytes = new Uint8Array(binary.length)
for (let i = 0; i < binary.length; i++) {
bytes[i] = binary.charCodeAt(i)
}
return bytes.buffer
// 生成混淆字符串(用于剪贴板混淆)
export function generateObfuscation(): string {
const bytes = new Uint8Array(32)
crypto.getRandomValues(bytes)
return Array.from(bytes, (byte) => byte.toString(16).padStart(2, '0')).join(
''
)
}
/**
* 從伺服器獲取公鑰
*/
export async function fetchServerPublicKey(): Promise<string> {
const response = await fetch('/api/crypto/public-key')
if (!response.ok) {
throw new Error('無法獲取伺服器公鑰')
// 验证私钥格式
export function validatePrivateKeyFormat(
value: string,
expectedLength: number = 64
): boolean {
const normalized = value.startsWith('0x') ? value.slice(2) : value
if (normalized.length !== expectedLength) {
return false
}
const data = await response.json()
return data.public_key
return /^[0-9a-fA-F]+$/.test(normalized)
}