【C#】パスワードをハッシュ化して DB に登録する

## はじめに

DB にパスワードを保存するとき、そのまま平文で保存すると、
データが漏れた場合に、パスワードが 見た瞬間わかってしまいます。

この記事では C# を使って DB にパスワードをハッシュ化して登録してみたいと思います。

## ハッシュ化の実装

パスワードを暗号化する際に`salt`を利用すると別々のユーザが同じパスワードを使っても別のハッシュを作ることができます。
認証の際は`salt`と平文のパスワードから作成できるハッシュが DB に登録されているハッシュと同じ場合、正しいパスワードです。


`Cryptography.KeyDerivation`を使用しています。

```sh
dotnet add PasswordHash.Lib package "Microsoft.AspNetCore.Cryptography.KeyDerivation"
```

```cs:PasswordHash/PasswordHash.Lib/PasswordService.cs
using Microsoft.AspNetCore.Cryptography.KeyDerivation;
using System;
using System.Security.Cryptography;

namespace PasswordHash.Lib {
  public class PasswordService : IPasswordService {

    // ハッシュ化...平文パスワードを渡すとハッシュ化パスワード、使用されたソルトが返る
    public (string hashedPassword, byte[] salt) HashPassword(string rawPassword) {
      byte[] salt = GetSalt();
      string hashed = HashPassword(rawPassword, salt);
      return (hashed, salt);
    }

    // 認証...ハッシュ化パスワード、平文パスワード・ソルトを渡すと正しいパスワードなら true が返る
    public bool VerifyPassword(string hashedPassword, string rawPassword, byte[] salt) =>
      hashedPassword == HashPassword(rawPassword, salt);

    private string HashPassword(string rawPassword, byte[] salt) =>
      Convert.ToBase64String(
        KeyDerivation.Pbkdf2(
          password: rawPassword,
          salt: salt,
          prf: KeyDerivationPrf.HMACSHA512,
          iterationCount: 10000,
          numBytesRequested: 256 / 8));

    private byte[] GetSalt() {
      using (var gen = RandomNumberGenerator.Create()) {
        var salt = new byte[128 / 8];
        gen.GetBytes(salt);
        return salt;
      }
    }
  }
}
```

## テスト　

使用例を兼ねたテストこんな感じです。

```cs:
using Xunit;

namespace PasswordHash.Lib.Test {
  public class PasswordServiceTest {
    [Fact]
    public void TestVerifyPassword() {
      //　平文パスワード
      var rawPassword = "nossa1234";
      // テスト対象のクラス
      var sut = new PasswordService();  // sut means System Under Test
      
      // パスワードをハッシュ化、使用したソルトを得る
      var (hashed, salt) = sut.HashPassword(rawPassword);

      // 「ハッシュ」と「パスワード・ソルトから作成したハッシュ」が一致するかテスト
      Assert.True(sut.VerifyPassword(hashed, rawPassword, salt));
    }
  }
}
```

```sh
PasswordHash> dotnet test PasswordHash.Lib.Test
# 中略
テストの合計数: 1。成功: 1。失敗:0。スキップ: 0。
テストの実行に成功しました。
テスト実行時間: 2.0115 秒
```
- テスト時のソルト
![image.png](https://qiita-image-store.s3.amazonaws.com/0/195024/46065dac-1ce1-16b0-a25d-ea602bf94004.png)

- ハッシュ化されたパスワード
![image.png](https://qiita-image-store.s3.amazonaws.com/0/195024/bebda278-3e48-2415-bd1d-1d0f2717cf7a.png)


## サンプルのWeb API

上記のライブラリを使用して簡易的な Web API を実装しました。
ユーザー登録と認証機能が使えます。


```cs:
    // UsersController の登録処理の抜粋
    [HttpPost]
    public IActionResult RegisterUser([FromBody]RegisterUserRequest request) {
      bool success = userService.Register(request.UserName, request.RawPassword);
      return success ? Ok() : (IActionResult)Conflict();
    }

    // LoginController の認証の抜粋
    [HttpPost]
    public IActionResult Authenticate([FromBody]AuthenticateRequest request) {
      bool ok = userService.Authenticate(request.UserName, request.RawPassword);
      return ok ? Ok() : (IActionResult)Unauthorized();
    }
```

上記のコントローラーが移譲している実際のユーザー登録と認証は以下の通りです。

```cs:
    // ユーザー登録
    public bool Register(string username, string rawPassword) {
      bool duplicated = dbContext.Users.Any(u => u.Name == username);
      if (duplicated) {
        return false;
      }
      (string hashed, byte[] salt) = passwordService.HashPassword(rawPassword);
      var user = new User {
        Name = username,
        HashedPassword = hashed,
        Salt = salt
      };
      dbContext.Users.Add(user);
      dbContext.SaveChanges();
      return true;
    }

    // 認証
    public bool Authenticate(string username, string rawPassword) {
      var user = dbContext.Users.SingleOrDefault(u => u.Name == username);
      if (user is null) {
        return false;
      }
      return passwordService.VerifyPassword(user.HashedPassword, rawPassword, user.Salt);
    }
```

ソースは GitHub に置きました。
https://github.com/sano-suguru/PasswordHash

以下のコマンドで試せます。
DB はインメモリDBを使っていますので、ストレージは汚しません。

```sh
git clone git@github.com:sano-suguru/PasswordHash.git
cd PasswordHash
dotnet run -p PasswordHash.App
```
- ユーザーを登録
![image.png](https://qiita-image-store.s3.amazonaws.com/0/195024/befdf15a-bb20-3144-7261-2b01f574a294.png)

- 認証
![image.png](https://qiita-image-store.s3.amazonaws.com/0/195024/49917e95-555c-b76b-e253-35826b005ceb.png)

- ユーザーが重複したとき
![image.png](https://qiita-image-store.s3.amazonaws.com/0/195024/c11eea20-d5ea-f6d5-c33f-9fc99b30b762.png)

- 認証失敗
![image.png](https://qiita-image-store.s3.amazonaws.com/0/195024/79d66faa-c5a3-a5d5-126c-6e3fa50250b6.png)


## 最後に

学習を兼ねて作成しましたが、実際に認証処理を開発するときは抜け漏れがあると大変なので、
 ASP.NET Identity や OAuth などを使用すると良いと思います。