Thursday, January 2, 2014

JCA Engine - MessageDigest

MessageDigest 消息摘要简介
消息摘要,Hash,杂凑法说的其实是同一件事情。通过消息摘要算法,可以将一个任意长度的信息作为输入,同时输出固定长度的摘要信息。对于消息摘要算法具有几个特性:
  • 对于相同的数据,相同的消息摘要算法,无论进行多少次消息摘要,得到的结果都是一样的。
  • 原始数据中一个很小的改动,将产生完全不一样的消息摘要结果。
  • 很难通过一个消息摘要的结果反推出原始的数据。
  • 很难通过计算的方式得到两个消息具有相同的消息摘要。

针对这些特性,消息摘要算法主要有下面几种应用场景:
  • 验证消息是否被篡改。比如从网络上下载了一个文件,我们可以通过比对这个文件的摘要信息来确定这个文件在传输过程中是否被有意或者无意的篡改过。因为如果数据被篡改了,那么通过摘要算法计算出来的消息摘要一定和原来不一样了。这一点也被广泛应用在数字证书上。
  • 密码验证。很多系统需要对用户进行管理,用户必须通过自己的用户名和密码来登录系统,而后才能进行操作。显然,将用户的密码存在数据库中是不安全的,这样当系统被黑以后,所有用户的密码信息就会泄漏。因此目前比较流行的做法是这样的,系统不会存储用户的密码,而是在用户注册的时候只保存一个用户密码的摘要。在用户登录的时候,先将用户输入的密码进行消息摘要,通过和数据库中保存的摘要进行比对,来验证密码是否正确。

消息摘要算法
目前主流的消息摘要算法可以分为三个系列:MD系列,SHA系列,MAC系列。
  • MD(Message Digest)系列包括MD2, MD3, MD4, MD5. 他们的摘要长度都是128 bits,也就是16个字节,可以映射为32个16进制位。你可能之前见到过使用MD5消息摘要的结果,都是长度为32的16进制字符串。
  • SHA(Secure Hash Algorithm)系列是从MD4发展而来,它包括包括SHA-1(摘要长度160bits-20Bytes), SHA-224, SHA-256, SHA-384, SHA-512,通常将后四种算法并称为SHA-2,并且摘要长度和算法名称保持一致,如SHA-256,表示摘要长度为256 bits.
  • MAC(Message Authentication Code)系列,是在原有的MD和SHA算法的基础上加入了密钥的消息摘要算法。也就是说对同一段数据,只有使用相同的密钥,才能保证摘要结果是一致的。这样可以避免一些恶意的修改,因为消息摘要算法是公开的,黑客可能会将数据和消息摘要一起进行修改。如果使用了MAC,只要密钥没有被黑客获取到,那么黑客就无法重新计算消息摘要,进而也就可以保证消息不被篡改。MAC也被称作Hmac(keyed-Hash Message Authentication Code),主要包含的算法有:HmacMD2, HmacMD4, HmacMD5, HmacSHA1, HmacSHA224, HmacSHA256, HmacSHA384, HmacSHA512,摘要长度与实际的算法的摘要长度相同。

使用方法
Java中提供了大部分的算法实现。为了进行消息摘要,我们需要使用MessageDigest这个JCA Engine类。同样,因为MessageDigest是Engine类,那么我们就可以大致推测出它的使用方法:
  1. 通过getInstance 方法获取实例,为什么要这样做请参见本系列的概述。
  2. 根据自己的需求,调用任何一个update方法的重载,将需要进行消息摘要的数据传入MessageDigest对象中。
  3. 调用digest方法得到消息摘要的结果,如果需要进行消息摘要的数据已经全部加载到了内存,那么也可以跳过第二步,直接在调用digest方法的时候一次性的传入数据。
  4. 得到的消息摘要是byte数组,为了便于存储,需要进行一些格式的转换,一般采用16进制的表示方法,一个byte对应于2个16进制位。
注意:调用digest方法之后,MessageDigest对象将会被reset。整个消息摘要的过程即宣布结束,也就是说之后如果再通过update方法传入的数据将作为新一轮消息摘要的数据,而不会附加到调用digest方法之前提供的数据后面。例:
public class SHA256Digest {
    public static void main(String[] args) {
        try {
            MessageDigest md = MessageDigest.getInstance("SHA-256");
            String message = "The message cannot be tampered.";
            byte[] data = message.getBytes("UTF-8");
            byte[] signature = md.digest(message.getBytes("UTF-8"));
            System.out.println(DatatypeConverter.printHexBinary(signature));
        } catch (NoSuchAlgorithmException | UnsupportedEncodingException ex) {
            throw new RuntimeException(ex);
        }
    }
}
    
执行程序将得到一个64位的16进制字符串:
        E08612E56A73BD9EF587A0C99322797228B0A62588BC956DBFA4A891AD4C1FE1
    
对于MD系列和SHA系列的消息摘要算法的使用方法都是类似的,唯一的不同就是获取MessageDigest实例的时候传入的Algorithm,Java官方文档中包括了所有系统自带的算法的Algorithm 名称。

同时这里顺带提一句,DatatypeConverter中提供了许多静态方法,用于将byte数组转换为各种格式的字符串(Hex, Base64...),同时也提供了从各种字符串形式转换回byte数组的方法。在加密解密的时候使用这个类会非常方便,否则我们就要自己去编写byte数组到16进制字符串的转换方法法。

想要知道如何自己写代码将字节数组转换成16进制的表现形式,请查看DataTypeCovnerter.printHexBinary方法的源码。
对于MAC系列的消息摘要算法使用起来比较特殊,因为里面涉及到了key,这里暂时不做详细的解释,仅仅给出例子:
public static void main(String[] args) {
    try {
        //随机生成一个SecretKey
        KeyGenerator kg = KeyGenerator.getInstance("HmacSHA256");
        SecretKey key = kg.generateKey();

        Mac mac = Mac.getInstance("HmacSHA512");    //从JCA中请求实现了HmacSHA256算法的实例
        mac.init(key);    //使用生成好的SecretKey对mac进行初始化

        byte[] digest = mac.doFinal("Hello Kevin!".getBytes("UTF-8"));    //注意这里调用的是doFinal方法,而不是digest方法
        System.out.println(DatatypeConverter.printHexBinary(digest));
    } catch (NoSuchAlgorithmException | InvalidKeyException | UnsupportedEncodingException ex) {
        throw new RuntimeException(ex);
    }
}
    
多次执行上述代码,将得到不同的结果:
  • 第一次:
    CEBCFBE5D7E175C0EC7FF006FCE4B43DAF4D711360C7BFDC85EDBC792D0F21687E6DA58516339B8B8C689479AE02BB5F45B740B61054348165F119A8016C502D
  • 第二次:
    EB2E3E53F6231275296E90118E000901F71E278629D9C917C059D8D625745F3D0E7363CC0E686F189633D9C322CD7CE0E3F13C9891BB28F59FDA0B4DDDD2578E
应该能够理解,为什么每次生成的消息摘要都不一样。因为在上述代码中,key是随机生成的,所以每次key都不一样,因此计算出来的消息摘要也不同。

关于安全性的补充
对于MD系列算法,已经被证实是不安全的。2004年被山东大学的王小云教授带领的团队破解。利用王小云教授的算法,在数小时之内就可以找到MD5碰撞。 详细的新闻报道请看这里

1 comment :