如何加密用户密码

作者 Daniel Fernandez

1 概述

几乎所有现代 Web 应用程序都需要以一种或另一种方式加密其用户的密码。我们可以说,从应用程序拥有用户并且用户使用密码登录的那一刻起,这些密码必须以加密方式存储。

这有一些直观的原因:我们的数据存储可能会受到影响,我们的通信也会受到影响。但最重要的原因是我们必须将用户的密码视为敏感的个人数据。他们的密码是他们隐私的关键,所以他们是个人的,他们是敏感的,没有人(甚至我们)无权知道他们。如果我们想获得用户的信任,我们必须尊重这一点。

2 算法

所以,我们必须加密密码,但是…如何?这是我们的第一条规则:

I. 使用单向技术加密密码,即摘要。

这是因为,除了一些特定的场景(主要是关于遗留集成),密码被解密是绝对没有理由的。如果您使用基于密码的加密(一种双向技术)来加密您的密码并且攻击者知道您的加密密码,那么您的所有用户密码都将被泄露(并且可能一次全部泄露)。如果您没有这样的加密密码(或密钥)来解密,这种风险就会消失,攻击者将不得不信任暴力破解或类似策略。

’但是如果我的一个用户丢失了他/她的密码怎么办?我不能提醒他/她吗?

答案是响亮而明确的“不”。不仅你不能做提醒他们密码这样的事情,而且事实上你甚至不应该有办法阅读/知道/查看你的用户的密码,不管你是系统管理员!如果您的某个用户丢失了他/她的密码,只需将其重置为新值并向他/她发送一封带有新密码的已验证电子邮件地址的消息,要求尽快更改。

既然密码摘要是必须的,那么我们应该使用哪种摘要算法?好吧,有几个,这在很大程度上取决于您的需求。最常用的是:

  • MD5算法

  • SHA 系列:SHA-1 算法和 SHA-2 变体(SHA-224、SHA-256、SHA-384 和 SHA-512)

在大多数情况下,MD5或SHA-1都是密码摘要的适当选择,尽管应用这些算法是不够的,我们将在后面看到。

当我们被告知应该使用摘要进行密码加密时,并且鉴于摘要是单向技术,我们脑海中浮现的下一个问题通常是:“如果我无法解密密码…我将如何检查我的用户输入正确的?

这个问题有一个非常简单的答案,我们将采用它作为我们的第二条规则:

Ⅱ通过比较摘要而不是未加密的字符串来匹配输入和存储的密码。

这意味着,一旦我们的用户在登录时输入了他们的密码,我们将使用我们之前存储密码时使用的相同算法来消化他们的输入,然后比较两个摘要。由于摘要算法保证两个相等的输入将获得相等的摘要(在相反方向上不正确),如果摘要匹配,我们可以认为用户输入的密码是有效的。

3 提高摘要的安全性

上面提到的所有摘要算法(也是加密散列函数)都有一个共同特点:它们是公开的。它们是众所周知的并且在很大程度上实现的算法,因此任何人都可以使用它们,而不仅仅是我们。

如果任何人都可以使用我们使用的相同算法,并且由于某种原因攻击者可以看到我们的密码摘要数据库,那么我们如何确保他们无法通过简单地尝试所有可能性来猜测我们的某些用户密码,直到他们找到一个与存储的摘要匹配的摘要(蛮力)?

嗯,答案是……我们不能。但我们可以让这成为他们不愿做的压倒性和耗时的任务,除非他们可以等待永恒。为了实现这一点,我们提供了两个概念:迭代计数

3.1 盐

盐是在被消化之前添加到密码中的字节序列。这使得我们的摘要不同于我们单独加密密码时的摘要,从而保护我们免受字典攻击。对于盐,我们可以采用两种不同的策略:

使用固定的 salt,我们将用于消化每个密码的字节序列。我们可以隐藏这种盐,并将其视为一个附加的安全价值,但它会使我们的系统更容易受到生日攻击,一般来说,攻击我们的密码数据库作为一个整体。

使用变量 salt,这通常是更安全的选择(如果它是随机的则更好)。这是为每个被消化的密码单独生成或计算的,它允许每个存储的密码与其他密码分离,从而创建更强大的整体保护,并大大提高针对我们整个密码数据库的攻击的安全性。

