/** * 端到端加密模組 * 使用混合加密: 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(''); } /** * 使用伺服器公鑰加密私鑰 * @param plaintext 明文私鑰 * @param serverPublicKeyPEM 伺服器 RSA 公鑰 (PEM 格式) * @returns Base64 編碼的加密數據 */ export async function encryptWithServerPublicKey( plaintext: string, serverPublicKeyPEM: string ): Promise { try { // 1. 導入伺服器公鑰 const publicKey = await importRSAPublicKey(serverPublicKeyPEM); // 2. 生成隨機 AES 密鑰 (256-bit) const aesKey = await crypto.subtle.generateKey( { 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 }, aesKey, encodedText ); // 4. 導出 AES 密鑰並用 RSA 加密 const exportedAESKey = await crypto.subtle.exportKey('raw', aesKey); const encryptedAESKey = await crypto.subtle.encrypt( { name: 'RSA-OAEP' }, publicKey, exportedAESKey ); // 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 { // 移除 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 { 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 = `

🔐 安全輸入 - 第一階段

請輸入私鑰的前 32 位字符

`; 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 = `

🔐 安全輸入 - 第二階段

請輸入私鑰的剩餘字符

`; 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(); }); } }; 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]); } 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 async function fetchServerPublicKey(): Promise { const response = await fetch('/api/crypto/public-key'); if (!response.ok) { throw new Error('無法獲取伺服器公鑰'); } const data = await response.json(); return data.public_key; }