💭

bcryptで学ぶ安全なパスワード保管方法

2025/02/24に公開

はじめに

フロントエンド開発者としてJWT(JSON Web Token)を学習する中で、認証システムの核心である「パスワードの保管方式」について深く理解する必要性を感じました。特にbcryptがなぜ使用されているのか、どのようなセキュリティ問題を解決するのかについてまとめてみました。

ハッシュ関数を使用したパスワード保存の基本概念

  • 一般的なハッシュの脆弱性(レインボーテーブル、ブルートフォース攻撃)
  • bcryptによるこれらの問題の解決方法
  • 実装時の考慮事項
  • 他のハッシュアルゴリズムとの比較

この記事の対象者

  • JWTやセッションベース認証を実装するフロントエンド開発者
  • バックエンドとの協業時にセキュリティ関連のコミュニケーションが必要な方
  • パスワードハッシュについて概念的に理解したい方

1. パスワードハッシュの必要性

ハッシュ(Hash)は一方向暗号化であり、元のデータからハッシュ値を生成できますが、ハッシュ値から元のデータを復元することはできません。パスワードをハッシュ化して保存する理由は以下の通りです:

  1. データ漏洩時の元パスワード保護

    • DBが漏洩しても実際のパスワードは分からない
    • 他のサービスで同じパスワードを使用している場合も保護
  2. サービス提供者も元パスワードを知ることができない

    • セキュリティの観点から、元のパスワードはユーザーのみが知るべき
    • 内部者による悪用を防止

2. 一般的なハッシュの二つの主要な問題

2.1 レインボーテーブル攻撃(Rainbow Table Attack)

問題の状況

// ハッカーが事前に用意したレインボーテーブル
const rainbowTable: Record<string, string> = {
  '5f4dcc3b5aa765d61d8327deb882cf99': 'password',
  '482c811da5d5b4bc6d497ffa98491e38': 'password123',
  '21232f297a57a5a743894a0e4a801fc3': 'admin',
  // ... 数百万の事前計算されたハッシュ値
};

// DB漏洩時に簡単に元のパスワードを特定可能
const hackPassword = (hash: string): string | null => {
  return rainbowTable[hash] || null;
};

2.2 総当たり攻撃(Brute Force Attack)

問題の状況

// ハッカーのブルートフォース攻撃例
const bruteForce = (hash: string): void => {
  for(let i = 0; i < 1000000; i++) {
    const guess = `password${i}`;
    const guessHash = md5(guess);  // 1秒間に数百万回の試行が可能
    
    if(guessHash === hash) {
      console.log('パスワード発見:', guess);
      break;
    }
  }
};

3. bcryptの解決戦略

3.1 Saltによるレインボーテーブル攻撃への対策

概念説明

Saltとは、パスワードをハッシュ化する前に追加される、ユーザーごとにユニークなランダムな文字列です。以下の特徴があります:

  • パスワードごとに異なるユニークな値が生成される
  • ハッシュ値と一緒にデータベースに保存される
  • 同じパスワードでも異なるハッシュ値が生成されるため、レインボーテーブルの事前計算を無効化する
  • bcryptが自動的に生成・管理するため、開発者が明示的に管理する必要がない

技術的実装

// Saltを使用したハッシュ生成
const hash1 = await bcrypt.hash('password123', 10);
// -> '$2b$10$LQv3c1yqBWVHxkd0LHAkCOYz6TtxMQJqhN8/LcdYwXoP4m4eC5/.'

const hash2 = await bcrypt.hash('password123', 10);
// -> '$2b$10$9oZhlzXQwtfa8UQNCe9ALOq7.5HgyfIVJ.anKR2mz9tBvJgFFJnB6'

3.2 roundsによるブルートフォース攻撃への対策

概念説明

roundsは、ハッシュ化の計算回数を指定するパラメータです。以下の特徴があります:

  • 2の累乗で指定され、値が大きいほど計算時間が長くなる
  • 正常なログイン時は許容できる程度の遅延
  • 総当たり攻撃時は膨大な時間が必要になる
  • サーバーの性能と要件に応じて調整可能
  • ハードウェアの性能向上に合わせて値を調整することで、一定のセキュリティ強度を維持できる

技術的実装

const rounds = 12;  // 2^12 = 4,096回の繰り返し

// 意図的な遅延時間の発生
console.time('hash');
await bcrypt.hash('password123', rounds); // 約300ms必要
console.timeEnd('hash');

// ブルートフォース試行時
// 100万回の試行 = 300ms * 1,000,000 = 約3.5日必要

4. 実装ガイド

4.1 安全なパスワードサービスの実装

// types.ts
export interface IPasswordService {
    hashPassword(password: string): Promise<string>;
    verifyPassword(password: string, hash: string): Promise<boolean>;
}

export interface PasswordConfig {
    rounds: number;
    pepper?: string;  // 追加セキュリティ用の固定文字列
}

// password.service.ts
import bcrypt from 'bcrypt';
import { IPasswordService, PasswordConfig } from './types';

export class PasswordService implements IPasswordService {
    private readonly rounds: number;
    private readonly pepper?: string;

    constructor(config: PasswordConfig) {
        this.rounds = config.rounds;
        this.pepper = config.pepper;
    }

    private addPepper(password: string): string {
        return this.pepper ? `${password}${this.pepper}` : password;
    }

