パスワードハッシュ化のベストプラクティス|bcrypt・Argon2の選び方と実装

kento_morota 18分で読めます

「パスワードはハッシュ化して保存すべき」——これは多くのエンジニアが知っているセキュリティの常識です。しかし、「どのアルゴリズムを使うべきか」「ソルトとは何か」「コストファクターはいくつに設定するか」を正しく答えられるでしょうか。

本記事では、パスワードハッシュ化の基本原理から、bcryptやArgon2といった現代のアルゴリズムの選定・実装方法までを実践的に解説します。

なぜパスワードをハッシュ化する必要があるのか

パスワードを平文(プレーンテキスト)でデータベースに保存することは、最も深刻なセキュリティ上の過ちの一つです。データベースが漏洩した場合、すべてのユーザーのパスワードが一瞬で攻撃者の手に渡ります。

データ漏洩のリスク

データベースの情報が流出する原因は多岐にわたります。

SQLインジェクション:入力値の不適切な処理により、攻撃者がデータベースの内容を取得する手法です。

内部不正:データベースにアクセス権限を持つ従業員が情報を持ち出すケースです。

バックアップの漏洩:暗号化されていないバックアップファイルが外部に流出するケースです。

設定ミス:クラウド環境でデータベースがインターネットに公開されてしまうケースです。

これらのリスクはゼロにはできません。そのため、パスワードを安全な形式で保存し、万が一漏洩しても被害を最小限に抑える必要があります。

ハッシュ化の基本原理

ハッシュ関数は、任意の長さの入力から固定長の出力(ハッシュ値)を生成する一方向関数です。

// ハッシュ化のイメージ
"password123"  →  ハッシュ関数  →  "ef92b778..."(元に戻せない)
"password124"  →  ハッシュ関数  →  "a1b2c3d4..."(1文字の違いで全く異なる結果)

重要な特性は以下の3つです。

一方向性:ハッシュ値から元のパスワードを復元できません。

決定性:同じ入力に対しては常に同じハッシュ値が生成されます。

衝突耐性:異なる入力から同じハッシュ値が生成されることは極めて困難です。

MD5やSHA-256をパスワードに使ってはいけない理由

「ハッシュ化しているから安全」と考えるのは危険です。MD5やSHA-256は汎用的なハッシュ関数であり、パスワードのハッシュ化には適していません。

高速すぎるという問題

MD5やSHA-256は高速に計算できるように設計されています。これは本来メリットですが、パスワードのハッシュ化においては重大な弱点になります。

// 処理速度の比較(概算)
// SHA-256:  1秒間に数十億回のハッシュ計算が可能(GPUを使用した場合)
// bcrypt:   1秒間に数千回程度
// Argon2:   1秒間に数百回程度(設定による)

攻撃者はGPUクラスターを使って、1秒間に数十億のパスワード候補を試すことができます。8文字の英数字パスワードであれば、SHA-256では数時間で解読されてしまいます。

レインボーテーブル攻撃

ソルトを使用しない場合、事前に計算されたハッシュ値の辞書(レインボーテーブル)で即座にパスワードを特定できます。

// ソルトなしのMD5ハッシュ
"password"  →  "5f4dcc3b5aa765d61d8327deb882cf99"
"123456"    →  "e10adc3949ba59abbe56e057f20f883e"

// 攻撃者はこれらの対応表を事前に用意している
// ハッシュ値を検索するだけで元のパスワードが判明する

ソルトの重要性

ソルトは、パスワードをハッシュ化する前に追加するランダムな文字列です。ユーザーごとに異なるソルトを使用することで、同じパスワードでも異なるハッシュ値が生成されます。

// ソルト付きハッシュ化
ユーザーA: "password" + "ランダムソルトA" → ハッシュ値A
ユーザーB: "password" + "ランダムソルトB" → ハッシュ値B
// 同じパスワードでも異なるハッシュ値になる

ソルトにより、レインボーテーブル攻撃は無効化されます。ただし、ソルトだけではブルートフォース攻撃への耐性は向上しません。パスワード専用のハッシュ関数が必要な理由はここにあります。

