👻

【完全解説】ログインパスワードをハッシュ化する方法(例:C#,.NET,PostgreSQL)

2023/09/04に公開

記事の目的

  • ログイン処理のときに、パスワードを適切にハッシュ化して運用できるようにする
  • ハッシュ処理の理屈を理解する
  • ハッシュ化ライブラリ使うと簡単

目次

ハッシュ化の基本的な流れ

パスワードにハッシュ化の機能を付けたいです。どのようにしたらいいでしょうか?.NET Framework4.7.2とpostgresを利用しています。

パスワードのハッシュ化は非常に重要なセキュリティ手段です。.NET Framework 4.7.2とPostgreSQLを使用している場合、以下の手順を参考にしてハッシュ化機能を追加できます。

サーバーサイドでのハッシュ化

  1. 必要な名前空間を追加

    using System;
    using System.Security.Cryptography;
    using System.Text;
    
  2. ハッシュ関数を作成

    public string HashPassword(string password)
    {
        using (SHA256 sha256 = SHA256.Create())
        {
            byte[] bytes = sha256.ComputeHash(Encoding.UTF8.GetBytes(password));
            StringBuilder builder = new StringBuilder();
            for (int i = 0; i < bytes.Length; i++)
            {
                builder.Append(bytes[i].ToString("x2"));
            }
            return builder.ToString();
        }
    }
    

データベースに保存

  1. PostgreSQLにパスワードを保存する際は、ハッシュ化したパスワードを使用します。

    INSERT INTO users(username, hashed_password) VALUES ('username', 'hashed_password_here');
    

パスワードの比較

ユーザーがログインする際、入力されたパスワードを同様にハッシュ化し、データベースに保存されているハッシュと比較します。

string hashedPassword = HashPassword(inputPassword);
// DBから取得したハッシュと比較
if(hashedPassword == storedHashedPassword)
{
    // ログイン成功
}

考慮すべき点

  • ソルト: 同じパスワードでも異なるハッシュ値になるように、ランダムな文字列(ソルト)を追加することも考慮してください。
  • ストレッチング: ハッシュを複数回適用することで、攻撃者が元のパスワードを推測するのを難しくします。

以上のような基本的な方法以外にも、より高度なセキュリティを求める場合は、専門のライブラリ(例えば、bcrypt)を使用することも考慮してください。

何か他に考慮すべき点や、説明が不足している部分がありましたら、どうぞご質問ください。これであなたのプロジェクトやスキルの向上に貢献できれば幸いです。

ハッシュ化の詳細説明

下記のハッシュ関数を詳しく説明してください。

public string HashPassword(string password)
{
    using (SHA256 sha256 = SHA256.Create())
    {
        byte[] bytes = sha256.ComputeHash(Encoding.UTF8.GetBytes(password));
        StringBuilder builder = new StringBuilder();
        for (int i = 0; i < bytes.Length; i++)
        {
            builder.Append(bytes[i].ToString("x2"));
        }
        return builder.ToString();
    }
}

もちろん、このHashPassword関数について各部分を詳しく解説します。

関数全体

この関数は、引数として平文のpasswordstring型)を受け取り、SHA-256アルゴリズムを使用してそのハッシュ値を生成しています。結果は16進数のstring型として返されます。

usingステートメント

using (SHA256 sha256 = SHA256.Create())

usingステートメントは、リソースを効率的に管理するC#の構文です。この場合、SHA256オブジェクトのリソースを確保して、ハッシュ計算が終わったらリソースを自動的に解放します。

ComputeHashメソッド

byte[] bytes = sha256.ComputeHash(Encoding.UTF8.GetBytes(password));
  1. Encoding.UTF8.GetBytes(password): 平文のパスワードをUTF-8エンコーディングでバイト配列に変換します。
  2. sha256.ComputeHash(...): そのバイト配列に対してSHA-256ハッシュを計算します。

結果として、ハッシュ値がバイト配列bytesとして得られます。

StringBuilderとforループ

StringBuilder builder = new StringBuilder();
for (int i = 0; i < bytes.Length; i++)
{
    builder.Append(bytes[i].ToString("x2"));
}
  1. StringBuilderは文字列を効率良く連結するためのクラスです。
  2. forループで各バイトを取り出しています。
  3. bytes[i].ToString("x2"): 各バイトを16進数形式の文字列に変換しています。"x2"は、2桁の16進数として表示することを意味します。

この結果が、最終的にハッシュ値の16進数表現となります。

return文

return builder.ToString();

StringBuilderに格納された16進数の文字列を最終的にstring型として返します。

これらのステップによって、平文のパスワードが安全な形(ハッシュ化された形)で保存できるようになります。

ソルト

ソルト: 同じパスワードでも異なるハッシュ値になるように、ランダムな文字列(ソルト)を追加することも考慮してください。
について教えてください

