← 中文资源

简体中文版本

常量时间字符串比较

通过逐字节异或且不提前返回的方式比较令牌与签名,减少可被观测的时间差泄漏。

阅读英文原文

功能说明

constantTimeEqual 用于比较 API 令牌、HMAC 签名、重置码等敏感字符串,执行时间只与输入长度相关,不因首个不同字节而提前退出。constantTimeEqualBytes 直接处理 Uint8Array,适合与 Web Crypto 或 Node Buffer 的结果配合。普通相等比较可能在第一个差异处返回,攻击者可通过大量计时样本逐步推断秘密。

关键代码

两个输入先用 TextEncoder 编码为 UTF-8,长度不同不会立即返回,而是把长度差异或进累积变量。循环上限取两者较长者,短的一侧缺失字节按零处理,从而让循环次数只依赖最大长度。每轮把对应字节异或后按位或到 difference,最后统一判断是否为零。字节版本跳过编码,直接对 Uint8Array 执行同样流程并校验类型。

使用注意

JavaScript 引擎的 JIT、垃圾回收和解释器优化无法提供严格的硬件级常量时间保证,高安全场景应优先使用运行时提供的 timingSafeEqual。长度本身仍会泄漏,秘密长度通常不敏感,但若敏感应先哈希到固定长度。比较前不要做 trim、大小写转换或解码,否则会改变语义。该函数返回布尔值,不负责签名验证。

验证方式

测试相同字符串、首字节不同、末字节不同、长度不同和空字符串,确认只在完全相等时返回 true。用多字节 UTF-8 字符与 emoji 验证编码路径。字节版本测试 Buffer 与 Uint8Array 的兼容、长度不等和非法类型抛错。还可重复运行大量样本做粗略计时,确认没有明显的首差异提前返回模式。

代码示例

const encoder = new TextEncoder();

// Constant-time comparison for secrets and signatures.
export function constantTimeEqual(left, right) {
  const a = encoder.encode(String(left));
  const b = encoder.encode(String(right));
  const length = Math.max(a.length, b.length);
  let difference = a.length ^ b.length;
  for (let index = 0; index < length; index += 1) {
    difference |= (a[index] ?? 0) ^ (b[index] ?? 0);
  }
  return difference === 0;
}

export function constantTimeEqualBytes(left, right) {
  if (!(left instanceof Uint8Array) || !(right instanceof Uint8Array)) {
    throw new TypeError("both arguments must be Uint8Array values");
  }
  const length = Math.max(left.length, right.length);
  let difference = left.length ^ right.length;
  for (let index = 0; index < length; index += 1) {
    difference |= (left[index] ?? 0) ^ (right[index] ?? 0);
  }
  return difference === 0;
}