62

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

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

任何帮助表示赞赏。

编辑:对不起,如果我不清楚,我的意思是问你加密/散列密码的程序是什么,而不是特定的加密/散列方法。

我应该:

  1. 备份数据库
  2. 更新登录/更新密码代码
  3. 几个小时后,检查用户表中的所有记录,对密码进行哈希处理并替换每一个
  4. 测试以确保用户仍然可以登录/更新密码

我想我的担忧更多来自于用户的数量,所以我想确保我做的正确。

4

15 回答 15

49

编辑(2016):按优先顺序使用Argon2scryptbcryptPBKDF2。在您的情况下使用尽可能大的减速因子。使用经过审查的现有实施。确保使用适当的盐(尽管您使用的库应该为您确保这一点)。


当您散列密码时,请使用DO NOT USE PLAIN MD5

使用PBKDF2,这基本上意味着使用随机盐来防止彩虹表攻击,并迭代(重新散列)足够多的时间来减慢散列速度 - 不是你的应用程序花费太长时间,而是足以让攻击者暴力破解大量不同的密码会通知

从文件:

  • 迭代至少 1000 次,最好更多 - 为你的实现计时,看看有多少迭代对你来说是可行的。
  • 8 字节(64 位)的 salt 就足够了,而且随机数不需要是安全的(salt 是未加密的,我们不担心有人会猜到它)。
  • 在散列时应用盐的一个好方法是将 HMAC 与您最喜欢的散列算法一起使用,使用密码作为 HMAC 密钥,盐作为要散列的文本(请参阅文档的这一部分)。

Python 中的示例实现,使用 SHA-256 作为安全哈希:

编辑:正如 Eli Collins 所述,这不是 PBKDF2 实现。您应该更喜欢遵循标准的实现,例如PassLib

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
于 2008-11-13T19:02:27.147 回答
38

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

细节决定成败。首先,很大程度上取决于所选择的哈希算法。像 PBKDF2 这样的密钥派生算法,基于基于哈希的消息身份验证代码,使得查找将产生给定输出(攻击者在数据库中找到的内容)的输入(在本例中为密码)“在计算上不可行” )。

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

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

通过预先计算,攻击者每次尝试都会计算哈希值。现在找到密码需要多长时间完全取决于散列候选人需要多长时间。该时间通过散列函数的迭代而增加。迭代次数一般是密钥推导函数的参数;今天,许多移动设备使用 10,000 到 20,000 次迭代,而服务器可能使用 100,000 或更多。(bcrypt 算法使用术语“成本因子”,它是所需时间的对数度量。)

于 2008-11-13T18:13:02.753 回答
19

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

然后,您将需要更新您的前端以在登录时对用户输入的密码进行哈希处理,并验证与存储的哈希值相比,而不是检查明文与明文。

在我最终将明文密码全部删除之前,将两列都保留一段时间以确保没有发生任何事情,这对我来说似乎是谨慎的。

也不要忘记,只要密码被访问,代码就必须更改,例如密码更改/提醒请求。您当然会失去通过电子邮件发送忘记密码的能力,但这并不是一件坏事。您将不得不使用密码重置系统。

编辑:最后一点,您可能要考虑避免我第一次尝试在测试平台安全登录网站上犯的错误:

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

于 2008-11-13T17:07:42.340 回答
4

遵循Xan 的建议,将当前密码列保留一段时间,这样如果出现问题,您可以快速轻松地回滚。

