こんにちは!システム開発の現場で、日々セキュリティと向き合っているホワイトハッカーの私です。
今回は、Webアプリケーションを作るうえで絶対に避けて通れない「パスワードの保存方法」について、少しマニアックだけどめちゃくちゃ大切な話をしますね。
新人のIT担当者や、「セキュリティって難しそう……」と少し身構えてしまう開発者の方に向けて、身近な例えを交えながら一歩ずつ優しく解説していきます。一緒に安全なパスワードの仕組みをマスターしていきましょう!
—
パスワードを守る金庫「bcrypt」の落とし穴
皆さんは、ユーザーが登録したパスワードをデータベースに保存するとき、そのままテキストで保存していませんよね? もちろん、不可逆な変換を行う「ハッシュ関数」という仕組みを使って、復元できない形にしているはずです。
その中でも、パスワード専用の強力な金庫として長年愛されてきたのが bcrypt(ビークリプト) です。
「すごく頑丈な金庫だから、bcryptを使っておけば安心!」……そう思っていませんか? 実は、この金庫には「72バイトの壁」という、ちょっと厄介な制限があるんです。
家の鍵に例えてみると……
想像してみてください。あなたは、世界一頑丈な特製の南京錠を買いました。ピッキングも物理的な破壊も一切受け付けない、完璧なセキュリティを誇る鍵です。
ただし、この南京錠には設計上の仕様として「鍵穴の大きさに合わせて、73文字目以降の鍵のパーツは物理的にハサミで切り落とされます」というルールがあったとしたらどうでしょう?
もし、あなたが「これは絶対に破られないぞ!」と意気込んで、100文字もある超長文の合鍵を作ったとします。でも、実際に鍵穴に差し込めるのは最初の72文字分だけで、残りの28文字分はパチンと切り捨てられてしまうのです。
これが、bcryptの「72バイト制限」の正体です。
—
72バイト制限が引き起こすリスク
「えっ、パスワードが切り捨てられるだけなら、最初の72文字が合っていればログインできるんだから問題ないのでは?」と思ったそこのあなた。甘い、甘いです! ここに攻撃者がつけ入る恐ろしい盲点があります。
切り捨てによる「衝突(コリジョン)」の恐怖
例えば、ユーザーが以下の2つのパスワードを設定したとしましょう(わかりやすく文字数で例えます)。
1. P@ssword1234567890...(Aという続きの文字列)
2. P@ssword1234567890...(Bというまったく異なる続きの文字列)
もし、このパスワードが72バイト(文字)を超える長さだった場合、bcryptは最初の72文字で強制的にスパッと文字列を切り捨ててしまいます。
そうすると何が起きるか? 切り捨てられた後の文字列がまったく同じであれば、bcryptが生成するハッシュ値(金庫の暗号結果)が完全に一致してしまうのです。
つまり、ユーザーBのパスワードを知らなくても、ユーザーAのパスワード(あるいは切り捨て後の文字列を総当たりで破ったもの)で、ユーザーBのアカウントにログインできてしまうという「衝突リスク」が発生します。
「そんなに長いパスワードを入れる人なんていないよ」と思うかもしれませんが、パスワードマネージャー(1PasswordやBitwardenなど)が自動生成するパスワードは、時として非常に長文化します。ユーザーが意図せずセキュリティの穴を踏んでしまう可能性があるのです。
—
攻撃者の視点:どうやってこの弱点を突くか?
サイバー攻撃者は、こうした仕様の隙を見逃しません。もしターゲットのWebサービスが「72バイトを超えるパスワードをそのままbcryptに渡している」と知ったら、彼らは以下のようなアプローチを考えます。
- 切り捨て部分の総当たり: 72バイト以降の文字が何であれ、ハッシュが同じになるなら、短い文字列だけで総当たり攻撃(ブルートフォース攻撃)を成立させやすくなります。
- 意図的なパスワード長の設定: 脆弱性を知る攻撃者は、あえて72バイト目までが同じで、後続の文字が異なる別のパスワードを用意し、不正アクセスを試みる可能性があります。
「頑丈な金庫だけど、鍵穴に入る長さに制限がある」という弱点を、攻撃者は冷静に狙っているのです。
—
現場で使える回避策:前処理としての「SHA-256」
では、この72バイト制限をクリアしつつ、bcryptの強力なストレッチング(パスワードハッシュの計算にわざと時間をかけさせて総当たりを難しくする仕組み)の恩恵を受けるにはどうすればよいでしょうか?
セキュリティ界隈で広く推奨されている現実的な解決策が、「bcryptに渡す前に、あらかじめ別のハッシュ関数(SHA-256など)で短く圧縮する」という前処理の手法です。
防御の仕組みを整理しよう
1. ユーザーの入力: 100バイトでも200バイトでも、どんなに長いパスワードでも受け付けます。
2. 前処理(SHA-256): 入力されたパスワードを SHA-256 に通します。SHA-256はどれだけ長い入力を入れても、出力されるハッシュ値は常に固定の32バイト(16進数表記で64文字)になります。
3. 本処理(bcrypt): この32バイトの綺麗に収まったデータを、bcryptに渡して安全にハッシュ化し、データベースに保存します。
「あれ? ハッシュの二重掛けって危なくないの?」と心配になるかもしれませんが、今回は「bcryptの文字数制限を回避するための一時的な圧縮」としてSHA-256を使っています。元のパスワードが持つエントロピー(ランダム性)を損なわずに、72バイトの壁を綺麗にすり抜けることができる非常にスマートな手法です。
—
実装サンプル(PHP)
百聞は一見にしかず。実際のPHPコードを用いて、この前処理の仕組みを見てみましょう。実務でそのまま参考にできるようにコメントを丁寧に添えています。
<?php
/**
* 安全なパスワードハッシュ化関数(bcryptの72バイト制限回避版)
*
* @param string $plainPassword ユーザーが入力した生のパスワード
* @return string ハッシュ化されたパスワード
*/
function createSecurePasswordHash(string $plainPassword): string {
// 【ステップ1】
// bcryptの72バイト制限を回避するため、まずSHA-256でハッシュ化(圧縮)します。
// SHA-256の出力は常に固定長(バイナリなら32バイト、16進数なら64文字)なので、
// どんなに長いパスワードが入力されても絶対に72バイトを超えません。
$preHashed = hash('sha256', $plainPassword, true); // 第3引数をtrueにすると生バイナリ(32バイト)で取得できます
// 【ステップ2】
// 32バイトに収まったデータを、パスワード専用の強力な金庫「bcrypt」に通します。
// PASSWORD_BCRYPTを指定し、コストパラメータ(処理の重さ)を適切に設定します。
$options = [
'cost' => 12, // 計算コスト(数字が大きいほど破られにくいがサーバー負荷が上がります)
];
$secureHash = password_hash($preHashed, PASSWORD_BCRYPT, $options);
return $secureHash;
}
/**
* パスワード検証関数
*
* @param string $plainPassword ログイン時にユーザーが入力した生のパスワード
* @param string $storedHash データベースに保存されているハッシュ値
* @return bool 一致していればtrue
*/
function verifyUserPassword(string $plainPassword, string $storedHash): bool {
// ログイン時も同様に、入力されたパスワードを同じ手順でSHA-256に通してから検証します
$preHashed = hash('sha256', $plainPassword, true);
return password_verify($preHashed, $storedHash);
}
// --- 実行例 ---
$userPassword = "ThisIsA_VeryLongAndSecurePasswordThatExceedsSeventyTwoBytesAndCouldPotentiallyCauseACollisionIssueIfDirectlyPassedToBcryptWithoutAnyPreProcessingStepsAtAll!";
// ハッシュを生成
$hashedPassword = createSecurePasswordHash($userPassword);
echo "生成されたハッシュ: " . $hashedPassword . "\n";
// 正しいパスワードで検証
if (verifyUserPassword($userPassword, $hashedPassword)) {
echo "ログイン成功:パスワードが一致しました!\n";
} else {
echo "ログイン失敗……。\n";
}
—
まとめ:一歩ずつ安全なシステムを作っていこう
今回は、bcryptが持つ「72バイト制限」という小さな仕様の落とし穴と、それをSHA-256の前処理でスマートに回避する方法について解説しました。
セキュリティの世界は、こうした「一見すると些細な仕様のすれ違い」が、思わぬ脆弱性につながることがよくあります。でも、恐れる必要はありません。仕組みを正しく理解し、適切な一手間(前処理やパラメータの調整)を加えることで、システムはぐっと強固になります。
「なぜこの処理を挟むのか?」という理由を一つずつ紐解きながら、一緒にセキュアな開発を楽しみましょう! それではまた次回の記事でお会いしましょう。
コメント