4
3

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

【C#】パスワードをデータベースに安全に保存する方法 ハッシュ化について

4
Last updated at Posted at 2026-02-09

はじめに

ユーザーログイン機能実装時に学んだ内容をまとめます。

本記事は、パスワードをデータベースに安全に保存するための基本的な考え方と設計指針をまとめたものです。

結論

パスワードはソルト付きの計算時間のかかるハッシュ化で管理する。

パスワードを平文で保存してはいけない理由

当たり前ですが、データベースにパスワードを平文(そのままの文字列)で保存してはいけません。
以下のリスクがあります:

1. 内部関係者による不正アクセス

データベースにアクセス権限を持つ開発者や管理者が、ユーザーのパスワードを閲覧できてしまいます。

2. データベース侵害時の被害拡大

万が一、SQLインジェクションやサーバー侵入によってデータベースが漏洩した場合、攻撃者はすべてのユーザーのパスワードをそのまま入手できます。

3. パスワード使い回しによる二次被害

多くのユーザーは複数のサービスで同じパスワードを使い回しています。1つのサービスから漏洩したパスワードが、他のサービスへの不正ログインに使われる可能性があります。

結論:パスワードは必ず不可逆な変換(元の文字列に戻せない変換)であるハッシュ化を施してから保存する必要があります。

ハッシュ化と暗号化の違い

パスワード保護で重要なのが、ハッシュ化暗号化の違いです。これらは全く異なる技術であり、目的も異なります。

暗号化(Encryption)

  • 双方向変換:暗号化したデータは、鍵があれば元のデータに復号できる
  • 用途:データを一時的に隠し、後で取り出す必要がある場合(通信の秘匿化、ファイルの保護など)
  • :AES、RSAなど
平文 + 鍵 → 暗号文
暗号文 + 鍵 → 平文(復号可能)

ハッシュ化(Hashing)

  • 一方向変換:ハッシュ化したデータは元に戻せない(不可逆)
  • 用途:データの同一性確認、パスワード検証など
  • :SHA-256、PBKDF2、bcrypt、Argon2など
平文 → ハッシュ値
ハッシュ値 → 平文(復元不可能)

パスワード保存にハッシュ化を使う理由

パスワード認証では、元のパスワードを知る必要はなく、入力されたパスワードが正しいかを確認できればよいため、一方向のハッシュ化が適しています。

【登録時】
ユーザー入力:"mypassword123"
↓ ハッシュ化
DB保存:"5e884898da28047151d0e56f8dc6292773603d0d6aabbdd62a11ef721d1542d8"

【ログイン時】
ユーザー入力:"mypassword123"
↓ 同じ方法でハッシュ化
計算結果:"5e884898da28047151d0e56f8dc6292773603d0d6aabbdd62a11ef721d1542d8"
↓ DBの値と比較
一致 → ログイン成功

このように、データベースには元のパスワードを保存する必要がなく、ハッシュ値の比較だけで認証が可能です。

ハッシュ関数の基本特性

優れたハッシュ関数は以下の特性を持ちます:

1. 決定性(Deterministic)

同じ入力に対して、必ず同じハッシュ値を返します。

2. 一方向性(One-way)

ハッシュ値から元のデータを逆算することが計算量的に不可能です。

3. 雪崩効果(Avalanche Effect)

入力が1文字でも変わると、ハッシュ値が大きく変化します。

SHA256("password")   "5e884898da28047151d0e56f8dc6292773603d0d6aabbdd62a11ef721d1542d8"
SHA256("Password")   "e7cf3ef4f17c3999a94f2c6f612e8a888e5b1026878e4e19398b23bd38ec221a"
// 1文字の大文字化で全く異なるハッシュ値になる

4. 衝突困難性(Collision Resistance)

異なる入力から同じハッシュ値が生成される確率が極めて低いです。

理論上、ハッシュ値は有限の長さ(例:SHA-256は256ビット)であるのに対し、入力は無限に存在するため、必ず衝突は発生します。しかし、実用上は問題にならない確率に設計されています。

パスワードハッシュに対する攻撃手法

ハッシュ化されたパスワードであっても、攻撃者はさまざまな方法で元のパスワードを推測しようとします。

