那次登入讓我第一次盯著這六位數字:手機上的碼已經跳了新值,我這邊算的還是上一個,填進去自然被拒。差的幾十秒去了哪裡,頁面不說,也沒有地方填我慢了多少秒。
30 秒一格,換碼只看跨不跨格
口令是 floor(sec/30) 那一格的函式。sec=1700000000 落在 counter 56666666 出 921300,本週期只剩 10 秒:往後連點 30 拍只出現 2 個碼值,前 10 秒 921300、後 20 秒 732303。我在 TOTP 動態口令產生頁逐秒跑完這串偏移,翻轉只看 counter 跨沒跨格。
時間因子只有 Date.now() 加 setInterval(tpTick, 1000) 心跳,沒有任何校準入口:TP.clockMs 賦值點 0 個,也沒有填秒數或選時區的表單。顯示層有三套互不相等的口徑(條寬 toFixed(1)、文字 Math.floor、環按常量 100.53 折算)——週期起點 sec=1699999980 條寬 100.0%、環 dashoffset 0.00 都是滿格,只有文字寫「本週期已過 0%」,真實已過 100.0% 那一格永不顯示。
±1 期反查最多倒推 60 秒
反查只倒推方向:填進手機上那 6 位,它在各帳戶的前後期裡找,窗口上限 ±2 期。基準 sec=1700000000、3 個帳戶(含 1 個 8 位)的命中數如下。
| 反查窗口 | k=-2 | k=-1 | k=0 | k=+1 | k=+2 |
|---|---|---|---|---|---|
| 該期真實口令 | 713364 | 276857 | 921300 | 732303 | 136087 |
| 前後各 1 期 | 0 | 1 | 1 | 1 | 0 |
| 前後各 2 期 | 1 | 1 | 1 | 1 | 1 |
偏到 60 秒時「前後各 1 期」餵 713364 命中 0,報「全部 3 個帳戶在 ±1 期裡都沒有這個碼」,開到 2 期才報快了 60 秒;秒數按 offset×period 折算,60 秒週期的帳戶命中 ±1 期就報 60 秒。「多半是密鑰抄錯了」不能當結論——位數不符與解不開都被 continue 掉,skipped=1 把兩種跳過混著報。
向量這一關只過了 SHA-1 一檔
自測表寫死 16 項,tpSelfTest() 傳回 items=16 pass=16 fail=0,提示語是「16 項官方向量全部命中」:SHA-1 確實 6/6,T=59 → 94287082、T=20000000000 → 65353130,HOTP 10 條逐字相同。
表裡沒有 SHA-256 與 SHA-512 的一行,模板只有 tpSha1 與 tpHmacSha1,L319–L326 那一段談演算法,卻沒有可選控件。對 RFC 6238 附錄 B 就露底:SHA-256 六條 0/6(T=59 期望 46119246,本頁 97599872)、SHA-512 同樣 0/6(期望 90693936,本頁 14779409);寫著 algorithm=SHA256 的連結照樣按 SHA-1 建帳戶,只追加一句固定按 SHA-1 計算。強度距離見 SHA-256 與 MD5 那篇。
Base32 的寬容度:髒寫法同碼,尾名字符也同碼
髒寫法真能混過去:同一份 32 字元密鑰的全小寫、每 4 字元一空格、連字符按 8 分組、尾部補 8 個填充符(39、35、40 字元)四種寫法都解出同一個 20 位元組,T=1111111111 的 8 位口令都是 14050471;被當空白跳過的共 9 種。攔的只有字元集:0、1、8、9、底線、中文一律拋錯並帶位置,只認 A–Z 和 2–7。TP_B32(L390)是 ABCDEFGHIJKLMNOPQRSTUVWXYZ234567,I 是第 9 個、L 是第 12 個,被去掉的只有那四個數字,「去掉了 0O1IL」那句與實作相反。
名義字元數也不等於強度:n 字元解出 floor(n*5/8) 位元組,20 字元檔的 bits 按 n*5 報 100 位,實際只落回 12 位元組 = 96 位,比標籤寫的「20 字元(128 bit)」差 28 位;20、16 字元兩檔都會觸發告警,只有 32 字元(160 位)不觸發。也沒一律走 crypto.getRandomValues,L1151–L1156 有 Math.random() 降級分支。
尾名丟棄最隱蔽:20 字元檔只換尾名時 32 種取值塌成 2 種位元組串,「…GEZA」與「…GEZB」同為 12 位元組、同一個碼 228934,只有 n%8=0 的 8/16/24/32 字元才 32 種互不相同。中間字符不受影響:32 字元第 5 位 G→H 讓 921300 變 188215。
位數、週期、計數器:靜默兜底不等於攔下來
位數那一欄幾乎不設防:1 到 10 全收,非法值靜默兜成 6,6.9 讀成 6、8abc 讀成 8;同一個 counter 0 的 30 位整數 1284755224 逐檔掉下來。
- 1/4/6/8/10 位分別是 4 / 5224 / 755224 / 84755224 / 1284755224,只有 10 位不丟資訊。
- 週期走 parseInt:
1e9讀成 1 秒、86401封頂成 86400;digits=12 靜默變 6 且 warn 陣列為空,文案仍寫「按 6 位處理」。
計數器這邊反而不怕截斷:tpCounterBytes 除 256 取餘寫 8 位元組大端,9007199254740991 得 891307;越過 2^64 才靜默回繞成 counter 0 的 755224,而時間因子要漲到 270215977642229760 秒(約 85.6 億年)才與規範第一次分歧。
Q:手機和電腦差一期,這頁能改哪一邊?
都不能改:沒有填偏移秒數或選時區的入口,反查只倒推方向,命中下一期讀作本機時間大約慢了 30 秒。
Q:密鑰少抄了一個字符會一直錯嗎?
抄在中間一定錯,抄在尾名可能是另一份密鑰卻同出 228934。長度不合規另說:11 字元拋「末尾剩 3 個湊不出完整位元組」,13 字元照樣算出 78121305,12 字元 93818264 並警告末尾 4 位不是 0。
Q:一條 otpauth 連結能帶走幾個帳戶?
一次一個,TP.linkId 是純量,100 個帳戶就是手點 100 次複製,密鑰解不開的點下去還拋未捕獲異常。頁面不上傳:fetch 與 XMLHttpRequest 各 0 次,帳戶只落在 localStorage(鍵名 u2-totp-accounts),qrcode 字樣 0 次。
繞回那次被拒的登入:改時鐘得回到系統裡去,這頁要麼在 921300 還剩的 10 秒裡填完,要麼按下一期的 732303 填。文案那側的帳也就只能算清:62 條主張裡 [一致] 40 條、[部分衝突] 16 條、[衝突] 6 條,被量錯的正是最有用的那幾句。