在数据库中存储Bcrypt哈希密码应该使用什么列类型/长度?

371

我想在数据库中存储一个哈希密码(使用BCrypt)。 对于此,什么类型是好的,正确的长度是多少? 使用BCrypt散列的密码始终具有相同的长度吗?

编辑

示例哈希:

$2a$10$KssILxWNR6k62B7yiX0GAe2Q7wwHlrzhF3LqtVvpyvHZf0MwvNfVu

一些密码经过哈希后,似乎BCrypt总是生成60个字符的哈希值。

编辑2

很抱歉没有提及实现。 我正在使用jBCrypt


还可以查看Openwall的PHP密码哈希框架(PHPass)。它是可移植的,并且经过加固,可以防御多种常见的用户密码攻击。编写该框架的人(SolarDesigner)也是编写John The Ripper并担任密码哈希竞赛的评委的同一人,因此他对密码攻击有所了解。 - jww
1
如果有人在寻找 scrypt 的解决方案,Gumbo的答案也适用于scrypt。我个人在MySQL中应用了BINARY(64),这使我后来可以在Python下测试字节相等性。 - Philippe Hebert
5个回答

418

bcrypt的模块化密码格式包括:

  • $2$$2a$$2y$,用于标识哈希算法和格式,具体请参见版本历史
  • 一个两位数字值,表示代价参数,后跟$
  • 一个53个字符长的base-64编码值(它们使用字母表./09AZaz,与标准Base 64编码字母表不同),包括:
    • 22个字符的盐(实际上只有132个解码位中的128个位)
    • 31个字符的加密输出(实际上只有186个解码位中的184个位)

因此,总长度分别为59或60个字节。

由于你使用了2a格式,所以需要60个字节。因此,对于MySQL,我建议使用CHAR(60) BINARYBINARY(60)(有关差异的信息,请参见_binbinary排序规则)。

CHAR 不是二进制安全的,它的相等性不仅取决于字节值,还取决于实际的排序规则;在最坏的情况下,A 被视为等同于 a。有关更多信息,请参见_bin 和 binary 排序规则


34
请注意,将字符串存储为二进制(60)可能会引起意想不到的行为,特别是在字符串相等性方面。在.NET中,可以通过使用String.Equals(fromDataBaseBinary60string,typicalishString,StringComparison.InvariantCulture)来解决这个问题。 - JHubbard80
12
如果您将该列定义为CHAR(60) CHARACTER SET latin1 COLLATE latin1_bin,则可以获得准确的字符串比较优势,而无需使用二进制列。 - Ben
3
@AndreFigueiredo SQL_Latin1_General_CP1_CS_AS 在MySQL中未知。已知的是latin1_general_cs - Gumbo
5
我不确定应该存储为不安全的二进制char还是具有意外行为的binary(60) - Sir
4
进行密码检查时,不应该比较哈希值。我遇到的每个bcrypt库都有一个密码检查函数。将密码存储为字符串(即使用CHAR、VARCHAR或VARCHAR2的字符数组),并使用库的密码检查函数将该字符串与用户提供的密码进行比较是可以的。 - Jason
显示剩余3条评论

61

一个Bcrypt哈希可以存储在BINARY(40)列中。

BINARY(60),正如其他答案所建议的那样,是最简单和最自然的选择,但如果您想最大化存储效率,您可以通过无损地分解哈希来节省20个字节。我在GitHub上对此进行了更全面的记录:https://github.com/ademarre/binary-mcf

Bcrypt哈希遵循一种称为模块化加密格式(MCF)的结构。二进制 MCF(BMCF)将这些文本哈希表示解码为更紧凑的二进制结构。在Bcrypt的情况下,生成的二进制哈希是40个字节。

Gumbo很好地解释了Bcrypt MCF哈希的四个组件:

$<id>$<cost>$<salt><digest>

将编码转换为BMCF的方法如下:

  1. $<id>$可以用3位表示。
  2. <cost>$,04-31,可以用5位表示。将它们放在一起组成1个字节。
  3. 22个字符的盐是128位的(非标准)base-64表示法。Base-64解码得到16个字节。
  4. 31个字符的哈希摘要可以被Base-64解码为23个字节。
  5. 将所有内容放在一起,共40个字节:1 + 16 + 23

您可以在上面的链接中阅读更多信息,或查看我的PHP实现,也可在GitHub上找到。


