我的开发人员的自制密码安全是对还是错,为什么?

信息安全 密码 哈希 bcrypt 算法
2021-09-02 21:25:22

一位开发人员,我们称他为“Dave”,坚持使用自制脚本来确保密码安全。请参阅下面的 Dave 的建议。

他的团队花了几个月的时间采用Bcrypt采用行业标准协议。该协议中的软件和方法并不新鲜,并且基于支持数百万用户的久经考验的实现。该协议是一组规范,详细说明了当前的技术状态、使用的软件组件以及应如何实现它们。该实现基于已知良好的实现。

戴夫从第一天起就反对这个协议。他的理由是,像 Bcrypt 这样的算法,因为它们是公开的,对黑客来说具有更大的可见性,并且更有可能成为攻击的目标。他还认为该协议本身过于庞大且难以维护,但我认为 Dave 的主要问题是Bcrypt已发布。

我希望通过在这里分享他的代码来达成共识:

  1. 为什么自制不是一个好主意,以及
  2. 他的剧本具体有什么问题
/** Dave's Home-brew Hash */

// user data
$user = '';
$password = '';

// timestamp, random #
$time = date('mdYHis');
$rand = mt_rand().'\n';

// crypt
$crypt = crypt($user.$time.$rand);

// hash
function hash_it($string1, $string2) {
    $pass = md5($string1);
    $nt = substr($pass,0,8);
    $th = substr($pass,8,8);
    $ur = substr($pass,16,8);
    $ps = substr($pass,24,8);

    $hash = 'H'.sha1($string2.$ps.$ur.$nt.$th);
    return $hash
}

$hash = hash_it($password, $crypt);
4个回答
/** Dave's Home-brew Hash^H^H^H^H^Hkinda stupid algorithm */

// user data
$user = '';
$password = '';

// timestamp, "random" #
$time = date('mdYHis'); // known to attackers - totally pointless
// ^ also, as jdm pointed out in the comments, this changes daily. looks broken!

// different hashes for different days? huh? or is this stored as a salt?
$rand = mt_rand().'\n'; // mt_rand is not secure as a random number generator
// ^ it's even less secure if you only ask for a single 31-bit number. and why the \n?

// crypt is good if configured/salted correctly
// ... except you've used crypt on the username? WTF.
$crypt = crypt($user.$time.$rand); 

// hash
function hash_it($string1, $string2) {
    $pass = md5($string1); // why are we MD5'ing the same pass when crypt is available?
    $nt = substr($pass,0,8); // <--- BAD BAD BAD - why shuffle an MD5 hash?!?!?
    $th = substr($pass,8,8);
    $ur = substr($pass,16,8);
    $ps = substr($pass,24,8); // seriously. I have no idea. why?
    // ^ shuffling brings _zero_ extra security. it makes _zero_ sense to do this.
    // also, what's up with the variable names?

    // and now we SHA1 it with other junk too? wtf?
    $hash = 'H'.sha1($string2.$ps.$ur.$nt.$th); 
    return $hash
}

$hash = hash_it($password, $crypt); // ... please stop. it's hurting my head.

summon_cthulhu();

戴夫,你不是密码学家。停下来。

这种自制方法对暴力攻击没有真正的抵抗力,并且给人一种“复杂”安全的错误印象。实际上,您所做的只是sha1(md5(pass) + salt)使用可能损坏且过于复杂的哈希。您似乎误以为复杂的代码可以提供更好的安全性,但事实并非如此。无论攻击者是否知道算法,强密码系统都是强的——这一事实被称为Kerckhoff 原理我意识到重新发明轮子并按照自己的方式做这件事很有趣,但是您正在编写进入关键业务应用程序的代码,该应用程序必须保护客户凭证。你有责任正确地做到这一点。

坚持使用久经考验的密钥派生算法,例如PBKDF2bcrypt,这些算法经过了广泛的专业和业余密码学家多年的深入分析和审查。

如果您想对正确的密码存储进行良好的教育,请查看以下链接:

公共协议的优点:

  • 可能是比你聪明的人写的
  • 被更多人测试过(可能他们中的一些人比你聪明)
  • 被更多人审查(可能其中一些人比你聪明),通常有数学证明
  • 被更多人改进(可能他们中的一些人比你聪明)
  • 目前,成千上万的人中只有一个人发现了一个缺陷,很多人开始修复它

