/** * arcrun platform_crypto Worker * * POST / → JSON input {action, ...} → JSON output * * Actions: * generate_api_key — HMAC-SHA256(email, ENCRYPTION_KEY) → ak_{hex[:32]} * encrypt — AES-GCM(plaintext, ENCRYPTION_KEY) → {encrypted, iv}(base64) * random_token — crypto random bytes → hex string * * 安全邊界:ENCRYPTION_KEY 只存在於 closure,永不進入外部(rule 02 §2.2)。 * 此 Worker 直接用 crypto.subtle 實作,不走 WASM runner。 * TinyGo WASM async host function 在 Cloudflare Workers 的 u6u namespace 不支援 Promise. * WASM 零件 (registry/components/platform_crypto/) 保留作為 edge-Go 移植時的參考。 */ import { Hono } from 'hono'; import { cors } from 'hono/cors'; type Env = { ENCRYPTION_KEY: string; }; type Input = { action: string; email?: string; plaintext?: string; bytes?: number; }; const app = new Hono<{ Bindings: Env }>(); app.use('*', cors()); app.get('/', (c) => c.json({ ok: true, component: 'platform_crypto' })); app.post('/', async (c) => { let input: Input; try { input = await c.req.json() as Input; } catch { return c.json({ success: false, error: 'request body must be JSON' }, 400); } const encryptionKey = c.env.ENCRYPTION_KEY; if (!encryptionKey) { return c.json({ success: false, error: 'ENCRYPTION_KEY not configured' }, 503); } try { switch (input.action) { case 'generate_api_key': { if (!input.email) return c.json({ success: false, error: 'email 必填' }, 400); const apiKey = await generateApiKey(input.email, encryptionKey); return c.json({ success: true, api_key: apiKey }); } case 'encrypt': { if (!input.plaintext) return c.json({ success: false, error: 'plaintext 必填' }, 400); const { encrypted, iv } = await aesEncrypt(input.plaintext, encryptionKey); return c.json({ success: true, encrypted, iv }); } case 'random_token': { const numBytes = (input.bytes ?? 32) > 0 ? (input.bytes ?? 32) : 32; const token = randomHex(numBytes); return c.json({ success: true, token }); } default: return c.json({ success: false, error: `不支援的 action: ${input.action}` }, 400); } } catch (e) { return c.json( { success: false, error: e instanceof Error ? e.message : String(e) }, 500, ); } }); export default app; // ── Crypto implementations (rule 02 §2.2: crypto.subtle 只准在 wasi-shim.ts 或 platform_crypto) ── async function generateApiKey(email: string, encryptionKey: string): Promise { const keyBytes = new TextEncoder().encode(encryptionKey.slice(0, 32)); const cryptoKey = await crypto.subtle.importKey( 'raw', keyBytes, { name: 'HMAC', hash: 'SHA-256' }, false, ['sign'], ); const sig = await crypto.subtle.sign('HMAC', cryptoKey, new TextEncoder().encode(email)); const hex = Array.from(new Uint8Array(sig)).map(b => b.toString(16).padStart(2, '0')).join(''); return 'ak_' + hex.slice(0, 32); } async function aesEncrypt(plaintext: string, encryptionKey: string): Promise<{ encrypted: string; iv: string }> { const keyBytes = new TextEncoder().encode(encryptionKey.slice(0, 32)); const cryptoKey = await crypto.subtle.importKey('raw', keyBytes, { name: 'AES-GCM' }, false, ['encrypt']); const iv = crypto.getRandomValues(new Uint8Array(12)); const enc = await crypto.subtle.encrypt( { name: 'AES-GCM', iv }, cryptoKey, new TextEncoder().encode(plaintext), ); const toB64 = (buf: ArrayBuffer | Uint8Array) => btoa(String.fromCharCode(...new Uint8Array(buf instanceof ArrayBuffer ? buf : buf))); return { encrypted: toB64(enc), iv: toB64(iv) }; } function randomHex(numBytes: number): string { const arr = crypto.getRandomValues(new Uint8Array(numBytes)); return Array.from(arr).map(b => b.toString(16).padStart(2, '0')).join(''); }