922a57fe34
Self-hosted 開源:WASM 零件 + recipe + cypher-executor,跑在你自己的 Cloudflare。 此為重建的乾淨歷史起點(移除曾誤 commit 的 GCP SA 金鑰,舊歷史保留在 richblack/arcrun 與本地 backup 分支)。含: - acr init --self-hosted installer(建 KV/R2 + codeload 拉預編譯 wasm + wrangler deploy + seed recipe) - recipe push 把關(資料外流提醒 + 打通檢查) - 19 個正當零件預編譯 wasm(claude_api/km_writer/kbdb_upsert_block 排除:違反 DECISIONS §1) - CLI / cypher-executor / registry / 完整 SDD Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
109 lines
3.9 KiB
TypeScript
109 lines
3.9 KiB
TypeScript
/**
|
||
* 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<string> {
|
||
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('');
|
||
}
|