leo 2026-07-20 明令:「已經改用 cf 自己的 secrets,不要再說它了」 「我希望以後再也看不到這個詞再出現」 背景:credential 早已遷移至 CF Workers per-script Secrets + D1 目錄, 舊的自管金鑰(client 端 AES-GCM + KV 密文 + crypto_decrypt)是遷移期遺留。 本次連根移除,含一併作廢的死 SaaS 碼。 移除: - 舊 KV 密文解密路徑(credential-injector.ts 整檔、dual-read fallback) 前置驗證:leo21c / youlin 兩帳號 CREDENTIALS_KV 實測 *:cred:* 皆 0 筆 - migrate-to-workers-secrets 搬家端點(回填已完成,無可回填) - /register 路由與 generateApiKey(HMAC 產 ak_ key 是 SaaS 遺物; self-hosted 走 namespace 明碼 D21,已無人使用) - platform_crypto component(三帳號實測 404 已退役,無 workflow 引用) 保留(附理由): - crypto_decrypt 保留為永遠回失敗的 stub——現役三個 auth .wasm 仍宣告該 import,缺項會讓 WASM instantiate 直接失敗。待零件重編後可真正刪除。 順帶修復(原不在範圍,但會實際壞事): - /auth/callback 有 `if (!key) redirect(server_error)` 閘,未設該 secret 的 實例會登入直接失敗 → 已移除 - OAuth 兩處把 provider token 寫進舊加密 KV(租戶鍵與實際 api_key 在 rotate 後必然分歧,已失效)→ 改導向 Workers Secrets,包 try/catch 不影響登入 - acr init Standard 模式呼叫已刪除的 /register → 改引導 OAuth 取 key - .claude/rules 與 system-dev/docs 是同一規範的兩份鏡像,先前只改 rules 導致鏡像仍在教舊做法 → 已同步(此類雙檔同步應納入檢查) 新用戶安裝從此零 secret 前置。 測試 187/188(唯一 fail 為 pre-existing,stash 驗證與本次無關); cypher-executor 與 cli typecheck 全綠。 Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
4.9 KiB
2026-05-29 credential 解密失敗(兩個 Worker 的 ENCRYPTION_KEY 漂移)
⚠️ 歷史記錄(2026-07-20 起本文所述機制已完全移除,本文僅供考古,勿依此操作)。 現行 credential 做法見
.claude/rules/01-tech-stack.md「Credential 儲存規範」。
症狀:
acr recipe test kbdb(credential 注入)回 HTTP 500,auth_static_key回credential kbdb_api_key 解密失敗根因(主):arcrun-auth-static-keyWorker 的ENCRYPTION_KEYsecret 跟正本(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 步驟 2(credential 注入鏈路)阻擋;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_keycredential 存在 KV(kv_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 上會解成垃圾 bytes,AES-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 為空)。
附帶 bug:Body has already been used
修對 key 後,/execute 端到端從 500 變成「Node n1 failed: Body has already been used」。
component-loader.ts 的 makeRecipeRunner / makeAuthRecipeRunner:
const data = await res.json().catch(() => res.text()); // ✗ res.json() 失敗時 body 已消費
KBDB /health 回非 JSON(純文字)→ res.json() throw → .catch(() => res.text()) 第二次讀 body → throw。
修法 — 讀一次:
async function readBodyOnce(res: Response): Promise<unknown> {
const text = await res.text();
try { return JSON.parse(text); } catch { return text; }
}
修法步驟
cd .component-builds/auth_static_key && wrangler secret put ENCRYPTION_KEY,貼正本 64-hex(=~/.arcrun/config.yaml的encryption_key)。richblack 手動(rule 05:runtime secret 不進 CI、CC 不碰)。component-loader.ts加readBodyOnce(),兩處res.json().catch(...)換掉。tsc --noEmit綠,deploy cypher-executor。- 修正源頭文件
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。