bcryptの仕組みと実装

bcryptは、1999年に設計されたパスワードハッシュ化アルゴリズムで、現在でも広く使われている実績のある選択肢です。

bcryptの特徴

コストファクター(ワークファクター)
計算に必要な時間を調整できるパラメータです。ハードウェアの性能が向上しても、コストファクターを増やすことで攻撃への耐性を維持できます。

自動ソルト生成
bcryptはソルトを自動的に生成し、ハッシュ値に組み込みます。開発者がソルト管理を行う必要がありません。

出力形式

// bcryptのハッシュ値の構造
$2b$12$LJ3m4ys3Lg2VJZgMN.ZOUOkXsOAEzcgIBf3YYjzGl3JOJ4xDP7aga
│  │  │                                              │
│  │  │  ソルト(22文字)とハッシュ値(31文字)         │
│  │  コストファクター(2^12 = 4096回のイテレーション)
│  バージョン
アルゴリズム識別子

各言語でのbcrypt実装

Node.js

const bcrypt = require('bcrypt');

// パスワードのハッシュ化
async function hashPassword(password) {
  const saltRounds = 12; // コストファクター
  const hash = await bcrypt.hash(password, saltRounds);
  return hash;
}

// パスワードの検証
async function verifyPassword(password, hash) {
  const isMatch = await bcrypt.compare(password, hash);
  return isMatch;
}

// 使用例
const hash = await hashPassword('mySecurePassword');
console.log(hash);
// $2b$12$LJ3m4ys3Lg2VJZgMN.ZOUOkXsOAEzcgIBf3YYjzGl3JOJ4xDP7aga

const isValid = await verifyPassword('mySecurePassword', hash);
console.log(isValid); // true

Python

import bcrypt

# パスワードのハッシュ化
def hash_password(password: str) -> str:
    salt = bcrypt.gensalt(rounds=12)
    hashed = bcrypt.hashpw(password.encode('utf-8'), salt)
    return hashed.decode('utf-8')

# パスワードの検証
def verify_password(password: str, hashed: str) -> bool:
    return bcrypt.checkpw(
        password.encode('utf-8'),
        hashed.encode('utf-8')
    )

# 使用例
hashed = hash_password("mySecurePassword")
print(verify_password("mySecurePassword", hashed))  # True

PHP

// PHPはpassword_hash関数がbcryptをデフォルトで使用
$hash = password_hash('mySecurePassword', PASSWORD_BCRYPT, [
    'cost' => 12,
]);

// 検証
$isValid = password_verify('mySecurePassword', $hash);

// コストファクターの自動調整(推奨)
// サーバー環境で適切なコストを計算
$timeTarget = 0.1; // 100ミリ秒を目標
$cost = 10;
do {
    $cost++;
    $start = microtime(true);
    password_hash("test", PASSWORD_BCRYPT, ["cost" => $cost]);
    $end = microtime(true);
} while (($end - $start) < $timeTarget);
echo "推奨コストファクター: " . $cost;

コストファクターの選び方

コストファクターは、サーバーの処理能力とセキュリティのバランスで決定します。目安として、ハッシュ化に100ミリ秒〜250ミリ秒かかる値が推奨されます。

2026年時点の一般的なサーバーでは、コストファクター12が標準的な選択肢です。高いセキュリティが求められる場合はコストファクター13〜14を検討してください。

Argon2の仕組みと実装

Argon2は、2015年のPassword Hashing Competition(PHC)で優勝した最新のパスワードハッシュ化アルゴリズムです。bcryptの後継として設計されており、より高いセキュリティを提供します。

Argon2の3つのバリアント

Argon2d:データ依存型。GPU攻撃に対して最も耐性が高いですが、サイドチャネル攻撃に対する耐性は低くなります。暗号通貨のマイニングなどに適しています。

