tanihito’s blog

デジタル・新規事業開発・健康など、興味のあることについてつらつらと書いてきます。

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)