こんにちは!インフラや開発の現場に飛び込んだばかりの新人IT担当者の皆さん、そしてセキュリティの扉を叩いたばかりの皆さん、日々の業務お疲れ様です。
「パスワードはハッシュ化して安全に保存しましょう」
「ソルトを加えてストレッチングをしましょう」
セキュリティの入門書を開くと、決まり文句のようにこんな言葉が出てきますよね。「なんとなく大事そうだからコピペして実装しているけれど、裏側で何が起きているのか、正直ピンとこないな……」そんな風に感じていませんか?
大丈夫です、安心してください!今日は、認証の仕組みの中でも特に見落としががちな「ソルトの使い回しが引き起こす恐怖の罠」について、身近な防犯のたとえ話を交えながら、一歩ずつ一緒に紐解いていきましょう。
—
1. 家の鍵で例える「パスワードのハッシュ化」と「ソルト」
まずは、基本のおさらいから始めましょう。
皆さんがWebサービスにログインするとき、システムは皆さんの「パスワードそのもの」をデータベースに保存していません。もしそのまま保存していたら、データベースがハッキングされた瞬間に全員のパスワードが丸見えになってしまいますよね。それはまるで、自宅の合鍵を玄関のドアノブにぶら下げておくようなものです。
だからこそ、パスワードを「ハッシュ関数(SHA-2やSHA-3など)」という不可逆なミキサーにかけ、ぐちゃぐちゃの文字列(ハッシュ値)に変換して保存します。
彩りを加える「ソルト(Salt)」の役割
しかし、このハッシュ値、実は同じパスワード(例えば password123 など)を入れると、いつも「まったく同じぐちゃぐちゃの文字列」が出てきてしまいます。これを狙って、悪い奴らはあらかじめ「よく使われるパスワードと、そのハッシュ値の答え合わせリスト(レインボーテーブル)」を用意し、破ろうとします。
ここで登場するのが「ソルト(Salt:塩)」です。
ソルトとは、パスワードをミキサーにかける直前に、ユーザーごとに異なるランダムな文字列をパラパラとかけ合わせるテクニックのことです。
- ユーザーAのソルト:
x8F2 - ユーザーBのソルト:
y9Q1
たとえユーザーAとユーザーBが同じ password123 というパスワードを使っていても、ソルトが違うため、データベースに保存されるハッシュ値は全く別のものになります。これで、一括して攻撃されるリスクをぐっと減らせるわけです。
—
2. 泥棒は大喜び?「ソルトの使い回し」が招くリスク
さて、ここからが本題です。
「ユーザーごとにランダムなソルトを使うのが大事」と聞いて、「よし、毎回考えるのも面倒だし、システム全体で共通の決まったソルトを使えばラクチンだな!」……なんて思っていませんか?
もしそんな実装をしていたら、ホワイトハッカーの視点からは「どうぞ我が家の宝箱を盗んでくださいと言っているようなもの」に見えてしまいます。これが、今回のテーマである「ソルトの再利用リスク」です。
全員で同じ「合言葉」を使う危険性
もし、サービス全体や複数のユーザー間で「同じソルト(例えば固定の文字列 securesalt2024 など)」を使い回していたらどうなるでしょうか?
ユーザーごとの「バラエティ豊かなカギ」が失われ、全員が同じスパイス(ソルト)で味付けされた状態になります。こうなると、攻撃者はユーザー一人ひとりのパスワードを個別に攻撃する必要がなくなります。
1. 攻撃者がデータベースを盗み出す。
2. 固定されたソルトを使って、世の中によくあるパスワード(123456 や password など)のハッシュ値を一網打尽に計算する。
3. えっ、これだけでシステム全体のユーザーの半数以上のパスワードが芋づる式に破られてしまった……!
これが、ソルトの使い回しが引き起こす最悪のシナリオです。SCRAM(Salted Challenge Response Authentication Mechanism:データベースやメールサーバーなどの認証で使われる仕組み)などの高度なプロトコルにおいても、このソルトが「使い回し」されていたり、「予測可能(ランダム性が低い)」だったりすると、一瞬でセキュリティの防壁が崩れ去ってしまいます。
—
3. 実務で守るべきガイドライン:ランダム性と十分な長さ
では、私たちは現場でどのようにこのリスクを防げばよいのでしょうか?
答えはシンプルです。「ユーザーごとに、予測不可能な、十分な長さのソルトをその都度(オンザフライで)生成する」ことです。
実務における具体的なポイントをいくつか見ていきましょう。
① 暗号学的に安全な擬似乱数生成器(CSPRNG)を使う
プログラミング言語の標準的なランダム関数(例えばJavaScriptの Math.random() など)は、実は予測が簡単なのでセキュリティ用途には使えません。必ず、暗号学的に安全な仕組み(Node.jsの crypto モジュールや、PHPの random_bytes() など)を使いましょう。
② 十分な長さを確保する
ソルトが短すぎると、コンピューターの力技(総当たり攻撃)ですぐに見破られてしまいます。一般的に、ソルトは最低でも16バイト(128ビット)以上の長さを確保することが推奨されています。
—
4. 実装サンプル:安全なソルト生成とパスワード保存のイメージ
言葉だけではイメージしにくいと思いますので、安全なソルトの生成とハッシュ化(ストレッチング含む)を行う実務的なコードの例をPythonで見てみましょう。
import os
import hashlib
import hmac
def create_secure_password_hash(password: str) -> tuple:
"""
パスワードに対する安全なハッシュ値と、一意のソルトを生成する関数
"""
# 1. 暗号学的に安全なランダムなソルトを16バイト(128ビット)生成する
# ※使い回しは絶対にNG!ユーザーごとに必ず新規生成します。
salt = os.urandom(16)
# 2. ストレッチング(PBKDF2などのアルゴリズムを使い、ハッシュ計算をあえて何度も繰り返す)
# これにより、攻撃者が総当たり攻撃を行う際のコストを跳ね上げます。
# SHA-256を使用し、100,000回反復計算を実行
iterations = 100000
password_hash = hashlib.pbkdf2_hmac(
'sha256',
password.encode('utf-8'),
salt,
iterations
)
# 3. データベースには「ソルト」「反復回数」「ハッシュ値」をセットで保存する
return salt, iterations, password_hash
# --- 使用例 ---
# ユーザーが新規登録またはパスワード変更を行った時
user_password = "MySuperSecretPassword123!"
salt, iterations, stored_hash = create_secure_password_hash(user_password)
print(f"生成されたソルト (Hex): {salt.hex()}")
print(f"保存されるハッシュ (Hex): {stored_hash.hex()}")
このコードでは、os.urandom(16) を使うことで、実行するたびに完全にユニークで予測不可能なソルトが生成されます。さらに、pbkdf2_hmac という関数を使ってストレッチング(何十万回も計算を繰り返す処理)を行い、攻撃者に膨大な時間と計算コストを強いる仕組みにしています。
—
まとめ
いかがでしたでしょうか?今回は「ソルトの再利用リスク」について、防衛の基本から実務的なコードの書き方までを解説しました。
- ソルトの使い回しは、全員で同じ合鍵を使うようなもの!
- ユーザーごとに、必ず「暗号学的に安全なランダムな値」を新規生成する。
- 十分な長さとストレッチングを組み合わせて、攻撃者のコストを徹底的に跳ね上げる。
セキュリティの世界は一見すると複雑で難しく感じられますが、「なぜそれが必要なのか」という理由(文脈)さえ分かってしまえば、日々のコーディングやインフラ構築がぐっと面白くなります。
一歩ずつ、確実に安全なシステムを作れるエンジニアを目指して、一緒にスキルアップしていきましょう!次の現場での実装にも、ぜひこの知識を生かしてみてくださいね。
コメント