Argon2i:データ非依存型。サイドチャネル攻撃に対する耐性が高いです。パスワードハッシュ化に適していますが、Argon2dよりもGPU耐性は低くなります。

Argon2id:Argon2dとArgon2iのハイブリッド。パスワードハッシュ化ではこのバリアントの使用が推奨されます。

Argon2のパラメータ

Argon2はbcryptよりも多くの調整パラメータを持ちます。

メモリコスト(m):使用するメモリ量(KiB単位)。GPUはメモリが限られているため、メモリコストを高くすることでGPU攻撃への耐性が向上します。

時間コスト(t):イテレーション回数。計算にかかる時間を制御します。

並列度(p):並列実行するスレッド数。

// OWASP推奨パラメータ(2026年時点)
// Argon2id
// メモリ: 19MiB (19456 KiB)
// イテレーション: 2
// 並列度: 1

各言語でのArgon2実装

Node.js

const argon2 = require('argon2');

// パスワードのハッシュ化
async function hashPassword(password) {
  const hash = await argon2.hash(password, {
    type: argon2.argon2id,    // Argon2idを使用
    memoryCost: 19456,         // 19MiB
    timeCost: 2,               // 2イテレーション
    parallelism: 1,            // 並列度1
  });
  return hash;
}

// パスワードの検証
async function verifyPassword(password, hash) {
  try {
    return await argon2.verify(hash, password);
  } catch (err) {
    return false;
  }
}

// 使用例
const hash = await hashPassword('mySecurePassword');
console.log(hash);
// $argon2id$v=19$m=19456,t=2,p=1$ランダムソルト$ハッシュ値

const isValid = await verifyPassword('mySecurePassword', hash);
console.log(isValid); // true

Python

from argon2 import PasswordHasher

# PasswordHasherのインスタンスを設定付きで生成
ph = PasswordHasher(
    time_cost=2,
    memory_cost=19456,
    parallelism=1,
    hash_len=32,
    salt_len=16,
)

# パスワードのハッシュ化
def hash_password(password: str) -> str:
    return ph.hash(password)

# パスワードの検証
def verify_password(password: str, hashed: str) -> bool:
    try:
        return ph.verify(hashed, password)
    except Exception:
        return False

# パラメータ更新の確認
def needs_rehash(hashed: str) -> bool:
    return ph.check_needs_rehash(hashed)

bcryptとArgon2の選び方

どちらのアルゴリズムを採用すべきか、プロジェクトの状況に応じて判断しましょう。

bcryptを選ぶべきケース

・既存システムで既にbcryptを使用しており、問題なく運用できている場合
・使用するプログラミング言語やフレームワークでArgon2のライブラリが十分に成熟していない場合
・シンプルな設定で確実な保護を求める場合(調整パラメータが少ない)

Argon2を選ぶべきケース

・新規プロジェクトでアルゴリズムを選択する場合(将来を見据えた選択)
・GPU攻撃への高い耐性が必要な場合
・メモリハード特性による追加の保護が求められる場合
・セキュリティ要件が厳しいシステム(金融系、医療系など)

ハッシュアルゴリズムの移行方法

既存のシステムでアルゴリズムを変更する場合、ユーザーに再設定を強いることなく段階的に移行できます。

// 段階的なアルゴリズム移行の実装例
async function loginAndMigrate(email, password) {
  const user = await User.findByEmail(email);

  // 現在のハッシュのアルゴリズムを判定
  const isOldHash = user.passwordHash.startsWith('$2b$'); // bcrypt
  const isNewHash = user.passwordHash.startsWith('$argon2id$'); // Argon2id

  let isValid;
  if (isOldHash) {
    isValid = await bcrypt.compare(password, user.passwordHash);
  } else {
    isValid = await argon2.verify(user.passwordHash, password);
  }

  if (!isValid) {
    return null;
  }

  // 旧アルゴリズムの場合、新アルゴリズムで再ハッシュ化
  if (isOldHash) {
    const newHash = await argon2.hash(password, {
      type: argon2.argon2id,
      memoryCost: 19456,
      timeCost: 2,
      parallelism: 1,
    });
    await User.updatePasswordHash(user.id, newHash);
  }

  return user;
}

