Files
Arcrun/docs/incidents/2026-05-29-encryption-key-drift.md
T
uncle6me-web 922a57fe34 arcrun — AI workflow execution engine (clean history)
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>
2026-06-03 15:52:38 +08:00

88 lines
4.7 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 2026-05-29 credential 解密失敗(兩個 Worker 的 ENCRYPTION_KEY 漂移)
> **症狀**`acr recipe test kbdb`credential 注入)回 HTTP 500`auth_static_key` 回 `credential kbdb_api_key 解密失敗`
> **根因(主)**`arcrun-auth-static-key` Worker 的 `ENCRYPTION_KEY` secret 跟正本(cypher-executor / CLI 用的那把)值不同、格式也不同(44-char base64 vs 64-char hex)。AES-GCM 用錯 key 必然解密失敗。
> **根因(附)**`component-loader.ts` 用 `res.json().catch(() => res.text())` 讀 response body → body 被讀兩次 → `Body has already been used`。
> **修法**(1) `wrangler secret put ENCRYPTION_KEY` 把 auth-static-key 對齊正本 64-hex(2) 新增 `readBodyOnce()` 先取 text 再 parse JSON。
> **影響**BACKLOG 步驟 2credential 注入鏈路)阻擋;Phase 3 降級假零件成 recipe 的前置。
---
## 症狀
`acr recipe test kbdb` 端到端打不到 2xx。直接 probe `auth_static_key`
```
POST https://auth-static-key.arcrun.dev/ {action:"authenticate", api_key:"ak_…", service:"kbdb"}
→ {"success":false, "error":"credential kbdb_api_key 解密失敗", ...}
```
前置都綠(排除誤判方向):
- `auth_recipe:kbdb` 存在、`primitive=static_key`kv_get 命中 410 bytes
- `kbdb_api_key` credential 存在 KVkv_get 命中 108 bytes 的 `{encrypted, iv}`
- 失敗精準落在「解密」這一步
## 定位(key-fingerprint 診斷,只印 SHA-256 前綴,不印 key/明文)
`aesGcmDecrypt``wasi-shim.ts`)暫加:
```
console.error(`[decrypt] ENCRYPTION_KEY sha256_prefix=${fpHex} keyLen=${len}`)
```
deploy auth-static-key + `wrangler tail` 抓到:
| 來源 | keyLen | sha256 前綴 | 格式 |
|---|---|---|---|
| 加密端(CLI `~/.arcrun/config.yaml``encryption_key` | 64 | `fa84f2ce9027` | hex(→32 bytes)✓ |
| 解密端(`arcrun-auth-static-key``ENCRYPTION_KEY` secret | **44** | **`ff219b123c89`** | base64 ✗ |
**兩個 mismatch 同時存在**:值不同 + 格式不同。`hexToUint8Array` 套在 44-char base64 上會解成垃圾 bytesAES-GCM 必失敗。
漂移源頭:`arcrun/.env` 裡的 `ENCRYPTION_KEY` 就是那把錯的 base64`ff219b123c89`),有人拿它去 `wrangler secret put` 設進 auth-static-key。
## 為什麼正本是 64-hex
`/register`register.ts:42)把 `encryption_key: c.env.ENCRYPTION_KEY` 原樣回給用戶 —— 即 **cypher-executor 的** `ENCRYPTION_KEY`。用戶 config 是 64-hex`fa84f2ce9027`),所以正本 = cypher-executor 那把 64-hex。CLI 加密 credential 也用這把。auth-static-key 必須跟它一致才能解開。
診斷用完即移除(`wasi-shim.ts` 還原,git diff 為空)。
## 附帶 bugBody has already been used
修對 key 後,`/execute` 端到端從 500 變成「Node n1 failed: Body has already been used」。
`component-loader.ts``makeRecipeRunner` / `makeAuthRecipeRunner`
```ts
const data = await res.json().catch(() => res.text()); // ✗ res.json() 失敗時 body 已消費
```
KBDB `/health` 回非 JSON(純文字)→ `res.json()` throw → `.catch(() => res.text())` 第二次讀 body → throw。
修法 — 讀一次:
```ts
async function readBodyOnce(res: Response): Promise<unknown> {
const text = await res.text();
try { return JSON.parse(text); } catch { return text; }
}
```
## 修法步驟
1. `cd .component-builds/auth_static_key && wrangler secret put ENCRYPTION_KEY`,貼正本 64-hex= `~/.arcrun/config.yaml``encryption_key`)。**richblack 手動**rule 05runtime secret 不進 CI、CC 不碰)。
2. `component-loader.ts``readBodyOnce()`,兩處 `res.json().catch(...)` 換掉。`tsc --noEmit` 綠,deploy cypher-executor。
3. 修正源頭文件 `arcrun/.env``ENCRYPTION_KEY` 改成 64-hex(避免下次再設錯)。
## 驗證證據
- 直接 probe auth-static-key**HTTP 200**, `success:true`, 產出 `Authorization: Bearer …`
- 端到端 `/execute`**HTTP 200**, trace 乾淨
- auth 確證:直接 curl KBDB `/blocks` 不帶 token → `401 {"error":"Missing token"}`;經 cypher-executor(注入 token)→ 過 auth,進 KBDB handler 回 ZodError(缺 `content`)。**無 401 = token 被接受**。
## 教訓
- **同一把 key 出現在 ≥2 個 Worker 的 secret = 漂移風險**。auth-static-key / auth_service_account / cypher-executor 都讀 `ENCRYPTION_KEY`,靠人各設一次必漂。長期應有單一發放來源或部署時自動同步。
- **debug 加密問題,先比 key 指紋(SHA-256 前綴),不要碰 key 明文**。一個 fingerprint log 就分辨出「值錯」vs「格式錯」vs「資料壞」。
- **`res.json().catch(() => res.text())` 是反模式** —— body 只能讀一次。永遠先 `res.text()``JSON.parse`