公共协议的缺点:

  • 如果确实发现了漏洞,
  • 并公开
  • 并且修复得不够快

然后攻击者将开始搜索易受攻击的站点/应用程序。但:

  • 他们可能会首先追求更重要的目标
  • 当漏洞暴露时,你知道并且可以锁定
  • 并非所有缺陷都是“严重的”(大多数缺陷就像“在某些条件下可能发生碰撞”或“暴力破解的时间不是 10 12年,而是 10 6年”)

默默无闻的安全性被认为是完全糟糕的。发明自己的属于这一类。

如果 Dave 真的是“您的”开发人员,就像您有权解雇他一样,那么您有权指示他使用文档更完善的方案,您应该这样做。

在密码学中,需要保留的秘密越少越好这尤其适用于“硬编码”机密,例如散列函数本身,一旦包含机密的代码离开您的建筑物,它们就不是机密。

这就是为什么密码学的开放标准是一件好事。如果这个方案已经存在了几年甚至几十年,并且基本算法和各种实现都是公开的,但仍然没有人找到进入的方法,那么这是一个很好的方案。

举个例子,多项式很好地分解了该方案的问题,但以下是所提出的哈希算法的主要缺陷:

  • MD5完全坏了虽然 MD5 被破坏的主要方式并没有使原像攻击变得容易得多(它极易受到已知明文强冲突的影响;知道消息和摘要,可以在 2^24 时间内产生冲突),散列算法太快了,无法抵御现代分布式破解硬件。永远不应该用于需要数据安全的应用程序中。

  • SHA-1 被认为是易受攻击的同样,对于原像攻击没有一个优雅的向量,但是有一个强碰撞攻击(160 位哈希的 2^61 次),并且哈希算法足够快并且密钥空间足够小,以至于当前的硬件可以可行地进行暴力破解-强迫它。

  • 更多的哈希并不一定意味着更好的哈希哈希是一个确定的过程;它们代表理论密码学中的“随机预言”,但由于它们必须始终在给定相同输入的情况下产生相同的输出,因此它们不能将任何熵添加到输入中已经固有的内容。

    简单地说,像 Dave 那样使用多个哈希的秘密组合,即使该组合是未知的,也不会为蛮力增加大量工作(三个哈希的相对顺序有 6 种可能性,增加了复杂性尝试所有可能性小于 2 的 3 次方),一旦攻击者能够反编译您的代码并发现散列函数的相对顺序,额外的复杂性就会消失。

  • 密码本质上是低熵的最好的“用户可消费”(非乱码)密码的复杂性最大约为 50 位熵,这意味着如果您对要查找的内容有所了解,它们可以在 2^50 或更短的时间内被破解. 这使得密码比您通常散列的其他内容(如证书摘要)更容易破解,需要额外的“工作证明”来提高其安全性。

  • 该方案没有添加任何重要的工作证明在宏伟的计划中,三个哈希而不是一个,在额外需要的工作中为该计划增加了大约 1.25 位的熵;您只需要求密码多一个字符就可以做得更好。

BCrypt 与上述所有方法相比,其主要优势在于极其缓慢的密钥派生函数或 KDF。构成 BCrypt 基础的加密密码 Blowfish 使用“SBox”;一大堆预先确定的初始值,由密钥和/或 IV 修改以生成密码的初始状态,通过该初始状态馈送明文。在 BCrypt 中,这个 SBox 设置变得更加复杂,方法是获取较小的密码和盐值,并通过“未加密”密码运行它们预定次数,将密码“预热”到理论上只能通过执行来产生的状态相同数量的设置迭代,使用相同的密码和盐。然后,这个“预热”的密码被输入一个恒定的明文以产生哈希值。在 CS 术语中,哈希形成“工作证明”;一项难以执行(耗费时间和/或资源)但易于检查正确性的计算任务(生成的哈希是否与我们已有的哈希匹配?)。

SBox 设置的“预定次数”迭代是可配置的;这是 BCrypt 的真正力量。散列时指定了许多“轮”密钥设置,并且对于n轮,执行 2^n 次密钥设置迭代。这意味着增加轮数会使哈希在相同硬件上执行两倍的工作并花费两倍的时间来生成必要的工作证明。因此,BCrypt 可以轻松地跟上摩尔定律,这是之前出现的许多哈希函数的祸根。

