個人用に、複数のサービスで使うためのパスワード生成ツールを作っていました。視認性を重視し、記号・英字・数字を組み合わせて、自分では覚えやすく、コピーもしやすい設計です。
最初は特に深く考えず、JavaScript でよく使われる Math.random() を乱数として使っていました。
実装も簡単で、動作も問題ありません。見た目上は、十分にランダムな文字列が生成されます。
1. Math.random() と乱数の「質」
Math.random() は JavaScript で最も身近な乱数生成手段です。
0 以上 1 未満の数値が返ります。
ところが、パスワード生成では、疑似乱数である Math.random() は望ましくないようです1。
「疑似乱数」とは、内部に状態(シード)を持ち、計算によって値を生成する仕組みです。
一見ランダムに見えても、理論上は予測可能な構造を持っています。
ここで重要なのは、「攻撃者がこのツールを知っているかどうか」ではありません。
問題は、同じ環境で何度も Math.random() を使うこと自体です。
たとえば、複数のサービス用パスワードを生成しました。
これは便利ですが、見方を変えると「同じ乱数源から複数のサンプルを取っている」状態です。
もし何らかの形で Math.random() の出力が部分的に観測されると、内部状態が推測される可能性があります。
すると、別のタイミングで生成したパスワードの予測精度まで上がってしまいます。
候補集合そのものが、予測可能な系列から作られることになるからです2。
出現しやすい文字や構造を絞り込めます。
2. crypto.getRandomValues() を調べてみる
そこで調べたのが crypto.getRandomValues() です3。
これは Web Crypto API の一部で、OS が提供する乱数源を利用します4。
特徴を簡単にまとめると、
- 内部状態を推測できない5
- 出力同士に相関がない
- 暗号用途を前提に設計されている
というものです。
3. 置き換えて分かったこと
体感的な動作は、ほとんど変わりません。
生成速度も問題なく、UI 上の違いはありません。
それでも、設計上の安心感は大きく変わりました。
- 同じ端末で何度生成しても問題ない
- 乱数の品質について悩まなくてよい
- 「たまたま大丈夫」ではなく「原理的に大丈夫」になる
この差は、実装後にコードを読み返したときに強く感じました。
4. バレない前提より、壊れない前提
最初は、「この生成器が使われているかどうかは分からないから大丈夫だろう」と考えていました。
しかし調べて実装してみると、その前提自体が不要だと分かります。
crypto.getRandomValues() を使うことで、
- 知られても成立する
- 繰り返し使っても劣化しない
という状態を作れました。
- MDNのドキュメントによると、Math.random()は暗号学的に安全な乱数を提供しないため、セキュリティに関連する用途には使用すべきではありません。 – Crypto: getRandomValues() method – Web APIs | MDN
- 疑似乱数生成器(PRNG)は決定論的アルゴリズムであるため、複数の出力を観測することで内部状態の推測が可能になり、将来の出力を予測できる可能性があります。 – Don’t use Math.random() • DeepSource
- Web Crypto APIの一部として提供されるこのメソッドは、OSが提供する乱数源を利用し、暗号学的に強力な乱数を生成します。 – Crypto: getRandomValues() method – Web APIs | MDN
- Linuxでは/dev/urandom、WindowsではBCryptGenRandomなど、OS固有の高品質な乱数源にアクセスします。 – Secure Random Generators (CSPRNG) | Practical Cryptography for Developers
- 暗号学的に安全な疑似乱数生成器(CSPRNG)は、出力から内部状態を推測することが計算上不可能であるという特性を持ちます。 – Cryptographically secure pseudorandom number generator – Wikipedia