Javaでパスワードのハッシュ化: PBKDF2

エフセキュアブログ : 再録:パスワードは本当にSHA-1+saltで十分だと思いますか? に望ましいパスワードのハッシュ化方法について具体的に載っていたので、Javaでどんな感じになるのか確認してみる。

使うのは現在一応安全だと認められているっぽいもので、Oracle JDKにも標準で含まれている PBKDF2 *1 という方式。Wikipedia によると色々なプロダクトでも使われているようだ。

テスト

package example;

import java.security.NoSuchAlgorithmException;
import java.security.SecureRandom;
import java.security.spec.InvalidKeySpecException;
import java.security.spec.KeySpec;
import java.util.Arrays;

import javax.crypto.SecretKey;
import javax.crypto.SecretKeyFactory;
import javax.crypto.spec.PBEKeySpec;
import javax.crypto.spec.SecretKeySpec;

public class PBKDF2 {

    public static void main(String[] args) throws NoSuchAlgorithmException, InvalidKeySpecException {
        String p1 = args[0];
        String p2 = args[1];
        printfln("password 1: %s", p1);
        printfln("password 2: %s", p2);

        byte[] salt = createSalt();
        printfln("salt: %s", Arrays.toString(salt));

        byte[] d1 = pbkdf2(p1.toCharArray(), salt);
        byte[] d2 = pbkdf2(p2.toCharArray(), salt);
        printfln("derived 1: %s", Arrays.toString(d1));
        printfln("derived 2: %s", Arrays.toString(d2));
    }

    // ランダムなsaltを生成
    static byte[] createSalt() throws NoSuchAlgorithmException {
        SecureRandom random = SecureRandom.getInstance("SHA1PRNG");
        byte[] salt = new byte[32];
        random.nextBytes(salt);
        return salt;
    }

    static byte[] pbkdf2(char[] password, byte[] salt) throws InvalidKeySpecException, NoSuchAlgorithmException {
        // Oracle JDKに含まれる PBKDF2 の唯一の実装
        SecretKeyFactory f = SecretKeyFactory.getInstance("PBKDF2WithHmacSHA1");
        // 繰り返し回数:10000回、結果の長さ:256bit
        KeySpec ks = new PBEKeySpec(password, salt, 10000, 256);
        SecretKey s = f.generateSecret(ks);
        return s.getEncoded();
    }

    // 補助
    private static void printfln(String format, Object ... args) {
        System.out.printf(format + "%n", args);
    }
}

確認(Maven2使用)

>mvn -q exec:java -Dexec.mainClass="example.PBKDF2" -Dexec.args="a a"
password 1: a
password 2: a
salt: [71, 25, 97, -89, -30, 77, 37, 19, -14, 52, 20, -46, 98, 36, -41, 54, 127,
 -76, -53, 97, 23, 53, 107, -67, -36, -120, 67, -108, 122, -106, -63, -64]
derived 1: [79, 18, 24, 95, 116, -121, -70, -72, 73, -68, -115, -48, -104, 119,
7, 56, 84, 98, -34, 90, 108, -31, 58, -107, 119, 105, 84, 45, -67, -6, -19, 4]
derived 2: [79, 18, 24, 95, 116, -121, -70, -72, 73, -68, -115, -48, -104, 119,
7, 56, 84, 98, -34, 90, 108, -31, 58, -107, 119, 105, 84, 45, -67, -6, -19, 4]
>mvn -q exec:java -Dexec.mainClass="example.PBKDF2" -Dexec.args="a b"
password 1: a
password 2: b
salt: [-30, 13, 106, -63, 31, -92, -61, 49, -29, -72, 48, 0, -96, -6, -124, 57,
-116, -78, 64, -23, -76, -68, -117, 29, 17, -119, 4, -85, -107, 29, 88, 60]
derived 1: [-121, -57, 70, -10, 111, 22, 79, -48, -113, -27, 85, 49, 80, 112, -2
7, 60, -114, 98, -88, -90, 117, -75, 28, 4, -10, 82, -68, -7, 31, 46, 20, -89]
derived 2: [40, -15, -42, -44, 125, -128, 122, -122, 81, -63, -31, -25, -16, 14,
 -105, -90, -48, -17, 54, -92, -59, 95, 28, 50, 88, 55, -41, 66, 63, -68, -89, -
109]

他、調査の過程で知ったこといろいろ。

その他のハッシュ方式について

現在では、 memory-bound なアルゴリズムを使った scrypt の方が、CPU-Boundなアルゴリズムである PBKDF2 よりも総当り攻撃に強いとされているらしい。

…と、いろんなところでそう書かれているけどどのくらい検証されているのかは不明。Java版の実装も GitHub - wg/scrypt: Java implementation of scrypt にあるようだけど、正しく実装されているか確認する時間がない…。

既存パスワードのハッシュ方式の変更方法

将来、CPU/GPUの性能が向上して、そろそろハッシュ方式を変更しなければならなくなったけど、元のパスワードは保管していないので再計算なんてできない! どうしよう?

答え:「ユーザのパスワードが古い形式で保管されていたら古い形式で認証し、ログイン成功したら、新しいハッシュ方式で計算し直した結果で上書き」という機能を仕込んでおけば良い。ある程度時間が必要だけど。

ハッシュ化されたパスワードの保管方法

計算した結果をDB等にどういう形式で保管するか。

考慮する点:

  • saltは後で必要なので一緒に保管する必要がある
  • 上で述べたように後でハッシュ化の方式を変更することを考えると、どの方式でハッシュ化されたかについての情報も含めた方が良いかも?*2

ひとつの候補として、UNIX関係で広く使われている Modular Crypt Format という形式があり、PythonのPBKDF2ライブラリがこれに近い形式の出力文字列を定めているので、この形式に従うのも良いかもしれない。

*1:略語が覚えにくいので、Password-Based Key Derivation Function: パスワードベースの鍵導出関数、と元の言葉を覚えたほうが早い

*2:漏洩した場合に方式だけ隠していてもあんまり意味がなさそうなので、含めても問題ないよね? たぶん…