76
更长字段的成本:每个记录增加20字节,当你达到一百万条记录时,这将会占用20MB的空间。 不正确地实现字段长度缩短的成本,在高度复杂的安全和工程领域中:$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$ 你自己算吧。 - Kzqai
9
@Kzqai,就像我之前说的一样,60字节列是最自然的选择,但是追求存储效率的程度因项目而异。例如,通常会尝试将整个数据库放入内存中,在内存受限的环境中,这里20 MB,那里20 MB,很快就会累加起来。 - Andre D
15
你的例子印证了我的观点。 --- 如果你想将数据库放入内存,请在处理bcrypt存储列之前优化其他每一列。 --- 如果你已经将其他每一列都进行了高度优化,只剩下bcrypt哈希列,那么就再增加一个内存吧,专门用于bcrypt。 --- 如果你已经做了以上两点……那就停止吧,你还没有对所有低 hanging fruit 列进行优化,而且你即将更改一个已经运作良好的测试过的加密安全系统,并用一个存在实现失败风险的更复杂的自制系统来替换它。 - Kzqai
16
这里不会削弱你的Bcrypt库的安全性风险。这是一种数据编码,在密码检查之前从存储中恢复时会被撤销。这不是“不要自己设计加密算法”的领域。 - Andre D
1
很好的解释。 :) 虽然您的解释提供了一个很好的想法,但我只想选择60个字符,甚至100个字符,以确保安全。与@Kzqai和AndreD的辩论也很不错 - Naveen Kumar V
@AndreD 我很高兴找到你的规范。我之前用41个字节做了类似的事情,通过分别存储方案和成本。(我也称之为BMCF)。你可以叫我怪人,但我喜欢我能节省1个字节的感觉。给其他读者:在检查之前实际上不需要“撤销”压缩。 - undefined

28
如果您正在使用PHP的password_hash()生成bcrypt哈希值,并且使用PASSWORD_DEFAULT算法(我认为这是阅读此问题的人的很大一部分),请记住在将来password_hash()可能会使用不同的算法作为默认值,因此这可能会影响哈希的长度(但它不一定会更长)。
从手册页面中可以看到:
请注意,该常量被设计为随着新的和更强大的算法添加到PHP而发生变化。因此,使用此标识符的结果的长度随时间而变化。因此,建议将结果存储在可以扩展超过60个字符的数据库列中(255个字符将是一个不错的选择)。
即使您有10亿用户(即您当前正在与Facebook竞争),使用bcrypt存储255字节密码哈希值只需要大约255 GB的数据 - 大约是小型固态硬盘的大小。极不可能存储密码哈希值成为应用程序的瓶颈。但是,如果由于某种原因存储空间真的是一个问题,您可以使用PASSWORD_BCRYPT来强制password_hash()使用bcrypt,即使这不是默认值。只需确保随时了解bcrypt中发现的任何漏洞并审核每个新的PHP版本的发行说明。如果默认算法发生更改,最好审核原因并做出明智的决定是否使用新算法。

24

我不认为在存储这个内容时有什么巧妙的技巧,就像使用MD5哈希一样。

我认为最好的方法是将其存储为CHAR(60),因为它始终是60个字符长。


尽管PHP文档指出列应该能够容纳更多的数据,以备将来的版本... - Julian F. Weinert
20
没有必要过度设计。如果你使用的软件需要六十个字节,那就分配六十个字节。如果未来版本的软件改变了这一点,那么在发布新版本时再考虑这个问题。你不应该自动安装会改变功能的更新。 - Tyler Crompton
2
我认为这是最好的答案。没有必要像其他答案那样深入算法的复杂性。有关二进制、排序等的所有细节应由使用的任何库处理。60个字符。这就是答案。 - Jason
请注意,对于某些数据库(如postgresql),列“size”不是必需的。 - Pointy

1
我认为最好的选择是非二进制类型,因为相比之下组合较少,速度应该更快。如果使用base64_encode对数据进行编码,则每个位置、每个字节只有64个可能的值。如果使用bin2hex进行编码,则每个字节只有16个可能的值,但字符串要长得多。在二进制中,每个字节有256个位置。 我在哈希中使用了以编码64形式表示的VARCHAR(255)列,采用ASCII字符集和相同的排序规则。 VARBINARY会导致与MySQL文档中描述的比较问题。我不知道为什么答案建议使用VARBINARY有这么多优点。 我在我的作者网站上检查过这一点,其中测量时间(刷新即可查看)。

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