awesome-hacks
Docs

パスワードハッシュ化の基礎

平文パスワードをDBに保存しない理由と、bcryptでハッシュ化・照合する流れを理解する

最終更新:2026/05/13

パスワードハッシュ化とは

パスワードハッシュ化とは、ユーザーが入力したパスワードを 元に戻せない文字列 に変換して保存すること。

たとえばユーザーが 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 -> hashB

bcryptでは、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);

oktrue ならパスワードが一致、false なら不一致。

実務で気をつけること

  • パスワードをログに出さない
  • passwordHash をAPIレスポンスに含めない
  • passwordHash をフロントエンドへ渡さない
  • bcrypt.hash(password, 10) のように、現実的な計算コストを設定する
  • パスワードリセットや漏洩時の対応は別途設計する

参考