关于安全:在数据库中加密/散列纯文本密码

Encrypting/Hashing plain text passwords in database

我继承了一个Web应用程序,我刚刚发现它在一个SQL Server数据库中以明文形式存储了超过300000个用户名/密码。我意识到这是一件非常糟糕的事情?.

既然知道我必须更新登录和密码更新过程来加密/解密,并且对系统其余部分的影响最小,那么您推荐什么方法作为从数据库中删除纯文本密码的最佳方法?

感谢您的帮助。

编辑:对不起,如果我不清楚的话,我想问一下您要加密/散列密码的过程是什么,而不是特定的加密/散列方法。

我应该只是:

  • 备份数据库
  • 更新登录/更新密码代码
  • 几个小时后,检查用户表中的所有记录,散列密码并替换每个密码。
  • 测试以确保用户仍然可以登录/更新密码
  • 我想我更关心的是用户的数量,所以我想确保我做的正确。


    编辑(2016):按优先顺序使用argon2、scrypt、bcrypt或pbkdf2。根据您的情况,尽可能使用减速系数。使用经过审查的现有实现。确保使用适当的盐(尽管您使用的库应该确保这一点)。

    散列密码时,请不要使用普通MD5。

    使用pbkdf2,这基本上意味着使用一个随机的salt来防止彩虹表攻击,并且迭代(重新散列)足够多的时间来降低散列速度-不是太长以至于应用程序需要花费太长时间,但足以让攻击者暴力地强制使用大量不同的密码。

    从文档中:

    • 迭代至少1000次,最好是更多的时间,看看您的实现有多少次迭代是可行的。
    • 8字节(64位)的salt就足够了,而且随机数据不需要是安全的(salt是未加密的,我们不担心有人会猜到)。
    • 散列时应用salt的一个好方法是将hmac与您最喜欢的散列算法一起使用,使用密码作为hmac键,使用salt作为要散列的文本(请参见文档的本节)。

    在python中使用sha-256作为安全哈希的示例实现:

    编辑:正如EliCollins所提到的,这不是一个pbkdf2实现。您应该更喜欢坚持标准的实现,比如passlib。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    from hashlib import sha256
    from hmac import HMAC
    import random

    def random_bytes(num_bytes):
      return"".join(chr(random.randrange(256)) for i in xrange(num_bytes))

    def pbkdf_sha256(password, salt, iterations):
      result = password
      for i in xrange(iterations):
        result = HMAC(result, salt, sha256).digest() # use HMAC to apply the salt
      return result

    NUM_ITERATIONS = 5000
    def hash_password(plain_password):
      salt=random_bytes(8) # 64 bits

      hashed_password = pbkdf_sha256(plain_password, salt, NUM_ITERATIONS)

      # return the salt and hashed password, encoded in base64 and split with","
      return salt.encode("base64").strip() +"," + hashed_password.encode("base64").strip()

    def check_password(saved_password_entry, plain_password):
      salt, hashed_password = saved_password_entry.split(",")
      salt=salt.decode("base64")
      hashed_password = hashed_password.decode("base64")

      return hashed_password == pbkdf_sha256(plain_password, salt, NUM_ITERATIONS)

    password_entry = hash_password("mysecret")
    print password_entry # will print, for example: 8Y1ZO8Y1pi4=,r7Acg5iRiZ/x4QwFLhPMjASESxesoIcdJRSDkqWYfaA=
    check_password(password_entry,"mysecret") # returns True


    基本策略是使用一个密钥派生函数用一些salt"散列"密码。salt和散列结果存储在数据库中。当用户输入密码时,salt及其输入以相同的方式散列,并与存储值进行比较。如果匹配,则对用户进行身份验证。

    细节是魔鬼。首先,很大程度上取决于所选的哈希算法。基于基于哈希的消息身份验证代码的密钥派生算法(如pbkdf2)使查找将生成给定输出(攻击者在数据库中找到的内容)的输入(在本例中是密码)变得"计算不可行"。

    预计算字典攻击使用从哈希输出到密码的预计算索引或字典。散列速度很慢(或者应该很慢),因此攻击者将所有可能的密码散列一次,并以给定散列的方式存储索引结果,他可以查找相应的密码。这是时间与空间之间的经典权衡。由于密码列表可能很大,所以有一些方法可以调整权衡(如彩虹表),这样攻击者就可以放弃一点速度来节省大量空间。

    使用"加密盐"可以阻止预计算攻击。这是一些用密码散列的数据。它不需要是一个秘密,只需要一个给定的密码是不可预测的。对于salt的每个值,攻击者都需要一个新的字典。如果使用一个字节的salt,攻击者需要256份字典副本,每个副本都使用不同的salt生成。首先,他使用salt查找正确的字典,然后使用hash输出查找可用的密码。但是如果你加4个字节呢?现在他需要40亿本字典。通过使用足够大的盐,可以避免字典攻击。在实践中,来自加密质量随机数生成器的8到16字节的数据是很好的salt。

    当预计算离开表时,攻击者已经在每次尝试时计算散列值。现在找到密码需要多长时间完全取决于散列候选密码需要多长时间。这一时间通过哈希函数的迭代而增加。数字迭代通常是密钥派生函数的一个参数;如今,许多移动设备使用10000到20000次迭代,而服务器可能使用100000次或更多。(bcrypt算法使用术语"成本因素",这是对所需时间的对数度量。)


    我想您必须为加密密码向数据库中添加一列,然后对所有获得当前密码的记录运行批处理作业,对其进行加密(正如其他人提到的那样,MD5这样的哈希是相当标准的编辑:但不应单独使用-有关好的讨论,请参阅其他答案),将其存储在新列中,并在一切顺利。

    然后,您将需要更新前端,以便在登录时散列用户输入的密码,并验证与存储的散列是否一致,而不是检查纯文本与纯文本。

    对于我来说,在最终将纯文本密码全部删除之前,将这两个列都保留一段时间,以确保没有发生任何事情。

    别忘了,任何时候访问密码时,代码都必须更改,例如密码更改/提醒请求。你当然会失去发送忘记的密码的能力,但这不是坏事。您必须使用密码重置系统。

    编辑:最后一点,您可能需要考虑避免我第一次尝试测试床安全登录网站时出错:

    在处理用户密码时,请考虑哈希发生的位置。在我的例子中,散列是由运行在Web服务器上的PHP代码计算的,但是密码是以明文形式从用户的机器传输到页面的!在我工作的环境中,这是正常的(ish),因为它在一个HTTPS系统(uni网络)中。但是,在现实世界中,我想您应该在密码离开用户系统之前,使用javascript等对密码进行哈希,然后将哈希传输到您的站点。


    按照xan的建议,将当前密码列保留一段时间,这样如果情况变糟,您可以快速回滚n-easy。

    就加密密码而言:

    • 用盐
    • 使用一个用于密码的哈希算法(例如,它很慢)

    看看托马斯·普塔切克的《彩虹表:关于安全密码方案的一些细节,你需要知道些什么》。


    我认为你应该做以下事情:

  • 创建一个名为hashed_password的新列或类似的列。
  • 修改代码,以便它检查这两列。
  • 逐步将密码从非哈希表迁移到哈希表。例如,当用户登录时,会自动将其密码迁移到哈希列并删除未缓存的版本。所有新注册的用户都将拥有哈希密码。
  • 几个小时后,您可以运行一个每次迁移n个用户的脚本。
  • 当您没有更多未清除的密码时,可以删除旧密码列(可能无法删除,具体取决于您使用的数据库)。此外,您还可以删除处理旧密码的代码。
  • 你完了!

  • 这是我几周前的一个问题。我们正在将一个大型MIS项目部署到975个不同的地理位置,在那里我们自己的用户凭证存储将用作不同的已实现和正在使用的应用程序集的验证器。我们已经提供了基于REST和SOAP的身份验证服务,但是客户坚持能够从其他应用程序访问用户凭证存储,只需要与相关表或视图的只读视图建立DB连接。叹息…(这种高度耦合的不良设计决策是另一个问题的主题)。

    这迫使我们坐下来,将我们的salt和迭代散列密码存储方案转换为一个规范,并提供一些不同的语言实现以便于集成。

    简而言之,我们称之为相当安全的哈希密码(fshp)。在python、ruby、php5中实现并将其发布到公共域。可在http://github.com/bdd/fshp上使用、分叉、燃烧或吐痰。

    fshp是一种盐化、迭代散列的密码散列实现。

    设计原理与RFC2898中的PBKDF1规范相似。(A.K.A:PKCS 5:基于密码的加密规范版本2.0。)fshp允许选择盐的长度、迭代次数和sha-1和sha-2之间的底层加密哈希函数(256、384、512)。每个输出开头的自定义元前缀使其可移植,同时允许用户选择自己的密码存储安全基线。

    安全性:

    默认的fshp1使用8字节的salts,使用4096次sha-256哈希迭代。-8字节的salt通过乘以需要2^64的空间。-4096次迭代导致蛮力攻击相当昂贵。-没有已知的针对SHA-256的攻击来发现与计算工作量小于2^128次操作这个版本。

    实施:

    • python:用2.3.5(w/hashlib)、2.5.1、2.6.1测试
    • 红宝石:用1.8.6测试
    • PHP5:用5.2.6测试

    欢迎所有人创建缺少的语言实现或打磨现在的。

    基本操作(使用python):

    1
    2
    3
    4
    5
    >>> fsh = fshp.crypt('OrpheanBeholderScryDoubt')
    >>> print fsh
    {FSHP1|8|4096}GVSUFDAjdh0vBosn1GUhzGLHP7BmkbCZVH/3TQqGIjADXpc+6NCg3g==
    >>> fshp.validate('OrpheanBeholderScryDoubt', fsh)
    True

    自定义密码:

    让我们弱化密码散列方案。-将盐的长度从默认值8减少到2。-将迭代轮数从默认值4096减少到10。-选择具有sha-1的fshp0作为底层哈希算法。

    1
    2
    3
    >>> fsh = fshp.crypt('ExecuteOrder66', saltlen=2, rounds=10, variant=0)
    >>> print fsh
    {FSHP0|2|10}Nge7yRT/vueEGVFPIxcDjiaHQGFQaQ==


    出于身份验证目的,应避免使用可逆加密存储密码,即只应存储密码哈希,并根据存储的哈希检查用户提供的密码的哈希。但是,这种方法有一个缺点:如果攻击者掌握了密码存储数据库,它很容易受到彩虹表攻击。

    您应该做的是存储预先选择的(和秘密)salt值的散列值+密码。即,将salt和密码连接起来,散列结果,并存储这个散列。进行身份验证时,请执行相同的操作-连接salt值和用户提供的密码hash,然后检查是否相等。这使得彩虹桌攻击不可行。

    当然,如果用户通过网络发送密码(例如,如果您正在使用Web或客户机-服务器应用程序),则不应通过明文发送密码,因此,您应该存储和检查哈希(salt+hash(password)),而不是存储哈希(salt+password),并让您的客户机预哈希用户提供的密码。通过网络发送。如果用户(和许多用户一样)出于多种目的重复使用同一密码,这也会保护用户的密码。


    正如其他人提到的,如果你能帮忙,你就不想解密。标准的最佳实践是使用单向散列进行加密,然后当用户登录散列时,对其密码进行比较。

    否则,您必须使用强加密来加密然后解密。我只建议在政治原因很强的情况下这样做(例如,你的用户习惯于打电话给服务台取回他们的密码,而你有来自高层的强大压力不改变密码)。在这种情况下,我将从加密开始,然后开始构建业务案例以转移到哈希。


    步骤1:将加密字段添加到数据库

    步骤2:更改代码,以便在更改密码时更新两个字段,但登录仍然使用旧字段。

    步骤3:运行脚本填充所有新字段。

    步骤4:更改代码以便登录使用新字段和更改密码停止更新旧字段。

    步骤5:从数据库中删除未加密的密码。

    这将允许您在不中断最终用户的情况下完成转换。

    也:我要做的是将新数据库字段命名为与密码完全无关的字段,比如"lastsessionid",或者类似的无聊内容。然后,不用删除密码字段,只需填充随机数据的散列。然后,如果你的数据库曾经被破坏,他们可以花费所有的时间试图解密"密码"字段。

    这可能并不能真正完成任何事情,但想到有人坐在那里试图找出毫无价值的信息是很有趣的。


    • 使用类似MD5的东西加密,将其编码为十六进制字符串
    • 您需要一个salt;在这种情况下,用户名可以用作salt(它必须是唯一的,用户名应该是可用的最唯一的值;-)
    • 使用旧密码字段存储MD5,但标记MD5(例如"MD5:687A878…."),以便旧(纯文本)和新(MD5)密码可以共存。
    • 如果存在MD5,则更改登录过程以验证MD5,否则更改为验证普通密码。
    • 更改"更改密码"和"新用户"功能以仅创建MD5'ED密码
    • 现在您可以运行转换批处理作业,这可能需要多长时间
    • 运行转换后,删除旧版支持


    MD5和SHA1显示了一些弱点(两个单词可能导致相同的哈希),因此建议使用sha256-sha512/迭代哈希来哈希密码。

    我会用编写应用程序所用的语言编写一个小程序,然后生成一个随机的salt,它对每个用户都是唯一的,并生成密码的散列值。我倾向于使用与验证相同的语言的原因是不同的加密库可以做一些稍微不同的事情(即填充),因此使用相同的库来生成哈希并验证它可以消除这种风险。如果您愿意,这个应用程序还可以在更新表后验证登录,因为它仍然知道纯文本密码。

  • 不使用MD5/SHA1
  • 生成一个好的随机salt(许多加密库都有一个salt生成器)
  • ORIP推荐的迭代哈希算法
  • 确保密码不会以明文形式通过网络传输。

  • 我不是安全专家,但我认为目前的建议是使用bcrypt/blowfish或sha-2变体,而不是md5/sha1。

    也许你也需要考虑全面的安全审计。


    我想建议对ORIP发布的Python示例进行一次改进。我将把random_bytes函数重新定义为:

    1
    2
    def random_bytes(num_bytes):
        return os.urandom(num_bytes)

    当然,您必须导入os模块。os.urandom函数提供了一个随机的字节序列,可以在加密应用程序中安全地使用。有关详细信息,请参阅此函数的参考帮助。


    和所有的安全决策一样,存在着权衡。如果您散列密码,这可能是您最容易的操作,您不能提供返回原始密码的密码检索功能,您的工作人员也不能查找个人密码以访问其帐户。

    您可以使用对称加密,这有其自身的安全缺陷。(如果您的服务器受到攻击,对称加密密钥也可能受到攻击)。

    您可以使用公钥加密,并在单独的计算机上运行密码检索/客户服务,该计算机将私钥与Web应用程序隔离存储。这是最安全的,但需要一个两机架构,可能还需要一个防火墙。


    要散列密码,可以使用hashbytes函数。返回varbinary,因此必须创建一个新列,然后删除旧的varchar列。

    喜欢

    1
    2
    3
    4
    ALTER TABLE users ADD COLUMN hashedPassword varbinary(max);
    ALTER TABLE users ADD COLUMN salt char(10);
    --Generate random salts and update the column, after that
    UPDATE users SET hashedPassword = HashBytes('SHA1',salt + '|' + password);

    然后修改代码以验证密码,使用类似

    1
    2
    SELECT count(*) from users WHERE hashedPassword =
    HashBytes('SHA1',salt + '|' + <password>)

    其中是用户输入的值。


    用MD5散列它们。这通常是用密码做的。