1. 総当たり攻撃(Brute Force Attack)

すべての文字の組み合わせを試す方法です。

"a" → ハッシュ化 → DBの値と比較
"b" → ハッシュ化 → DBの値と比較
"c" → ハッシュ化 → DBの値と比較
...
"password" → ハッシュ化 → 一致!

脅威度:パスワードが短い、または単純な場合に有効です。現代のGPUを使えば、8文字以下の単純なパスワードは数時間で解析可能です。

2. 辞書攻撃(Dictionary Attack)

よく使われる単語やパスワードのリストを使って試す方法です。

辞書ファイル:
"password"
"123456"
"admin"
...

脅威度:ユーザーが単純なパスワードを設定している場合、非常に高速に解析されます。

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

事前に計算されたハッシュ値のデータベースを使って逆引きする方法です。

【レインボーテーブルの例】
パスワード        → ハッシュ値(SHA-256)
"password"       → "5e884898da2804715..."
"123456"         → "8d969eef6ecad3c29..."
"qwerty"         → "65e84be33532fb784..."
...(数億件のデータ)

攻撃者はこのテーブルを参照するだけで、瞬時に元のパスワードを特定できます。

脅威度:ソルト(後述)がない場合、極めて有効です。

4. ハイブリッド攻撃

辞書攻撃と総当たり攻撃を組み合わせた方法です。

"password" + "1"
"password" + "2"
"password" + "123"
"admin" + "2024"
...

脅威度:「単語+数字」のような単純な変形パターンを使うユーザーに有効です。

攻撃速度の現実

現代のGPU(例:NVIDIA RTX 4090)を使用した場合、SHA-256では1秒間に数十億回のハッシュ計算が可能です。この速度では、単純なパスワードは短時間で解析されてしまいます。

なぜSHA-256はパスワード保存に不適切か

SHA-256は優れたハッシュ関数ですが、パスワード保存には速すぎるという致命的な問題があります。

SHA-256の本来の用途

  • ファイルの整合性チェック(ダウンロードしたファイルが改ざんされていないか)
  • デジタル署名

これらの用途では、高速であることが重要

パスワード保存での問題

パスワード認証では、正規のユーザーと攻撃者の間に大きな非対称性があります:

立場 実行回数 許容時間
正規ユーザー 1日数回のログイン 0.1秒でも1秒でも体感差なし
攻撃者 1秒間に数十億回の試行 速ければ速いほど有利

SHA-256が速いと、攻撃者は短時間で大量のパスワードを試すことができます。一方、正規ユーザーにとっては、ログインに0.1秒かかっても1秒かかっても、体感的な差はほとんどありません。

解決策:意図的に遅いハッシュ関数

この非対称性を解消するため、パスワード保存には意図的に計算コストを高くしたハッシュ関数を使います:

  • PBKDF2(Password-Based Key Derivation Function 2)
  • bcrypt
  • scrypt
  • Argon2

これらは、攻撃者の試行速度を大幅に低下させる一方、正規ユーザーのログイン体験にはほとんど影響を与えません。

ソルトの役割と必要性

ソルトとは

**ソルト(Salt)**とは、パスワードをハッシュ化する前に付加するランダムな文字列です。

// ソルトなし
Hash("password")  "5e884898da28047151d0e56f8dc6292773603d0d6aabbdd62a11ef721d1542d8"

// ソルトあり
Hash("password" + "a8f5f167f44f4964e6c998dee827110c")  "3c9909afec25354d551dae21590bb26e38d53f2173b8d3dc3eee4c047e7ab1c1"

ソルトの効果

1. レインボーテーブル攻撃の無効化

ソルトがない場合、攻撃者は1つのレインボーテーブルで全ユーザーのパスワードを解析できます。

ソルトを使うと、ユーザーごとに異なるハッシュ値が生成されるため、事前計算されたテーブルが使えなくなります。

【ソルトなし】
ユーザーA:"password" → "5e884898da2804715..."
ユーザーB:"password" → "5e884898da2804715..." (同じハッシュ値)
→ レインボーテーブルで一度に解析可能