BCrypt 的主要理论弱点是内存使用率低;虽然计算时间可以成倍增加,但所需的内存量实际上保持不变(随着迭代的增加,SBox 不会变得更大,并且不需要保留“中间结果”)。因此,虽然 BCrypt 阻碍了 GPU 的纯流水线计算能力,但它仍然容易被更复杂的“现场可编程门阵列”(基本上是一组大规模、高度模块化的“软连线”逻辑单元,即当前最先进的高度分布式计算),因为低内存限制意味着您可以将更多相对便宜的电路板用于解决问题。

一种较新的算法 SCrypt 通过成倍增加内存需求以及计算哈希的计算成本来对抗 FPGA 破解者,因此每个 FPGA 可用的有限内存很快也使它们变得不可行(分布式破解基本上需要大量 CPU /FSB/RAM 组合,基本上是完整的计算机,连接在一起)。唯一的问题是 SCrypt 只有 2 或 3 岁,所以它没有 BCrypt(13 岁)的密码分析谱系。真的,如果你不得不担心一个 FPGA 武装的破解者会在可行的时间内获得密码(就像在你可以更改所有密码之前一样),你已经惹恼了一些非常 强大的人,并且已经暴露了另一个严重的漏洞(允许攻击者首先获取您存储的密码哈希,因此他们可以“离线”破解一个)。

底线,使用 BCrypt 进行密码散列。它已有 13 年的历史,基于 20 年的密码算法,当时还没有发现有助于密码破解的已知漏洞。它像糖蜜一样慢,并且可以配置为始终如此,前提是您将其与用户每 90 天左右更改一次密码的要求相结合,因此新密码会不断被更昂贵的配置散列。