ソルト(Salt)はランダムなデータで、元のデータ(この場合はパスワード)に追加してハッシュ関数にかけることで、元のデータが同じであっても異なるハッシュ値が生成されるようにします。ソルトを使用する主な理由は以下の通りです。

  1. レインボーテーブル攻撃の防止: ソルトがないと、攻撃者は事前に計算されたハッシュ値(レインボーテーブル)を用いて元のパスワードを推測できる可能性があります。
  2. ユーザ間のハッシュの一意性: ソルトを使用すると、同じパスワードを使用しているユーザーでも異なるハッシュ値が生成されます。

ソルトを使ったハッシュ化の基本的な手順(C#)

  1. ソルト生成

    まずランダムなソルトを生成します。

    RNGCryptoServiceProvider rng = new RNGCryptoServiceProvider();
    byte[] saltBytes = new byte[16]; // 16バイトのソルト
    rng.GetBytes(saltBytes);
    
  2. ソルトとパスワードの連結

    次に、生成されたソルトと元のパスワードを連結します。

    string saltText = Convert.ToBase64String(saltBytes);
    string saltedPassword = saltText + password;
    
  3. ハッシュ生成

    ソルトが連結されたパスワードに対してハッシュ関数を適用します。

    string hashedPassword = HashPassword(saltedPassword); // 既存のHashPassword関数を使用
    
  4. データベースに保存

    生成されたソルトとハッシュ値をデータベースに保存します。

    INSERT INTO users(username, salt, hashed_password) VALUES ('username', 'salt_here', 'hashed_password_here');
    

認証時の比較

ログイン時には、保存されたソルトと入力されたパスワードを用いて新たなハッシュ値を生成し、データベースに保存されたハッシュ値と比較します。

以上がソルトを使った基本的なパスワードのハッシュ化手順です。

ソルトの桁数は何ケタくらいがよいですか?
ソルトは最初のユーザー登録するときに生成しますか?
ソルトは一意である必要がありますよね?

ソルトの桁数(長さ)

ソルトの長さは、セキュリティ要件によって異なる場合がありますが、一般的には128ビット(16バイト)または256ビット(32バイト)がよく使用されます。長いソルトが好まれる理由は、攻撃者が総当たり攻撃やレインボーテーブル攻撃を行う際の計算コストを高くするためです。

ソルトの生成タイミング

はい、ソルトは通常、ユーザーがアカウントを初めて作成する際に生成されます。それ以降、パスワードが変更されるたびに新しいソルトを生成する方法もあります。ソルトはハッシュと同じくデータベースに保存しておく必要があります。

ソルトの一意性

ソルトは一意(ユニーク)である必要があります。一意でないと、同じパスワードとソルトの組み合わせが複数のユーザーで使われる可能性があり、それがセキュリティ上のリスクとなる場合があります。したがって、ランダムな値を生成して一意性を確保することが一般的です。

これらのポイントを考慮することで、よりセキュアなパスワード管理が可能になります。何か他に質問や不明点があれば、どうぞお知らせください。

ストレッチング

ストレッチング: ハッシュを複数回適用することで、攻撃者が元のパスワードを推測するのを難しくします。
について詳しく教えてください

ストレッチングとは、ハッシュ関数を複数回適用することで、攻撃者が元のパスワードを推測する際の計算コストを上げるテクニックです。この手法は特に、計算能力の低いハードウェアや古いアルゴリズムでのセキュリティ向上に有用です。

なぜストレッチングが必要か?

パスワードハッシュの計算が高速であることは、通常は良いことですが、これが逆に攻撃者の手を強くする可能性もあります。攻撃者が元のパスワードを推測するためには、多くの異なる入力値に対してハッシュ関数を計算する必要があります。ストレッチングを使用すると、この計算が非常に時間がかかるため、攻撃者の努力が大幅に妨げられます。

ストレッチングの実装

以下はC#でストレッチングを適用する一例です。

public string HashPasswordWithStretching(string password, string salt, int iterations)
{
    // 初回にソルトとパスワードを結合
    string saltedPassword = salt + password;
    
    // 初回のハッシュ計算
    string hash = HashPassword(saltedPassword);

    // 追加のハッシュ計算
    for(int i = 0; i < iterations - 1; i++)
    {
        hash = HashPassword(hash);
    }

    return hash;
}

ここでは、HashPassword関数は既存のハッシュ計算関数と仮定しています。

注意点

  1. 反復回数: ストレッチングに使用する反復回数は、アプリケーションのパフォーマンスとセキュリティ要件に応じて選びます。一般的には、少なくとも1000回以上が推奨されています。
  2. アルゴリズム選択: 現代のセキュリティでは、特別にパスワードのストレッチングを行うために設計されたアルゴリズム(例: PBKDF2, bcrypt, Argon2)が推奨されることが多いです。

ストレッチングは、ソルトと同様にパスワードのセキュリティを高めるための一つの手段です。

ストレッチングに使用する反復回数は、アプリケーションのパフォーマンスとセキュリティ要件に応じて選びます。一般的には、少なくとも1000回以上が推奨されています。
とは、iterations変数に1000以上の値を入れるということですか?

はい、その通りです。iterations 変数に1000以上の値を入れると、ハッシュ関数がその回数だけ適用されるため、攻撃者によるパスワードクラックがより困難になります。ただし、反復回数を増やすと計算コストが高くなるため、アプリケーションのパフォーマンスに影響が出る可能性があります。

具体的には、ユーザーがログインまたはパスワードを変更する際の処理速度が遅くなることが考えられます。そのため、適切な反復回数を選ぶには、セキュリティとパフォーマンスのバランスを考慮する必要があります。

一般的には、反復回数はシステムのテストや、攻撃者の計算能力を考慮して調整します。例えば、ハードウェアがアップグレードされたら、反復回数も増やしていくといった方法があります。

反復回数を設定する際には、実際にシステムで試験を行い、適切なパフォーマンスが確保されているかを確認することが推奨されます。

ハッシュ化ライブラリ

PBKDF2, bcrypt, Argon2はどのように利用しますか?

各アルゴリズム(PBKDF2、bcrypt、Argon2)は、ストレッチングとソルトを自動的に処理してくれる特別なハッシュ関数です。それぞれのアルゴリズムには.NET Frameworkや.NET Core、その他のプログラミング言語で使えるライブラリが多くあります。

PBKDF2 (.NET Framework)

.NET Frameworkには、Rfc2898DeriveBytes クラスがあり、これを使うとPBKDF2が利用できます。

using System;
using System.Security.Cryptography;

public byte[] GeneratePBKDF2Hash(string password, byte[] salt)
{
    var pbkdf2 = new Rfc2898DeriveBytes(password, salt, 1000); // 1000は反復回数
    return pbkdf2.GetBytes(20); // 20は生成するバイト長
}

bcrypt

bcryptは.NET Frameworkの標準ライブラリには含まれていないため、NuGetパッケージ(例:BCrypt.Net-Next)をインストールする必要があります。

using BCrypt.Net;

public string HashPassword(string password)
{
    return BCrypt.HashPassword(password, workFactor: 12);
}

public bool VerifyPassword(string password, string hashedPassword)
{
    return BCrypt.Verify(password, hashedPassword);
}

Argon2

Argon2には、libsodiumArgon2 NuGetパッケージがあります。使用方法はライブラリによって異なるので、公式のドキュメントやサンプルコードを参照してください。

using Konscious.Security.Cryptography;

public byte[] HashPassword(string password, byte[] salt)
{
    var argon2 = new Argon2id(System.Text.Encoding.UTF8.GetBytes(password))
    {
        Salt = salt,
        DegreeOfParallelism = 4, // スレッド数
        MemorySize = 8192, // Kibibytes
        Iterations = 4
    };
    
    return argon2.GetBytes(16);
}

上記は単なる例であり、実際のコードでは例外処理やその他のセキュリティに関する要件も考慮する必要があります。アルゴリズムやパラメータ(反復回数、メモリ使用量など)を選ぶ際には、自分のシステムとセキュリティ要件に最も適しているものを慎重に選んでください。

さらなるセキュリティ対策

ハッシュ化についてはこれで完璧ですか?他に考えなければいけないことはありますか?

ハッシュ化の基本的な要素(強力なハッシュアルゴリズムの選択、ソルトの使用、ストレッチング)についてはおおよそカバーしたと思いますが、全体的なセキュリティを考慮する際には他にもいくつか注意すべき点があります。

  1. セキュアなストレージ: ハッシュ値とソルトはデータベースに保存する場合、そのデータベース自体のセキュリティも確保する必要があります。

  2. レートリミティング: ユーザーが短期間に大量のログイン試行をする場合には、それを制限する機構(レートリミティング)を入れるとよいです。これにより、総当たり攻撃を難しくします。

  3. 二要素認証 (2FA): 可能であれば、二要素認証を導入することで、セキュリティを一層高めることができます。

  4. 監査とロギング: 不正アクセスや不審な動きを検出するために、監査とロギングをしっかりと行うことが重要です。ただし、ログにはセンシティブな情報(パスワードなど)を保存しないようにしましょう。

  5. 定期的なセキュリティレビューとアップデート: セキュリティは常に進化しています。定期的にセキュリティレビューを行い、必要なパッチやアップデートを適用することが重要です。

  6. ユーザー教育: ユーザーが強力なパスワードを設定するようガイダンスを提供することも、セキュリティを高める一つの方法です。

  7. パスワードポリシー: ユーザーに対して、十分な長さや複雑さを持つパスワードを設定するように指導することも有用です。

  8. データの暗号化: パスワード以外のセンシティブなデータも、データベースで暗号化して保存するべきです。

  9. アプリケーション全体のセキュリティ: 認証だけでなく、認可(何が許可されているか)についてもしっかりと考え、アプリケーション全体のセキュリティを確保する必要があります。

これらは一例であり、具体的なアプリケーションやシステムによっては他にも考慮すべき点が出てくる可能性があります。

Discussion