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実装jBCryptSpringSecurityPicketlinkに若干の修正の上、引用されている
    • 今回はサンプルとしてjBCryptを使う
      • SpringSecurityをpom.xmlに追加すると、BCryptクラスを使いたいだけなのに、warサイズが増えるため
    • SpringSecurityのライセンスがApache License, Version 2.0だったので、jBCryptが怪しい場合はライセンスファイルと一緒にソースコード引用すれば良いと思う

実装する

公式コミュニティにおいても、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に置いてあります。

まとめ

  • jBCryptをApache Shiroに組み込んでみました
  • Apache ShiroではPasswordServiceインタフェースを拡張することで、様々なハッシュアルゴリズムに対応できます