はじめに
DB にパスワードを保存するとき、そのまま平文で保存すると、
データが漏れた場合に、パスワードが 見た瞬間わかってしまいます。
この記事では C# を使って DB にパスワードをハッシュ化して登録してみたいと思います。
ハッシュ化の実装
パスワードを暗号化する際にsalt
を利用すると別々のユーザが同じパスワードを使っても別のハッシュを作ることができます。
認証の際はsalt
と平文のパスワードから作成できるハッシュが DB に登録されているハッシュと同じ場合、正しいパスワードです。
Cryptography.KeyDerivation
を使用しています。
dotnet add PasswordHash.Lib package "Microsoft.AspNetCore.Cryptography.KeyDerivation"
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;
}
}
}
}
テスト
使用例を兼ねたテストこんな感じです。
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));
}
}
}
PasswordHash> dotnet test PasswordHash.Lib.Test
# 中略
テストの合計数: 1。成功: 1。失敗:0。スキップ: 0。
テストの実行に成功しました。
テスト実行時間: 2.0115 秒
サンプルのWeb API
上記のライブラリを使用して簡易的な Web API を実装しました。
ユーザー登録と認証機能が使えます。
// 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();
}
上記のコントローラーが移譲している実際のユーザー登録と認証は以下の通りです。
// ユーザー登録
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を使っていますので、ストレージは汚しません。
git clone git@github.com:sano-suguru/PasswordHash.git
cd PasswordHash
dotnet run -p PasswordHash.App
最後に
学習を兼ねて作成しましたが、実際に認証処理を開発するときは抜け漏れがあると大変なので、
ASP.NET Identity や OAuth などを使用すると良いと思います。