在前面讲到哈希算法时,我们说,存储用户的哈希口令时,要加盐存储,目的就在于抵御彩虹表攻击。
我们回顾一下哈希算法:
digest = hash(input)
正是因为相同的输入会产生相同的输出,我们加盐的目的就在于,使得输入有所变化:
digest = hash(salt + input)
这个salt可以看作是一个额外的“认证码”,同样的输入,不同的认证码,会产生不同的输出。因此,要验证输出的哈希,必须同时提供“认证码”。
Hmac算法就是一种基于密钥的消息认证码算法,它的全称是Hash-based Message Authentication Code,是一种更安全的消息摘要算法。
Hmac算法总是和某种哈希算法配合起来用的。例如,我们使用MD5算法,对应的就是HmacMD5算法,它相当于“加盐”的MD5:
HmacMD5 ≈ md5(secure_random_key, input)
因此,HmacMD5可以看作带有一个安全的key的MD5。使用HmacMD5而不是用MD5加salt,有如下好处:
可见,Hmac本质上就是把key混入摘要的算法。验证此哈希时,除了原始的输入数据,还要提供key。
为了保证安全,我们不会自己指定key,而是通过Java标准库的KeyGenerator生成一个安全的随机的key。下面是使用HmacMD5的代码:
import javax.crypto.*;
import java.util.HexFormat;
public class Main {
public static void main(String[] args) throws Exception {
KeyGenerator keyGen = KeyGenerator.getInstance("HmacMD5");
SecretKey key = keyGen.generateKey();
// 打印随机生成的key:
byte[] skey = key.getEncoded();
System.out.println(HexFormat.of().formatHex(skey));
Mac mac = Mac.getInstance("HmacMD5");
mac.init(key);
mac.update("HelloWorld".getBytes("UTF-8"));
byte[] result = mac.doFinal();
System.out.println(HexFormat.of().formatHex(result));
}
}
和MD5相比,使用HmacMD5的步骤是:
HmacMD5
获取KeyGenerator
实例;KeyGenerator
创建一个SecretKey
实例;HmacMD5
获取Mac
实例;SecretKey
初始化Mac
实例;Mac
实例反复调用update(byte[])
输入数据;Mac
实例的doFinal()
获取最终的哈希值。我们可以用Hmac算法取代原有的自定义的加盐算法,因此,存储用户名和口令的数据库结构如下:
username | secret_key (64 bytes) | password |
---|---|---|
bob | a8c06e05f92e...5e16 | 7e0387872a57c85ef6dddbaa12f376de |
alice | e6a343693985...f4be | c1f929ac2552642b302e739bc0cdbaac |
tim | f27a973dfdc0...6003 | af57651c3a8a73303515804d4af43790 |
有了Hmac计算的哈希和SecretKey
,我们想要验证怎么办?这时,SecretKey
不能从KeyGenerator
生成,而是从一个byte[]
数组恢复:
import javax.crypto.*;
import javax.crypto.spec.*;
import java.util.HexFormat;
public class Main {
public static void main(String[] args) throws Exception {
byte[] hkey = HexFormat.of().parseHex(
"b648ee779d658c420420d86291ec70f5" +
"cf97521c740330972697a8fad0b55f5c" +
"5a7924e4afa99d8c5883e07d7c3f9ed0" +
"76aa544d25ed2f5ceea59dcc122babc8");
SecretKey key = new SecretKeySpec(hkey, "HmacMD5");
Mac mac = Mac.getInstance("HmacMD5");
mac.init(key);
mac.update("HelloWorld".getBytes("UTF-8"));
byte[] result = mac.doFinal();
System.out.println(HexFormat.of().formatHex(result)); // 4af40be7864efaae1473a4c601b650ae
}
}
恢复SecretKey
的语句就是new SecretKeySpec(hkey, "HmacMD5")
。
Hmac算法是一种标准的基于密钥的哈希算法,可以配合MD5、SHA-1等哈希算法,计算的摘要长度和原摘要算法长度相同。