mirror of
https://github.com/NoFxAiOS/nofx.git
synced 2026-07-09 22:10:57 +08:00
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:
@@ -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
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user