安全比较和简单的==(=)有什么区别?

30

Github的保护Webhook页面说:

不建议使用普通的==运算符。像secure_compare这样的方法执行“常量时间”字符串比较,使其免受针对常规等号运算符的某些时序攻击。

比较密码时,我使用bcrypt.compare('string', 'computed hash')

什么是“安全比较”,我是否可以使用Node中的标准crypto库执行此操作?


1
我撤销了添加 Ruby 标签的编辑,因为看起来 OP 没有使用 Ruby,并且链接页面使用 Ruby 作为示例,但讨论的是与语言无关的概念。 - Wally Altman
@WallyAltman 实际上,我特别想知道是否有一种 Nodejs 的方法可以进行安全比较而不需要第三方模块。但你说得对,问题的精神是与语言无关的。 - Amin Shah Gilani
4个回答

56
"常数时间"字符串比较的目的是无论比较目标是什么(未知值),比较所需的时间都完全相同。这种"常数时间"不会向攻击者透露有关未知目标值的任何信息。通常的解决方案是比较所有字符,即使在发现不匹配后也是如此,因此无论在哪里发现不匹配,比较所需的时间都相同。
其他形式的比较可能会在某些条件为真时更快地返回答案,这使得攻击者可以了解他们可能缺失的内容。例如,在典型的字符串比较中,只要发现一个不相等的字符,比较就会返回false。如果第一个字符不匹配,则比较所需的时间将比匹配时短。勤奋的攻击者可以利用这些信息来进行更智能的暴力攻击。
"常数时间"比较消除了这些额外的信息,因为无论两个字符串如何不相等,函数都会在相同的时间内返回其值。"
在查看nodejs v4加密库时,我没有看到任何做常量时间比较的函数,而且根据这篇文章,有一个讨论是关于nodejs加密库缺少该功能的问题。
编辑:Node v6现在有crypto.timingSafeEqual(a, b)
此外,在buffer-equal-constant-time模块中也有类似的常量时间比较函数。

5
Node.js自v6.6.0版本开始新增了crypto.timingSafeEqual方法,详情请见:https://nodejs.org/dist/latest-v6.x/docs/api/crypto.html#crypto_crypto_timingsafeequal_a_b - Saugat
@SaugatAcharya - 谢谢,我已经将它添加到我的答案中了。 - jfriend00
4
比较字符串a和b的完整示例(不能将字符串传递给timingSafeEqual函数): const isEqual = crypto.timingSafeEqual( Buffer.from(a), Buffer.from(b) ); - Freewalker
请注意:如果ab的字节长度不同,timingSafeEqual()会抛出异常。 - mamacdon
@mamacdon - 是的,正如timingSafeEqual()文档所述:“a和b必须都是缓冲区、类型化数组或数据视图,并且它们必须具有相同的字节长度。”。 - jfriend00
Node在timingSafeEqual方面真的搞砸了。我只想要一个时间安全的函数来比较两个字符串并返回true或false,但他们却给了我们这个东西,如果字符串不是最基本的相等,就会抛出异常,并需要“ArrayBufferViews”。我不喜欢自己编写加密函数,但我想我会为这个特殊情况做个例外。 - Kaiser Keister

8
jfriend的回答总体上是正确的,但就这个特定情境(比较bcrypt操作的输出和存储在数据库中的内容)而言,使用“==”没有任何风险。
请记住,bcrypt是一个专门设计用于抵抗密码猜测攻击的单向函数,当攻击者获取到数据库时。如果我们假设攻击者拥有数据库,则攻击者不需要时间泄漏信息来知道他猜测的密码哪个字节是错误的:他可以通过查看数据库自己检查。如果我们假设攻击者没有数据库,则时间泄漏信息可能会告诉我们他猜测的哪个字节是错误的,在攻击者理想的情况下(根本不现实)。即使他能获得这些信息,bcrypt的单向属性也会防止他利用所获得的知识。
总结:一般来说,防止时间攻击是个好主意,但在这个特定情境下,使用“==”不会让你处于任何危险之中。

编辑:即使没有这样做也绝对没有安全风险,但 bcrypt.compare() 函数已经 被编程以抵御时间攻击


为什么GitHub会在这种情况下认为 === 没有安全风险,并强调他们的建议呢?https://developer.github.com/webhooks/securing/ - Freewalker
1
@LukeWilliams,比较HMAC签名与比较bcrypt哈希输出是不同的上下文。作为一名安全专业人员,我通常建议您使用默认安全选项:程序员不是密码学专家,无法理解每种可能用例的微妙之处。另一方面,我是一名密码学家,我的观点是在比较bcrypt哈希时,不太安全的选项是不可利用的,并且该评论针对那些寻求更深入理解的非常聪明的人。 - TheGreatContini

4
想象一长串材料需要比较。如果第一个块不匹配且比较函数立即返回,则会向攻击者泄漏数据。他可以在第一块数据上工作直到例程运行时间变长,此时他将知道第一块已匹配。
比较数据更安全的两种方法是对两组数据进行散列并比较散列值,或者对所有数据执行异或操作并将结果与0进行比较。如果“==”仅扫描两个数据块并在找到差异时返回,则可能会无意中玩“热/冷”游戏,并指导对手正确匹配他想要的秘密文本。

0
区别在于安全比较秘密和测试字符串的过程不会在运行时间内泄露有关秘密的信息。 例如,假设秘密是aaaaaaaaab,你将其与测试字符串bbbbbbbbbbbaaaaaaaaaa进行比较。 经典比较对于b会立即返回false,因为长度不匹配。 对于bbbbbbbbbb,由于第一个字符不匹配,也会几乎立即返回false。 对于aaaaaaaaaa,也是false,但不是那么立即(需要更多微秒或纳秒)。 因此,比较所需的时间越长,测试字符串与秘密越接近。 攻击者可以尝试通过发送许多不同长度的测试字符串并找到最小长度来猜测长度。 然后,他可以继续以类似的方式找到第一个字符,依此类推,直到找到秘密。
为了避免这种情况,建议采用@WDS的方法,可以使用一个简单的for循环代替。
// (Javascript)
function equals(test, secret) {
  // String comparison that does not leak timing information
  let diffs = test.length !== secret.length ? 1 : 0;
  for (let i = 0; i < test.length; i++) if (test[i] !== secret[i]) diffs++;
  return diffs == 0;
}

接受的答案指向了Node的timingSafeEqual(a, b)函数,该函数主要用于哈希输入,因为正如评论中所说,它期望输入具有相同的长度。 如果在没有哈希的情况下使用此函数,可能会导致问题,因为当输入长度不匹配时,您不能只返回false,这样会泄露有关秘密长度的信息。

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接