public static boolean isEqual(byte[] a, byte[] b) {
// 先判断长度是否相同
if (a.length != b.length) {
// 长度不同,返回false
return false;
}
// 一个一个字符,循环遍历判断
for (int i = 0; i < a.length; i++) {
if (a[i] != b[i]) {
// 只要一个字符不同,返回false
return false;
}
}
// 全部字符相同,返回true
return true;
}
然而,MessageDigest.isEqual却被报了bug,并在JDK 1.6.0_17中被fix成了以下的版本:
public static boolean isEqual(byte[] a, byte[] b) {
if (a.length != b.length) {
for (int i = 0; i < a.length; i++) {
// 返回结果为0,说明字符串全部相同,返回true
(1)当所有字符都相同时,result必为0,两个字符串才完全相同,返回true;
(2)只要有两个字符不同,result必不为0,一定会返回false;
同时,当输入的参数,是两个相同的字符串时,新旧算法的时间复杂度是相同的:
都需要遍历每一个字符,然后返回true。
(1)旧版本代码,只要发现两个字符串有1个字符不同,直接返回false;
(2)新版本代码,会坚持检查完所有字符,再返回false;
这里大家就要有疑问了,新版本的代码,性能不是降低了吗?
要更彻底的解释这个问题,先得从计时攻击
(Timing Attack)
说起。
最常见的,采用暴力穷举破解。但当密码位数较长,字符值域较广的时候,破解难度较大。
新型计时攻击
(Timing Attack)
,是怎么破解密码?
hacker不停的
测试不同长度的“探测密码”,然后对执行时间进行计时
。
a
aa
aaa
aaaa
...
在进行完N倍放大(执行很多遍)之后,hacker会发现,
有一个长度为N的“探测密码”,执行的时间比其他时间都更长一些
。以此来定位,密码的长度为N位。
为什么执行时间更长的N位“探测密码”,就代表真正的密码也是N位呢?
正是利用了这一点点执行时间的差异,hacker就能够确定真实密码的长度。
确定了真实密码的长度之后,第二步,确定密码的第一位字符。
hacker不停的
测试首位不同的“探测密码”,然后对执行时间进行计时
。
aaaa
baaa
caaa
daaa
...
在进行完N倍放大(执行很多遍)之后,hacker会发现,
有一个首位字母为x的“探测密码”,执行的时间比其他时间都更长一些
。以此来定位,真实密码的首位为x。
为什么执行时间更长的,首位字母为x的“探测密码”,能够确认真实密码的首位为x呢?
正是利用了这一点点执行时间的差异,hacker就能够确定真实密码的首位为x。
采用相同的方法,通过N次不断的计时攻击,hacker最终能够破解出真实密码的每一位字符。
(1)已经有hacker用此方法破解了OpenSSL 0.9.7的RSA,以及基于此OpenSSL的web-server;
新版本的isEqual,采用了一种固定时间的字符串比较方法
(time-constant comparison)。
for (int i = 0; i < a.length; i++) {
result |= a[i] ^ b[i];
}
不管探测密码与真实密码第几位不同,进行比较的时间,都是相同的。
画外音:非攻击者输入正确的密码时,新版本isEqual效率没有损失。