From e566495df990592c3b38f779b56b4ce0c22a497a Mon Sep 17 00:00:00 2001 From: uncle6me-web Date: Tue, 14 Jul 2026 20:43:41 +0800 Subject: [PATCH] =?UTF-8?q?bugfix=EF=BC=88#24=20#25=EF=BC=89=EF=BC=9APBKDF?= =?UTF-8?q?2=20600k=E2=86=92100k=EF=BC=88CF=20runtime=20=E4=B8=8A=E9=99=90?= =?UTF-8?q?=EF=BC=8C=E7=9C=9F=E9=9B=B2=20bootstrap=20500=EF=BC=89=EF=BC=8B?= =?UTF-8?q?http=5Frequest=20=E8=A3=9C=201042=20flag?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - portal-auth.ts PBKDF2_ITERATIONS 600_000→100_000:CF Workers 正式 runtime 的 PBKDF2 上限 100k,crypto.subtle.deriveBits 超過直接拒絕 → /portal/admin/bootstrap 真雲 500(2026-07-14 uncle6 T6-cloud 實撞,證據 arcrun-rag docs/manual/ uncle6-deploy-record.md);miniflare 無此限制=本機全綠假象。儲存格式自帶迭代數, 平台放寬可無痛升。 - 補測試「驗 600k 舊 hash 仍通」(verify 從儲存值解析 iterations;誠實邊界:真雲 deriveBits 同樣封頂,但真雲 bootstrap 從未成功→零存量 hash 無遷移面)。 - .component-builds/http_request/wrangler.toml 補 global_fetch_strictly_public (self-hosted 同帳號 fetch 1042 正解,Arcrun#33 同族;官方跨 zone 行為不變, 原理同 cypher 前例 rules/03 1042 段)。 - SDD:design D-5 修訂註+tasks Bugfix 記錄;wiki status 更新。 - 驗證:cypher tsc 0;169/170(唯一失敗 pre-existing);portal 三套 56/56; http_request wrangler --dry-run 過。不 merge 待總管審。 Co-Authored-By: Claude Fable 5 --- .component-builds/http_request/wrangler.toml | 6 ++++- cypher-executor/src/lib/portal-auth.ts | 14 ++++++++---- cypher-executor/tests/portal-auth.test.ts | 22 ++++++++++++++----- system-dev/docs/3-specs/portal-auth/design.md | 6 +++++ system-dev/docs/3-specs/portal-auth/tasks.md | 13 +++++++++++ system-dev/wiki/status.md | 20 ++++++++++++++++- 6 files changed, 70 insertions(+), 11 deletions(-) diff --git a/.component-builds/http_request/wrangler.toml b/.component-builds/http_request/wrangler.toml index acd9c86..24790bf 100644 --- a/.component-builds/http_request/wrangler.toml +++ b/.component-builds/http_request/wrangler.toml @@ -1,7 +1,11 @@ name = "arcrun-http-request" main = "src/index.ts" compatibility_date = "2025-02-19" -compatibility_flags = ["nodejs_compat"] +# global_fetch_strictly_public:self-hosted 同帳號部署時,http_request 零件外呼同 workers.dev +# zone 的目標會撞 CF same-zone 1042——此 flag 讓 fetch 走公網前門(Arcrun#33 同族; +# 正解同 cypher 前例,見 .claude/rules/03 的 1042 段)。官方部署本就跨 zone(arcrun.dev route), +# 加此 flag 行為不變 → 官方/self-hosted 共用同一份 toml。 +compatibility_flags = ["nodejs_compat", "global_fetch_strictly_public"] workers_dev = true [vars] diff --git a/cypher-executor/src/lib/portal-auth.ts b/cypher-executor/src/lib/portal-auth.ts index b5c00cb..d0c6371 100644 --- a/cypher-executor/src/lib/portal-auth.ts +++ b/cypher-executor/src/lib/portal-auth.ts @@ -6,8 +6,8 @@ * credential 原語(那些屬 WASM auth primitive,本檔不碰 crypto.subtle.decrypt / * RSASSA / template 展開)。只用 WebCrypto 原生 PBKDF2(crypto.subtle.deriveBits)。 * - * 規格(OWASP 現行建議值): - * - PBKDF2-SHA256、600,000 iterations、salt 16 bytes、輸出 256-bit + * 規格: + * - PBKDF2-SHA256、100,000 iterations(CF Workers runtime 上限,見下)、salt 16 bytes、輸出 256-bit * - 儲存格式 `pbkdf2-sha256$$$`(自帶演算法前綴, * 未來換 KDF 可共存漸進遷移——verify 按前綴解析,不寫死參數) * - 驗證用常數時間比對(同 mcp/src/oauth/crypto.ts PR#15 慣例) @@ -15,7 +15,13 @@ */ export const PBKDF2_ALGO_PREFIX = 'pbkdf2-sha256'; -export const PBKDF2_ITERATIONS = 600_000; +/** + * CF Workers **正式 runtime** 的 PBKDF2 上限是 100,000 iterations——超過時 + * `crypto.subtle.deriveBits` 直接拒絕(2026-07-14 uncle6 真雲實撞:/portal/admin/bootstrap 500; + * miniflare 無此限制 → 本機測試全綠的假象)。OWASP 建議 600k 但平台封頂只能 100k。 + * 儲存格式自帶迭代數(verify 從儲存值解析)→ 未來平台放寬可無痛升,舊 hash 照驗。 + */ +export const PBKDF2_ITERATIONS = 100_000; function b64encode(bytes: Uint8Array): string { let bin = ''; @@ -61,7 +67,7 @@ export function constantTimeEqual(a: string, b: string): boolean { return diff === 0; } -/** 雜湊一組密碼 → `pbkdf2-sha256$600000$$`。 */ +/** 雜湊一組密碼 → `pbkdf2-sha256$100000$$`。 */ export async function hashPassword(password: string, iterations: number = PBKDF2_ITERATIONS): Promise { const salt = crypto.getRandomValues(new Uint8Array(16)); const hash = await deriveBits(password, salt, iterations); diff --git a/cypher-executor/tests/portal-auth.test.ts b/cypher-executor/tests/portal-auth.test.ts index 227bd86..d4f26cb 100644 --- a/cypher-executor/tests/portal-auth.test.ts +++ b/cypher-executor/tests/portal-auth.test.ts @@ -2,13 +2,14 @@ * portal-auth P2 測試(design §2/§4/§5,Gitea #24/#25) * * 覆蓋(=tasks.md P2 測試項): - * 1. KDF:pbkdf2-sha256$600000$… 格式、驗證對錯、壞格式誠實 false + * 1. KDF:pbkdf2-sha256$100000$… 格式(CF Workers runtime 上限 100k,2026-07-14 真雲實撞)、 + * 驗證對錯、壞格式誠實 false、600k 舊 hash 相容(迭代數從儲存值解析) * 2. bootstrap 閘:無 console session → 401;建 admin 寫 {tenant}::portal 子 namespace; * 已有 admin → 409 * 3. 登入對錯:成功發 token(回應**無租戶字串**)、密碼錯 401、停用 403、未知 email 401 * 4. 節流:5 次失敗 → 429(KV TTL 計數) * 5. session:每請求回讀 record;停用即拒(既有 session 立即失效) - * 6. 改密碼:驗舊密;新 hash 以 600k 格式落 slot + * 6. 改密碼:驗舊密;新 hash 以 100k 格式落 slot * 7. role 閘:非 admin 打 admin 端點 → 403;admin 列表**剝除 password_hash** * * KBDB 打 fetchMock 假 host(wrangler.test.toml KBDB_BASE_URL=https://kbdb.test)+ @@ -101,17 +102,28 @@ async function seedPortalSession(token: string, recordId: string) { // ═══════════════ 1. KDF 單元 ═══════════════ describe('PBKDF2 模組(lib/portal-auth)', () => { - it('hashPassword 預設格式 = pbkdf2-sha256$600000$salt$hash,且驗證通過', async () => { + it('hashPassword 預設格式 = pbkdf2-sha256$100000$salt$hash(CF runtime 上限),且驗證通過', async () => { const h = await hashPassword('some-password-123'); const parts = h.split('$'); expect(parts.length).toBe(4); expect(parts[0]).toBe('pbkdf2-sha256'); expect(parts[1]).toBe(String(PBKDF2_ITERATIONS)); - expect(PBKDF2_ITERATIONS).toBe(600_000); + // CF Workers 正式 runtime PBKDF2 封頂 100k(600k 在真雲 deriveBits 直接拒絕 → bootstrap 500) + expect(PBKDF2_ITERATIONS).toBe(100_000); + expect(PBKDF2_ITERATIONS).toBeLessThanOrEqual(100_000); expect(await verifyPassword('some-password-123', h)).toBe(true); expect(await verifyPassword('wrong-password-x', h)).toBe(false); }); + it('600k 舊 hash 相容:verify 從儲存值解析迭代數,仍驗得動(miniflare 無平台上限可造)', async () => { + // 誠實註:此相容性在 miniflare / 未來平台放寬時成立;真 CF runtime 對 deriveBits 一律封頂 + // 100k,600k hash 在真雲仍會被拒——但真雲 bootstrap 本身就 500,雲端不存在 600k hash,無遷移面。 + const legacy = await hashPassword(PASSWORD, 600_000); + expect(legacy.startsWith('pbkdf2-sha256$600000$')).toBe(true); + expect(await verifyPassword(PASSWORD, legacy)).toBe(true); + expect(await verifyPassword('wrong-password-x', legacy)).toBe(false); + }); + it('壞格式 / 前綴不認得 / 被竄改 → false(誠實拒絕不拋錯)', async () => { expect(await verifyPassword('x', '')).toBe(false); expect(await verifyPassword('x', 'bcrypt$10$abc$def')).toBe(false); @@ -306,7 +318,7 @@ describe('POST /portal/me/password', () => { expect(res.status).toBe(401); }); - it('舊密碼對 → PATCH 新 hash(600k 格式、非明碼、與舊 hash 不同)', async () => { + it('舊密碼對 → PATCH 新 hash(現行 100k 格式、非明碼、與舊 hash 不同)', async () => { await seedPortalSession('tok-4', 'rec_1'); mockGetRecord('rec_1', activeUserValues()); let patched = ''; diff --git a/system-dev/docs/3-specs/portal-auth/design.md b/system-dev/docs/3-specs/portal-auth/design.md index d908b23..d9f738f 100644 --- a/system-dev/docs/3-specs/portal-auth/design.md +++ b/system-dev/docs/3-specs/portal-auth/design.md @@ -102,6 +102,12 @@ kbdb-graph-plugin 的 `/graph/neighbors/:name` 只吃節點名,**無 owner/庫 | argon2id | 記憶體硬化更強,但 Workers 無原生,要引 WASM 模組(新依賴+冷啟成本);Workers 128MB 記憶體下參數也開不大 | | bcrypt | 純 JS 實作燒 CPU(Workers CPU limit),無原生 | - 格式 `pbkdf2-sha256$600000$$` 存 `password_hash` slot(見 D-6);驗證用常數時間比對(同 PR#15 慣例)。 +- **【修訂 2026-07-14,T6-cloud 真雲實撞】迭代數 600k → 100k**:CF Workers **正式 runtime** 的 + PBKDF2 上限是 100,000(`crypto.subtle.deriveBits` 超過直接拒絕)→ `/portal/admin/bootstrap` + 真雲 500;miniflare 無此限制=本機全綠假象(證據:arcrun-rag `docs/manual/uncle6-deploy-record.md`)。 + OWASP 建議 600k 但平台封頂——儲存格式自帶迭代數(verify 從儲存值解析),未來平台放寬可無痛升。 + 誠實記:600k 舊 hash 的「漸進遷移」只在 miniflare/放寬後成立,真雲 deriveBits 同樣封頂; + 所幸真雲 bootstrap 從未成功,雲端不存在 600k hash,無實際遷移面。 - 順帶:**不動 console-auth 既有 3 輪 SHA-256**(Admin Console 現狀不動鐵律);portal 是新面、直接用對的。 ### 4.2 決策 D-6:雜湊放 slot(KBDB),不放 CF Secrets【建議=定案傾向強】 diff --git a/system-dev/docs/3-specs/portal-auth/tasks.md b/system-dev/docs/3-specs/portal-auth/tasks.md index c1a6d6d..8167f5b 100644 --- a/system-dev/docs/3-specs/portal-auth/tasks.md +++ b/system-dev/docs/3-specs/portal-auth/tasks.md @@ -130,6 +130,19 @@ --- +## Bugfix(P1-P4 交付後) + +- [x] **PBKDF2 600k → 100k(T6-cloud 部署抓到的框架蟲,2026-07-14,分支 `fix-pbkdf2-cf-limit`)**: + CF Workers 正式 runtime PBKDF2 上限 100,000 iterations,600k 時 `crypto.subtle.deriveBits` + 直接拒絕 → `/portal/admin/bootstrap` 真雲 500(uncle6 實撞,證據 arcrun-rag + `docs/manual/uncle6-deploy-record.md`);miniflare 無此限制=本機測試全綠假象。 + 修:`portal-auth.ts` `PBKDF2_ITERATIONS` 100_000+註明平台封頂;design D-5 已加修訂註。 + verify 相容性:迭代數從儲存值解析,補測試「600k 舊 hash 仍通」(miniflare 造);誠實邊界 + =真雲 deriveBits 同樣封頂、600k hash 真雲仍拒,但真雲 bootstrap 從未成功過 → 無存量 hash、 + 無遷移面。同 PR 順手:`.component-builds/http_request/wrangler.toml` 補 + `global_fetch_strictly_public` flag(self-hosted 同帳號 fetch 1042 正解,Arcrun#33 同族, + 官方行為不變原理同 cypher 前例 rules/03 1042 段)。 + ## 第二波(不在本 SDD 動工範圍,掛號) - MCP token 綁庫集合(design §9;PR#15 擴充,只動 `mcp/`) diff --git a/system-dev/wiki/status.md b/system-dev/wiki/status.md index 95f3d7b..78b4a42 100644 --- a/system-dev/wiki/status.md +++ b/system-dev/wiki/status.md @@ -15,7 +15,25 @@ metadata: ## 📍 當前位置 -> **2026-07-14 本 session(portal-auth P4:admin 帳號+庫管理頁——本 SDD 最後一棒,PR 待總管審不 merge)**: +> **2026-07-14 本 session(bugfix:PBKDF2 CF runtime 上限+http_request 1042 flag,分支 `fix-pbkdf2-cf-limit`,PR 待總管審不 merge)**: +> - **T6-cloud 部署抓到的框架蟲**:CF Workers **正式 runtime** PBKDF2 上限 100,000 iterations—— +> portal-auth 設 600k → `crypto.subtle.deriveBits` 真雲直接拒絕 → `/portal/admin/bootstrap` 500 +> (uncle6 實撞,證據 arcrun-rag `docs/manual/uncle6-deploy-record.md`)。**miniflare 無此限制= +> 本機測試全綠的假象**(新教訓:平台 crypto 限額 miniflare 不模擬,涉 WebCrypto 參數上限的驗收 +> 只有真雲驗得出)。 +> - **修(小刀)**:`portal-auth.ts` `PBKDF2_ITERATIONS` 600_000→**100_000**+註明平台封頂(儲存 +> 格式自帶迭代數,未來放寬可無痛升);design.md D-5 加修訂註+tasks.md 加 Bugfix 記錄。 +> verify 相容性確認:迭代數從儲存值解析,**補測試「驗 600k 舊 hash 仍通」**(miniflare 造)綠; +> 誠實邊界=真雲 deriveBits 同樣封頂、600k hash 真雲仍拒,但真雲 bootstrap 從未成功→雲端零存量 +> hash、無遷移面。 +> - **同 PR 順手(部署 agent 點名)**:`.component-builds/http_request/wrangler.toml` +> compatibility_flags 補 `global_fetch_strictly_public`(self-hosted 同帳號 fetch 1042 正解, +> Arcrun#33 同族;官方本就跨 zone 行為不變,原理同 cypher 前例 rules/03 1042 段)。 +> - **驗證**:cypher tsc 0;全套 169/170(唯一失敗=executor「不存在的零件」pre-existing); +> portal 三套 56/56 全綠(含新 600k 相容測試);http_request `wrangler deploy --dry-run` 打包過。 +> - 不 merge 待總管審(B 類流程);merge 後部署仍走 gated leo21c/uncle6 wrangler 直推。 +> +> **2026-07-14 上一輪(portal-auth P4:admin 帳號+庫管理頁——本 SDD 最後一棒,PR 待總管審不 merge)**: > - **實作(分支 `portal-auth-p4-admin-ui`,3+1 小步 commit)**:① `portal.ts` 補 **last-admin > 鎖死保護**(總管派工明定、design 未明寫)——PATCH 停用/降級「最後一個 active admin」→ 409 > (另一 admin 是 disabled 不算數;只在目標是 active admin 且會失格時才多打一次 list,平時零成本)。