# 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 步驟 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_key` credential 存在 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`：

```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 05：runtime 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`。
