Math.random() でパスワードを生成しているコードをたまに見かけるが、これは危険。ブラウザ標準の Web Crypto API(crypto.getRandomValues)を使えば、暗号学的に安全なランダム文字列を外部ライブラリなしで生成できる。
ぱんだツールズの1機能として、npm依存ゼロのパスワード生成ツールを実装した。
Math.random() がパスワード生成に使えない理由
Math.random() は疑似乱数生成器(PRNG)で、内部状態から次の値が予測可能。MDN でも「暗号学的に安全な乱数を提供するものではない」と明記されている。
パスワード生成に使うと、攻撃者が生成アルゴリズムの内部状態を推測できた場合、生成されたパスワードを再現できてしまう。
Web Crypto API とは
- ブラウザ標準API(全モダンブラウザ対応、IE11を除く)
-
crypto.getRandomValues()は暗号学的に安全な乱数を生成する - OSのエントロピーソース(/dev/urandom 等)を利用
- Node.js でも同じAPIが使える(v15+)
- 外部ライブラリ不要
実装
パスワード生成の全コードはこれだけ。
const CHARS = {
upper: 'ABCDEFGHIJKLMNOPQRSTUVWXYZ',
lower: 'abcdefghijklmnopqrstuvwxyz',
digits: '0123456789',
symbols: '!@#$%^&*()_+-=[]{}|;:,.<>?',
}
function generatePassword(
length: number,
useUpper: boolean,
useLower: boolean,
useDigits: boolean,
useSymbols: boolean,
): string {
let charset = ''
if (useUpper) charset += CHARS.upper
if (useLower) charset += CHARS.lower
if (useDigits) charset += CHARS.digits
if (useSymbols) charset += CHARS.symbols
if (charset.length === 0) return ''
const array = new Uint32Array(length)
crypto.getRandomValues(array)
return Array.from(array)
.map((n) => charset[n % charset.length])
.join('')
}
ポイントは Uint32Array を使っていること。Uint8Array(0〜255)だと 256 % 94 = 68 で、先頭68文字が他の文字より約1.4倍選ばれやすくなる。Uint32Array(0〜4,294,967,295)なら値域が十分に大きいため、94文字の文字セットに対しても偏りは無視できるレベルになる。
modulo バイアスについて
n % charset.length には理論上モジュロバイアスがある。たとえば charset が94文字の場合、4,294,967,296 % 94 = 54 なので、先頭54文字が1回分多く選ばれる確率がある。
ただし実用上の影響は極めて小さい。偏りの量は 94 / 4,294,967,296 ≈ 0.000002% で、16文字のパスワードに対するエントロピーの損失は事実上ゼロ。rejection sampling で完全に除去することもできるが、このユースケースではオーバーエンジニアリングと判断した。
文字種の組み合わせと強度
| 文字種 | 文字数 | 16文字の組み合わせ数 | ブルートフォース耐性 |
|---|---|---|---|
| 数字のみ | 10 | 10^16 | 数時間〜数日 |
| 英小文字 | 26 | 26^16 ≈ 4.4 × 10^22 | 数年 |
| 英数字 | 62 | 62^16 ≈ 4.7 × 10^28 | 数万年 |
| 英数字+記号 | 94 | 94^16 ≈ 3.7 × 10^31 | 数百万年 |
16文字・英数字記号の組み合わせで約104ビットのエントロピー。現時点では十分すぎる強度。
ワンクリックコピー(Clipboard API)
生成したパスワードはワンクリックでコピーできるようにしている。
const handleCopy = useCallback(async () => {
if (!password) return
try {
await navigator.clipboard.writeText(password)
setCopied(true)
setTimeout(() => setCopied(false), 2000)
} catch {
// Clipboard API が使えない環境ではフォールバックなし
}
}, [password])
コピー完了後は2秒間「コピー済み」表示に切り替わる。地味だが、パスワード生成ツールではコピー機能のUXが最も重要。生成 → コピー → ペーストの流れが最短になるよう設計している。
UIの設計
長さの調整
スライダーで8〜64文字の範囲で調整できる。
<input
type="range"
min={8}
max={64}
value={length}
onChange={(e) => setLength(Number(e.target.value))}
/>
最小8文字はNISTのガイドライン(SP 800-63B)を参考にした。最大64文字はほとんどのサービスで受け入れられる上限。
文字種の選択
チェックボックスで大文字・小文字・数字・記号のON/OFFを切り替えられる。すべてOFFの状態では生成ボタンを無効化して、空文字列が生成されることを防いでいる。
const noneSelected = !useUpper && !useLower && !useDigits && !useSymbols
<button
onClick={handleGenerate}
disabled={noneSelected}
>
パスワードを生成
</button>
パフォーマンス
| 項目 | 値 |
|---|---|
| 生成速度 | 1ms未満(常に瞬時) |
| バンドルサイズへの影響 | ほぼゼロ(Web Crypto APIはネイティブ) |
| 外部依存 | なし |
Web Crypto API はブラウザに組み込まれているため、npmパッケージのインストールも不要。バンドルサイズへの影響がゼロなのは、他のツール(heic2any: 2.6MB、pdf-lib: 23MB)と比べると大きなメリット。
まとめ
- パスワード生成には必ず
crypto.getRandomValuesを使う。Math.random()は危険 -
Uint32Arrayを使えばモジュロバイアスを実用上無視できるレベルまで抑えられる - 外部ライブラリ不要で、バンドルサイズへの影響がゼロ
ぱんだツールズ では他にも PDF・画像・CSV・テキスト処理などの開発者向けツールを 50 個以上公開中。全部無料・登録不要・ブラウザ完結で使える。
https://sakutto-panda.com
この記事は Zenn にも同じ内容を投稿しています。