SHA256 でパスワードを二重に暗号化 (疑問点はコメントにて解説頂けました)で赤っ恥と引き換えに貴重な知見を頂けたので、早速紹介いただいたBCryptとPBKDF2でのパスワード 暗号化 ハッシュ化をやってみます。
今回はBCrypt編。
使用したライブラリ
今回は前述の記事のコメントで教えて頂いたそのまんまのspring-securityを使ってBCrypt 暗号化 ハッシュ化を行います。
Spring全体まではいらないので、mvnrepositoryからspring-securityのjarだけ借りてきました。
※apacheのcommon-loggingがないと動きません。以下ソースのimport文には入っていませんがクラスパスには追加しておく必要があります。
まずはエンコードと認証
※import文を含むクラス全体のコードは記事末尾にあります。
String passStr = "password_desu_4";
String hashStr;
// エンコード
BCryptPasswordEncoder bpe = new BCryptPasswordEncoder();
hashStr = bpe.encode(passStr);
System.out.println("original string : " + passStr);
System.out.println("encoded by Bcrypt : " + hashStr);
// 認証
boolean isMatch = bpe.matches(passStr, hashStr);
System.out.println("result : " + (isMatch ? "match" : "not match"));
スマート!
結果はこんな感じ
original string : password_desu_4
encoded by Bcrypt : $2a$10$HQwFeyajSohLrmbHTEzJbuHzPgITU7CJ/fQbJfEBCkBJXz5lCk8ke
result : match
ストレッチ回数ごとの処理時間
BCryptPasswordEncoderクラスのコンストラクタに4から31までのintを渡すことでstrengthの設定ができます。
streach timesではなくてstrengthです。
2の、引数分の乗数回ストレッチするそうで。
当然処理時間も指数関数的に増えていくと思われます。
さあ頑張れ32bit i7コアのラップトップよ。
※コードは記事末尾を参照
= strength : 4, streach times : 16 ========
encoding time : 0.003 sec.
verifying time : 0.002 sec.
= strength : 5, streach times : 32 ========
encoding time : 0.004 sec.
verifying time : 0.004 sec.
= strength : 6, streach times : 64 ========
encoding time : 0.007 sec.
verifying time : 0.008 sec.
= strength : 7, streach times : 128 ========
encoding time : 0.015 sec.
verifying time : 0.014 sec.
= strength : 8, streach times : 256 ========
encoding time : 0.029 sec.
verifying time : 0.030 sec.
= strength : 9, streach times : 512 ========
encoding time : 0.058 sec.
verifying time : 0.060 sec.
= strength : 10, streach times : 1024 ======== ★デフォルトのstrength
encoding time : 0.118 sec.
verifying time : 0.117 sec.
= strength : 11, streach times : 2048 ========
encoding time : 0.240 sec.
verifying time : 0.231 sec.
= strength : 12, streach times : 4096 ========
encoding time : 0.462 sec.
verifying time : 0.463 sec.
= strength : 13, streach times : 8192 ========
encoding time : 0.924 sec.
verifying time : 0.926 sec.
= strength : 14, streach times : 16384 ========
encoding time : 1.847 sec.
verifying time : 1.840 sec.
= strength : 15, streach times : 32768 ========
encoding time : 3.702 sec.
verifying time : 3.691 sec.
= strength : 16, streach times : 65536 ========
encoding time : 7.440 sec.
verifying time : 7.402 sec.
= strength : 17, streach times : 131072 ========
encoding time : 14.867 sec.
verifying time : 14.776 sec.
= strength : 18, streach times : 262144 ========
encoding time : 30.416 sec.
verifying time : 29.593 sec.
= strength : 19, streach times : 524288 ========
encoding time : 60.6590 sec.
verifying time : 60.180 sec.
= strength : 20, streach times : 1048576 ========
まだ途中ですが処理を待つのがきつくなってきました
まあ処理時間なんて環境によるので、私の環境での正確な秒数を記録することにあまり意味はないでしょう。
ということで、ここまでで読み取れる傾向は以下。
- ハッシュ化にかかる時間と認証にかかる時間はだいたい同じ
- strengthが1増えれば処理時間は約2倍になる
…処理量が2倍になるんだから当たり前ですね。
というわけで簡易化した理論値を出すと
strength | 処理時間 |
---|---|
20 | 2分 |
21 | 4分 |
22 | 8分 |
23 | 16分 |
24 | 32分 |
25 | 1時間4分 |
26 | 2時間8分 |
27 | 4時間16分 |
28 | 8時間32分 |
29 | 17時間4分 |
30 | 1日と10時間8分 |
31 | 2日と20時間16分 |
指数関数って怖い。
ハイスペックな環境でやったら当然縮むとはいえ、パスワード一つにこの処理量。
strengthはデフォルトで10(ストレッチ1024回)ですが、正常ログイン時の待ち時間を考えても、せいぜい0.5秒(今回の環境ではstrength=12)くらいが限界じゃないかと思います。
これはなるほどセキュリティ措置になりえます。
身をもって実感しました。31まで試さなくて良かった
クラス全文
import java.math.BigInteger;
import java.util.Date;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
public class CryptUtil {
/**
* 処理時間を計測しながらBcyptでハッシュ化
* @param passStr パスワード文字列
* @param strength ハッシュ化の回数。4~31(2の何乗か)
*/
public static String testBcryptEncode(String passStr, int strength) {
// 開始時刻
long startTime = (new Date()).getTime();
// BCryptでのハッシュ化
BCryptPasswordEncoder bpe = new BCryptPasswordEncoder(strength);
String hashStr = bpe.encode(passStr);
// ハッシュ化終了時刻
long endTime = (new Date()).getTime();
String timeDelta = String.format("%.4f", ((float)(endTime - startTime) / 1000));
System.out.println("encoding time : " + timeDelta + " sec.");
return hashStr;
}
/**
* 処理時間を計測しながら生パスワードとハッシュが合致するかの認証
* @param passStr 生
* @param hashStr ハッシュ
* @param strength
*/
public static void testBcyptVerify(String passStr, String hashStr, int strength) {
// 開始時刻
long startTime = (new Date()).getTime();
// パスワードがハッシュに一致するかのチェック
BCryptPasswordEncoder bpe = new BCryptPasswordEncoder(strength);
boolean isMatch = bpe.matches(passStr, hashStr);
// 検証終了時刻
long endTime = (new Date()).getTime();
String timeDelta = String.format("%.4f", ((float)(endTime - startTime) / 1000));
if (isMatch) {
System.out.println("verifying time : " + timeDelta + " sec.");
} else {
}
}
public static void main(String[] args) {
String passStr = "password_desu_4";
String hashStr;
// 単純なエンコードと認証
BCryptPasswordEncoder bpe = new BCryptPasswordEncoder();
hashStr = bpe.encode(passStr);
System.out.println("original string : " + passStr);
System.out.println("encoded by Bcrypt : " + hashStr);
boolean isMatch = bpe.matches(passStr, hashStr);
System.out.println("result : " + (isMatch ? "match" : "not match"));
System.out.println("");
// strengthごとに時間計測
double streachTimes;
for (int strength=4; strength<=31; strength++) {
streachTimes = Math.pow(2, strength);
System.out.println("= strength : " + strength + ", streach times : " + String.format("%1$.0f", streachTimes) + " ========");
hashStr = testBcryptEncode(passStr, strength);
testBcyptVerify(passStr, hashStr, strength);
}
}
}