こんにちは。SI部の満石です。

今回はJava SE 8限定となりますが、安全なパスワードを生成する方法についてご紹介します。

安全なパスワードの最小限の要件

今年のベネッセの情報漏洩事件はまだ記憶に新しいと思いますし、あまり大きく取り上げられていないようですが最近500万件のGmailのアカウント情報が流出したらしく、おそらくその影響で様々なサイトでなりすましログインが発生しているようです。このような例もあり、ここ数年で個人情報を守るためにセキュリティがどんどん重要視されるようになってきています。

もはやシステムを設計するにあたって、SQLインジェクション、XSS(クロスサイトスクリプティング)、CSRF(クロスサイトリクエストフォージェリ)のような代表的な脆弱性について対応することは当たり前になっているのではないでしょうか。

しかしながら、パスワードを保存するということについては、とりあえず平文じゃなければOKという考えでMD5、SHA-1、SHA-256のようなハッシュアルゴリズムでハッシュ化するだけの対応で済ませていませんか?

もしそうなら、万が一パスワード情報が外部に漏洩した場合にその対応では不十分です。以下のページをご覧ください。

本当は怖いパスワードの話
「体系的に学ぶ 安全なWebアプリケーションの作り方」の著者で、Webアプリケーションセキュリティの第一人者である徳丸浩さんがパスワードの安全な保存方法について書かれた記事です。

セキュリティの重要課題:ユーザーのパスワードを安全に保管する方法について
アンチウイルスソフトやその他様々なセキュリティ関連製品をリリースしているSOPHOS社が、エンジニア向けに詳細にパスワードを安全に保管する方法について書いた記事です。

このページのどちらも、「ハッシュ」・「ソルト」・「ストレッチング」の3つの対応を行うことを推奨しています。
SOPHOS社のページには最小限の要件が書かれていますが、その中で以下の項目を満たせるコードを今回は作成してみました。

  • 強力な乱数生成器を使用して 16 バイト以上のソルトを作成します。
  • そのソルトとパスワードを PBKDF2 アルゴリズムに提供します。
  • PBKDF2 内のコアハッシュとして HMAC-SHA-256 を使用します。
  • 10,000 回以上繰り返して実行します (2013 年 11 月時点)。
  • 32 バイト (256 ビット) の出力を PBKDF2 から最終的なパスワードハッシュとして取り出します。

ソルトについては徳丸浩さんが必ずしも乱数を使う必要は無いと書かれていますので、今回はユーザーIDをソルトとして使用する想定で作成することにしました。

パスワード生成に使用するアルゴリズム

さて、ここまでの説明で見慣れない単語が2つあると思います。「PBKDF2」と「HMAC-SHA-256」ですね。

「PBKDF2」はPKCS(Public-Key Cryptography Standards、公開鍵暗号標準)の#5で定められているアルゴリズムで、「Password-Based Key Derivation Function 2(パスワードベースのキー派生関数2)」の頭文字を取ったものです。

「HMAC-SHA-256」の「HMAC」は「Hash-based Message Authentication Code(ハッシュベースのメッセージ認証符号)」の頭文字を取ったもので、「SHA-256」がこのアルゴリズムで使用するハッシュ関数です。

SOPHOS社のページには簡単な解説がありますが、「PBKDF2」はストレッチングのアルゴリズムで、「HMAC-SHA-256」は「PBKDF2」の中で今回は使われているようです。

専門家ではないためこれ以上の解説はできないのですが、これほどのアルゴリズムであればJavaでライブラリとして簡単に使えるようになっていないだろうかと思い、調べてみました。

すると、Java SE 8なら標準APIとしてPBKDF2とHMAC-SHA-256を一緒に使用するアルゴリズムが用意されていることが分かりました。
Java Cryptography Architecture Standard Algorithm Name Documentation for JDK 8のSecretKeyFactory
「Algorithm Name」が「PBKDF2With<prf>」の「Description」の例に「PBKDF2WithHmacSHA256」と書かれています。

残念なことに、Java SE 6とJava SE 7のSecretKeyFactoryには同様の記述は見当たりませんでした。
・Java SE 6
http://docs.oracle.com/javase/6/docs/technotes/guides/security/StandardNames.html#SecretKeyFactory

・Java SE 7
http://docs.oracle.com/javase/7/docs/technotes/guides/security/StandardNames.html#SecretKeyFactory

