先に結論です。Math.random() は普通の擬似乱数です。MDN の Math.random の説明は、暗号に使用可能な安全性を備えておらず、セキュリティに関連する目的では使わない、と書いています。ログイン用パスワード、ワンタイムトークン、鍵は、ウェブ暗号 API の crypto.getRandomValues() へ切り替えてください。見た目が乱れていること、オフラインでも作れること、アドレスバーに錠があること、はそれぞれ別の境界を示します。「生成関数が読んだ API はどれか」の答えにはなりません。
検索意図に一文で答える
Math.random() は [0, 1) の区間で、ほぼ均一に分布する擬似乱数の浮動小数点数を返します。MDN の言い方は硬いです。その値は暗号学的に安全な乱数ではないので、セキュリティに関わる処理には使わず、Crypto.getRandomValues() を使います。「見た目が十分乱れている」は、出力の文字の混ざり方を述べているだけです。ソースを読んだ人、あるいは出力を数本見た人が、内部状態を戻して次の候補を列挙できるかどうかには、何も答えていません。
この記事は、「オンラインのパスワード生成」をレビューする開発者や運用と、同僚から生成ページを渡されて、使う前に自分の目で見たい人向けです。パスワード生成の操作説明の代わりにはならず、「平文がこのタブを出たか」の Network 総論も繰り返しません。持ち帰るべきなのは、普通の擬似乱数と暗号論的擬似乱数生成器(CSPRNG)が違う層、剰余を取ると良い API でも偏ること、そして「作る」のクリックからソースを下へ読む具体的な手順です。
見た目が乱れていることは、文字が混ざっているかもしれない、まで
人の目で「乱れている」と判断するのは安く、精度は低いです。aB3$kP9x も Tr0ub4dor&3 も、パスワードらしく見えます。前者は広い文字集合から引いただけかもしれません。後者は、覚えやすい一文をよくある置換にしただけかもしれません。見た目が答えるのは「パスワードらしいか」であり、「次のビットが計算できるか」ではありません。
文字集合に本当に大文字、小文字、数字、記号が入っていても、添字の列が予測できれば、攻撃側は空間全体を総当たりする必要がありません。同じ生成器を再現するか、出力を数本見たあとに内部状態を戻し、「まだ使われていない候補」だけを数えれば足ります。ログイン用パスワードでは、「記号が入っている」より先に、予測できないことが条件になります。
長さは、別の問いとして残ります。同じ CSPRNG でも、6 文字と 16 文字では総当たりのコストが桁違いです。8 文字未満は、API が正しくても空間が狭いです。FastPwd のランダムモードは 6〜128 文字、初期値は 16 文字、8 文字未満では安全性が低いと示します。これは長さの警告であり、「乱数アルゴリズムが弱くなった」ではありません。二つは分けて見てください。アルゴリズムが答えるのは予測不能さ、長さが答えるのは探索空間です。
Math.random はどの種類の乱数か
ECMAScript は Math.random() を、0 以上 1 未満の正の Number を、実装定義のアルゴリズムまたは方針でほぼ均一に引く、と書いています。仕様は予測耐性を求めておらず、種が OS のエントロピー源から来ることも求めていません。エンジンは、統計的な性質は良くても状態が可逆な擬似乱数生成器(PRNG)を選んでよい、ということです。
Chrome の V8 は、2015 年末まで MWC1616 を使っていました。V8 自身の解説は、内部状態が 64 ビットしかなく、品質に偏りがあり、TestU01 スイートで落ちる、と書いています。V8 4.9.41(Chrome 49 同梱)から xorshift128+ に切り替わりました。状態は 128 ビット、周期は 2^128 - 1、TestU01 は通過します。Firefox と Safari も、のちに同系統へ移っています。これは統計上の改善であり、セキュリティ上の格上げではありません。
同じ記事は境界を一文で切っています。xorshift128+ は旧実装よりずっと良いが、それでも暗号学的に安全ではない。ハッシュ、署名、暗号化/復号のような用途に、普通の PRNG は向きません。使うべきなのはウェブ暗号 API の window.crypto.getRandomValues です。「統計的に十分乱れている」と「公開された出力から、あとの出力を安く復元できない」は、基準が違います。パスワードが必要なのは後者です。
GitHub の CodeQL クエリ Insecure randomness も、パスワードを Math.random で作ることを指摘します。値が予測できれば、新しい利用者としてログインできるからです。Math.random() だけを呼んで文字表へ写す生成器は、「目で見て乱れている」「単純な頻度チェックを通る」を満たしても、「ソースを読んだ人、あるいは出力を数本見た人」を仮定したレビューは通りません。「Math.random を使っているが、文字集合が広い」を合格と書かないでください。
getRandomValues が入れ替える層
crypto.getRandomValues(typedArray) は、渡した整数型配列(Uint8Array や Uint32Array など)を、暗号学的に十分強い乱数で埋めます。MDN は性能との兼ね合いも書いています。ブラウザは毎回ハードウェア乱数を直接読むわけではなく、Unix の /dev/urandom や Windows のシステム乱数インタフェースのような OS のエントロピー源で、暗号用途に向く PRNG に種を入れます。アルゴリズムはブラウザごとに変わり得ます。設計目標は変わりません。公開された出力から、内部状態を安く戻させないことです。
二つの API は、同じ仕様書にいません。Math.random は ECMA-262、getRandomValues はウェブ暗号です。1回の呼び出しが 65536 バイトを超えると QuotaExceededError になります。16 文字のパスワードは、この上限には遠く及びません。加えて、getRandomValues は Crypto インタフェースのうち、非セキュアコンテキストでも呼べる数少ないメソッドです。鍵導出と subtle.encrypt は、いまも HTTPS のセキュアコンテキストを要求します。本番の生成ページはもともと HTTPS であるべきです。「HTTP でも getRandomValues が呼べる」を、TLS を外してよい理由にしないでください。
「このタブが乱数を引いた」と「サーバーが引いて送り返した」も分けてください。後者は、サーバーが高品質な CSPRNG を使っていても、パスワードはすでに相手のメモリ、ログ、その HTTP を、平文または戻せる形で通っています。アドレスバーの錠が守るのは、このブラウザから今の TLS 終点までの区間です。終点が本文を読めない、という意味ではありません。ページが「ローカル生成」と書くなら、生成の一回でパスワードを POST してエコーするインタフェースは出てはいけません。
| 見えた現象 | 支えられる結論 | 支えられない結論 |
|---|---|---|
| 出力が乱れて見える | 文字種が多いかもしれない | 生成過程が予測不能 |
ソースに Math.random がある |
ページのどこかで普通の擬似乱数を使っている | パスワード生成がそれを使っている |
ソースに getRandomValues がある |
ページが CSPRNG の API を取り込んでいる | 今押したボタンがそれを呼んだ |
| オフラインでも作れる | リアルタイムの遠隔生成 API に依存していない | 使っているのが Math.random ではない |
| Network でパスワードがヒットしない | この観察では業務データとしてアップロードしていない | 乱数 API が CSPRNG である |
剰余バイアス:CSPRNG でも偏る
よくある二番目の失敗は、getRandomValues を呼んだあとに起きます。スクリプトは 0–255 の 1 バイトを取り、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 キー、マスクしていないチケットで実験しないでください。捨ててよいテスト用の文字列を作り、使い終わったら捨ててください。確認するのは生成経路と通信であり、新しい秘密をもう一度晒すことではありません。
その場で確認する:ソース、コンソール、Network
スローガンは証拠になりません。本番の通信に出ない目印を用意してください。長さを 16 にして一度作り、できた文字列全体をカナリアにします。以下の三ステップは、今のタブで終わります。アカウントは不要です。
- 開発者ツールの Sources を開くか、「ページのソースを表示」を使います。
Math.randomとgetRandomValuesを別々に検索し、当たったファイルと関数名をメモします。ここではまだ結論を出さないでください。 - 生成ボタン(FastPwd では「パスワードを作る」)のクリック処理から下へ読み、文字が結果文字列へつながるところまで行きます。この経路に
crypto.getRandomValuesが出るはずです。Math.random()、Date.now()、固定の種だけなら、この回の生成は CSPRNG ではありません。 - Network に切り替え、Preserve log を入れ、フィルタはまず All のままにします。もう一度生成し、今得た文字列全体を検索欄へ貼ります。リクエスト行、クエリ、本文、アクセス解析のいずれにも、このパスワードは出てはいけません。
コンソールが証明するのは、API があることだけ
Console で typeof crypto.getRandomValues === "function" が true になっても、証明できるのは「今の文書のブラウザがこのメソッドを出せる」ことだけです。ページのスクリプトが呼んだ証明にはなりません。コンソールで自分が一行使って、ページが作ったパスワードも「確認済み」にするのは、無関係な二本のコードです。
オフライン再測が証明するのは、遠隔生成が無いことだけ
先にページを読み切り、Network で Offline にするか、システムのネットワークを切ってから生成します。結果がまだ出るなら、この回はリアルタイム API に依存していません。「このタブで作った」ことの強い信号です。ただし Math.random もネットワークを使いません。オフラインを通したあとも、ソース経路とカナリア検索は残ります。
バンドル後のスクリプトは、名前を一文字に潰すことがあります。getRandomValues が見つからないときは、crypto と Uint32Array を検索し、生成のあとに新しい業務 POST が出ないかも見てください。圧縮は検査免除ではなく、検索が一段遠くなる、という意味だけです。
「ランダム」を読みすぎる三つの場面
一つ目は、アドレスバーの錠を、生成アルゴリズムが CSPRNG である証拠にすることです。HTTPS / TLS が守るのは、このブラウザから今の TLS 終点までの伝送です。POST したパスワードをサーバーが受け取っても、錠は点いたままです。錠は Math.random と getRandomValues のどちらにも答えません。
二つ目は、「バンドルに getRandomValues が出た」を、生成ボタンが CSPRNG を使った証拠にすることです。同じファイルが、ファイル暗号化、ワンタイムリンク、デモにウェブ暗号を使い、パスワード生成だけ Math.random のまま、ということがあり得ます。逆も同じです。広告やアニメーションの Math.random は、パスワード経路を否定できません。証拠になるのは、クリックの呼び出しスタックです。
三つ目は、「アップロードしていない」を「予測不能」に読み替えることです。Network がきれいなのは、この観察ではパスワードが HTTP の業務欄としてタブを出ていない、ということだけです。拡張機能は DOM を読めます。OS のクリップボード履歴は、コピーした文字列を残すことがあります。それは別の境界です。乱数の品質、アップロードの有無、コピーしたあとにどこかに残るか、は三つの別の問いです。
よくある質問
見た目が乱れていれば、ログイン用パスワードにしてよいですか?
見た目だけでは判断できません。文字が混ざっていることは、文字種が多いかもしれないことしか示しません。生成過程が予測不能である証明にはなりません。MDN は Math.random をセキュリティ用途に使わないと書いています。長さが足りるか、複数のサイトで使い回していないか、生成経路が crypto.getRandomValues かも見てください。
オフラインでも作れれば、乱数は安全ですか?
証明できるのは、この回の生成がリアルタイム API に頼っておらず、サーバーが代わりに乱数を引いていないことだけです。Math.random もオフラインで動きます。オフラインを通したあとでも、ソース上の生成経路に getRandomValues が見えることと、Network に今作ったパスワードが出ていないことが残ります。
ソースに Math.random と getRandomValues の両方があるサイトは使えますか?
使えるかどうかは、生成ボタンがどちらを呼ぶかで決まります。アニメーションや表示順のシャッフルは Math.random でも構いません。パスワード、トークン、鍵は getRandomValues でなければなりません。Sources を開き、クリック処理から下へ読んでください。バンドル全体のヒット数だけで止めないでください。
FastPwd のパスワード生成はどの乱数を使い、アップロードしますか?
ランダム文字も覚えやすいパスフレーズも crypto.getRandomValues を使い、32 ビット整数を文字集合へ写すときに棄却サンプリングして、剰余の末尾を捨てます。この API が無いときは生成を拒否します。長さは 6〜128 文字、初期値は 16 文字。8 文字未満では安全性が低いと示します。生成結果はデフォルトでアップロードしません。開いてすぐ使え、アカウントは不要です。
同じテスト用パスワードを、開いてすぐ使えるツールで一度照合する
乱数の境界がページ上に書いてある画面で練習したいときは、FastPwd の パスワード生成 を開いてください。登録は不要です。ランダムモードは初期値の 16 文字のまま、捨ててよい一本を作ってください。上の確認手順どおりに進めてください。Sources では「パスワードを作る」から getRandomValues と棄却サンプリングのループまで読み、Network では文字列全体を検索し、リクエスト行と本文に当たらないことを確認します。
作ったあと、広く悪用されているパスワードに似ていないかも見たいときは、別の捨ててよい文字列を パスワード強度チェック に入れ、公開の高頻度一覧と照合してください。対象パスワードはアップロードしません。未ヒットは、全ウェブで見たことがない証明にはなりません。ヒットは、ありふれてすぎている、という意味だけです。それは弱いパスワード一覧の境界であり、CSPRNG の境界ではありません。これから有効にする本物のパスワードを、どのオンライン検査欄にも貼らないでください。
次に見知らぬ生成ページを開く前に、三つを覚えておいてください。第一に、見た目が乱れていることは、予測不能の証明になりません。第二に、Math.random は統計的な性質が良くても、暗号学的に安全な乱数ではありません。第三に、ソースに getRandomValues があるだけでは足りません。自分が押した経路まで読み、カナリアでパスワードが業務データとして出ていないことを確認してください。繰り返せる観察だけを、自分たちのレビュー一覧に書いてください。
関連記事
「ローカル暗号化、アップロードなし」も証拠にはなりません。カナリアで Network を検索する手順は、ブラウザのローカル暗号化で、平文が端末を出ていないことを確認する にあります。ローカルの弱いパスワード一覧がよくある弱いパスワードを捕まえられること、全ウェブ照会にはならないことは、ローカルの弱いパスワード一覧が証明できること、それが全ウェブ照会ではない理由 を見てください。作ったあとに同僚へ一度だけ渡すなら、# の後ろがデフォルトで HTTP に入らない理由は URLの#以降に鍵を置ける理由と、その保護が崩れる場面 にあります。