プログラミングでよく使う「乱数」。ゲームのキャラ配置とか、くじ引きプログラムとか、色々な場面で登場しますよね。
「ランダムな数字でしょ?簡単簡単!」なんて思っていたら、ちょっと待ってください!実は、その安易な考えが、とんでもないセキュリティホールを生む可能性があるんです。
想像してみてください。
もし、Webサービスのログイン状態を管理する「セッションID」や、パスワード再設定に使う「トークン」が、簡単な計算で予測できてしまったら…? 悪意のある人に簡単に成りすまされたり、アカウントを乗っ取られたりするかもしれません。怖すぎますよね!
セキュアコーディング、つまり安全なプログラムを作る上で、予測困難な「安全な乱数生成」は絶対に欠かせない知識なのです。
推測されやすい乱数を使ってしまうと、せっかく作ったサービスが危険に晒されてしまいます。この記事で、なぜ安全な乱数生成が必要なのか、その理由をしっかり掴んでいきましょう!
安全な乱数生成とは何か予測困難な乱数の仕組み
では、「安全な乱数生成」っていったい何でしょう?
ポイントは「予測がものすごく難しい」ことです。サイコロを何度も振って出る目を記録しても、次に出る目を当てるのが難しいのと同じようなイメージですね。
コンピューターが生み出す乱数には、大きく分けて2つの種類があります。
- 暗号論的擬似乱数生成器(CSPRNG)
→ 安全性が高い、予測困難な乱数を作る仕組み。 - 擬似乱数生成器(PRNG)
→ 比較的単純な計算で作られる、予測される可能性がある乱数。
セキュアコーディングの世界では、迷わず「CSPRNG」を選ぶ必要があります。
なぜなら、PRNGは特定の条件下では次にどんな数字が出るか予測できてしまう場合があるから。セキュリティに関わる部分でPRNGを使うのは、玄関の鍵を開けっ放しにしておくようなものなのです!
暗号論的擬似乱数生成器CSPRNGとは
CSPRNGは、Cryptographically Secure Pseudo-Random Number Generator の略です。名前がちょっと長いですが、要は「暗号技術にも使われるくらい、予測がめちゃくちゃ難しい乱数を作る専門家」と考えてください。
CSPRNGが作る乱数は、統計的に見ても偏りがほとんどなく、過去の乱数から次の乱数を予測することが、現実的な計算時間の中ではほぼ不可能です。
だから、セッションIDやトークンなど、セキュリティが求められる場面で安心して使えるのですね。まさに、安全な乱数生成の主役です!
擬似乱数生成器PRNGとの違いと使い分け
一方、PRNG(Pseudo-Random Number Generator)は、比較的シンプルな計算式で乱数っぽい数列を作り出します。計算が速いというメリットはあるのですが、セキュリティ用途には全く向いていません。
PRNGの大きな特徴は、「シード」と呼ばれる初期値を与えると、毎回同じ順番で同じ数字の列が出てくることです。
もし、このシード値がばれてしまったり、推測しやすい値(例えばプログラムを実行した時刻など)だったりすると、生成される乱数列が丸わかりになってしまいます。これは大変危険です!
ただし、PRNGが全く役に立たないわけではありません。ゲームのランダム要素や、科学技術計算のシミュレーションなど、予測されてもセキュリティ上の問題がない場面では、その速さが活きることもあります。
大切なのは、用途に応じてCSPRNGとPRNGを正しく使い分ける意識を持つことです。でも、迷ったら安全なCSPRNGを選んでおけば間違いありません。
言語別に見る安全な乱数生成の実装方法
「じゃあ、実際にプログラムで安全な乱数を作るにはどうすればいいの?」
ここからは、皆さんがよく使うかもしれないプログラミング言語をいくつかピックアップして、安全な乱数生成(CSPRNG)の具体的な書き方を見ていきましょう!
多くの言語では、安全な乱数を生成するための専用の機能が用意されています。古い、安全でない関数も残っていたりするので、必ず推奨される方法を使うようにしましょうね。
Pythonで安全な乱数を生成する
Pythonで安全な乱数を生成するなら、`secrets`モジュール一択です!覚えてくださいね。
昔からある`random`モジュールは、シミュレーションなどには便利ですが、セキュリティ目的には使ってはいけません。`secrets`モジュールは、OSが提供する安全な乱数源を利用して、予測困難な乱数を生成してくれる優れものです。
例えば、ランダムなバイト列(トークン生成などに使う)や、指定した範囲の安全な整数を生成できます。
# 書き方 import secrets import string # 安全なランダムバイト列を16バイト生成 (URLセーフなBase64エンコード) secure_token = secrets.token_urlsafe(16) print(f"安全なトークン: {secure_token}") # 0から99までの安全なランダム整数を生成 secure_int = secrets.randbelow(100) print(f"安全な整数 (0-99): {secure_int}") # 安全なランダムな文字列を生成 (英数字8文字) alphabet = string.ascii_letters + string.digits secure_password = ''.join(secrets.choice(alphabet) for i in range(8)) print(f"安全なパスワードっぽい文字列: {secure_password}")
ね、簡単でしょう? Pythonでセキュリティに関わる乱数が必要になったら、迷わず`secrets`モジュールですよ!
Javaで安全な乱数を生成する
Javaの世界では、`java.security.SecureRandom`クラスが安全な乱数生成を担当します。
よく似た名前の`java.util.Random`や、手軽に見える`Math.random()`がありますが、これらはPRNGなので、セキュリティが求められる場面では絶対に使わないでくださいね。`SecureRandom`は、内部でOSの乱数生成機能などを利用して、予測困難な乱数を提供してくれます。
インスタンスを作って、バイト列や整数を生成するのが基本的な使い方です。
// 書き方 import java.security.SecureRandom; import java.util.Base64; public class SecureRandomExample { public static void main(String[] args) { // SecureRandomインスタンスの生成 (推奨されるデフォルトのコンストラクタ) SecureRandom secureRandom = new SecureRandom(); // 安全なランダムバイト列を16バイト生成 byte[] randomBytes = new byte[16]; secureRandom.nextBytes(randomBytes); // バイト列をBase64エンコードして表示 (トークンなどによく使う形式) String secureToken = Base64.getUrlEncoder().withoutPadding().encodeToString(randomBytes); System.out.println("安全なトークン: " + secureToken); // 安全なランダム整数を生成 (0から99まで) // nextInt(n) は 0 以上 n 未満の整数を返す int secureInt = secureRandom.nextInt(100); System.out.println("安全な整数 (0-99): " + secureInt); // long型の安全なランダム整数 long secureLong = secureRandom.nextLong(); System.out.println("安全なlong型整数: " + secureLong); } }
`SecureRandom`を使うのがJavaにおけるセキュアコーディングの基本です。しっかり覚えておきましょう。
PHPで安全な乱数を生成する
PHPを使っている皆さん、安心してください!PHPにも強力な安全な乱数生成関数が用意されています。
PHP 7以降であれば、`random_bytes()`関数と`random_int()`関数を使うのがベストプラクティスです。これらはCSPRNGを利用しています。
古い`rand()`や`mt_rand()`関数は、予測可能であるためセキュリティ用途には絶対に使わないでください。これらの古い関数を使っているコードを見つけたら、すぐに修正を検討しましょう。
`random_bytes()`は指定したバイト数のランダムなバイト列を、`random_int()`は指定した範囲内のランダムな整数を安全に生成します。
<?php // 書き方 // 安全なランダムバイト列を16バイト生成 try { $randomBytes = random_bytes(16); // トークンとして使うために16進数文字列に変換 $secureToken = bin2hex($randomBytes); echo "安全なトークン: " . $secureToken . "\n"; } catch (Exception $e) { // エラー処理 (例: 乱数生成器が利用できない場合) echo "エラー: 安全な乱数を生成できませんでした - " . $e->getMessage() . "\n"; } // 1から100までの安全なランダム整数を生成 try { $secureInt = random_int(1, 100); echo "安全な整数 (1-100): " . $secureInt . "\n"; } catch (Exception $e) { // エラー処理 echo "エラー: 安全な整数を生成できませんでした - " . $e->getMessage() . "\n"; } ?>
PHP 7以降なら迷わず`random_bytes()`と`random_int()`を使う、これが合言葉です!
JavaScriptNodejsで安全な乱数を生成する
JavaScript、特にサーバーサイドのNode.jsや、最近のブラウザ環境でも安全な乱数を生成できます。
まず絶対に覚えておいてほしいのは、`Math.random()`はセキュリティ目的には絶対に使ってはいけない、ということです。これはPRNGであり、予測可能です。
Node.js環境では、標準の`crypto`モジュールにある`randomBytes()`関数や`randomInt()`関数(Node.js v14.10.0以降)を使いましょう。これらはOSの機能を利用したCSPRNGです。
ブラウザ環境では、Web Crypto APIの`window.crypto.getRandomValues()`を使います。型付き配列(`Uint8Array`など)に直接、安全な乱数を書き込んでくれます。
// 書き方 (Node.js) const crypto = require('crypto'); // 安全なランダムバイト列を16バイト生成 const randomBytes = crypto.randomBytes(16); // Bufferを16進数文字列に変換 const secureToken = randomBytes.toString('hex'); console.log(`Node.js - 安全なトークン: ${secureToken}`); // 0から99までの安全なランダム整数を生成 (Node.js v14.10.0以降) try { // crypto.randomInt(min, max) は min 以上 max 未満の整数を返す const secureInt = crypto.randomInt(0, 100); console.log(`Node.js - 安全な整数 (0-99): ${secureInt}`); } catch (err) { // 古いNode.jsバージョンでは使えない場合の代替処理など console.log('crypto.randomInt はこのNode.jsバージョンでは利用できません。'); // 代替として randomBytes を使う方法もある (少し複雑) } // --- // 書き方 (ブラウザ - Web Crypto API) // HTMLファイル内で実行するか、ブラウザの開発者コンソールで試せます // 安全なランダムバイト列を16バイト生成 const randomByteArray = new Uint8Array(16); window.crypto.getRandomValues(randomByteArray); // バイト配列を16進数文字列に変換するヘルパー関数 function bytesToHex(bytes) { return Array.from(bytes, byte => byte.toString(16).padStart(2, '0')).join(''); } const browserSecureToken = bytesToHex(randomByteArray); console.log(`ブラウザ - 安全なトークン: ${browserSecureToken}`); // 安全なランダム整数 (0-99) を生成する例 (少し工夫が必要) // getRandomValuesはバイト配列を生成するため、整数にするには変換処理が必要 // 例: 1バイトの乱数値 (0-255) を取得し、100未満になるまで繰り返す function generateSecureInt(min, max) { const range = max - min; if (range <= 0) { throw new Error('max must be greater than min'); } // 必要なバイト数を計算 (単純化のため1バイトで十分と仮定) const randomByte = new Uint8Array(1); let randomNumber; do { window.crypto.getRandomValues(randomByte); // 0-255の値を範囲内にマッピングする (偏りを減らす努力) // より厳密な方法は複雑になるため、ここでは簡易的な例を示す randomNumber = randomByte[0]; } while (randomNumber >= (256 - (256 % range))); // 偏りを減らすためのリジェクト処理 return min + (randomNumber % range); } try { const browserSecureInt = generateSecureInt(0, 100); // 0 から 99 まで console.log(`ブラウザ - 安全な整数 (0-99): ${browserSecureInt}`); } catch(e) { console.error("エラー:", e); }
Node.jsなら`crypto`モジュール、ブラウザなら`window.crypto.getRandomValues()`。`Math.random()`は封印です!
安全な乱数生成におけるよくある間違いと対策
安全な乱数生成の方法がわかったところで、今度は初心者の頃に「やっちゃいがち」な間違いを見ていきましょう。
せっかく安全な関数を知っていても、使い方を間違えると意味がなくなってしまうことも…。ここでしっかり落とし穴をチェックして、未然に防げるようになりましょう! セキュアコーディングのレビュー(チェック)をする時にも役立ちますよ。
不適切な乱数生成器を選んでしまう
これは一番よくある間違いかもしれません。
「手軽だから」「昔から使われているサンプルコードにあったから」という理由で、`Math.random()`や`rand()`、Pythonの`random`モジュールなどを、セキュリティが求められる場面で使ってしまうケースです。
何度もお伝えしていますが、これらはPRNGであり、予測可能です。
セッションID、トークン、パスワード生成など、外部から推測されては困る値を作る場合は、必ずCSPRNG(`secrets`, `SecureRandom`, `random_bytes`/`random_int`, `crypto`モジュールなど)を選んでください。面倒くさがらずに、安全な方を選ぶ癖をつけましょう!
シード値を安易に設定管理してしまう
もし、何らかの理由でPRNGを使わざるを得ない場合(推奨はしませんが)、その「シード値」の扱いには最大限の注意が必要です。
シード値は、PRNGが乱数列を作り始めるための「種」のようなもの。この種が同じなら、何度やっても同じ数列が出てきます。プログラムを実行した時刻や、単純な連番などをシード値に使うのは、絶対にやめましょう。
簡単に推測されてしまい、乱数列が予測可能になってしまいます。
ちなみに、この記事で紹介したCSPRNGを使っていれば、基本的にシード値を自分で管理する必要はありません。OSなどが安全な方法で初期化してくれるため、開発者はその心配から解放されます。CSPRNGを選ぶメリットはここにもあるのです。
生成した乱数の加工方法が不適切
「よし、CSPRNGで安全な乱数を作ったぞ! …あとは、これを1から6の範囲にすればサイコロの出来上がりだ!」
ちょっと待ってください! 安全な乱数生成器を使っても、その後の加工方法がまずいと、せっかくの安全性が損なわれる可能性があります。
よくあるのが、剰余演算(`%`)を使って範囲を限定しようとするケースです。例えば、0から255までのランダムなバイト値を生成して、`randomByte % 6 + 1` のように計算すると、出る目に偏りが生じてしまう場合があるのです(特定の値が出やすくなる)。
対策としては、言語が提供している安全な範囲指定関数(PHPの`random_int()`、Pythonの`secrets.randbelow()`など)を使うのが一番です。
もし自分で加工する必要がある場合は、乱数の分布に偏りが出ないように、慎重なアルゴリズム(例えばリジェクトサンプリングなど)を検討する必要がありますが、まずは言語標準の安全な関数を探すことをお勧めします。
安全な乱数生成の応用例
さて、安全な乱数生成の重要性や方法がわかってきたところで、実際にどんな場面で活躍しているのか、具体的な応用例を見ていきましょう。
「こんな身近なところでも使われてるんだ!」と分かれば、その必要性をさらに実感できるはずです。セキュアコーディングは、机上の空論ではなく、現実のサービスを守るための実践的な技術なのです。
セッションIDや各種トークンの生成
Webサービスにログインした後、ブラウザを閉じてもう一度開いてもログイン状態が保たれていますよね? あれは、サーバーが発行した「セッションID」という秘密の合言葉をブラウザが持っているからです。
もし、このセッションIDが簡単に推測できるような乱数で作られていたらどうでしょう? 悪意のある人が他人のセッションIDを推測して、その人になりすましてログインできてしまいます(セッションハイジャック攻撃)。
同じように、不正なリクエストを防ぐためのCSRFトークンなども、推測不可能な乱数で作られている必要があります。
認証や認可に関わるIDやトークンの生成には、安全な乱数生成が絶対に不可欠なのです。十分な長さと複雑さを持つように生成しましょう。
# 書き方 (PythonでのセッションID/CSRFトークン生成例) import secrets # セッションIDやCSRFトークンに適した、十分な長さ(32バイト = 約43文字)を持つURLセーフな文字列を生成 # これくらいの長さがあれば、推測は極めて困難になります session_id = secrets.token_urlsafe(32) csrf_token = secrets.token_urlsafe(32) print(f"生成されたセッションID (例): {session_id}") print(f"生成されたCSRFトークン (例): {csrf_token}")
一時パスワードやパスワードリセット用トークン
パスワードを忘れた時に、「パスワードリセット用のリンクをメールで送ります」という機能、よくありますよね。あのリンクに含まれる一時的なトークンや、最初に発行される仮パスワードなども、安全な乱数で作られていなければなりません。
もし、これらの値が予測可能だったら、攻撃者は簡単に他人のパスワードをリセットしてアカウントを乗っ取ることができてしまいます。
一時的なものであっても、アカウントのセキュリティに直結する値は、必ずCSPRNGを使って生成し、さらに有効期限を短く設定するなどの対策も併せて行うべきです。
# 書き方 (Pythonでの一時パスワード/リセットトークン生成例) import secrets import string # --- パスワードリセット用トークン生成 (URLに含めることを想定) --- # セッションID同様、十分な長さのURLセーフな文字列が良いでしょう password_reset_token = secrets.token_urlsafe(32) print(f"パスワードリセットトークン (例): {password_reset_token}") # --- 一時パスワード生成 (読みやすさも少し考慮しつつ、推測困難に) --- # 使用する文字種を定義 (英大文字小文字 + 数字) # 紛らわしい文字(I, l, 1, O, 0)を除外する場合もありますが、ここでは含めます alphabet = string.ascii_letters + string.digits # 例えば12文字の一時パスワードを生成 temp_password_length = 12 temporary_password = ''.join(secrets.choice(alphabet) for i in range(temp_password_length)) print(f"一時パスワード (例): {temporary_password}")
【まとめ】安全な乱数生成をマスターしてセキュアな開発を
今回は「安全な乱数生成」をテーマに、セキュアコーディングの基本的な考え方から具体的な実装方法まで見てきました。
もう一度、大事なポイントをおさらいしておきましょう。
- 安易な乱数生成は深刻な脆弱性を生む危険がある。
- セキュリティ目的では予測困難なCSPRNGを使うこと。PRNGはNG。
- 各言語で用意されている安全な乱数生成関数(`secrets`, `SecureRandom`, `random_bytes`/`random_int`, `crypto`など)を正しく使う。
- 古い関数(`random`, `rand`, `mt_rand`, `Math.random`など)は使わない。
- 生成後の不適切な加工にも注意する。
- セッションIDやトークンなど、多くの場面で安全な乱数生成は必須である。
「乱数なんてどれも同じ」と思っていた方も、この記事を読んで「安全な乱数生成、めっちゃ大事じゃん!」と感じていただけたなら嬉しいです。
セキュアコーディングは、一見地味かもしれませんが、皆さんが作るサービスやシステムを守るための土台となるスキルです。今回学んだことを活かして、ぜひ自信を持って安全なコードを書いていってくださいね!
さらに学びを深めたい方は、OWASP(Open Web Application Security Project)などのセキュリティ専門組織が出している資料を読むのもおすすめです。
0 件のコメント:
コメントを投稿
注: コメントを投稿できるのは、このブログのメンバーだけです。