在实践中,随机(或至少是可变的)盐是一个更好的主意,因为尽管它是随机的,但它会迫使我们将它与摘要一起存储为未加密的(以便我们可以恢复它),这对于攻击者来说是微不足道的要知道它,它仍然会让我们每个用户的密码与其他用户的密码保持分离,因此他们将不得不单独受到攻击。

想一想,如果我们使用固定盐并且攻击者知道了这个固定盐,整个密码数据库的安全性将大大降低。攻击者可以通过多种方式了解这种盐,例如对自己的密码或他/她以某种方式从有效用户那里获得的密码进行暴力破解。在固定盐的情况下,弱用户可能会导致我们进入弱整体密码系统。

尽管如此,如果您仍然想保留盐的某些部分秘密,一个好的方法可能是两种技术的混合,使用由固定秘密部分和随机部分组成的盐,只有随机字节被存储未消化与摘要结果。

salt 的最小推荐大小为 8 个字节。如果使用混合方法,则至少有 8 个字节应该是随机的。这就是说,我们可以陈述我们的第三条规则:

Ⅲ 使用至少包含 8 个随机字节的盐,并将这些未消化的随机字节附加到结果中。

3.2. 迭代次数

迭代计数是指我们正在消化的哈希函数应用于其自身结果的次数。

这意味着,一旦我们选择了盐并将密码连接到它,我们将不得不应用散列函数(例如,MD5 算法),获取结果,然后再次将其作为输入传递给相同的散列函数,然后一次又一次地做同样的事情…很多次。

建议的最小迭代次数为 1,000,它将为我们提供大量的额外安全性。想一想,当您为新用户创建单个密码摘要时,应用哈希函数一次或一千次之间的差异对您来说不是问题,可能是几百毫秒…但攻击者会要在暴力破解时生成大量暂定密码摘要,对于攻击者来说,应用哈希函数一次和每次尝试应用一千次之间的区别将是一个真正的计算问题。

所以,我们似乎有第四条规则:

Ⅳ 至少迭代哈希函数 1,000 次。

3.3. 以图形方式放置

我们可以用图形表示使用盐和迭代计数的整个过程,如下所示:
在这里插入图片描述
密码加密过程

4 字符串到字节序列的翻译

在能够正确存储我们的密码摘要之前,我们还需要关心一些事情,那就是字符串和字节序列之间的不匹配:根据用于翻译的编码,两个相同的字符串可能用不同的字节序列表示(ISO-8859-1、UTF-8 等…)。

这会影响我们,因为密码通常由用户作为字符串输入,但摘要算法在字节级别工作。

4.1 密码输入时的编码疑难解答

假设一个新用户使用包含非 ASCII 字符的密码进行注册,而您的注册应用程序在西欧的 Windows 2000 机器上运行,将输入的字符串转换为字节,而无需为操作选择特定的编码,因此使用默认的,在那个 Windows 系统中是 ISO-8859-1。注册逻辑会消化结果字节并存储密码。

然后,该用户再次出现,这一次他/她没有访问您的注册应用程序,而是访问了在相同密码数据库但在 Linux 机器上运行的其他应用程序。用户正确输入了他/她的密码并且…他们不匹配!

为什么?因为大部分Linux系统使用UTF-8作为默认编码,因此两种不同场景下用户输入的密码被每台机器翻译成不同的字节序列,影响密码的正确匹配。

如何解决这个问题:通过为字符串到字节的转换设置一个固定的编码。第五条规则来了:

V. 在消化之前,使用固定的编码,最好是 UTF-8 进行字符串到字节的序列转换。

如果您使用的是 Java,其中 String 对象与编码无关(尽管由 UTF-16 支持),您不必担心您的应用程序是否使用 ISO-8859-1 或任何其他编码而不是其用户的 UTF-8界面,因为密码摘要的编码不必与用户界面的编码相匹配。这种编码必须是固定的,UTF-8 将为您带来大小和字符集完整性 (Unicode) 之间的正确平衡。

此外,您应该关心 Unicode 规范化,因为根据输入系统,您可以为相同的 unicode 视觉字符表示获得不同的字符序列(以及字节序列)。此处可能需要确保您的代码采用 NFC 形式的规范化操作。有关 Unicode 规范化的更多信息,请参阅本期 Core Java Technologies Tech Tips。

4.2 摘要存储中的编码故障排除

我们通常希望将摘要密码作为字符串进行管理和存储,但摘要函数将输出一个字节序列,该字节序列不一定代表任何编码中的有效字符串。因此,将我们消化的字节序列转换回字符串是不可能的,并且在这样做时我们可能会冒数据丢失的风险。

