コンテンツへスキップ
HexaTransfer
ブログへ戻る
暗号化とセキュリティ

安全な乱数生成:強力な暗号の基盤

安全な乱数生成は暗号化に不可欠。crypto.getRandomValuesの仕組みと弱い乱数がセキュリティモデル全体を壊す理由。

安全な乱数生成は、他のすべての暗号技術が依拠する基盤です。JavaScript では、crypto.getRandomValues(buffer) が TypedArray を OS の CSPRNG からの暗号学的に安全なランダムバイトで埋めます(Linux/macOS では /dev/urandom、Windows では BCryptGenRandom、iOS/macOS では SecRandomCopyBytes)。セキュリティに関わるものに Math.random() は絶対に使わないでください。これは速度のために設計された Mulberry32 や xorshift スタイルの PRNG で、予測不可能性の保証がなく、数サンプルを観察するだけで出力が予測可能です。弱い乱数生成器は AES 鍵、TLS ハンドシェイク、GCM でのノンスの一意性、トークンの推測困難性、そして予測不可能なビットに依存するすべての他のセキュリティプリミティブを破壊します。

ランダムと暗号学的ランダムの違い

PRNG(疑似乱数生成器)はシードから決定論的なストリームを生成します。シードとアルゴリズムがわかれば、すべての出力を再現できます。ゲーム、シミュレーション、モンテカルロ法には問題ありません。暗号論では致命的です。

CSPRNG(暗号学的に安全な PRNG)は真のエントロピー源(熱雑音、割り込みタイミング、Intel の RDSEED などのハードウェア RNG 命令)からシードされ、出力が真のランダム性と計算上区別できないように設計されており、過去の出力を観察しても将来の出力を予測する助けになりません。

JavaScript では両方が提供されます。Math.random() は PRNG です。crypto.getRandomValues() は OS の CSPRNG のラッパーです。コードの1行の違いが、セキュリティの大きな違いをもたらします。

正しい使い方

// 256ビット AES 鍵分のランダムバイト生成
const keyBytes = crypto.getRandomValues(new Uint8Array(32));

// 96ビット GCM ノンス生成
const nonce = crypto.getRandomValues(new Uint8Array(12));

// 128ビットソルト生成
const salt = crypto.getRandomValues(new Uint8Array(16));

