SMD5の実装
パスワードを平文で保存するのは危険であり、MD5などでパスワードをハッシュ化した値を持っておくのが一般的です。しかしMD5を使った場合、一般的なパスワードでは辞書攻撃で解読されてしまいます。例えば、「5f4dcc3b5aa765d61d8327deb882cf99」というハッシュ値をmd5.rednoize.comで検索すると、ハッシュ前のパスワードである「password」が分かります。
そこでSMD5 (Salted MD5) が登場してきます。SMD5とは「パスワード+ソルト(ランダムな文字列)」からMD5を使ってハッシュ値を計算する方法です。パスワードごとに別のソルトを用いることで辞書作成を難しくして、辞書攻撃を防ぐことができます。
SMD5をJavaで実装したところ、以下のような結果になりました。変換の1回目と2回目でハッシュ値が異なることが分かります。
実行結果
変換前:70617373776f7264 変換後(1回目):64706b8b2890a9fe35ff272ab2c4550dfcbdcdd202c4c248 変換後(2回目):f6ed43dfc3939be23916d6dbf4e382be3afc47ecdd31b983 正しいパスワードとの比較:true 間違ったパスワードとの比較:false
SMD5Test.java
public class SMD5Test { public static void main(String[] args) throws Exception { byte[] password = "password".getBytes(); System.out.print("変換前:"); System.out.println(byte2Hex(password)); System.out.print("変換後(1回目):"); byte[] encryptedPassword1 = SMD5.encryptPassword(password, null); System.out.println(byte2Hex(encryptedPassword1)); System.out.print("変換後(2回目):"); byte[] encryptedPassword2 = SMD5.encryptPassword(password, null); System.out.println(byte2Hex(encryptedPassword2)); System.out.print("正しいパスワードとの比較:"); System.out.println(SMD5.comparePassword(password, encryptedPassword1)); System.out.print("間違ったパスワードとの比較:"); byte[] wrongpass = "wrongpass".getBytes(); System.out.println(SMD5.comparePassword(wrongpass, encryptedPassword1)); } private static String byte2Hex(byte[] bytes) { StringBuilder hex = new StringBuilder(); for (int i = 0; i < bytes.length; i++) { if( (0xFF & bytes[i]) < 16) { hex.append("0"); } hex.append(Integer.toHexString(0xFF & bytes[i])); } return hex.toString(); } }
SMD5.java
import java.security.MessageDigest; import java.security.NoSuchAlgorithmException; import java.security.SecureRandom; import java.util.Arrays; public class SMD5 { public static final int MD5_LENGTH = 16; public static boolean comparePassword(byte[] receivedPassword, byte[] encryptedStoredPassword) { int saltLength = encryptedStoredPassword.length - MD5_LENGTH; byte[] hashedPassword = new byte[MD5_LENGTH]; byte[] salt = new byte[saltLength]; split(encryptedStoredPassword, 0, hashedPassword, salt); byte[] encryptedReceivedPassword = encryptPassword(receivedPassword, salt); return Arrays.equals(encryptedReceivedPassword, encryptedStoredPassword); } public static byte[] encryptPassword(String password) { return encryptPassword(password.getBytes(), null); } public static byte[] encryptPassword(byte[] password, byte[] salt) { if (salt == null) { salt = new byte[8]; new SecureRandom().nextBytes(salt); } byte[] hashedPassword = digest(password, salt); byte[] hashedPasswordWithSalt = new byte[hashedPassword.length + salt.length]; merge(hashedPasswordWithSalt, hashedPassword, salt); return hashedPasswordWithSalt; } private static byte[] digest(byte[] password, byte[] salt) { MessageDigest md; try { md = MessageDigest.getInstance("MD5"); } catch (NoSuchAlgorithmException e) { return null; } if (salt != null) { md.update(salt); } md.update(password); return md.digest(); } private static void merge(byte[] all, byte[] left, byte[] right) { System.arraycopy(left, 0, all, 0, left.length); System.arraycopy(right, 0, all, left.length, right.length); } private static void split(byte[] all, int offset, byte[] left, byte[] right) { System.arraycopy(all, offset, left, 0, left.length); System.arraycopy(all, offset + left.length, right, 0, right.length); } }
参考
http://d.hatena.ne.jp/koichiarchi/20060807/1154949536
16進数文字列(String)⇔バイト配列(byte[]) - lambda {|diary| lambda { diary.succ! } }.call(hatena)
パスワードの保存に SMD5 (Salted MD5) や SSHA1を使う (MD5 への辞書攻撃とか) - まちゅダイアリー(2007-10-23)