这就是 BASE64 编码发挥作用的地方。通过在 BASE64 中编码我们消化的字节序列,我们将确保输出字节序列代表一个有效的、可显示的 US-ASCII 字符串。因此,我们将能够安全地将 BASE64 编码的字节序列转换为指定 US-ASCII 作为编码的字符串。

Ⅵ 最后,应用 BASE64 编码并将摘要存储为 US-ASCII 字符串。

作为 BASE64 的替代方案,您还可以将输出编码为十六进制字符串,这将是一种同样有效的方法(尽管您会得到更长的摘要字符串)。

5 用户密码加密规则总结

我们现在有一个完整的规则列表:

I. 使用单向技术加密密码,即摘要。

Ⅱ、通过比较摘要而不是未加密的字符串来匹配输入和存储的密码。

Ⅲ、使用至少包含 8 个随机字节的盐,并将这些未消化的随机字节附加到结果中。

Ⅳ。至少迭代哈希函数 1,000 次。

V. 在消化之前,使用固定的编码,最好是 UTF-8 进行字符串到字节的序列转换。

Ⅵ、最后,应用 BASE64 编码并将摘要存储为 US-ASCII 字符串。

6 用Java做

使用所解释的技术在 Java 中加密密码的最简单方法是使用 Jasypt,它已经为您透明地完成了所有这些处理。

如果我们不能使用 jasypt,或者出于某种原因我们希望自己开发加密功能,我们将需要以下工具来完成我们的任务:

java.security.MessageDigest用于创建摘要。此类允许指定我们希望使用的摘要算法。

java.security.SecureRandom用于以安全方式生成随机盐,使用 SHA1PRNG 等算法。

java.lang.String.getBytes (String charsetName)方法,用于从输入字符串中 获取字节序列,指定固定编码(“UTF-8”)。

org.apache.commons.codec.binary.Base64,Apache Commons-Codec 库的一部分,用于对哈希输出执行 BASE64 转换。

java.text.Normalizer(仅在 Java SE 6 中)或com.ibm.icu.text.Normalizer( Unicode 包的国际组件的一部分),用于 Unicode 规范化操作。

此外,可以将Source Repository中提供的org.jasypt.digest.StandardByteDigester 和org.jasypt.digest.StandardStringDigester类的源代码用作指南。

7 典型攻击防御

7.1 蛮力攻击

执行时间:单个用户密码。

描述:攻击者试图通过详尽地生成所有可能的密码、消化它们并测试它们是否与用户的密码摘要匹配来获取用户的密码。 了解更多 [wikipedia.org]。

我们的防御:通过将哈希函数迭代到 1,000(建议的最小值)之类的数字,在注册或登录时为用户创建密码摘要的开销并不显着,但暴力攻击者生成的累积成本数以百万计的摘要将非常可观。请记住,保护您的加密数据的最佳方法之一是让破坏您的安全性的成本太高以至于不值得付出努力。

7.2. 字典攻击

执行于:单个用户密码或整个用户密码数据库。

描述:攻击者试图通过将其摘要与一组“最可能的”密码摘要进行匹配来获取用户的密码,这些摘要通常是从字典中的单词列表生成的。这种攻击利用了当今应用程序中的一个严重弱点,因为大量用户将字典单词设置为他们的密码。了解更多 [wikipedia.org]。

我们的防御:通过添加随机盐,减少了许多人使用的基于字典的密码的弱点(它们不再是字典单词),并且摘要出现在攻击者先前创建的一组摘要中的可能性很小.

7.3. 生日攻击

执行于:作为一个整体的用户密码数据库。

描述:此攻击利用了生日悖论 [wikipedia.org],该悖论简要说明,拥有大量用户密码摘要,生成摘要与集合中至少一个摘要冲突的密码的概率非常高。远高于你的直觉预期。随着集合的大小(用户数量)的增加,这种概率会急剧增加。了解更多 [wikipedia.org]。

我们的防御:通过添加随机盐,生日攻击成功的可能性最小,因为攻击者必须单独攻击每个密码,而不是整个密码集,才能找到冲突。这是因为他/她必须找到一个密码,该密码使用用于消化它的相同盐创建与被攻击密码相同的摘要,每个密码都不同(这将成为暴力攻击) .

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值