はじめに
Web アプリケーションで安全にパスワードを保存するためには、ソルト+ハッシュ+ストレッチングという実装方法が2013年頃から啓蒙されてきていましたが、Webセキュリティの大家である徳丸浩さんによると独自実装は既に時代遅れであるようです。
ソルト付きハッシュとストレッチングというのは原理の話で、PBKDF2などは実装の話ですよね。徳丸本2には、その両方が紹介されています(宣伝)。原理抜きで実装の説明をすることは、私にとっては *ありえない* ことなので。徳丸本初版には独自実装例がありましたが、2版では削除しました https://t.co/U4MNcayd5a
— 徳丸 浩 (@ockeghem) 2019年2月8日
これからの Web アプリケーションでは、ソルト+ハッシュ+ストレッチングを独自実装で行うのではなく、以下のように専用のハッシュ関数を利用することが推奨されます。
パスワード保存に適したハッシュ関数として、BCryptやPBKDF2、Argon2などがあります。
(中略)
また、PHPに限らず、パスワード保存機能は独自実装せずに、安全なライブラリやフレームワークの機能を用いることを推奨します。
この記事では、PHP, Java, .NET でパスワードを保存するのに適した実装方法を具体的にご紹介します。
目次
ハッシュ関数の実装
BCryptやPBKDF2、Argon2などのハッシュ関数の具体的な実装ライブラリなどは以下のようになります。
PHP | Java | .NET | 備考 | |
BCrypt | password_hash
password_verify (>=PHP5.5.0) ※関数のデフォルト |
Spring Security | BCrypt.NET
(MIT) |
主要なBCrypt関数はパスワードの72文字以降を切り捨てる模様。 |
PBKDF2 | hash_pbkdf2
(>=PHP5.5.0) |
Spring Security | Rfc2898DeriveBytes
(.NET) |
|
Argon2 | password_hash
password_verify (>=PHP5.5.0) |
– | – |
ハッシュ関数の実装例(PHP)
PHPでパスワードのハッシュ値を取得するには、password_hash 関数を使用します。
オプションで PASSWORD_DEFAULT と指定することで、BCrypt を使用できます。
<?php // パスワードのハッシュ値を取得 $hash = password_hash("password", PASSWORD_DEFAULT); echo $hash;
ハッシュ値の出力例は以下のようになります。
$2y$10$WNy1AhuqzrjRfeZfoxBJWeUJFvW5EhWVbmsYmyYBEysP4QnEzNN.i
パスワードをチェックするには password_verify 関数を使用します。
以下のソースで、$password にはユーザーが入力したパスワードを、$hashにはデータベースに格納されているユーザーのハッシュ値を指定します。
<?php $password = "password"; $hash = '$2y$10$WNy1AhuqzrjRfeZfoxBJWeUJFvW5EhWVbmsYmyYBEysP4QnEzNN.i'; // パスワードをチェックする $result = password_verify($password, $hash); if ($result) { echo "password is valid!"; } else { echo "password is invalid!"; }
実行結果は以下のようになります。
password is valid!
ハッシュ関数の実装例(Java)
Java でパスワードのハッシュ値を取得するには、Spring Framework の Spring Security の機能を使用することがよいでしょう。
Spring Framework は現在デファクトスタンダードですし、活発に開発が行われているので信頼性が高いためです。
pom.xml の設定
Spring Framework でパスワード用のハッシュ関数を使用するには、pom.xml に以下の設定を追加する必要があります。
<dependency> <groupId>org.springframework.security</groupId> <artifactId>spring-security-core</artifactId> </dependency> <dependency> <groupId>org.springframework.security</groupId> <artifactId>spring-security-web</artifactId> </dependency> <dependency> <groupId>org.springframework.security</groupId> <artifactId>spring-security-config</artifactId> </dependency>
BCrypt の実装
BCrypt の Spring Security の実装は以下のようになります。Spring Boot での実装ですが、パスワードの部分は非常に簡単に実装できることが分かると思います。
package com.example.demo; import org.springframework.stereotype.Controller; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RequestMethod; import org.springframework.web.bind.annotation.RequestParam; import org.springframework.web.servlet.ModelAndView; import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder; import org.springframework.security.crypto.password.PasswordEncoder; @Controller public class HelloSpringBootWebController { // BCrypt PasswordEncoder passwordEncoder = new BCryptPasswordEncoder(); @RequestMapping(value="/", method=RequestMethod.GET) public ModelAndView index(ModelAndView mv) { mv.setViewName("index"); // パスワードを設定 String password = "password"; // ハッシュ値取得 String hash = GetHashedPassword(password); // 新規登録時はハッシュ値を保存する。 // パスワードと保存してあったハッシュ値をチェック CheckHashedPassword(password, hash); return mv; } // ハッシュ値取得 private String GetHashedPassword(String password) { String hash = passwordEncoder.encode(password); System.out.println("ハッシュ値 : " + hash); return hash; } // パスワードチェック private void CheckHashedPassword(String password, String hash) { if (passwordEncoder.matches(password, hash)) { System.out.println("パスワードが一致しました。"); } else { System.out.println("パスワードが一致しません。"); } } }
このコードの実行結果は、以下のようになります。
ハッシュ値 : $2a$10$PwzoVb6xybS821tPLsG0je3RrFGJs.M0cJ9.njr0JixXl4zcL21r2 パスワードが一致しました。
PBKDF2 の実装
PBKDF2 は BCrypt とほとんど同じ実装で実現できます。
PasswordEncoder が変わるだけです。
実装コードは以下のようになります。
package com.example.demo; import org.springframework.stereotype.Controller; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RequestMethod; import org.springframework.web.bind.annotation.RequestParam; import org.springframework.web.servlet.ModelAndView; import org.springframework.security.crypto.password.Pbkdf2PasswordEncoder; import org.springframework.security.crypto.password.PasswordEncoder; @Controller public class HelloSpringBootWebController { // PBKDF2 PasswordEncoder passwordEncoder = new Pbkdf2PasswordEncoder(); @RequestMapping(value="/", method=RequestMethod.GET) public ModelAndView index(ModelAndView mv) { mv.setViewName("index"); // パスワードを設定 String password = "password"; // ハッシュ値取得 String hash = GetHashedPassword(password); // 新規登録時はハッシュ値を保存する。 // パスワードと保存してあったハッシュ値をチェック CheckHashedPassword(password, hash); return mv; } // ハッシュ値取得 private String GetHashedPassword(String password) { String hash = passwordEncoder.encode(password); System.out.println("ハッシュ値 : " + hash); return hash; } // パスワードチェック private void CheckHashedPassword(String password, String hash) { if (passwordEncoder.matches(password, hash)) { System.out.println("パスワードが一致しました。"); } else { System.out.println("パスワードが一致しません。"); } } }
このコードの実行結果は、以下のようになります。
ハッシュ値 : adc7c5d3843c66d5f5e952d82ea3095cf1d1ca464b52711ce1d83d98c025029d7e6b3a6bdc21f430 パスワードが一致しました。
ハッシュ関数の実装例(.NET)
BCrypt
.NET で BCrypt の実装を行うには、BCrypt.Net (MIT ライセンス) を使用することが簡単です。
以下の実装は、.NET Core 2.1 のコンソールアプリケーションに、Nuget から BCrypt.Net-Next をインストールしたものになります。
using System; using BCrypt.Net; namespace BCryptSample { class Program { static void Main(string[] args) { // パスワードを設定 string password = "password"; // ハッシュ値取得 string hash = GetHashedPassword(password); // 新規登録時はハッシュ値を保存する。 // パスワードと保存してあったハッシュ値をチェック CheckHashedPassword(password, hash); Console.ReadKey(); } // ハッシュ値取得 private static string GetHashedPassword(string password) { string hash = BCrypt.Net.BCrypt.HashPassword(password); Console.WriteLine($"ハッシュ値:{hash}"); return hash; } // パスワードチェック private static void CheckHashedPassword(string password, string hash) { if (BCrypt.Net.BCrypt.Verify(password, hash)) { Console.WriteLine("パスワードが一致しました。"); } else { Console.WriteLine("パスワードが一致しません。"); } } } }
実行結果は以下のようになります。
ハッシュ値:$2a$11$64p87vIms0fzqFh53fCfruWTiI7jyEfXBoIcta/uyQUtRu7mRtO2q パスワードが一致しました。
PBKDF2 の実装
.NET で PBKDF2 を実装するには、System.Security.Cryptography.Rfc2898DeriveBytes を使用します。
.NET の標準機能で実装できますが、若干手間がかかります。
ソルトとハッシュ値の両方を保存する必要があります。
以下の実装は、.NET Core 2.1 のコンソールアプリケーションになります。
using System; using System.Text; using System.Security.Cryptography; namespace PBKDF2Sample { class Program { // ソルトのサイズ private const int SALT_SIZE = 32; // ハッシュのサイズ private const int HASH_SIZE = 32; //ストレッチング回数 private const int STRETCH_COUNT = 1000; static void Main(string[] args) { // パスワードを設定 string password = "password"; // ソルトを取得 string salt = GetSalt(SALT_SIZE); // ハッシュ値を取得 string hash = GetHash(password, salt, HASH_SIZE, STRETCH_COUNT); // 新規登録時はソルトとハッシュ値を保存する // 保存してあったハッシュ値(これはダミー) string savedHash = hash; // 保存してあったハッシュ値と計算したハッシュ値が同じならOK CheckHashedPassword(hash, savedHash); Console.ReadKey(); } // ソルトを取得 private static string GetSalt(int size) { var bytes = new byte[size]; using (var rngCryptoServiceProvider = new RNGCryptoServiceProvider()) { rngCryptoServiceProvider.GetBytes(bytes); } string salt = Convert.ToBase64String(bytes); Console.WriteLine($"ソルト:{salt}"); return salt; } // ハッシュ値を取得 private static string GetHash(string password, string salt, int size, int cnt) { byte[] bytes = Encoding.ASCII.GetBytes(salt); byte[] bytesSalt; using (var rfc2898DeriveBytes = new Rfc2898DeriveBytes(password, bytes, cnt)) { bytesSalt = rfc2898DeriveBytes.GetBytes(size); } string hash = Convert.ToBase64String(bytesSalt); Console.WriteLine($"ハッシュ値:{hash}"); return hash; } // ハッシュ値を比較 private static void CheckHashedPassword(string hash, string savedHash) { if (hash == savedHash) { Console.WriteLine("パスワードが一致しました。"); } else { Console.WriteLine("パスワードが一致しません。"); } } } }
実行結果は以下のようになります。
ソルト:cPDY1AORTFWWIz3gBalSa0tyMLsKwU90yXEDHXVJAgs= ハッシュ値:Ap+0VtZ550vABI6lX8mj5w6CfRedwro1PjKbkuShfXs= パスワードが一致しました。
おわりに
パスワードを保存するのに適したハッシュ関数である BCrypt、PBKDF2、Argon2とその実装(PHP, Java, .NET) を見てきました。
この内容をまとめてある記事はあまりないと思うので、参考になれば幸いです。
また、この記事を書くにあたり、関連する記事のアップデートも行いました。
コメント