这 10 行比较字符串相等的代码给我整懵了,不信你也来看看
The following article is from 程序猿石头 Author 码农唐磊
(给程序员的那些事加星标)
作者:程序猿石头 - 唐磊
抱歉用这种标题吸引你点进来了,不过你不妨看完,看看能否让你有所收获。
先直接上代码:
boolean safeEqual(String a, String b) {
if (a.length() != b.length()) {
return false;
}
int equal = 0;
for (int i = 0; i < a.length(); i++) {
equal |= a.charAt(i) ^ b.charAt(i);
}
return equal == 0;
}
上面的代码是我根据原版(Scala
)翻译成 Java
的,Scala
版本如下:
def safeEqual(a: String, b: String) = {
if (a.length != b.length) {
false
} else {
var equal = 0
for (i <- Array.range(0, a.length)) {
equal |= a(i) ^ b(i)
}
equal == 0
}
}
刚开始看到这段源码感觉挺奇怪的,这个函数的功能是比较两个字符串是否相等,首先“长度不等结果肯定不等,立即返回”这个很好理解。
再看看后面的,稍微动下脑筋,转弯下也能明白这其中的门道:通过异或操作1^1=0, 1^0=1, 0^0=0
,来比较每一位,如果每一位都相等的话,两个字符串肯定相等,最后存储累计异或值的变量equal
必定为 0,否则为 1。
再想一下呢?
for (i <- Array.range(0, a.length)) {
if (a(i) ^ b(i) != 0) // or a(i) != b[i]
return false
}
我们常常讲性能优化,从效率角度上讲,难道不是应该只要中途发现某一位的结果不同了(即为1)就可以立即返回两个字符串不相等了吗?(如上所示)。
这其中肯定有……
再细想一下呢?
结合方法名称 safeEquals
可能知道些眉目,与安全有关。
以前知道通过延迟计算等手段来提高效率的手段,但这种已经算出结果却延迟返回的,还是头一回!
我们来看看,JDK 中也有类似的方法,如下代码摘自 java.security.MessageDigest
:
public static boolean isEqual(byte[] digesta, byte[] digestb) {
if (digesta == digestb) return true;
if (digesta == null || digestb == null) {
return false;
}
if (digesta.length != digestb.length) {
return false;
}
int result = 0;
// time-constant comparison
for (int i = 0; i < digesta.length; i++) {
result |= digesta[i] ^ digestb[i];
}
return result == 0;
}
看注释知道了,目的是为了用常量时间复杂度进行比较。
但这个计算过程耗费的时间不是常量有啥风险?(脑海里响起了背景音乐:“小朋友,你是否有很多问号?”)
safeEquals("abcdefghijklmn", "xbcdefghijklmn")
(只有首位不相同)和调用 safeEquals("abcdefghijklmn", "abcdefghijklmn")
(两个完全相同的字符串)的所耗费的时间一样。防止通过大量的改变输入并通过统计运行时间来暴力破解出要比较的字符串。password
,通过从a到z(实际范围可能更广)不断枚举第一位,最终统计发现 p0000000
的运行时间比其他从任意a~z
的都长(因为要到第二位才能发现不同,其他非 p
开头的字符串第一位不同就直接返回了),这样就能猜测出用户密码的第一位很可能是p
了,然后再不断一位一位迭代下去最终破解出用户的密码。// Compares two strings using the same time whether they're equal or not.
// This function should be used to mitigate timing attacks;
// for instance, when testing crypt() password hashes.
bool hash_equals ( string $known_string , string $user_string )
//This function is safe against timing attacks.
boolean password_verify ( string $password , string $hash )
其实各种语言版本的实现方式都与上面的版本差不多,将两个字符串每一位取出来异或(^
)并用或(|
)保存,最后通过判断结果是否为 0 来确定两个字符串是否相等。
如果刚开始没有用 safeEquals
去实现,后续的版本还会通过打补丁的方式去修复这样的安全隐患。
例如 JDK 1.6.0_17 中的Release Notes中就提到了MessageDigest.isEqual
中的bug的修复,如下图所示:
- EOF -
关注「程序员的那些事」加星标,不错过圈内事
圈内事,我在看❤️