// URL セーフなランダムトークン生成
const tokenBytes = crypto.getRandomValues(new Uint8Array(32));
const token = btoa(String.fromCharCode(...tokenBytes))
  .replace(/\+/g, '-').replace(/\//g, '_').replace(/=+$/, '');

crypto.getRandomValues() は同期的で、バッファをインプレースで埋め、バッファを返します。1回の呼び出しでの最大リクエストサイズは65,536バイトです(ブロッキングを防ぐための仕様上のクォータ)。より大きなランダム素材が必要な場合は繰り返し呼び出します。

Node.js での対応

const { randomBytes, randomFillSync, webcrypto } = require('crypto');

const keyBytes = randomBytes(32);  // Buffer を返す
// または Web Crypto 互換
const nonce = webcrypto.getRandomValues(new Uint8Array(12));

Node の randomBytes は Web Crypto と同じ根本の CSPRNG から引き出します。どちらの API スタイルもコードに合わせて選べます。ブラウザと両方の環境で動くアイソモーフィックなコードには webcrypto.getRandomValues がブラウザと完全に一致します。

Math.random が失敗する理由

V8(Chrome/Node)、SpiderMonkey(Firefox)、JavaScriptCore(Safari)はすべて Math.random() を暗号学的保証のない高速 PRNG として実装しています。V8 は xorshift128+ の変形を使います。研究者たちは約5つの出力を観察した後、攻撃者が内部状態を回復してすべての将来の出力を予測できることを示しました。2015年に Mike Pound らが実際のバグバウンティで V8 の Math.random の状態を逆算しました。

Math.random() を使ってセッショントークン、パスワードリセットリンク、暗号化ノンス、共有 ID を生成した場合、それらのいくつかを観察した攻撃者は残りを予測できます。これは理論的な話ではなく、監査でよく見つかるバグのクラスです。

よくある誤用

Math.random() で PRNG ライブラリをシードする:下流に、シードよりもランダムなものは何もありません。代わりに crypto.getRandomValues(new Uint32Array(1))[0] を使います。

Date.now() をエントロピーとして使う:時間は狭いウィンドウ内で推測可能です。わずかなランダム要素と組み合わせても、タイムスタンプは攻撃者がビットを絞り込むのに十分な情報を漏らします。

ソースを XOR して自前でミックスする:やめてください。OS の CSPRNG はすでに有用なエントロピーソースをすべてミックスしています。自分でかき混ぜると、増やすのではなく大抵エントロピーを減らします。

範囲生成時のモジュロバイアスrandomBytes[0] % 10 は0〜9に均等に分布しません。256が10の倍数でないためです。均等な整数生成には、拒絶サンプリングを使います:

function randomInt(max) {
  const range = new Uint32Array(1);
  const threshold = 2**32 - (2**32 % max);
  do {
    crypto.getRandomValues(range);
  } while (range[0] >= threshold);
  return range[0] % max;
}

エントロピーソースと起動時の問題

Linux では /dev/urandom は早期起動後は常に安全です。ハードウェア RNG を持たないシステムの起動から最初の数秒間は、カーネルプールのシードが不十分な場合があります。これが2008年の Debian OpenSSL バグで悪用されました。パッチがエントロピーミックスを削除し、プロセス ID だけがシードとして残り、この期間に生成された鍵には 2^15 通りの可能な値しかなく、秒単位で列挙できました。

現代のシステムでは次のソースからカーネル CSPRNG をシードします。x86-64 上の RDSEED(利用可能な場合)、ARMv8.5-A RNG 命令、各種周辺機器からの熱雑音、割り込みタイミング、インタラクティブな場合はキーボード/マウスです。Intel Ice Lake や AMD Zen 3+ などのサーバーハードウェアでは、起動からマイクロ秒単位で CSPRNG がシードされます。

セッショントークンと共有 ID

ファイル転送サービスでランダムな識別子を生成する場面を挙げます。URL 内のファイル ID(攻撃者が有効な ID を推測できないように)、パスワード保護リンクの共有トークン、CSRF トークン、暗号化鍵(ファイルごとの AES 鍵)、GCM のノンスです。

最小長は衝突防止と推測困難性のために128ビット(16バイト)、鍵には256ビット(32バイト)です。base64url エンコーディングで長さが約33% 増し、16進エンコードで100% 増しになります。32バイトの base64url エンコードトークンは43文字で、2^256 で事実上衝突しません。

弱い乱数生成器のテスト方法

乱数生成器が壊れているか弱い兆候を示します。異なるリクエストで同一のトークンが生成される(本来広大な空間での衝突)。出力が視覚的なテストを通過しても dieharderPractRand の統計テストで失敗する。プロセス再起動後のシード再利用(各デプロイが同じ初期状態を使う)。生成された鍵がパターンに入る(例:最初の4バイトは変化するが残り28バイトが同一)。

本番環境では、何か致命的に間違っていない限り通常は見えません。失敗のモードは通常サイレントです。攻撃が、2^256 の探索空間であるはずのものに対して現実的になってしまいます。

監査として、コードベース内のすべての Math.random() の呼び出しをレビューすべきです。ソースツリーで Math.random を grep することは週次の衛生チェックとして有効です。セキュリティに関連する呼び出しを crypto.getRandomValues に変換するのは数分で完了し、実際の脆弱性を防ぎます。

UUID と人間向け識別子

人間向けの識別子には crypto.randomUUID() が標準フォーマットで v4 UUID(122ビットのランダム性)を返します。Chrome 92+、Firefox 95+、Safari 15.4+、Node 14.17+ でサポートされています。データベースの主キー、API リクエスト ID、セキュリティクリティカルでない一意識別子に適しています。カスタムフォーマットや高エントロピーが必要なものには明示的な getRandomValues を使います。

まとめ:退屈なほど重要な基礎

ファイル転送アプリのすべての暗号化は予測不可能なランダムバイトに依存しています。AES 鍵、GCM ノンス、PBKDF2 ソルト、共有トークン、CSRF 対策トークン、セッション ID — すべてが同じプリミティブを必要とします。ブラウザでは crypto.getRandomValues()、Node では crypto.randomBytes() または webcrypto.getRandomValues です。それを使ってください。Math.random() は使わない。タイムスタンプも使わない。自前のミキサーも使わない。

このプリミティブがまさに退屈なのは、退屈でなければならないからです。退屈で、正確で、どこでも使える — 暗号の基礎はまさにそうあるべきです。

hexatransfer.comでお試しください — 無料、登録不要、最大10GB。

エンドツーエンド暗号化で大容量ファイルを安全に送信

エンドツーエンド暗号化で最大10GBのファイルを無料で転送。アカウント不要。ファイルはアップロード前にブラウザで暗号化されるため、他の誰にも読まれません。

ファイルを送信