【ソルトあり】
ユーザーA:"password" + "salt_A" → "3c9909afec2535..."
ユーザーB:"password" + "salt_B" → "8f3d5c7e2a9b16..." (異なるハッシュ値)
→ 各ユーザーごとに個別の攻撃が必要

2. 同一パスワードの識別防止

複数のユーザーが同じパスワードを使っていても、ソルトが異なれば別のハッシュ値になるため、攻撃者はそれを知ることができません。

ソルトの設計原則

1. ユーザーごとに異なるソルトを生成

// ❌ 全ユーザーで同じソルト(意味がない)
string salt = "my_application_salt";

// ✅ ユーザーごとに異なるソルト
byte[] salt = RandomNumberGenerator.GetBytes(32);

2. 十分な長さを確保

推奨:最低16バイト、できれば32バイト以上

3. 暗号学的に安全な乱数生成器を使用

// ❌ 予測可能な乱数
Random random = new Random();
byte[] salt = new byte[32];
random.NextBytes(salt);

// ✅ 暗号学的に安全な乱数
byte[] salt = RandomNumberGenerator.GetBytes(32);

4. ソルトは秘密にする必要はない

ソルトはハッシュ値と一緒にデータベースに保存します。ソルトが漏洩しても、レインボーテーブル攻撃を防ぐという目的は達成できます。

【データベースのイメージ】
UserID | PasswordHash                              | Salt
-------|-------------------------------------------|----------------------------------
1      | 3c9909afec25354d551dae21590bb26e38d53f21 | a8f5f167f44f4964e6c998dee827110c
2      | 7f2d8e4c3a9b16f5e8d3c7a2b1f4e6d9c8a7b5 | c3f7e9d2a8b4f6e1d3c5a9b7f8e2d4c6

PBKDF2による実装例(学習用)

.NET環境では、**PBKDF2(RFC 2898)**が標準ライブラリで利用可能です。

PBKDF2の特徴

  1. .NET標準ライブラリに含まれている(追加パッケージ不要)
  2. 枯れた技術(2000年から使われている実績)
  3. NIST推奨の標準アルゴリズム
  4. 意図的に遅い設計(攻撃者の試行速度を下げる)

他のハッシュ関数

パスワードハッシュ化には、他にも以下のようなアルゴリズムがあります:

  • Argon2:最新の推奨アルゴリズム(.NET標準ライブラリには含まれていない)
  • bcrypt:広く使われている(.NETでは標準サポートなし)
  • scrypt:メモリハード関数(.NETでは標準サポートなし)

PBKDF2の仕組み

PBKDF2は、内部でHMAC(Hash-based Message Authentication Code)を使い、それを大量に反復することで計算コストを高めています。

PBKDF2(パスワード, ソルト, 反復回数, ハッシュ長) = ハッシュ値

内部処理:
1. HMAC(パスワード, ソルト + カウンタ) を計算
2. その結果を入力として再度HMACを計算
3. これを指定回数繰り返す
4. 各反復結果をXORして最終的なハッシュ値を生成

反復回数が多いほど、1回のハッシュ計算に時間がかかります。

C#での基本的な実装

using System.Security.Cryptography;

public class PasswordHasher
{
    private const int SaltSize = 32; // 32バイト = 256ビット
    private const int HashSize = 32; // 32バイト = 256ビット
    private const int Iterations = 600000; // OWASP 2023推奨最小値(スペックにより変更)

    // パスワードをハッシュ化(登録時)
    public static (byte[] hash, byte[] salt) HashPassword(string password)
    {
        // ソルトを生成
        byte[] salt = RandomNumberGenerator.GetBytes(SaltSize);

        // PBKDF2でハッシュ化
        byte[] hash = Rfc2898DeriveBytes.Pbkdf2(
            password,
            salt,
            Iterations,
            HashAlgorithmName.SHA256,
            HashSize
        );

        return (hash, salt);
    }