サンプルコード

Java SE 8でアルゴリズムとして「PBKDF2WithHmacSHA256」が使用可能なことがわかったので、サンプルコードを実装してみました。ソルトはユーザーIDを想定しているため、そのままバイト配列にはせず、SHA-256でハッシュ化することで確実に16バイト以上(※SHA-256だと必ず32バイト)になるようにしています。PBEKeySpecクラスのコンストラクタで生成される鍵の長さはHMAC-SHA-256に合わせて256を指定しています。その結果、SecretKeyクラスによって生成されるバイト配列は32バイトとなるため、16進数文字列へ変換後は64文字となります。

PasswordUtil.java

import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import java.security.spec.InvalidKeySpecException;

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

public class PasswordUtil {

	/** パスワードを安全にするためのアルゴリズム */
	private static final String ALGORITHM = "PBKDF2WithHmacSHA256";
	/** ストレッチング回数 */
	private static final int ITERATION_COUNT = 10000;
	/** 生成される鍵の長さ */
	private static final int KEY_LENGTH = 256;

	/**
	 * 平文のパスワードとソルトから安全なパスワードを生成し、返却します
	 *
	 * @param password 平文のパスワード
	 * @param salt ソルト
	 * @return 安全なパスワード
	 */
	public static String getSafetyPassword(String password, String salt) {

		char[] passCharAry = password.toCharArray();
		byte[] hashedSalt = getHashedSalt(salt);

		PBEKeySpec keySpec = new PBEKeySpec(passCharAry, hashedSalt, ITERATION_COUNT, KEY_LENGTH);

		SecretKeyFactory skf;
		try {
			skf = SecretKeyFactory.getInstance(ALGORITHM);
		} catch (NoSuchAlgorithmException e) {
			throw new RuntimeException(e);
		}

		SecretKey secretKey;
		try {
			secretKey = skf.generateSecret(keySpec);
		} catch (InvalidKeySpecException e) {
			throw new RuntimeException(e);
		}
		byte[] passByteAry = secretKey.getEncoded();

		// 生成されたバイト配列を16進数の文字列に変換
		StringBuilder sb = new StringBuilder(64);
		for (byte b : passByteAry) {
			sb.append(String.format("%02x", b & 0xff));
		}
		return sb.toString();
	}

	/**
	 * ソルトをハッシュ化して返却します
	 * ※ハッシュアルゴリズムはSHA-256を使用
	 *
	 * @param salt ソルト
	 * @return ハッシュ化されたバイト配列のソルト
	 */
	private static byte[] getHashedSalt(String salt) {
		MessageDigest messageDigest;
		try {
			messageDigest = MessageDigest.getInstance("SHA-256");
		} catch (NoSuchAlgorithmException e) {
			throw new RuntimeException(e);
		}
		messageDigest.update(salt.getBytes());
        return messageDigest.digest();
	}
}

このクラスを使って安全なパスワードを生成してみましょう。
Main.java

public class Main {
	public static void main(String[] args) {
		String safetyPassword1 = PasswordUtil.getSafetyPassword("password", "USERID0001");
		System.out.println(safetyPassword1);
		String safetyPassword2 = PasswordUtil.getSafetyPassword("password", "USERID0002");
		System.out.println(safetyPassword2);
	}
}

実行結果は
dc9a72d80b7513a31895f6624de2b8d3441b32218ae7454fc740794bd5dae89e
55aeb3acc1d7b658f9e0f39e9779e641086c618fb0f50abdf51c3da9d4312f93
となり、同じパスワードでもソルトが異なるため全く異なる結果となりました。

ちなみにこのコードをJava SE 7で実行したら、
java.security.NoSuchAlgorithmException: PBKDF2WithHmacSHA256 SecretKeyFactory not available
が発生し、やはりエラーとなっていまいました。

実装してみた感想

Java SE 8ならサードパーティのライブラリを使用することなく、標準APIだけで簡単にSOPHOS社が推奨する最小限の要件を満たせるパスワードを生成することが出来ました。

ストレッチングについては単純にハッシュ化を繰り返すだけでも効果はあると思いますが、可能ならこのような信頼できるアルゴリズムを使用した方がいいと思います。

Java SE 8はLambda式やDate and Time APIが注目されていますが、地味にこのようなところでも改善されていることを知ることができたのはいい収穫でした。