对 Dave 公平地说,就自制密码安全而言,这是更好的情况之一,因为它只是有点混淆(实际上并不多)掩盖了 MD5 哈希字节顺序的可逆交换在hash = SHA1(salt + MD5'(Password))哪里。MD5'现在用户名/时间/随机/加密部分只是用来生成盐,我们对盐的唯一要求是它们只需要具有很好的唯一性;所以虽然它过于复杂,但谈论它真的没有用。

再次hash = sha1($salt+md5'($password))重新排列 MD5 不会增加安全性(swap(00112233445566778899aabbccddeeff)变成ccddeeff8899aabb0011223344556677),交换不会阻止您在之后使用 md5 彩虹表(例如,查看代码并反转交换)。然而,独特盐的存在使得彩虹表不可行。

现在至关重要的是,这归结为应用了两次的简单加密哈希函数;这比存储明文密码(请参阅明文违规者)更好,并且比存储未加盐的哈希(请参阅linkedin泄漏)更好。然而,在廉价的大规模并行 GPU 时代,这对于现代使用来说太弱了。任何有一点通用 GPU 编程经验并以某种方式进入实时服务器(用他们的 salt 获取哈希)的人都可能会看到他的源代码,尝试他们自己的密码作为测试用例,然后可以暴力破解任何每个 GPU 每秒尝试数十亿次的特定密码。

因此,如果任何用户使用的密码列表包含大约一百万个以前泄露的密码(例如来自linkedin),攻击者几乎可以立即破解它。如果某个用户的密码是字符集 A-Za-z0-9 中的随机 8 个字母;每个 GPU 平均需要大约 60 小时才能中断(因此,如果您有 60 个 GPU;将需要 1 小时)。使用利用常见密码形式的常见破解技术可以显着加快速度。还值得注意的是,由于$password通过 128 位 MD5 散列函数,在密码中使用超过 128 位的熵绝对没有任何好处(但公平地说,这是一个非常安全的密码;就像一个 10 字的 diceware 密码或随机 22 个字符的字母数字密码)。

确实,您应该使用迭代的加密哈希函数;这有点像 bcrypt 或 PBKDF,它可以通过一个很大的常数因子(比如 10 5 )来降低攻击者的暴力破解速度(因此从 62 字符集(A- Za-z0-9);使用单个 GPU 需要 600 万小时(大约 700 年可能会被破解),并且使用更强的密码会变得更好(例如,10 个字符的密码需要大约 300 万年;所以即使使用100 万个 GPU 需要 3 年)。因此,一点点密钥强化会将相对较弱的密码(62 个字符集中的 8 个随机字符)移出被攻击者破解的可行性范围。有关使用简单密码上限的更多信息keystrengthened password 看到这个答案

碰撞攻击与图像前攻击(或为什么对哈希函数的碰撞攻击与密码哈希无关)

KeithS 的回答虽然给出了很好的建议(使用 bcrypt 而不是简单的加密散列函数来散列您的密码),但最初批评 MD5 和 SHA1 的理由是错误的(不要使用 MD5,因为它容易受到碰撞攻击)。原像攻击和碰撞攻击之间存在细微差别,并且评论中的争论过于浓缩,所以我在这里详细说明。我强烈建议阅读关于原像攻击的维基百科文章

前映像攻击说给定一个用十六进制写的特定 128 位哈希: h=ad2baf26a87795b3c8a8366a08b44112,一个特定的哈希函数H,请找到任何消息m,例如h=H(m); 请注意,有 N=2 128个不同的不同哈希值。现在,如果您的加密散列函数没有损坏,那么对于随机消息 m,散列中的每个位都有 50% 的机会为 0 或 1。然后,我平均需要为大约 N=2 128个散列生成散列,然后我才幸运地找到任何m这样的h=H(m)消息(此消息可能与最初用于生成散列的输入不同——但这仍然下降在“原像”攻击的类别下)。

碰撞攻击说找到我任意两条消息m1等等m2H(m1) = H(m2)请注意m1, m2, (和H(m1)) 都可以自由更改。这种情况是一个更容易的问题,因为如果我为 M 个不同的消息生成 M 个散列,我不仅仅是将 M 个消息与一个特定的散列进行比较(因此有 M 个发现冲突的机会),现在我有 M*(M- 1)/2 对散列,因此大约有 M^2 发生冲突的机会。所以在这种情况下,我需要粗略地生成大约 sqrt(N)~2 64 个散列,然后它们中的一个可能会在理想的 128 位散列上与另一个发生冲突。

让我们看看这两种生日问题。碰撞问题转化为常见的生日“悖论”;在一个房间里需要多少人才能让两个人在一年中 N=365 天共享一个生日。答案是一个悖论,因为您只需要大约 sqrt(N) ~ 23 个人,就可能有两个人共享一个生日(就像一个房间里有 23 个人,您有 253 对不同的人可以匹配)。(我确实知道 sqrt(365) != 23:我使用的是近似数学,而不是关注无关紧要的常数因素。然后用 sqrt(365) 重新计算~房间里有 19 人P(two share birthday) = 19! * comb(365,19)/365**n = 37.9%,虽然不是严格意义上的 50%,但仍然意味着它很有可能发生)。注意碰撞生日问题,一个房间里不能有 N+1=366 人,并且有可能没有碰撞(忽略闰日);充其量是前 365 个人的生日不同,最后一个人产生了碰撞。

原像问题是一个非常不同的问题,在一个人很可能有一个特定的生日(比如 B = 12 月 18 日)之前,我需要在一个房间里有多少人。在这种情况下,我需要大约 N ~ 365 人才能发生。例如,房间里有 365 个人,P(somebody has birthday B) = 1 - (1 - 1/365)^(# people)所以# people = 365你有 63% 的机会某人的生日是某个固定日期 B。在这种情况下,你可以很容易地想象房间里有任意数量的人,但没有在某个特定日期过生日的人。(假设您仅在生日不是给定日期的情况下才邀请人们进入房间;您可以邀请的人数没有限制)。

当像 MD5/SHA1 这样的哈希函数因碰撞攻击而被破坏时,这意味着您可以在比 sqrt(N) ~ 2 numbits/2的蛮力时间更少的工作中产生碰撞对于 MD5,它只需要大约 2^24 时间来产生碰撞;对于 SHA1,它需要 ~ 2^61 时间。这意味着对 MD5 的碰撞攻击非常容易进行;但是对 SHA1 的实际攻击仍然很困难。但是碰撞攻击只有在你不关心你试图匹配什么哈希时才有意义。这些冲突攻击与某些应用程序非常相关,例如对消息进行加密签名以确保消息完整性,因此在这些情况下要小心使用 MD5/SHA1。然而,当你有一个唯一的盐,并且你试图匹配一个特定的哈希来进行身份验证时,碰撞攻击就无关紧要了。