先記結論:Math.random() 是普通偽隨機數,MDN 繁中說明寫明它不提供密碼學上安全的亂數,不要用它處理與安全性有關的事情。登入口令、一次性權杖和金鑰必須改用 Web Crypto 的 crypto.getRandomValues()。看起來夠亂、斷線還能點、網址列有鎖,這三件事各自證明別的邊界,都替代不了「產生函式讀的是哪一個 API」。
一句話先回答搜尋意圖
MDN 對 Math.random() 的邊界寫得很硬:它回傳 [0, 1) 上大致均勻的浮點偽隨機數,不提供密碼學安全的亂數;安全相關用途應改用 Crypto.getRandomValues()。密碼「看起來夠亂」只描述輸出的字元形態,不描述產生器的內部狀態能不能被還原。
這篇文章給要評審「線上密碼產生器」的開發者和維運,以及被同事丟過來一個產生頁、想自己看一眼再決定用不用的人。它不代替 密碼產生器 的操作說明,也不重複「明文有沒有離開分頁」那一套 Network 總論。你要帶走的是:普通偽隨機和 CSPRNG 差在哪一層、取模為什麼會讓好介面變偏,以及從點擊「產生密碼」往下讀原始碼的具體步驟。
看起來夠亂,只能說明字元可能混雜
人眼判斷「亂」非常便宜,也非常不準。一串 aB3$kP9x 和一串 Tr0ub4dor&3 看起來都像密碼;前者可能來自夠大的字元集,後者可能只是一句口令做了常見替換。觀感回答的是「像不像密碼」,不是「下一個位元能不能被算出來」。
即便字元集真的包含大小寫、數字和符號,只要下標來自可預測的序列,攻擊者就不需要窮舉整個空間。他可以重現同一套產生器,或在觀察到若干輸出後還原內部狀態,再枚舉「還沒被用掉的那些候選」。對登入口令來說,不可預測比「看起來有特殊符號」更先要滿足。
長度仍然重要。同一套安全亂數來源,6 字元和 16 字元的窮舉成本差幾個數量級;低於 8 字元時,即使用對了 CSPRNG,空間也偏小。FastPwd 亂數模式允許 6–128 字元、預設 16 字元,低於 8 字元會提示安全性較低——那是長度警告,不是「亂數演算法變弱了」。兩件事要分開看:演算法解決不可預測,長度解決搜尋空間。
Math.random 是哪一類亂數
ECMAScript 把 Math.random() 寫成:回傳一個正號、大於等於 0 且小於 1 的 Number,用實作自行定義的演算法或策略近似均勻地抽取。規範沒有要求它抗預測,也沒有要求種子來自作業系統的熵池。引擎可以選統計性質很好、但狀態可逆的偽隨機數產生器(PRNG)。
Chrome 所用的 V8 引擎在 2015 年底之前用 MWC1616。V8 自己的說明寫過:該演算法內部狀態只有 64 位元,品質偏差,在 TestU01 套件裡會失敗。從 V8 4.9.41(隨 Chrome 49 發布)起,改成 xorshift128+:128 位元狀態,週期 2^128 - 1,並通過 TestU01。Firefox 與 Safari 後來也切到同一類演算法。細節見 V8 對 Math.random 的說明。
同一篇文章把邊界寫死了:xorshift128+ 比舊實作好很多,仍然不是密碼學安全的。雜湊、簽章、加解密這類用途,普通 PRNG 不適用;應使用 Web Cryptography API 的 window.crypto.getRandomValues。統計上「夠亂」和「看見若干輸出之後無法反推後續輸出」,是兩套標準。口令需要的是後者。
公開的還原實作也把這層差距寫得很具體:xorshift128+ 是線性的,蒐集到足夠連續輸出後,可用約束求解器還原 128 位元狀態,再算出還沒被吐出的下一批數字。這不是理論題。因此,一個產生器如果只呼叫 Math.random() 再對應到字元表,它可能通過「目測很亂」和簡單的頻率檢查,卻過不了「假設有人讀到原始碼或截獲若干輸出」這一關。評審時不要把「用了 Math.random,但字元集很寬」寫成已經合格。
getRandomValues 具體換了哪一層
crypto.getRandomValues(typedArray) 會用密碼學意義上足夠強的亂數填滿你傳入的整型陣列,例如 Uint8Array、Uint32Array。MDN 與 Web Cryptography Level 2 都寫明:為了效能,實作通常不是「真隨機硬體每次直接吐位元」,而是把作業系統熵源(如 Unix 的 /dev/urandom、Windows 的系統亂數介面)種進適合密碼學用途的 PRNG。演算法可隨瀏覽器變化,但設計目標是:不能從已公開的輸出廉價還原內部狀態。
它和 Math.random 不在同一份語言規範裡。後者屬於 ECMA-262;前者屬於 Web Cryptography。一次請求超過 65536 位元組會拋 QuotaExceededError,產生一條 16 字元密碼遠遠到不了這個上限。另外,getRandomValues 是 Crypto 介面裡少數能在非安全內容呼叫的方法;金鑰衍生和 subtle.encrypt 仍要求 HTTPS 安全內容。正式環境的產生頁本來就應該是 HTTPS,不要用「HTTP 裡也能呼叫 getRandomValues」當成可以關掉 TLS 的理由。
還要把「本機抽亂數」和「伺服器代抽再傳回來」分開。後者即使伺服器用了高品質 CSPRNG,口令也已經以明文或可逆形式經過對方的記憶體、日誌和這次 HTTP。網址列小鎖只說明瀏覽器到 TLS 終點之間的傳輸被加密,不說明終點看不到正文。若頁面聲稱「本地產生」,產生這一下來就不該出現把口令 POST 出去再回顯的介面。
| 你看見的現象 | 它能支持的結論 | 它支持不了的結論 |
|---|---|---|
| 輸出看起來很亂 | 字元種類可能夠多 | 產生過程不可預測 |
原始碼裡有 Math.random |
頁面用了普通偽隨機 | 一定拿它來產生口令 |
原始碼裡有 getRandomValues |
頁面引入了 CSPRNG 介面 | 你點的這個按鈕一定走了它 |
| 斷線後仍能產生 | 不依賴即時代抽介面 | 用的不是 Math.random |
| Network 搜不到口令 | 這次沒當業務資料上傳 | 亂數演算法合格 |
取模偏差:CSPRNG 用錯了也會偏
常見的第二處失誤發生在「已經呼叫了 getRandomValues」之後。指令碼拿到一個位元組 0–255,再寫 charset[byte % charset.length]。若字元集長度不能整除 256,靠前的那些字元會多佔一截餘數,出現頻率略高。對 16 字元口令,偏差通常很小,但它說明實作沒有把「均勻」做到位;字元集更怪、或用 8 位元去對應很大的詞表時,偏差會更明顯。
拒絕取樣的做法是:先算出不超過 256(或 2^32)且能被字元集大小整除的上限,抽出的整數大於等於該上限就丟掉重抽,再用取模。FastPwd 的亂數字元和好記口令都走 crypto.getRandomValues,用 Uint32Array(1) 抽 32 位元整數,上限寫成 Math.floor(0x100000000 / max) * max,超限則迴圈再抽。缺少 getRandomValues 時直接拒絕產生,而不是退回 Math.random。你在別的網站評審時,也應沿著「抽整數 → 是否丟棄尾部 → 再對應字元」往下讀,而不是搜到函式名就停。
不要用真實登入口令、正式環境 API Key 或未遮蔽工單做實驗。產生一串可丟棄的測試口令,用完即棄。核對的是產生路徑和流量,不是把新口令再暴露一遍。
當場核對:原始碼、主控台、Network
口號不能當證據。準備一枚不會出現在真實業務裡的標記,例如把產生長度調到 16,產生後把整串當作金絲雀。下面三步可以在目前分頁做完,不需要帳號。
- 開啟開發者工具的 Sources,或對頁面使用「檢視網頁原始碼」。分別搜尋
Math.random和getRandomValues。記下命中檔案和函式名,先不要下結論。 - 從「產生密碼」按鈕的點擊處理函式往下讀,直到字元被拼進結果字串。這一路徑上應出現
crypto.getRandomValues;若只有Math.random()、Date.now()或固定種子,這一次產生不能當成 CSPRNG。 - 切到 Network,勾選 Preserve log,過濾先用 All。再點一次產生,把剛得到的整串貼進搜尋框。請求列、查詢字串、請求體和分析上報裡都不該出現這串口令。
主控台只證明介面在不在
在 Console 裡執行 typeof crypto.getRandomValues === "function",得到 true 只說明目前文件的瀏覽器用得了這個介面。它證明不了頁面指令碼呼叫了它。有人會在主控台自己跑一行 getRandomValues,然後把頁面產生的口令也當成「已經核過」——那是兩段不相干的程式。
斷線複測只證明沒有代抽
先讓頁面完整載入,再在 Network 裡開啟 Offline,或關掉系統網路,然後產生。若結果仍然出現,說明這一次不依賴即時介面。這是「本地產生」的強訊號,但 Math.random 同樣不需要網路。所以斷線要通過,原始碼路徑和金絲雀搜尋仍要做。
打包後的指令碼可能把名字壓成單字母。搜不到 getRandomValues 時,再搜 crypto 和 Uint32Array,並對照點擊「產生密碼」之後有沒有新的業務 POST。壓縮不是免檢聲明,只是搜尋要多繞一步。
三種把「亂數」讀過頭的情況
第一,把網址列小鎖當成產生演算法合格。HTTPS / TLS 保護的是瀏覽器到目前 TLS 終點的傳輸,常見區分是「傳輸加密 ≠ 端到端、終點看不到正文」。伺服器用 HTTPS 接收你 POST 上去的口令,小鎖仍然是亮的。小鎖不回答 Math.random 還是 getRandomValues。
第二,把「原始碼裡出現過 getRandomValues」當成按鈕已走 CSPRNG。同一份打包檔案可能給檔案加密、閱後即焚或示範用了 Web Crypto,密碼產生卻仍走 Math.random。反向同樣成立:廣告或動畫裡的 Math.random 不能用來否定口令路徑。必須以點擊產生的呼叫堆疊為準。
第三,把「沒上傳」當成「不可預測」。Network 乾淨只說明這次觀察裡,口令沒有作為 HTTP 業務欄位離開分頁。擴充功能仍可能讀 DOM,作業系統剪貼簿歷史仍可能留下你複製的那一串。那是另一篇文章的邊界;本文只要求你先分清:亂數品質、是否上傳、複製之後還在不在,是三件不同的事。
常見問題
密碼看起來很亂,是不是就可以當登入口令?
不能只憑觀感。看起來混雜只說明字元種類可能夠多,證明不了產生過程不可預測。MDN 寫明 Math.random 不能用於安全場景。還要看長度是否足夠、有沒有在多個站重複使用,以及產生路徑是不是 crypto.getRandomValues。
斷線後還能產生,是不是亂數就安全?
只能證明這一次產生不依賴即時介面,伺服器沒有代抽亂數。Math.random 同樣能在斷線後執行。斷線要通過,原始碼裡仍要看到產生路徑走 getRandomValues,並且 Network 裡不應出現剛產生的口令。
原始碼裡同時出現 Math.random 和 getRandomValues,還能用嗎?
能不能用取決於「產生密碼」按鈕綁到哪一段。動畫、打亂展示順序可能用 Math.random;口令、權杖、金鑰必須走 getRandomValues。開啟 Sources,從點擊處理函式往下讀,不要只看全站搜尋的命中次數。
FastPwd 的密碼產生器用哪種亂數?會不會上傳?
亂數字元和好記口令都走 crypto.getRandomValues,並把 32 位元整數對應到字元集時做拒絕取樣,避免取模偏差。缺少該介面時會拒絕產生。長度 6–128 字元、預設 16 字元;低於 8 字元會提示安全性較低。產生結果預設不上傳。開啟即用,無需帳號。
把同一串測試口令用開啟即用的工具對照一次
若你希望用一個把亂數邊界寫清楚的頁面來練習,開啟 FastPwd 的 密碼產生器 即可,無需註冊。亂數模式先用預設 16 字元,產生一串可丟棄的口令。按第五節做完:Sources 裡沿「產生密碼」讀到 getRandomValues 和拒絕取樣迴圈;Network 裡用整串搜尋,請求列和 body 不應命中。
產生之後,若還要判斷這串像不像已被廣泛濫用的密碼,把另一串可丟棄的測試字元拿到 密碼檢測 裡對照公開高頻名單。待測密碼不上傳。未命中不能證明全網沒見過,命中只說明太常見;那是弱密碼名單的邊界,不是 CSPRNG 的邊界。不要把剛要啟用的真口令貼進任何線上檢測框。
下次開啟陌生產生頁之前記住三件事。第一,看起來夠亂證明不了不可預測。第二,Math.random 即使統計性質已經很好,仍不是密碼學安全亂數。第三,getRandomValues 出現在原始碼裡還不夠,必須讀到你點擊的那條路徑,並用金絲雀確認口令沒有作為業務資料出站。能重複的觀察,才值得寫進你們自己的評審清單。
延伸閱讀
「本地加密、不上傳」不能當證據,要用金絲雀搜 Network,見 瀏覽器本地加密怎麼核對明文沒有離開本機。本地弱密碼名單能抓住常見弱密碼、不能當成全網撞庫,見 本地弱密碼名單能證明什麼,為什麼它不是全網撞庫。若產生之後還要把口令一次性交給同事,井號後面為什麼預設不進 HTTP,見 URL 的井號後面為什麼適合放金鑰,以及什麼時候會失效。