验证 HMAC 或令牌时直接用 equals 比较,耗时会泄露「前面几个字节猜对了」。MessageDigest.isEqual 为什么是正解?这篇用大量重复测量,看不同比较方式的耗时怎么随第一处不同的位置变化,以及小数组上测量的局限。
两个 4096 字节的数组,第一处不同出现在不同的位置,比较一次的耗时(每批 500 次,200 批的中位数): 手写循环(早退出) 位置0:14.7 位置1024:223.7 位置2048:414.2 位置4095:827.8 位置无(相同):830.2 ns Arrays.equals 位置0:22.5 位置1024:49.7 位置2048:43.2 位置4095:76.0 位置无(相同):79.4 ns MessageDigest.isEqual 位置0:1986.3 位置1024:1989.7 位置2048:1977.1 位置4095:1984.0 位置无(相同):1981.3 ns
[32 字节] 第一个字节就不同 vs 最后一个字节才不同(32 字节的测量被 JIT 与调用点状态左右,正负号都可能反过来,不能用来下结论):
手写循环 早不同 中位 4.1 ns(四分位距 4.0–4.2);晚不同 中位 24.0 ns(四分位距 8.8–24.9);中位数之差 +19.88 ns(四分位距 16.06 ns,差 / 四分位距 = 1.2)
Arrays.equals 早不同 中位 21.1 ns(四分位距 1.8–22.2);晚不同 中位 2.3 ns(四分位距 2.3–2.4);中位数之差 -18.81 ns(四分位距 20.40 ns,差 / 四分位距 = 0.9)
MessageDigest.isEqual 早不同 中位 18.8 ns(四分位距 18.7–20.4);晚不同 中位 18.6 ns(四分位距 18.6–18.7);中位数之差 -0.19 ns(四分位距 1.71 ns,差 / 四分位距 = 0.1)
[4096 字节] 第一个字节就不同 vs 最后一个字节才不同:
手写循环 早不同 中位 3.1 ns(四分位距 3.0–3.1);晚不同 中位 836.3 ns(四分位距 831.6–842.9);中位数之差 +833.21 ns(四分位距 11.33 ns,差 / 四分位距 = 73.5)
Arrays.equals 早不同 中位 3.0 ns(四分位距 3.0–3.0);晚不同 中位 77.4 ns(四分位距 77.4–77.4);中位数之差 +74.35 ns(四分位距 0.08 ns,差 / 四分位距 = 890.5)
MessageDigest.isEqual 早不同 中位 2206.3 ns(四分位距 2197.8–2271.5);晚不同 中位 2202.0 ns(四分位距 2196.8–2209.0);中位数之差 -4.25 ns(四分位距 73.71 ns,差 / 四分位距 = 0.1)MessageDigest.isEqual(a, a) = true;长度不同 isEqual([1,2], [1,2,3]) = false isEqual(null, null) = true isEqual(null, [1]) = false 验证 HMAC 的推荐写法:重新计算标签,用 MessageDigest.isEqual 比较两个定长标签:true;猜错一个 bit → false 长度不同时 isEqual 每次 21.7 ns;长度相同(相等)每次 20.6 ns(这次测量没有看到显著差异;比较的是固定长度的标签时,长度本身不是秘密) String.equals(5000 个字符):末尾才不同 中位 193.9 ns(四分位距 193.8–199.6);开头就不同 中位 19.7 ns(四分位距 19.6–19.8)(字符串比较同样是早退出,不能用来比较密钥或标签)