    // パスワードを検証(ログイン時)
    public static bool VerifyPassword(string password, byte[] storedHash, byte[] storedSalt)
    {
        // 入力されたパスワードを同じ方法でハッシュ化
        byte[] computedHash = Rfc2898DeriveBytes.Pbkdf2(
            password,
            storedSalt,
            Iterations,
            HashAlgorithmName.SHA256,
            HashSize
        );

        // 固定時間比較(タイミング攻撃対策)
        return CryptographicOperations.FixedTimeEquals(computedHash, storedHash);
        // すべてのバイトを必ず比較するため、時間差が生じない
    }
}

データベース設計

PostgreSQLの場合

CREATE TABLE users (
    user_id SERIAL PRIMARY KEY,
    username VARCHAR(100) NOT NULL UNIQUE,
    password_hash BYTEA NOT NULL,  -- バイナリ型
    password_salt BYTEA NOT NULL,
    created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);

Accessの場合

Accessでは VARBINARY 型がないため、Base64エンコードした文字列として保存します。

CREATE TABLE Users (
    UserID AUTOINCREMENT PRIMARY KEY,
    Username TEXT(100) NOT NULL,
    PasswordHash TEXT(255) NOT NULL,
    PasswordSalt TEXT(255) NOT NULL,
    CreatedAt DATETIME DEFAULT NOW()
);
// Accessへの保存時はBase64エンコード
string hashBase64 = Convert.ToBase64String(hash);
string saltBase64 = Convert.ToBase64String(salt);

// 取得時はデコード
byte[] hash = Convert.FromBase64String(hashBase64);
byte[] salt = Convert.FromBase64String(saltBase64);

やってはいけないこと チェックリスト

実装前に、以下の項目を必ず確認してください。

❌ 絶対にやってはいけないこと

  • パスワードを平文で保存
  • MD5やSHA-1を使用(脆弱性が発見されている)
  • SHA-256を単純に使用(速すぎる)
  • ソルトなしでハッシュ化
  • すべてのユーザーで同じソルトを使用
  • Random クラスでソルトを生成
  • パスワードを暗号化して保存(復号可能にする必要はない)