パスワード管理のベストプラクティス

ハッシュ化アルゴリズムの選択以外にも、パスワード管理全体で押さえるべきポイントがあります。

パスワードポリシーの設計

最新のNIST(米国標準技術研究所)のガイドラインに基づいた推奨事項をまとめます。

推奨される方針

・最低8文字以上(可能であれば12文字以上を推奨)
・最大文字数は十分に長く(64文字以上)設定する
・ユニコード文字を含むすべての印刷可能文字を許可する
・漏洩したパスワードリスト(Have I Been Pwned等)との照合を行う

非推奨とされた方針

・「大文字・小文字・数字・記号をすべて含める」という複雑性要件
・定期的なパスワード変更の強制(漏洩が判明した場合を除く)
・秘密の質問(本人確認としての信頼性が低い)

// パスワード強度チェックの実装例
const zxcvbn = require('zxcvbn'); // パスワード強度評価ライブラリ

function validatePassword(password) {
  const errors = [];

  // 長さチェック
  if (password.length < 8) {
    errors.push('パスワードは8文字以上で入力してください');
  }

  if (password.length > 128) {
    errors.push('パスワードは128文字以内で入力してください');
  }

  // 強度チェック(zxcvbnは0-4のスコアを返す)
  const result = zxcvbn(password);
  if (result.score < 3) {
    errors.push(`パスワードの強度が不十分です: ${result.feedback.suggestions.join(' ')}`);
  }

  return { isValid: errors.length === 0, errors };
}

ペッパー(Pepper)の活用

ペッパーは、ソルトに加えてサーバー側で保持する秘密鍵です。データベースが漏洩しても、ペッパーが漏れなければパスワードの解読はさらに困難になります。

const crypto = require('crypto');

const PEPPER = process.env.PASSWORD_PEPPER; // 環境変数で管理

// ペッパー適用後にハッシュ化
async function hashWithPepper(password) {
  const peppered = crypto
    .createHmac('sha256', PEPPER)
    .update(password)
    .digest('hex');

  return await argon2.hash(peppered, {
    type: argon2.argon2id,
    memoryCost: 19456,
    timeCost: 2,
    parallelism: 1,
  });
}

// 検証
async function verifyWithPepper(password, hash) {
  const peppered = crypto
    .createHmac('sha256', PEPPER)
    .update(password)
    .digest('hex');

  return await argon2.verify(hash, peppered);
}

ペッパーはデータベースとは別の場所(環境変数、HSM、シークレット管理サービスなど)に保管してください。

まとめ:パスワード保護を正しく実装しよう

パスワードのハッシュ化は、ユーザーデータを守るための最も基本的かつ重要なセキュリティ対策です。

本記事の要点をまとめます。

アルゴリズムの選択
・MD5やSHA-256はパスワードのハッシュ化に使わない
・新規プロジェクトではArgon2id、既存システムではbcrypt(コストファクター12以上)を使用する
・アルゴリズムの移行はログイン時に段階的に行える

実装のポイント
・各言語の信頼されたライブラリを使用する(自作しない)
・コストファクターは100ms〜250msを目安に設定する
・ペッパーの導入で多層防御を実現する

運用の考慮事項
・NISTガイドラインに基づいたパスワードポリシーを採用する
・漏洩パスワードリストとの照合を実装する
・ハードウェア性能の向上に合わせてパラメータを見直す

セキュリティは「設定して終わり」ではなく、継続的な改善が求められます。本記事を参考に、自社のパスワード管理体制を見直してみてください。

#パスワード#ハッシュ#セキュリティ
共有:
無料メルマガ

週1回、最新の技術記事をお届け

AI・クラウド・開発の最新記事を毎週月曜にメールでお届けします。登録は無料、いつでも解除できます。

プライバシーポリシーに基づき管理します

起業準備に役立つ情報、もっとありますよ。

まずは話だけ聞いてもらう