← 解説「パスワードのハッシュ化 — bcrypt」に戻る
サンプルコードで身につける: パスワードのハッシュ化 — bcrypt
解説で学んだ概念を、5つの具体例で確認します。素朴な例から実務寄りの例へと進みます。
1なぜ平文保存が危険か
パスワードを平文のまま持つとどうなるかをコメントで整理した例です。DB が漏えいした瞬間に全員のパスワードがそのまま読めてしまう、という最悪の事態を避けるのがハッシュ化の出発点です。
// 悪い例: パスワードを平文のまま保存する
// users テーブル
// id | email | password
// ---+-------------------+-----------
// 1 | alice@example.com | hunter2 ← 漏えいで即読める
// 2 | bob@example.com | password123 ← 使い回しなら他サービスも危険
//
// 良い例: ハッシュ化した「元に戻せない文字列」を保存する
// id | email | password_hash
// ---+-------------------+--------------------------------
// 1 | alice@example.com | $2b$10$N9qo8uLOickgx2ZMRZo...
//
// ハッシュは一方向: 保存値から元のパスワードは復元できない
2ハッシュ化と照合の最小形
bcrypt の hash() で保存用の値を作り、compare() で照合する基本2メソッドを示す例です。保存時はハッシュ化、ログイン時は平文に戻さず照合、という一方向の流れがパスワード管理の型です。
import * as bcrypt from "bcrypt";
// 登録時: 平文をハッシュ化して保存する
async function register(plain: string): Promise<string> {
const hash = await bcrypt.hash(plain, 10); // 10 = saltRounds
return hash; // この文字列を DB に保存する
}
// ログイン時: 入力と保存済みハッシュを照合する(復号はしない)
async function login(plain: string, stored: string): Promise<boolean> {
return bcrypt.compare(plain, stored); // 一致すれば true
}
3ソルトが毎回違うことを再現する
bcrypt が同じパスワードでも毎回違うハッシュを生成する理由「ソルト」を、プレーン TypeScript で再現した例です。ランダムなソルトを混ぜるため保存値は毎回変わり、事前計算したレインボーテーブル攻撃が効かなくなります。
TypeScript
4MD5/SHA-256 を使ってはいけない理由
汎用ハッシュ(MD5/SHA-256)がパスワード保存に不向きな理由を整理した例です。これらは高速なのが取り柄ですが、その速さこそが総当たり攻撃を助けてしまうため、わざと遅い bcrypt や argon2 を使います。
// パスワード保存に汎用ハッシュ(MD5 / SHA-256)を使うのは誤り
//
// SHA-256: 1秒間に数十億回計算できる = 攻撃者も高速に総当たりできる
// 攻撃者の試行イメージ
// sha256("aaaa") → 不一致
// sha256("aaab") → 不一致 … 1秒で数十億通り試せてしまう
//
// bcrypt / argon2: あえて遅く・メモリを食うよう設計されている
// saltRounds=10 なら1回の計算に数十ミリ秒かかる
// → 総当たりが現実的でなくなる(これが狙い)
//
// 結論: パスワードは必ずパスワード専用のハッシュ関数を使う
5登録とログインをサービスにまとめる
ユーザー登録とパスワード検証を1つのサービスにまとめた実務寄りの例です。ハッシュ化と照合をこのサービスに閉じ込め、平文パスワードがここから外へ漏れないようにするのが設計の要点です。
import { Injectable, UnauthorizedException } from "@nestjs/common";
import * as bcrypt from "bcrypt";
type StoredUser = { id: number; email: string; passwordHash: string };
@Injectable()
export class AuthService {
constructor(private readonly users: UsersRepository) {}
// 登録: 平文は即ハッシュ化し、ハッシュだけを保存する
async register(email: string, plain: string): Promise<void> {
const passwordHash = await bcrypt.hash(plain, 10);
await this.users.create({ email, passwordHash });
}
// 検証: ユーザーを引いて compare、失敗時は同じ 401 で返す
async validateUser(email: string, plain: string): Promise<StoredUser> {
const user = await this.users.findByEmail(email);
// 「該当なし」も「パスワード不一致」も区別せず弾く(情報を漏らさない)
if (!user || !(await bcrypt.compare(plain, user.passwordHash))) {
throw new UnauthorizedException("メールまたはパスワードが違います");
}
return user;
}
}