至于加密你的密码:

  • 用盐
  • 使用适用于密码的哈希算法(即 - 它很

有关详细信息,请参阅 Thomas Ptacek 的《彩虹表足够:您需要了解的有关安全密码方案的内容》。

于 2008-11-13T17:18:31.927 回答
3

我认为您应该执行以下操作:

  1. 创建一个名为 HASHED_PASSWORD 或类似名称的新列。
  2. 修改您的代码,使其检查两列。
  3. 逐渐将密码从非散列表迁移到散列表。例如,当用户登录时,将他或她的密码自动迁移到散列列并删除未散列的版本。所有新注册的用户都将拥有散列密码。
  4. 几个小时后,您可以运行一个脚本,该脚本一次迁移 n 个用户
  5. 当您没有更多未散列的密码时,您可以删除旧密码列(您可能无法这样做,具体取决于您使用的数据库)。此外,您可以删除处理旧密码的代码。
  6. 你完成了!
于 2008-11-13T17:19:31.817 回答
3

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

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

我们简称它为相当安全的散列密码或FSHP。在 Python、Ruby、PHP5 中实现它并将其发布到公共领域。可在http://github.com/bdd/fshp上的 GitHub 上消费、分叉、燃烧或吐出

FSHP 是一种加盐的、迭代散列的密码散列实现。

设计原理与RFC 2898 中 的PBKDF1规范(又名:PKCS #5:基于密码的加密规范 2.0 版) 类似。FSHP 允许在 SHA-1 和 SHA-2 中选择盐长度、迭代次数和底层加密哈希函数(256、384、512)。在每个输出的开头自定义元前缀使其可移植,同时让消费者选择自己的密码存储安全基线。

安全

默认 FSHP1 使用 8 字节盐,SHA-256 散列的 4096 次迭代。- 通过将所需空间乘以 2^64,8 字节盐使彩虹表攻击变得不切实际。- 4096 次迭代导致蛮力攻击相当昂贵。- 在此版本发布时,没有已知的针对 SHA-256 的攻击可以通过少于 2^128 次操作的计算工作来发现冲突。

实现:

  • Python:使用 2.3.5(带 hashlib)、2.5.1、2.6.1 进行测试
  • Ruby:用 1.8.6 测试
  • PHP5:用 5.2.6 测试

非常欢迎每个人创建缺失的语言实现或完善当前的实现。

基本操作(使用 Python)

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

自定义地穴:

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

>>> fsh = fshp.crypt('ExecuteOrder66', saltlen=2, rounds=10, variant=0)
>>> print fsh
{FSHP0|2|10}Nge7yRT/vueEGVFPIxcDjiaHQGFQaQ==
于 2009-01-02T17:00:52.237 回答
2

正如其他人所提到的,如果可以提供帮助,您就不想解密。标准的最佳实践是使用单向哈希进行加密,然后在用户登录时对其密码进行哈希比较。

否则,您将不得不使用强加密来加密然后解密。如果政治原因很强烈(例如,您的用户习惯于致电服务台以检索他们的密码,并且您有来自高层的强烈压力不要更改密码),我只会建议这样做。在这种情况下,我将从加密开始,然后开始构建业务案例以转向散列。

于 2008-11-13T17:06:52.187 回答
2

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

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

当然,如果用户通过网络发送密码(例如,如果您正在使用 Web 或客户端-服务器应用程序),那么您不应该以明文形式发送密码,而不是存储散列(salt +密码)您应该存储并检查哈希(盐+哈希(密码)),并让您的客户端对用户提供的密码进行预哈希并通过网络发送该密码。这也可以保护您的用户密码,以防用户(许多人)将相同的密码用于多种用途。

于 2008-11-13T17:09:19.187 回答
1
  • 使用 MD5 之类的加密,将其编码为十六进制字符串
  • 你需要盐;在您的情况下,用户名可以用作盐(它必须是唯一的,用户名应该是可用的最唯一的值;-)
  • 使用旧密码字段存储 MD5,但标记 MD5(例如“MD5:687A878....”)以便旧(纯文本)和新(MD5)密码可以共存
  • 如果有 MD5,则更改登录过程以验证 MD5,否则验证明文密码
  • 更改“更改密码”和“新用户”功能以仅创建 MD5 密码
  • 现在您可以运行转换批处理作业,这可能需要尽可能长的时间
  • 运行转换后,删除 legacy-support
于 2008-11-13T17:21:33.560 回答
1

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

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

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

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

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

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

另外:我要做的是将新数据库字段命名为与密码完全无关的名称,例如“LastSessionID”或类似无聊的名称。然后不用删除密码字段,只需填充随机数据的哈希值。然后,如果您的数据库遭到入侵,他们可以花费所有时间尝试解密“密码”字段。

这实际上可能不会完成任何事情,但想想有人坐在那里试图找出毫无价值的信息是很有趣的

于 2008-11-14T17:25:52.210 回答
0

与所有安全决策一样,需要权衡取舍。如果您对密码进行哈希处理(这可能是您最简单的操作),您将无法提供返回原始密码的密码检索功能,您的员工也无法通过查找某人的密码来访问他们的帐户。

您可以使用对称加密,它有其自身的安全缺陷。(如果您的服务器遭到破坏,对称加密密钥也可能遭到破坏)。

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

于 2008-11-13T17:10:40.633 回答
0

MD5 和 SHA1 显示出一些弱点(两个字可能导致相同的散列),因此建议使用 SHA256-SHA512 / 迭代散列来散列密码。

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

  1. 不要使用 MD5/SHA1
  2. 生成一个好的随机盐(许多加密库都有一个盐生成器)
  3. orip 推荐的迭代哈希算法
  4. 确保密码不是通过网络以纯文本形式传输的
于 2008-11-14T16:45:32.493 回答
0

我想建议对Orip 发布的出色 python 示例进行一项改进。我会将random_bytes函数重新定义为:

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

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

于 2009-04-22T17:37:01.860 回答
-1

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

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);

然后你修改代码来验证密码,使用类似的查询

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

其中 <password> 是用户输入的值。

于 2008-11-13T17:07:43.230 回答
-1

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

可能您也需要考虑全面的安全审计

于 2008-11-13T18:05:08.543 回答