こんにちは!セキュリティバイブル主筆の〇〇です。皆さんの大切なシステムやサービスを守るために、日々奮闘されている開発者やIT担当者の皆さん、お疲れ様です!
今回は、私たちが普段何気なく使っている「パスワード」について、その安全な保管方法、つまり「暗号学的ハッシュ関数を用いたパスワードストレージの設計」という、ちょっと専門的なテーマに挑んでいきましょう。
「暗号学的ハッシュ関数?」「ストレージ?」って、なんだか小難しそうな言葉が並んでいて、「うわ、もう無理!」って思っちゃった人もいるかもしれませんね。でも大丈夫!ご安心ください。私は皆さんがセキュリティの第一歩を踏み出すお手伝いをしたいんです。
この記事では、皆さんの大切な「家の鍵」や「金庫」、そしてそれを狙う「泥棒さん」に例えながら、パスワードがどう攻撃され、どう守るべきかを、現場の泥臭い経験に基づいたリアルな視点から、徹底的にわかりやすく解説していきます。
さあ、一歩ずつ、セキュリティの知識を深めていきましょう!
—
🔒 パスワードは「デジタルな家の鍵」:なぜ厳重に守る必要があるの?
まず最初に、皆さんに考えてほしいことがあります。皆さんの家には、もちろん鍵がありますよね? その鍵を、玄関マットの下に隠したり、近所の人全員に教えたりしますか? ほとんどの人が「そんなことしない!」と答えるはずです。
それと同じで、私たちがインターネット上で使うパスワードも、言わば「デジタルな家の鍵」なんです。銀行口座、SNS、仕事のシステム……これら全てが、パスワードという名の鍵で守られた、あなたの「大切な情報が入った家」なんですよね。
もし、このデジタルな鍵が泥棒(サイバー攻撃者)の手に渡ってしまったら、どうなるでしょう? そう、あなたのプライベートな情報が盗まれたり、大切なアカウントが乗っ取られたり、最悪の場合、金銭的な被害に遭ってしまう可能性だってあるんです。
だからこそ、私たちはパスワードを最高のセキュリティレベルで守る責任があるんです。
—
🕵️♀️ 泥棒はどこを狙う?「パスワードをそのまま保存」が一番危険な理由
「パスワード、ちゃんとシステムに登録してるから大丈夫でしょ?」
そう思っているとしたら、それはちょっと危険な考え方かもしれません。実は、システムの裏側で皆さんのパスワードを「そのままの形(平文と呼びます)」で保存している、なんてことが時々起こっているんです。
これは、例えるなら「家の鍵を、鍵の形そのままコピーして、金庫にしまっている」ようなもの。
もし、泥棒がその金庫(データベース)をこじ開けてしまったらどうなると思いますか? そう、全ての鍵が、そのままの形で泥棒の手に渡ってしまいますよね。泥棒はそれを元に、何の苦労もなく、あなたの家(アカウント)に侵入し放題、ということになってしまうんです。
過去には、有名企業のシステムからパスワードが漏洩し、そのパスワードが平文で保存されていたために、何百万、何千万というユーザーのパスワードが「丸見え」になってしまった、という痛ましい事件も何度か発生しています。これは、セキュリティを現場で担当している私たちにとっては、本当に悪夢のような話なんです。
だから、パスワードをそのまま保存する、というのは絶対に避けなければいけない、セキュリティの基本中の基本なんです。
—
🔑 解決策その1:パスワードを「指紋」に変える「ハッシュ関数」って何?
では、どうすればパスワードを安全に保存できるのでしょうか?
ここで登場するのが、今回の主役の一つ「暗号学的ハッシュ関数」です!
「また難しい言葉が…」って思ったかもしれませんね。でも大丈夫。これはとってもシンプルな仕組みなんですよ。
例えるなら、ハッシュ関数は「パスワードの指紋を取る機械」だと思ってください。
あなたがパスワード(例:「MySecretPassword123!」)を入力すると、この機械はそれを複雑な計算によって、全く別の、決まった長さの文字列(例:「$2a$10$abcdefghijklmnopqrstuvwxyz…」)に変換します。これが「ハッシュ値」と呼ばれるものです。
この「指紋」には、大きく分けて3つの特徴があります。
1. 一方向性(元に戻せない): 指紋から元の指の形を完全に復元できないように、ハッシュ値から元のパスワードを復元することは、非常に困難(事実上不可能)です。
2. 入力が少しでも変わると出力が全く変わる: たとえパスワードが「MySecretPassword123!」から「MySecretPassword123!!」と、たった一文字変わっただけでも、出力されるハッシュ値は全く別のものになります。
3. 同じ入力からは必ず同じ出力: 同じパスワードを何度入れても、常に同じハッシュ値が出力されます。
システムは、皆さんのパスワードをそのまま保存するのではなく、この「ハッシュ値」だけを保存します。そして、次に皆さんがログインする際には、入力されたパスワードから再びハッシュ値を生成し、保存されているハッシュ値と一致するかどうかを比較する、という仕組みなんです。
これなら、泥棒がデータベースを盗んだとしても、手に入るのは「指紋」だけで、そこから元のパスワードを割り出すことはできない、というわけですよね!
⚠️ ちょっと待って!「弱いハッシュ関数」には落とし穴が…
「じゃあ、ハッシュ関数を使えば全部解決じゃん!」って思いましたか? 残念ながら、話はそう単純ではありません。
かつては「MD5」や「SHA-1」といったハッシュ関数が使われていましたが、これらは現在では「非常に危険」とされています。なぜでしょう?
それは、泥棒も進化しているからです。彼らは、あらかじめ大量のパスワードとそのハッシュ値を計算しておき、それをリスト化した「レインボーテーブル」と呼ばれるものを作成するようになりました。
例えるなら、泥棒が「よくある鍵の形とその指紋のリスト」を大量に持っているようなものです。データベースからハッシュ値(指紋)を盗んだら、そのリストと照合して、元のパスワード(鍵の形)を割り出そうとするんです。
特に、MD5やSHA-1は計算が速すぎて、現代のコンピュータの性能をもってすれば、このレインボーテーブル攻撃や、一つずつパスワードを試していく「ブルートフォース攻撃(総当たり攻撃)」に簡単に突破されてしまうんです。
だからこそ、私たちは「より強力で、攻撃に時間のかかるハッシュ関数」を選ぶ必要があるんです。
—
🌈 攻撃者の盲点を作る!「ソルト」という名の隠し味
泥棒が「よくある鍵の形とその指紋のリスト(レインボーテーブル)」を持っているなら、どうすればいいでしょう?
そう、「一つ一つの鍵の形に、少しずつ違う加工を施して、全く異なる指紋を作り出せばいい」んです!
この「少しずつ違う加工」こそが、セキュリティ用語で言うところの「ソルト(Salt)」になります。
ソルトとは、パスワードをハッシュ化する際に、パスワードとは別に付与される、ランダムでユニークなデータのことです。
仕組みはこうです。
1. ユーザーがパスワード(例:「MySecretPassword123!」)を入力します。
2. システムは、その都度、ランダムなソルト(例:「sdg87y3h」)を生成します。
3. パスワードとソルトを混ぜ合わせ(例:「MySecretPassword123!sdg87y3h」)、これをハッシュ関数に入力してハッシュ値(例:「$2a$10$abcdefghijklmnopqrstuvwxyz…」)を生成します。
4. システムは、生成されたハッシュ値と、使用したソルトの両方をデータベースに保存します。
このようにすることで、たとえ同じパスワードを使っているユーザーが複数人いたとしても、それぞれに異なるソルトが付与されるため、生成されるハッシュ値は全く別のものになります。
「MySecretPassword123!」 + 「ソルトA」 → ハッシュ値X
「MySecretPassword123!」 + 「ソルトB」 → ハッシュ値Y
これによって、泥棒がせっかく苦労して作った「レインボーテーブル」は、ほとんど役に立たなくなります。なぜなら、泥棒が盗んだハッシュ値は、それぞれ固有のソルトと混ざり合っているため、レインボーテーブルにある「ソルトなしのハッシュ値」とは一致しないからです。泥棒は、一つ一つのハッシュ値に対して、そのハッシュ値がどのソルトと混ざっていたかを推測し、その上でレインボーテーブルを再構築するか、総当たり攻撃を仕掛けるしかなくなります。これは非常に時間とコストがかかる作業なんです。
ポイント: ソルトは、ユーザーごとにランダムでユニークなものを生成し、パスワードハッシュ値と一緒に保存してください。決して使い回したり、固定値にしたりしてはいけません!
—
💪 攻撃者に「時間稼ぎ」をさせる!計算コストの高いハッシュ関数
ソルトでレインボーテーブル攻撃は防げました。しかし、泥棒は諦めません。「じゃあ、一つ一つパスワードを試していけばいいじゃないか!(ブルートフォース攻撃)」と考えます。
そこで次に登場するのが、「意図的に計算が遅くなるように設計されたハッシュ関数」です!
これは例えるなら、「鍵開けにものすごく時間がかかる金庫」のようなものです。
もし金庫をこじ開けるのに1秒かかるとして、泥棒は1秒で1つの鍵を試せます。しかし、もし金庫をこじ開けるのに10秒かかるなら、泥棒は10秒で1つの鍵しか試せません。つまり、同じ時間でも、試せる鍵の数が10分の1になるわけです。
パスワードのハッシュ化もこれと同じ。わざと計算に時間とリソース(CPUやメモリ)を消費させることで、泥棒がブルートフォース攻撃を仕掛けてきたときに、一つ一つのパスワードを試すのにかかる時間を大幅に増やし、攻撃の効率を極端に落とすことができるんです。
現在、パスワードストレージに推奨されているのは、主に以下の2つの強力なハッシュ関数です。
1. Argon2id:
- 2015年にパスワードハッシュ化コンテストで優勝した、最も新しい、そして最も推奨されているハッシュ関数です。
- CPUの計算コスト、メモリの使用量、そして並列処理の3つの要素を調整でき、ブルートフォース攻撃だけでなく、高性能なGPUを使った攻撃や、大量のメモリを消費する攻撃にも非常に強い耐性を持っています。
2. bcrypt:
- 古くから使われていますが、今なお非常に強力で信頼性の高いハッシュ関数です。
- CPUの計算コストを調整する「コストファクター」という仕組みがあり、この値を大きくすることで、ハッシュ化にかかる時間を長くすることができます。
どちらも、私たちがパスワードを登録する際には少し時間がかかりますが、これは「安全性を高めるための大切な待ち時間」なんです。
🚨 最重要ポイント:パラメーター設定は「生きたセキュリティ」
Argon2idもbcryptも、その強さは「パラメーター設定」によって大きく変わります。
- Argon2idであれば、
time_cost(計算反復回数)、memory_cost(メモリ使用量)、parallelism(並列処理数) - bcryptであれば、
rounds(コストファクター)
これらのパラメーターを適切に設定することで、攻撃者がパスワードを破るために必要な時間とコストを、劇的に増大させることができます。
現場での泥臭い知見から言うと、このパラメーター設定は「一度決めたら終わり」ではありません。 ハードウェアの進化や、攻撃者の技術の向上に合わせて、定期的に見直し、必要に応じて引き上げていく必要があります。例えば、5年前の推奨値が、今の最新のGPUにとっては「鼻歌交じりで突破できる」レベルになっている、なんてことはザラにあるんです。
皆さんのシステムが稼働するサーバーのスペックと、ユーザー体験(パスワード登録やログインにかかる時間)のバランスを見ながら、可能な限り高い設定値を採用することを目指しましょう。目安としては、パスワードハッシュ化に数百ミリ秒〜1秒程度かかるのが理想的とされています。
—
🧑💻 実践編:Argon2idとbcryptを使ってみよう!
では、実際にこれらの強力なハッシュ関数をどう使うのか、Pythonを例に見ていきましょう。
1. Argon2id の実装例(Python + passlib ライブラリ)
Pythonでは passlib という高機能なパスワードハッシュライブラリが便利です。
まず、ライブラリをインストールします。
pip install passlib
次に、コード例です。
from passlib.hash import argon2
— パスワードのハッシュ化 —
def hash_password_argon2(password: str) -> str:
“””
Argon2id を使用してパスワードをハッシュ化します。
最適なセキュリティとパフォーマンスのバランスを取るために、パラメーターを設定します。
“””
# ここがパラメーター設定の肝です!
# time_cost: 計算反復回数。数字が大きいほど計算に時間がかかり、攻撃に強くなります。
# memory_cost: メモリ使用量。数字が大きいほどメモリを消費し、GPUを使った攻撃に強くなります。
# parallelism: 並列処理数。CPUコア数に合わせて調整します。
# サーバーのCPU性能やメモリ量、ユーザーの待ち時間(ログイン処理時間)を考慮して調整してください。
# 最新の推奨値やハードウェアの進化に合わせて、定期的に見直すことが重要です。
hashed_password = argon2.using(
time_cost=10, # 例: 10回の反復
memory_cost=65536, # 例: 64MBのメモリ (2^16)
parallelism=2 # 例: 2つのCPUコアを使用
).hash(password)
print(f”Argon2idでハッシュ化されたパスワード: {hashed_password}”)
return hashed_password
— パスワードの検証 —
def verify_password_argon2(password: str, hashed_password: str) -> bool:
“””
入力されたパスワードが、保存されているハッシュ値と一致するかを検証します。
“””
try:
# verify()メソッドが、自動的にハッシュ値からソルトやパラメーターを読み取り、
# 正しいパスワードと比較してくれます。
is_valid = argon2.verify(password, hashed_password)
if is_valid:
print(“Argon2id: パスワードは一致しました。”)
else:
print(“Argon2id: パスワードが一致しませんでした。”)
return is_valid
except ValueError:
# ハッシュ値のフォーマットが不正な場合などに発生
print(“Argon2id: ハッシュ値のフォーマットが不正です。”)
return False
実行例
user_password = “MySuperSecretPassword123!”
stored_hash_argon2 = hash_password_argon2(user_password)
ログイン時の検証
is_correct = verify_password_argon2(user_password, stored_hash_argon2)
wrong_password = verify_password_argon2(“WrongPassword”, stored_hash_argon2)
2. bcrypt の実装例(Python + bcrypt ライブラリ)
bcryptも同様に、専用のライブラリがあります。
まず、ライブラリをインストールします。
pip install bcrypt
次に、コード例です。
import bcrypt
— パスワードのハッシュ化 —
def hash_password_bcrypt(password: str) -> str:
“””
bcrypt を使用してパスワードをハッシュ化します。
“””
# cost: 計算コストファクター。2^cost の反復回数になります。
# 数字が大きいほど計算に時間がかかり、攻撃に強くなります。
# サーバーのCPU性能やユーザーの待ち時間を考慮して調整してください。
# 少なくとも10〜12以上の値を推奨しますが、これも定期的な見直しが必要です。
# 注意: bcryptのhashpwはバイト列を期待するため、エンコードが必要です。
hashed_password = bcrypt.hashpw(password.encode(‘utf-8’), bcrypt.gensalt(rounds=12))
print(f”bcryptでハッシュ化されたパスワード: {hashed_password.decode(‘utf-8’)}”)
return hashed_password.decode(‘utf-8’) # データベース保存のため文字列にデコード
— パスワードの検証 —
def verify_password_bcrypt(password: str, hashed_password: str) -> bool:
“””
入力されたパスワードが、保存されているハッシュ値と一致するかを検証します。
“””
# checkpwもバイト列を期待するため、エンコードが必要です。
is_valid = bcrypt.checkpw(password.encode(‘utf-8’), hashed_password.encode(‘utf-8’))
if is_valid:
print(“bcrypt: パスワードは一致しました。”)
else:
print(“bcrypt: パスワードが一致しませんでした。”)
return is_valid
実行例
user_password_b = “MySuperSecretPassword123!”
stored_hash_bcrypt = hash_password_bcrypt(user_password_b)
ログイン時の検証
is_correct_b = verify_password_bcrypt(user_password_b, stored_hash_bcrypt)
wrong_password_b = verify_password_bcrypt(“WrongPassword”, stored_hash_bcrypt)
重要な注意点:
- これらのコードはあくまで例です。実際のプロダクション環境では、エラーハンドリングやログ出力などを適切に実装してください。
- パスワードを直接ログに出力したり、ネットワーク経由で平文で送信したりすることは絶対に避けてください。
—
🛡️ さらにもう一歩!複合的な防御でシステムを守り抜く
パスワードストレージの設計は、セキュリティの非常に重要な柱の一つです。しかし、これで全てが解決するわけではありません。サイバー攻撃者は常に、システムの最も弱いリンクを探しています。
CISSPホルダーとして、そして現場で数々のインシデントを経験してきた者として言えるのは、「セキュリティは複合的な防御の組み合わせである」ということです。
パスワードストレージを強固にしても、以下のような対策がなければ、システムはまだ危険に晒される可能性があります。
- 強力なパスワードポリシーの強制:
- ユーザーに「推測されにくい、長く複雑なパスワード」を設定させる仕組みを導入しましょう。
- ただし、「毎月パスワード変更」のような厳しすぎるポリシーは、かえってユーザーが簡単なパスワードにしたり、メモに書き残したりする原因になることもあります。バランスが重要です。
- 多要素認証(MFA)の導入:
- パスワードが万が一漏洩してしまっても、もう一つの認証要素(スマホの認証アプリ、生体認証など)がなければログインできないようにする、非常に強力な対策です。これはもはや、どんなシステムでも必須級の対策と言えるでしょう。
- アカウントロックアウトポリシー:
- 一定回数パスワードを間違えたら、そのアカウントを一時的にロックする機能です。これにより、ブルートフォース攻撃の効率をさらに下げることができます。ただし、攻撃者が意図的に特定のアカウントをロックさせる「サービス妨害(DoS)攻撃」に利用されないよう、しきい値やロック期間には注意が必要です。
- パスワード漏洩検知サービスとの連携:
- ユーザーが設定したパスワードが、過去のデータ漏洩で公開されたパスワードリストに含まれていないかチェックするサービス(例:Have I Been Pwned APIなど)と連携し、危険なパスワードの設定をブロックすることも有効です。
これら全てを組み合わせることで、初めて「強固な防御体制」が築かれるんです。
—
🚀 まとめ:セキュリティの旅は始まったばかり!
今回は、パスワードの安全な保管方法について、泥棒と鍵の例えを交えながら、Argon2idやbcrypt、そしてソルトの重要性を解説してきました。
おさらいしましょう。
1. パスワードは平文で保存しない:泥棒に鍵そのものを渡すようなものです。
2. ハッシュ関数で「指紋」に変換する:一方向性で元に戻せない特性を利用します。
3. 「ソルト」でレインボーテーブル攻撃を防ぐ:ユーザーごとにユニークなソルトを付与し、ハッシュ値を個別化します。
4. 計算コストの高いハッシュ関数(Argon2id/bcrypt)でブルートフォース攻撃を遅らせる:意図的に計算時間を長くし、攻撃者のコストを増大させます。
5. パラメーター設定は定期的に見直す:常に最新の推奨値やハードウェアの進化に合わせて、設定値を最適化しましょう。
6. パスワードストレージは複合的な防御の一部:MFA、アカウントロックアウトなど、多層的なセキュリティ対策と組み合わせることで真に強固なシステムになります。
セキュリティは、一度対策したら終わり、というものではありません。常に進化する攻撃手法に対応し、システムを継続的に強化していく「旅」のようなものです。
今日の学びが、皆さんのシステムをより安全にするための一助となれば、これほど嬉しいことはありません。
これからも、皆さんと一緒にセキュリティの知識を深め、より安全なデジタル社会を築いていけるよう、私もしっかりと情報発信を続けていきますね。
一歩ずつ、着実にセキュリティのスキルを身につけていきましょう!
それではまた、次回の記事でお会いしましょう!
コメント