Apache ShiroでBCryptを使う
先日のJava EE Advant Calander 2014にApache Shiroを使ってみましたを書いたところ、Shiroのデフォルトのハッシュポリシーソルト付きSHA-256, 500000 イテレーション
について、ハッシュイテレーションのアルゴリズムをShiroのように独自実装せずに、総当たり攻撃耐性を持たせるために低速化させた既存のアルゴリズム(PBKDF2, BCrypt)もあるよ*1*2*3*4 と教えてもらったため、Shiroに組み込めないか試してみる。
ハッシュアルゴリズムをどうするか
@watarinさんのJavaでパスワードをハッシュ化するのに良い方法を調べてみたを入り口に色々と調べてみたが、以下の理由でBCrypt (Java実装はjBCrypt) を選んでみた。
- Java7で動かしたい (PBKDF2WithHMAC-SHA-256がJava8から)
- JBossEAP6.3が今のところJava8をサポートしてないため
- ここで言及されている意見
- しかし、実検証データがないのはちょっと不安
- 処理において配列アクセスの多いBCryptがGPU対策に本当によいのかは不明
- Java実装jBCryptはSpringSecurityやPicketlinkに若干の修正の上、引用されている
実装する
公式コミュニティにおいても、SHIRO-290 Create a BCrypt Hash implementationとしてBCryptサポートが要望されているが、添付されている.patch案がストレッチ強度が調整できないようになってたので、作ってみる。
BCrypt向けPasswordServiceを作る
Shiro1.2より、パスワードのハッシュ化と照合を担うPasswordServiceインタフェースが用意されているため、デフォルト実装であるDefaultPasswordServiceクラスのコードを参考にして作ってみる。
public class BCryptPasswordService implements PasswordService { public static final int DEFAULT_BCRYPT_ROUND = 10; private int logRounds; public BCryptPasswordService() { this.logRounds = DEFAULT_BCRYPT_ROUND; } public BCryptPasswordService(int logRounds) { this.logRounds = logRounds; } @Override public String encryptPassword(Object plaintextPassword) { if (plaintextPassword instanceof String) { String password = (String) plaintextPassword; return BCrypt.hashpw(password, BCrypt.gensalt(logRounds)); } throw new IllegalArgumentException( "BCryptPasswordService encryptPassword only support java.lang.String credential."); } @Override public boolean passwordsMatch(Object submittedPlaintext, String encrypted) { if (submittedPlaintext instanceof char[]) { String password = String.valueOf((char[]) submittedPlaintext); return BCrypt.checkpw(password, encrypted); } throw new IllegalArgumentException( "BCryptPasswordService passwordsMatch only support char[] credential."); } public void setLogRounds(int logRounds) { this.logRounds = logRounds; } public int getLogRounds() { return logRounds; } }
ハッシュパスワード生成コードの修正
アカウント生成時にDefaultPasswordServiceを使っている部分を今作ったBCryptPasswordServiceに置き換える。
// PasswordService ps = new DefaultPasswordService(); // ラウンド数をデフォルトから変えたい場合はコンストラクタに設定 // デフォルトは10 // PasswordService ps = new BCryptPasswordService(12); PasswordService ps = new BCryptPasswordService(); String encryptedPassword = ps.encryptPassword(form.getPassword());
コメントアウトしているように、ストレッチ強度を返る時にはコンストラクタに設定する。BCryptの特徴として、引数のラウンド数は2のXX乗のような指数で示すため、デフォルトの10を20に変更したら、計算量は2倍ではなく1024倍になるので注意。
ストレッチ強度はマシンスペックおよび許容できる遅延に応じて調整する。
shiro.iniの修正
パスワード照合時に先ほど作ったBCryptPasswordServiceが使われるように、shiro.iniのpasswordMatcherを修正する。
[main] # Default SHA-256, 500000 hash iteration, use salt #passwordService = org.apache.shiro.authc.credential.DefaultPasswordService # BCrypt PasswordService passwordService = net.agetsuma.sample.shiro.security.BCryptPasswordService passwordMatcher = org.apache.shiro.authc.credential.PasswordMatcher passwordMatcher.passwordService = $passwordService
ハッシュパスワード(例: $2a$10$e4l5...)にイテレーションのラウンド数(例では10)が組み込まれているため、shiro.iniで明示的にラウンド数を設定しなくてもパスワード照合が可能。
試しに作ってみたものの一式はGithubに置いてあります。
まとめ
*1:エフセキュアブログ : 再録:パスワードは本当にSHA-1+saltで十分だと思いますか?
*3:ユーザーのパスワードを安全に保管する方法について - 11月 - 2013 - Sophos Press Releases, Security News and Press Coverage - Sophos Press Office | Sophos News and Press Releases - ソフォス
*4:6.4. パスワードハッシュ化 — TERASOLUNA Global Framework Development Guideline 1.0.0.publicreview documentation