    async hashPassword(password: string): Promise<string> {
        const pepperedPassword = this.addPepper(password);
        return await bcrypt.hash(pepperedPassword, this.rounds);
    }

    async verifyPassword(password: string, hash: string): Promise<boolean> {
        const pepperedPassword = this.addPepper(password);
        return await bcrypt.compare(pepperedPassword, hash);
    }
}

// 使用例
export const passwordService = new PasswordService({
    rounds: 12,
    pepper: process.env.PASSWORD_PEPPER
});

4.2 API実装例

// auth.controller.ts
import { Request, Response } from 'express';
import { passwordService } from './password.service';

export class AuthController {
    async register(req: Request, res: Response): Promise<void> {
        try {
            const { password } = req.body;
            const hashedPassword = await passwordService.hashPassword(password);
            // DB保存ロジック...
            res.status(201).json({ message: '登録完了' });
        } catch (error) {
            res.status(500).json({ message: 'サーバーエラー' });
        }
    }

    async login(req: Request, res: Response): Promise<void> {
        try {
            const { password } = req.body;
            // DBからユーザー照会...
            const storedHash = 'DBから取得したハッシュ';
            
            const isValid = await passwordService.verifyPassword(
                password, 
                storedHash
            );

            if (!isValid) {
                res.status(401).json({ message: '認証失敗' });
                return;
            }

            res.json({ message: 'ログイン成功' });
        } catch (error) {
            res.status(500).json({ message: 'サーバーエラー' });
        }
    }
}

5. 他のハッシュアルゴリズムとの比較

アルゴリズム 速度 Salt対応 安全性 主な用途
MD5 非常に速い なし チェックサム
SHA-256 速い なし 整合性検証
bcrypt 遅い(調整可能) 自動 パスワード保存
Argon2 非常に遅い(調整可能) 自動 非常に高 最高レベルセキュリティ

6. セキュリティ考慮事項

bcryptを使用したパスワードシステムの実装において、考慮すべきセキュリティ事項が複数存在します。これらの考慮事項は、システム全体のセキュリティレベルを向上させるのに役立ちます。

6.1 bcryptの設定最適化

bcrypt使用時の最も重要な設定は、rounds値です。この値はハッシュ関数の反復回数を決定し、セキュリティと性能のバランスを取る上で重要な要素となります。

最適なrounds値を選択する際は、以下の点を考慮する必要があります:

  • ハッシュ生成時間が0.5秒を超えないように設定します。これより長い時間はユーザー体験を損なう可能性があります。
  • サーバーのCPU性能と同時に処理する必要のあるリクエスト数を考慮します。高性能なサーバーでも、同時に多くのリクエストを処理する必要がある場合は適切な調整が必要です。
  • 定期的にハードウェアの性能向上を考慮してrounds値を見直し、調整します。一般的に1-2年周期での見直しが推奨されます。

6.2 追加のセキュリティ層

bcryptだけでも強力なセキュリティを提供しますが、追加のセキュリティ層を実装することで、さらに堅牢なシステムを構築できます。

Pepperを導入することで、データベース漏洩時でも追加の保護層を提供できます。Pepperはbcryptが提供する機能ではなく、開発者が実装するセキュリティ層です。サービス全体で共通して使用される秘密鍵をパスワードに追加する方式で、この鍵は環境変数や設定ファイルで個別に管理されます。

async function hashPassword(password: string, pepper: string): Promise<string> {
  const pepperedPassword = password + pepper;
  return await bcrypt.hash(pepperedPassword, 12);
}

6.3 アクセス制御とモニタリング

パスワードシステムのセキュリティを強化するには、アクセス制御とモニタリングも重要です:

  • Rate Limitingを実装し、IPアドレスやアカウントごとにログイン試行回数を制限します。一般的に特定の時間内での最大試行回数を設定します。
  • 異常なログイン試行を検知してログを記録するシステムを構築します。例えば、短時間での多数の失敗が発生する場合などを監視します。
  • セキュリティイベントの通知システムを構築し、管理者が迅速に対応できるようにします。

6.4 エラー処理とセキュリティメッセージ

セキュリティ関連のエラー処理では、最小限の情報のみを開示する必要があります:

  • ログイン失敗時は「IDまたはパスワードが間違っています」のように、具体的な失敗理由を明示しません。
  • 内部エラー発生時に詳細なエラーメッセージやスタックトレースをクライアントに露出させません。
  • ユーザーに提供するフィードバックは、セキュリティを損なわない範囲で明確で有用な情報に限定します。

7. リファレンス文書

公式ドキュメント

セキュリティ標準

結論

bcryptの核心的な価値は「意図的なパフォーマンス低下」によるセキュリティ強化です:

  1. 単なる速度低下ではなくセキュリティ戦略

    • 正常ユーザー:気付かないレベルの遅延
    • 攻撃者:現実的に不可能な時間が必要
  2. 適応型セキュリティ

    • rounds調整でハードウェア発展に対応
    • 時間が経過しても一定のセキュリティ強度を維持
  3. 実証済みの方式

    • 1999年のリリース以降、主要な脆弱性は発見されていない
    • 実環境で十分に検証されたアルゴリズム

これらの特性がbcryptを現代のWebセキュリティ標準にしています。

Discussion