パスワードハッシュ化の基礎
平文パスワードをDBに保存しない理由と、bcryptでハッシュ化・照合する流れを理解する
パスワードハッシュ化とは
パスワードハッシュ化とは、ユーザーが入力したパスワードを 元に戻せない文字列 に変換して保存すること。
たとえばユーザーが password123 と入力しても、DBにはそのまま保存しない。
password123
↓ ハッシュ化
$2b$10$xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxこの変換後の文字列を パスワードハッシュ と呼ぶ。
何のためにやるのか
一番の目的は、DBが漏れたときにユーザーのパスワードを守るため。
もしDBに平文パスワードを保存していると、DBが漏れた瞬間に次のような被害が起きる。
- そのサービスのアカウントにログインされる
- 同じパスワードを使い回している別サービスにも侵入される
- 運営側がユーザーのパスワードを読めてしまう
なので、実務ではパスワードを 平文のまま保存しない のが大前提。
ハッシュ化は暗号化と違う
ここは混同しやすい。
- 暗号化 … 鍵があれば元に戻せる
- ハッシュ化 … 基本的に元に戻せない
パスワード保存では「元に戻せる」必要がない。ログイン時に必要なのは、入力されたパスワードが正しいかどうかを確認することだけ。
そのため、DBには「元に戻せないハッシュ」を保存する。
bcryptを解析すれば復号できるのでは?
できない。
理由は、bcryptが「復号用の鍵を使って元に戻す処理」ではなく、元の情報を捨てながら別の値に変換する処理だから。
暗号化なら、仕組みと鍵があれば元に戻せる。
平文 password123
↓ 暗号化(鍵あり)
暗号文 xxxxx
↓ 復号(同じ鍵など)
平文 password123一方、ハッシュ化は元に戻す経路を持たない。
平文 password123
↓ bcrypt
ハッシュ $2b$10$...
↓ 復号?
できないbcryptのアルゴリズム自体は公開されている。攻撃者も仕組みを知っている前提で設計されている。
それでも復号できないのは、ハッシュ値の中に「元のパスワードを取り出すための情報」が残っていないから。
ただし、推測して当てることはできる。
たとえば攻撃者が「もしかして password123 では?」と推測し、その候補をbcryptにかけて、保存済みハッシュと一致するかを試すことはできる。これが総当たり攻撃や辞書攻撃。
候補 password123
↓ bcrypt.compare
保存済みハッシュと一致する?だからbcryptは、計算コストを重くして大量試行を遅くする。
「復号できない」ことと「弱いパスワードなら推測される可能性がある」ことは別問題。
ログイン時はどう照合するのか
「元に戻せないなら、どうやってログイン判定するの?」と思うかもしれない。
流れは次の通り。
ユーザー登録時:
1. ユーザーが password123 を送る
2. サーバーが bcrypt.hash(password123) する
3. DBに passwordHash を保存する
ログイン時:
1. ユーザーが password123 を送る
2. DBから passwordHash を取り出す
3. bcrypt.compare(password123, passwordHash) で一致判定するつまり、保存済みハッシュを復号するのではなく、bcryptの比較関数で「この入力パスワードは、このハッシュと対応しているか」を確認する。
bcryptとは
bcryptは、パスワード保存向けによく使われるハッシュ化アルゴリズム。
普通のハッシュ関数(例: SHA-256)をそのままパスワード保存に使うより、パスワード用途に向いた性質を持っている。
代表的な特徴は次の2つ。
- saltを含められる
同じパスワードでも、毎回違うハッシュになりやすい。これにより、よくあるパスワードの事前計算攻撃に強くなる。 - 計算コストを調整できる
bcrypt.hash(password, 10)の10のような値で、計算の重さを調整できる。攻撃者が大量に総当たりしにくくなる。
saltとは
saltは、ハッシュ化するときに混ぜるランダムな値。
たとえば、同じ password123 でもsaltが違うと、保存されるハッシュは変わる。
password123 + saltA -> hashA
password123 + saltB -> hashBbcryptでは、salt情報もハッシュ文字列の中に含まれる。そのため、通常はsaltを別カラムで保存しなくてもよい。
passwordHashは返してよいのか
返さない。
passwordHash は平文パスワードではないが、公開してよい情報でもない。APIレスポンス、画面表示、ログ、監視ツール、管理画面などに不用意に出さない。
理由は、passwordHash が漏れると攻撃者が手元でパスワード候補を試せるから。
漏れた passwordHash
候補 password123
↓
bcryptで一致するか試すこれはサーバーにログインリクエストを投げ続ける攻撃とは違い、攻撃者の手元で試せる。これを オフライン攻撃 と呼ぶ。
bcryptは計算を重くして総当たりを遅くするが、passwordHash が漏れてよいという意味ではない。
基本方針は次の通り。
- DBには
passwordHashを保存する - APIレスポンスには含めない
- フロントエンドへ渡さない
- ログに出さない
- 管理画面にも原則表示しない
「平文ではないから安全」ではなく、漏らさない前提で扱う秘密寄りのデータ と考える。
NestJS記事での使い方
NestJS Guard / JWT認証では、ユーザー作成時に次のように使う。
const passwordHash = await bcrypt.hash(password, 10);DBには password ではなく passwordHash を保存する。
ログイン時は次のように照合する。
const ok = await bcrypt.compare(dto.password, user.passwordHash);ok が true ならパスワードが一致、false なら不一致。
実務で気をつけること
- パスワードをログに出さない
passwordHashをAPIレスポンスに含めないpasswordHashをフロントエンドへ渡さないbcrypt.hash(password, 10)のように、現実的な計算コストを設定する- パスワードリセットや漏洩時の対応は別途設計する