✅ 必ずやるべきこと

  • PBKDF2、bcrypt、scrypt、Argon2のあたりを使用
  • ユーザーごとに異なるソルトを生成
  • 安全な乱数生成器を使用(RandomNumberGenerator
  • ソルトは最低16バイト以上(推奨32バイト)
  • ハッシュ値とソルトをセットでデータベースに保存
  • パスワード検証時は固定時間比較を使用(CryptographicOperations.FixedTimeEquals

まとめ

パスワードを安全に保存するための基本原則をまとめます:

  1. パスワードは平文で保存しない

    • 暗号化(復号可能)ではなく、ハッシュ化(不可逆)を使う
  2. 適切なハッシュ関数を選ぶ

    • SHA-256は速すぎるのでパスワードには不適切
    • PBKDF2、bcrypt、scrypt、Argon2などの「遅い」ハッシュ関数を使う
  3. ソルトは必須

    • ユーザーごとに異なるランダムなソルトを生成
    • レインボーテーブル攻撃を防ぐ
  4. .NET環境での学習には標準ライブラリのPBKDF2が有用

    • 追加パッケージ不要で仕組みを学べる
    • 実装にはPasswordHasherを推奨(記事末尾参照)

実装時の追加検討事項

実際のシステム実装では以下のセキュリティ対策も併せて検討してください:

  • パスワードポリシーの実装

    • 最小文字数の設定(推奨:8文字以上)
    • 複雑性要件(大文字・小文字・数字・記号の組み合わせ)
    • 最大文字数の制限(推奨:128文字以下)
  • アカウントロックアウト機能

    • ログイン失敗回数の記録
    • 一定回数失敗後の一時ロック(例:5回失敗で15分間ロック)
    • ロックアウト解除機能(管理者権限)

追記:現在推奨されている方法(2026/02/10更新)

2026/02/10 @YuneKichi さんにコメントで教えていただきました。

ASP.NET Core を利用する Web アプリでは、PasswordHasher の利用が Microsoft 公式で推奨されています。
フォームアプリケーションにおいてもパスワードハッシュ処理の実装を簡潔にする手段として利用可能です。

Microsoft公式ドキュメントによると、
KeyDerivation.Pbkdf2 は PBKDF2 の低レベルAPIであり、ソルト管理・保存形式・再ハッシュ対応などは開発者責任となります。新規アプリでは、これらを安全に内包したASP.NET Core Identity の PasswordHasher を利用するのを推奨します。
とのこと。

PasswordHasherクラスとは

PasswordHasherは.NET Coreに標準で含まれているクラスで、パスワードのハッシュ化と検証を簡単に実装できます。

特徴

  • 自動的なソルト生成:ソルトを手動で管理する必要がない
  • バージョニング対応:アルゴリズムの更新に対応できる設計
  • シンプルなAPI:ハッシュ化と検証が1行で実装可能

TUserパラメータについて

PasswordHasher<TUser>のジェネリックパラメータTUserは、メソッドのシグネチャには含まれていますが、実際のハッシュ化処理では使用されていません

public virtual string HashPassword(TUser user, string password)
{
    // 内部実装では user パラメータは使われず、password のみが使用される
}

このため、実装例ではnull!を渡します。

実装例

NuGetパッケージのインストール

Install-Package Microsoft.AspNetCore.Identity

または

dotnet add package Microsoft.AspNetCore.Identity

実装方法

using Microsoft.AspNetCore.Identity;

// PasswordHasherのインスタンスを生成
var passwordHasher = new PasswordHasher<string>();

// ユーザー登録時:パスワードをハッシュ化
string password = "MySecurePassword123!";
string hashedPassword = passwordHasher.HashPassword(null!, password);

// データベースに保存
// INSERT INTO Users (Username, PasswordHash) VALUES (@username, @hashedPassword)

// ログイン時:パスワードを検証
string inputPassword = "MySecurePassword123!";
var result = passwordHasher.VerifyHashedPassword(null!, hashedPassword, inputPassword);

if (result == PasswordVerificationResult.Success)
{
    Console.WriteLine("ログイン成功");
}
else
{
    Console.WriteLine("パスワードが間違っています");
}

データベース設計の変更点

PasswordHasherを使う場合、ハッシュ値にソルトやアルゴリズム情報が含まれている為、個別に保存する必要がなくなります。

PostgreSQLの場合

CREATE TABLE users (
    user_id SERIAL PRIMARY KEY,
    username VARCHAR(100) NOT NULL UNIQUE,
    password_hash TEXT NOT NULL,  -- ハッシュ値のみ(ソルトは不要)
    created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);

Accessの場合

CREATE TABLE Users (
    UserID AUTOINCREMENT PRIMARY KEY,
    Username TEXT(100) NOT NULL,
    PasswordHash TEXT(255) NOT NULL,  -- ハッシュ値のみ
    CreatedAt DATETIME DEFAULT NOW()
);

PasswordHasherの内部動作

PasswordHasherは内部でPBKDF2を使用していますが、以下の追加機能があります:

  1. バージョン情報の埋め込み

    • ハッシュ文字列の先頭にバージョン情報を含む
    • 将来的なアルゴリズム変更に対応可能
  2. ソルトの自動管理

    • ハッシュ値にソルトが含まれる
  3. 自動的なパラメータ調整

    • 反復回数などのパラメータが適切に設定される

自動再ハッシュ機能

PasswordHasherの重要な機能の一つが自動再ハッシュです。

VerifyHashedPasswordの戻り値

パスワード検証時、VerifyHashedPasswordメソッドは以下の3つの結果を返します:

public enum PasswordVerificationResult
{
    Failed = 0,                // パスワードが不正
    Success = 1,              // パスワードが正しい
    SuccessRehashNeeded = 2   // パスワードは正しいが、再ハッシュ化が必要
}

再ハッシュが必要になるケース

  1. 古いバージョンのハッシュを使用している場合

    • 旧バージョンで保存されている
  2. 反復回数が現在の設定より少ない場合

    • デフォルトより高い反復回数を設定している場合
    • 既存のハッシュがより少ない反復回数で生成されている

結論

現在(2026年2月時点)では、PasswordHasherクラスの使用が推奨されます。

本記事で紹介したPBKDF2の実装は、パスワードハッシュ化の仕組みを理解するためのもので、実装ではPasswordHasherが適しています。


参考リンク

4
3
2

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
4
3

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?