加密/散列数据库中的纯文本密码

时间:2008-11-13 17:00:42

标签: security encryption passwords security-by-obscurity

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

知道我必须更新登录和密码更新过程才能加密/解密,并且对系统其他部分的影响最小,你会建议什么作为从中删除明文密码的最佳方法数据库?

感谢任何帮助。

编辑:对不起,如果我不清楚,我打算问一下加密/哈希密码的程序,而不是特定的加密/哈希方法。

我应该只是:

  1. 备份数据库
  2. 更新登录/更新密码
  3. 数小时后,浏览用户表中记录密码并替换每个密码的所有记录
  4. 测试以确保用户仍然可以登录/更新密码
  5. 我想我的关注更多来自于绝对数量的用户,所以我想确保我正确地做到这一点。

16 个答案:

答案 0 :(得分:49)

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


当您对密码进行哈希处理时,请使用不要使用PLAIN MD5

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

来自文件:

  • 迭代至少1000次,最好是更多 - 实施时间,看看有多少次迭代是可行的。
  • 8个字节(64位)的盐就足够了,并且随机不需要是安全的(盐是未加密的,我们不担心有人会猜到它。)
  • 在散列时应用salt的一种好方法是使用HMAC和您喜欢的哈希算法,使用密码作为HMAC密钥,使用salt作为哈希文本(请参阅文档的this section)。 / LI>

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

答案 1 :(得分:38)

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

魔鬼在于细节。首先,很大程度上取决于所选择的哈希算法。像PBKDF2这样的密钥派生算法,基于基于散列的消息认证代码,使得在计算上不可行地找到将产生给定输出的输入(在这种情况下,密码)(攻击者在数据库中找到了什么) )。

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

使用“加密盐”阻碍了预计算攻击。这是一些使用密码进行哈希处理的数据。 它不需要是秘密,它只需要对给定的密码不可预测。对于盐的每个值,攻击者需要一个新的字典。如果使用一个字节的salt,攻击者需要256个字典副本,每个副本使用不同的盐生成。首先,他使用salt查找正确的字典,然后他使用hash输出来查找可用的密码。但是如果添加4个字节怎么办?现在他需要40亿份字典。通过使用足够大的盐,排除了字典攻击。实际上,来自加密质量随机数生成器的8到16个字节的数据可以很好地生成盐。

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

答案 2 :(得分:19)

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

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

在最终删除明文密码之前,将两个列保留一段时间以确保没有任何异常发生,这似乎是谨慎的。

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

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

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

答案 3 :(得分:4)

按照Xan's advice保持当前密码列一段时间,如果情况变坏,您可以快速回滚。

加密密码:

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

有关详细信息,请参阅Thomas Ptacek的Enough With The Rainbow Tables: What You Need To Know About Secure Password Schemes

答案 4 :(得分:3)

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

  1. 创建一个名为HASHED_PASSWORD的新列或类似的东西。
  2. 修改您的代码,以便检查这两列。
  3. 逐渐将密码从非散列表迁移到散列表。例如,当用户登录时,将其密码自动迁移到散列列并删除未散列的版本。所有新注册的用户都将使用哈希密码。
  4. 下班后,您可以运行一个时间迁移n个用户的脚本
  5. 当您没有剩余未加密码的密码时,您可以删除旧密码列(您可能无法删除,取决于您使用的数据库)。此外,您可以删除代码来处理旧密码。
  6. 你已经完成了!

答案 5 :(得分:3)

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

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

我们称之为公平安全哈希密码或简称为FSHP。 在Python,Ruby,PHP5中实现它并将其发布到Public Domain。可以在http://github.com/bdd/fshp

在GitHub上消费,分叉,火焰或吐痰

FSHP是一个盐渍的,迭代哈希的密码哈希实现。

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

安全

默认FSHP1使用8字节盐,具有4096次SHA-256散列迭代。    - 8字节的盐使得彩虹表攻击通过乘以而变得不切实际     所需空间为2 ^ 64。    - 4096次迭代导致暴力攻击相当昂贵。    - 没有已知的针对SHA-256的攻击来查找与之发生冲突的攻击     计算工作量少于2 ^ 128次的操作     这个版本。

<强>实现:

  • Python:使用2.3.5(w / 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

自定义CRYPT:

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

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

答案 6 :(得分:2)

正如其他人所提到的,如果你能提供帮助,你不想解密。标准的最佳做法是使用单向散列进行加密,然后在用户登录哈希密码时对其进行比较。

否则,您必须使用强加密来加密然后解密。如果政治原因很强,我只会建议这样做(例如,您的用户习惯于可以致电服务台来检索他们的密码,并且您有来自顶层的强大压力而不是改变它)。在那种情况下,我将从加密开始,然后开始构建业务案例以转向哈希。

答案 7 :(得分:2)

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

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

当然,如果用户通过网络发送密码(例如,如果您正在使用Web或客户端 - 服务器应用程序),那么您不应该以明文形式发送密码,因此不要存储哈希(salt +密码)你应该存储和检查哈希(salt + hash(密码)),并让你的客户端预先哈希用户提供的密码并通过网络发送密码。如果用户(尽可能多)重复使用相同的密码用于多种用途,这也可以保护用户的密码。

答案 8 :(得分:1)

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

答案 9 :(得分:1)

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

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

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

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

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

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

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

这实际上可能无法实现任何目标,但想到有人坐在那里试图找出毫无价值的信息会很有趣

答案 10 :(得分:0)

与所有安全决策一样,存在权衡。如果您对密码进行散列(这可能是最简单的移动),则无法提供返回原始密码的密码检索功能,您的员工也无法查找用户密码以访问其帐户。

您可以使用对称加密,它有自己的安全缺陷。 (如果您的服务器遭到入侵,对称加密密钥也可能会受到损害)。

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

答案 11 :(得分:0)

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

或许您需要考虑完整的安全审核

答案 12 :(得分:0)

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

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

  1. 请勿使用MD5 / SHA1
  2. 生成一个好的随机盐(许多加密库有一个盐生成器)
  3. 推荐使用orip的迭代哈希算法
  4. 确保密码不通过电汇以纯文本传输

答案 13 :(得分:0)

我想建议对the great python example posted by Orip进行一项改进。我将random_bytes函数重新定义为:

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

当然,您必须导入os模块。 os.urandom函数提供可以在加密应用程序中安全使用的随机字节序列。有关详细信息,请参阅the reference help of this function

答案 14 :(得分:-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>)

其中&lt; password&gt;是用户输入的值。

答案 15 :(得分:-5)

用md5哈希。这就是密码通常所做的事情。