如何安全的存储用户密码?(上)

wKioL1jGO5KjDCCmAABGFK4qfcE692.jpg

本文介绍了对密码哈希加密的基础知识,以及什么是正确的加密方式。还介绍了常见的密码破解方法,给出了如何避免密码被破解的思路。相信读者阅读本文后,就会对密码的加密有一个正确的认识,并对密码正确进行加密措施。
php

做为一名Web开发人员,咱们常常须要与用户的账号系统打交道,而这其中最大的挑战就是如何保护用户的密码。常常会看到用户帐户数据库频繁被黑,因此咱们必须采起一些措施来保护用户密码,以避免致使没必要要的数据泄露。保护密码的最好办法是使用加盐密码哈希( salted password hashing)。java

在对密码进行哈希加密的问题上,人们有不少争论和误解,多是因为网络上有大量错误信息的缘由吧。对密码哈希加密是一件很简单的事,但不少人都犯了错。本文将会重点分享如何进行正确加密用户密码。程序员

重要警告:请放弃编写本身的密码哈希加密代码的念头!由于这件事太容易搞砸了。就算你在大学学过密码学的知识,也应该遵循这个警告。全部人都要谨记这点:不要本身写哈希加密算法!存储密码的相关问题已经有了成熟的解决方案,就是使用 phpass,或者在defuse/password-hashing 或 libsodium 上的 PHP 、 C# 、 Java 和 Ruby 的实现。算法

密码哈希是什么?数据库

wKiom1jGO6aD9JL_AAA4Nf86c8Y538.jpg

哈希算法是一种单向函数。它把任意数量的数据转换为固定长度的“指纹”,并且这个过程没法逆转。它们有这样的特性:若是输入发生了一点改变,由此产生的哈希值会彻底不一样(参见上面的例子)。这个特性很适合用来存储密码。由于咱们须要一种不可逆的算法来加密存储的密码,同时保证咱们也可以验证用户登录的密码是否正确。编程

在基于哈希加密的账号系统中,用户注册和认证的大体流程以下。数组

1.   用户建立本身的账号。浏览器

2.   密码通过哈希加密后存储在数据库中。密码一旦写入到磁盘,任什么时候候都不容许是明文形式。安全

3.   当用户试图登陆时,系统从数据库取出已经加密的密码,和通过哈希加密的用户输入的密码进行对比。服务器

4.   若是哈希值相同,用户将被授予访问权限。不然,告知用户他们输入的登录凭据无效。

5.   每当有人试图尝试登录,就重复步骤3和4。

在步骤4中,永远不要告诉用户输错的到底是用户名仍是密码。就像通用的提示那样,始终显示:“无效的用户名或密码。”就好了。这样能够防止***者在不知道密码的状况下枚举出有效的用户名。

应当注意的是,用来保护密码的哈希函数,和数据结构课学到的哈希函数是不一样的。例如,实现哈希表的哈希函数设计目的是快速查找,而非安全性。只有加密哈希函数( cryptographic hash function)才能够用来进行密码哈希加密。像SHA256 、 SHA512 、 RIPEMD 和 WHIRLPOOL 都是加密哈希函数。

人们很容易认为,Web开发人员所作的就是:只需经过执行加密哈希函数就可让用户密码得以安全。然而并非这样。有不少方法能够从简单的哈希值中快速恢复出明文的密码。有几种易于实施的技术,使这些“破解”的效率大为下降。网上有这种专门破解MD5的网站,只需提交一个哈希值,不到一秒钟就能获得破解的结果。显然,单纯的对密码进行哈希加密远远达不到咱们的安全要求。下一节将讨论一些用来破解简单密码哈希经常使用的手段。

如何破解哈希? 

字典***和暴力***( Dictionary and Brute Force Attacks)

字典***

暴力***

Trying apple         : failed

Trying aaaa : failed

Trying blueberry    : failed

Trying aaab : failed

Trying justinbeiber : failed

Trying aaac : failed

Trying letmein      :  failed

Trying acdb : failed

Trying s3cr3t       :  success!

Trying acdc : success!

破解哈希加密最简单的方法是尝试猜想密码,哈希每一个猜想的密码,并对比猜想密码的哈希值是否等于被破解的哈希值。若是相等,则猜中。猜想密码***的两种最多见的方法是字典***和暴力***。

字典***使用包含单词、短语、经常使用密码和其余可能用作密码的字符串的字典文件。对文件中的每一个词都进行哈希加密,将这些哈希值和要破解的密码哈希值比较。若是它们相同,这个词就是密码。字典文件是经过大段文本中提取的单词构成,甚至还包括一些数据库中真实的密码。还能够对字典文件进一步处理以使其更为有效:如单词“hello” 按网络用语写法转成 “h3110” 。

暴力***是对于给定的密码长度,尝试每一种可能的字符组合。这种方式会消耗大量的计算,也是破解哈希加密效率最低的办法,但最终会找出正确的密码。所以密码应该足够长,以致于遍历全部可能的字符组合,耗费的时间太长使人没法承受,从而放弃破解。

目前没有办法来组织字典***或暴力***。只能想办法让它们变得低效。若是密码哈希系统设计是安全的,破解哈希的惟一方法就是进行字典***或暴力***遍历每个哈希值了。

 查表法( LookupTables)

Searching:5f4dcc3b5aa765d61d8327deb882cf99: FOUND: password5

Searching:6cbe615c106f422d23669b610b564800:  not in database

Searching:630bf032efe4507f2c57b280995925a9: FOUND: letMEin12

Searching:386f43fab5d096a7a66d67c8f213e5ec: FOUND: mcd0nalds

Searching:d5ec75d5fe70d428685510fae36492d9: FOUND: p@ssw0rd!

对于破解相同类型的哈希值,查表法是一种很是高效的方式。主要理念是预先计算( pre-compute)出密码字典中的每一个密码的哈希值,而后把他们相应的密码存储到一个表里。一个设计良好的查询表结构,即便包含了数十亿个哈希值,仍然能够实现每秒钟查询数百次哈希。

若是你想感觉查表法的速度有多快,尝试一下用 CrackStation 的 free hash cracker 来破解下面的 SHA256。

wKiom1jGO8OC5zNuAAA6hDad2yI637.jpg

 反向查表法(Reverse Lookup Tabs)

wKiom1jGO87RZq3TAABWIgq9v6w719.jpg

这种***容许***者无需预先计算好查询表的状况下同时对多个哈希值发起字典***或暴力***。

首先,***者从被黑的用户账号数据库建立一个用户名和对应的密码哈希表,而后,***者猜想一系列哈希值并使用该查询表来查找使用此密码的用户。一般许多用户都会使用相同的密码,所以这种***方式特别有效。

 彩虹表( RainbowTables)

彩虹表是一种以空间换时间的技术。与查表法类似,只是它为了使查询表更小,牺牲了破解速度。由于彩虹表更小,因此在单位空间能够存储更多的哈希值,从而使***更有效。可以破解任何最多8位长度的 MD5 值的彩虹表已经出现。

接下来,咱们来看一种谓之“加盐( salting)”的技术,可以让查表法和彩虹表都失效。

 加盐( AddingSalt)

wKiom1jGO9ijNAl0AABSA-qcibU034.jpg

查表法和彩虹表只有在全部密码都以彻底相同的方式进行哈希加密才有效。若是两个用户有相同的密码,他们将有相同的密码哈希值。咱们能够经过“随机化”哈希,当同一个密码哈希两次后,获得的哈希值是不同的,从而避免了这种***。

咱们能够经过在密码中加入一段随机字符串再进行哈希加密,这个被加的字符串称之为盐值。如上例所示,这使得相同的密码每次都被加密为彻底不一样的字符串。咱们须要盐值来校验密码是否正确。一般和密码哈希值一同存储在账号数据库中,或者做为哈希字符串的一部分。

盐值无需加密。因为随机化了哈希值,查表法、反向查表法和彩虹表都会失效。由于***者没法事先知道盐值,因此他们就没有办法预先计算查询表或彩虹表。若是每一个用户的密码用不一样的盐再进行哈希加密,那么反向查表法***也将不能奏效。

接下来,咱们看看加盐哈希一般会有哪些不正确的措施。

错误的方法:短盐值和盐值复用

最多见的错误,是屡次哈希加密使用相同的盐值,或者盐值过短。

 盐值复用( SaltReuse)

一个常见的错误是每次都使用相同的盐值进行哈希加密,这个盐值要么被硬编码到程序里,要么只在第一次使用时随机得到。这样的作法是无效的,由于若是两个用户有相同的密码,他们仍然会有相同的哈希值。***者仍然可使用反向查表法对每一个哈希值进行字典***。他们只是在哈希密码以前,将固定的盐值应用到每一个猜想的密码就能够了。若是盐值被硬编码到一个流行的软件里,那么查询表和彩虹表能够内置该盐值,以使其更容易破解它产生的哈希值。

用户建立账号或者更改密码时,都应该用新的随机盐值进行加密。

 短盐值( ShortSlat)

若是盐值过短,***者能够预先制做针对全部可能的盐值的查询表。例如,若是盐值只有三个 ASCII 字符,那么只有 95x95x95=857,375种可能性。这看起来不少,但若是每一个查询表包含常见的密码只有 1MB,857,375个盐值总共只需837GB,一块时下不到100美圆的 1TB硬盘就能解决问题了。

出于一样的缘由,不该该将用户名用做盐值。对每个服务来讲,用户名是惟一的,但它们是可预测的,而且常常重复应用于其余服务。***者能够用常见用户名做为盐值来创建查询表和彩虹表来破解密码哈希。

为使***者没法构造包含全部可能盐值的查询表,盐值必须足够长。一个好的经验是使用和哈希函数输出的字符串等长的盐值。例如, SHA256 的输出为256位(32字节),因此该盐也应该是32个随机字节。

 双重哈希和古怪的哈希函数

本节将介绍另外一种常见的密码哈希的误解:古怪哈希的算法组合。人们很容易冲昏头脑,尝试不一样的哈希函数相结合一块儿使用,但愿让数据会更安全。但在实践中,这样作并无什么好处。它带来了函数之间互通性的问题,并且甚至可能会使哈希变得更不安全。永远不要试图去创造你本身的哈希加密算法,要使用专家设计好的标准算法。有人会说,使用多个哈希函数会下降计算速度,从而增长破解的难度。可是使破解过程变慢还有更好的办法,咱们将在后面讲到。

下面是在网上见过的古怪的哈希函数组合的一些例子。

·        md5(sha1(password))

·        md5(md5(salt) + md5(password))

·        sha1(sha1(password))

·        sha1(str_rot13(password + salt))

·        md5(sha1(md5(md5(password) + sha1(password)) +md5(password)))

不要使用其中任何一种。

注意:此部分是有争议的。我收到了一些电子邮件,他们认为古怪的哈希函数是有意义的,理由是,若是***者不知道系统使用哪一个哈希函数,那么***者就不太可能预先计算出这种古怪的哈希函数彩虹表,因而破解起来要花更多的时间。

当***者不知道哈希加密算法的时候,是没法发起***的。可是要考虑到柯克霍夫原则,***者一般会得到源代码(尤为是免费或者开源软件)。经过系统中找出密码-哈希值对应关系,很容易反向推导出加密算法。使用一个很难被并行计算结果的迭代算法(下面将予以讨论),而后增长适当的盐值防止彩虹表***。

若是你真的想用一个标准的“古怪”的哈希函数,如 HMAC ,亦无不可。可是,若是你目的是想下降哈希计算速度,那么能够阅读下面有关密钥扩展的部分。

若是创造新的哈希函数,可能会带来风险,构造希函数的组合又会致使函数互通性的问题。它们带来一点的好处和这些比起来微不足道。很显然,最好的办法是,使用标准、通过完整测试的算法。

 哈希碰撞( HashCollisions)

因为哈希函数将任意大小的数据转化为定长的字符串,所以,一定有一些不一样的输入通过哈希计算后获得了相同的字符串的状况。加密哈希函数( Cryptographic hash function)的设计初衷就是使这些碰撞尽可能难以被找到。如今,密码学家发现***哈希函数愈来愈容易找到碰撞了。最近的例子是MD5算法,它的碰撞已经实现了。

碰撞***是指存在一个和用户密码不一样的字符串,却有相同的哈希值。然而,即便是像MD5这样的脆弱的哈希函数找到碰撞也须要大量的专门算力( dedicatedcomputing power),因此在实际中“意外地”出现哈希碰撞的状况不太可能。对于实用性而言,加盐MD5 和加盐 SHA256 的安全性同样。尽管如此,可能的话,要使用更安全的哈希函数,好比 SHA256 、 SHA512 、RipeMD 或 WHIRLPOOL 。

如何正确进行哈希加密

本节介绍了究竟应该如何对密码进行哈希加密。第一部分介绍基础知识,这部分是必须的。后面阐述如何在这个基础上加强安全性,使哈希加密变得更难破解。

 基础知识:加盐哈希(Hashing with Salt)

咱们已经知道,恶意***者使用查询表和彩虹表,破解普通哈希加密有多么快。咱们也已经了解到,使用随机加盐哈希能够解决这个问题。可是,咱们使用什么样的盐值,又如何将其混入密码中?

盐值应该使用加密的安全伪随机数生成器( Cryptographically Secure Pseudo-Random Number Generator,CSPRNG )产生。CSPRNG和普通的伪随机数生成器有很大不一样,如“ C ”语言的rand()函数。顾名思义, CSPRNG 被设计成用于加密安全,这意味着它能提供高度随机、彻底不可预测的随机数。咱们不但愿盐值可以被预测到,因此必须使用 CSPRNG 。下表列出了一些当前主流编程平台的 CSPRNG 方法。

Platform

CSPRNG

PHP

mcrypt_create_iv,  openssl_random_pseudo_bytes

Java

java.security.SecureRandom

Dot NET (C#, VB)

System.Security.Cryptography.RNGCryptoServiceProvider

Ruby

SecureRandom

Python

os.urandom

Perl

Math::Random::Secure

C/C++ (Windows API)

CryptGenRandom

Any language on GNU/Linux or Unix

Read from /dev/random or /dev/urandom

每一个用户的每个密码都要使用独一无二的盐值。用户每次建立账号或更改密码时,密码应采用一个新的随机盐值。永远不要重复使用某个盐值。这个盐值也应该足够长,以使有足够多的盐值能用于哈希加密。一个经验规则是,盐值至少要跟哈希函数的输出同样长。该盐应和密码哈希一块儿存储在用户账号表中。

存储密码的步骤:

1.   使用CSPRNG 生成足够长的随机盐值。

2.   将盐值混入密码,并使用标准的密码哈希函数进行加密,如Argon二、 bcrypt 、scrypt 或 PBKDF2 。

3.   将盐值和对应的哈希值一块儿存入用户数据库。

校验密码的步骤:

1.   从数据库检索出用户的盐值和对应的哈希值。

2.   将盐值混入用户输入的密码,而且使用通用的哈希函数进行加密。

3.   比较上一步的结果,是否和数据库存储的哈希值相同。若是它们相同,则代表密码是正确的;不然,该密码错误。

 在 Web 应用中,永远在服务端上进行哈希加密

若是您正在编写一个 Web 应用,你可能会疑惑究竟在哪里进行哈希加密,是在用户的浏览器上使用JavaScript 对密码进行哈希加密呢,仍是将明文发送到服务端上再进行哈希加密呢?

就算浏览器上已经用JavaScript哈希加密了,但你你仍是要在服务端上将获得的密码哈希值再进行一次哈希加密。试想一个网站,将用户在浏览器输入的密码通过哈希加密,而不是在传送到服务端再进行哈希。为了验证用户,这个网站将接受来自浏览器的哈希值,并和数据库中的哈希值进行匹配便可。由于用户的密码从未明文传输到服务端,这样子看上去更安全,但事实并不是如此。

问题是,从客户端的角度来看,通过哈希的密码,从逻辑上成为用户的密码了。全部用户须要作的认证就是将它们的密码哈希值告诉服务端。若是一个***者获得了用户的哈希值,他们能够用它来经过认证,而没必要知道用户的明文密码!因此,若是***者使用某种手段拖了网站的数据库,他们就能够随意使用每一个人的账号直接访问,而无需猜想任何密码。

这并非说你不该该在浏览器进行哈希加密,可是若是你这样作了,你必定要在服务端上再进行一次哈希加密。在浏览器中进行哈希加密无疑是一个好主意,但实现的时候要考虑如下几点:

·        客户端密码哈希加密不是 HTTPS(SSL/TLS)的替代品。若是浏览器和服务端之间的链接是不安全的,那么中间人***能够修改 JavaScript 代码,删除加密函数,从而获取用户的密码。

·        某些浏览器不支持 JavaScript ,还有一些用户在浏览器中禁用 JavaScript 功能。所以,为了更好的兼容性,您的应用应该检测浏览器是否支持 JavaScript ,若是不支持,就须要在服务端模拟客户端进行哈希加密。

·        客户端的哈希加密一样须要加盐。显而易见的解决方案是使客户端脚本向服务端请求用户的盐值。可是不提倡这样作,由于它可让***者可以在不知道密码的状况下检测用户名是否有效。既然你已经在服务端上对密码进行了加盐哈希(使用合格的盐值),那么在客户端,将用户名(或邮箱)加上网站特有的字符串(如域名)做为客户端的盐值也是可行的。

 使密码更难破解:慢哈希函数( Slow Hash Function)

加盐能够确保***者没法使用像查询表和彩虹表***那样对大量哈希值进行破解,但依然不能阻止他们使用字典***或暴力***。高端显卡( GPU )和定制的硬件每秒能够进行十亿次哈希计算,因此这些***仍是颇有效的。为了下降使这些***的效率,咱们可使用一个叫作密钥扩展( key stretching)的技术。

这样作的初衷是为了将哈希函数变得很是慢,即便有一块快速的 GPU 或定制的硬件,字典***和暴力***也会慢得使人失去耐心。终极目标是使哈希函数的速度慢到足以令***者放弃,但由此形成的延迟又不至于引发用户的注意。

密钥扩展的实现使用了一种 CPU 密集型哈希函数( CPU-intensive hash function)。不要试图去创造你本身的迭代哈希加密函数。迭代不够多的话,它能够被高效的硬件快速并行计算出来,就跟普通的哈希同样。要使用标准的算法,好比 PBKDF2 或 bcrypt 。你能够在这里找到 PBKDF2 在 PHP 上的实现。

这类算法采起安全因子或迭代次数做为参数。此值决定哈希函数将会如何缓慢。对于桌面软件或智能手机应用,肯定这个参数的最佳方式是在设备上运行很短的性能基准测试,找到使哈希大约花费半秒的值。经过这种方式,程序能够尽量保证安全而又不影响用户体验。

若是您想在一个 Web 应用使用密钥扩展,须知你须要额外的计算资源来处理大量的身份认证请求,而且密钥扩展也容易让服务端遭受拒绝服务***( DoS )。尽管如此,我仍是建议使用密钥扩展,只不过要设定较低一些的迭代次数。这个次数须要根据本身服务器的计算能力和预计每秒须要处理的认证请求次数来设置。消除拒绝服务的威胁能够经过要求用户每次登录时输入验证码( CAPTCHA )来作到。系统设计时要将迭代次数可随时方便调整。

若是你担忧计算带来负担,但又想在 Web 应用中使用密钥扩展,能够考虑在浏览器中使用 JavaScript 完成。斯坦福大学的 JavaScript 加密库就包含了 PBKDF2 的实现。迭代次数应设置足够低,以适应速度较慢的客户端,如移动设备。同时,若是用户的浏览器不支持 JavaScript ,服务端应该接手进行计算。客户端密钥扩展并不能免除服务端端进行哈希加密的须要。你必须对客户端生成的哈希值再次进行哈希加密,就跟普通口令的处理同样。

 不可能破解的哈希加密:密钥哈希和密码哈希设备

只要***者可使用哈希来检查密码的猜想是对仍是错,那么他们能够进行字典***或暴力***。下一步是将密钥( secret key)添加到哈希加密,这样只有知道密钥的人才能够验证密码。有两种实现的方式,使用ASE算法对哈希值加密;或者使用密钥哈希算法 HMAC 将密钥包含到哈希字符串中。

实现起来并没那么容易。这个密钥必须在任何状况下,即便系统由于漏洞被攻陷,也不能被***者获取。若是***者彻底进入系统,密钥无论存储在何处,总能被找到。所以,密钥必须密钥必须被存储在外部系统,例如专用于密码验证一个物理上隔离的服务端,或者链接到服务端,例如一个特殊的硬件设备,如 YubiHSM 。

我强烈建议全部大型服务(超过10万用户)使用这种方式。我认为对于任何超过100万用户的服务托管是很是有必要的。

若是您难以负担多个服务端或专用硬件的费用,依然有办法在标准的Web服务端上使用密钥哈希技术。大多数数据库被***是因为 SQL 注入***,所以,不要给***者进入本地文件系统的权限(禁止数据库服务访问本地文件系统,若是有此功能的话)。若是您生成一个随机密钥并将其存储在一个经过 Web 没法访问的文件上,而后进行加盐哈希加密,那么获得的哈希值就不会那么容易被破解了,就算数据库已经遭受注入***,也是安全的。不要将密钥硬编码到代码中,应该在安装应用时随机生成。这么作并不像使用一个独立的系统那样安全,由于若是 Web 应用存在 SQL 注入点,那么有可能存在其余一些问题,如本地文件包含漏洞( Local File Inclusion ),***者能够利用它读取本地密钥文件。不管如何,这个措施总比没有好。

请注意,密钥哈希并不意味着无需进行加盐。高明的***者最终会千方百计找到密钥,所以,对密码哈希仍然须要进行加盐和密钥扩展,这一点很是重要。

 其余安全措施

密码哈希仅仅在安全受到破坏时保护密码。它并不能使整个应用更加安全。首先有不少事必须完成,来保证密码哈希值(和其余用户数据)不被窃取。

即便是经验丰富的开发人员也必须学习安全知识,才能编写安全的应用。此处有关于Web应用漏洞的重要资源: The Open Web ApplicationSecurity Project (OWASP)。还有一个很好的介绍: OWASP Top TenVulnerability List 。除非你理解了列表中的全部漏洞,不然不要去尝试编写一个处理敏感数据的Web应用程序。雇主也有责任确保全部开发人员在安全应用开发方面通过充分的培训。

对您的应用进行第三方“***测试”是一个很好的主意。即便最好的程序员也可能会犯错,因此,让安全专家审计代码寻找潜在的漏洞是有意义的。找一个值得信赖的机构(或招聘人员)来按期审计代码。安全审计应该从开发初期就着手进行,并贯穿整个开发过程。

监控您的网站来发现***行为也很重要。我建议至少雇用一名全职人员负责监测和处理安全漏洞。若是某个漏洞没被发现,***者可能经过网站利用恶意软件感染访问者,所以,检测漏洞并及时处理是极为重要的。

常见疑问 我应该使用什么样的哈希算法?

可使用:

·        精心设计的密钥扩展算法如 PBKDF2 、bcrypt 和scrypt 。

·        OpenWall的的 Portable PHP password hashing framework。

·        PBKDF2在PHP、C#、Java和Ruby的实现。

·        crypt 的安全版本。

不可以使用:

·        快速加密哈希函数,如 MD5 、SHA一、SHA25六、SHA5十二、RipeMD、WHIRLPOOL、SHA3等。

·        crypt()的不安全版本。

·        任何本身设计的加密算法。只应该使用那些在公开领域中的、由经验丰富的密码学家完整测试过的技术。

尽管目前尚未一种针对MD5或SHA1很是高效的***手段,但它们过于古老以致于被普遍认为不足以用来存储密码(可能有些不恰当)。因此我不推荐使用它们。可是也有例外,PBKDF2中常用SHA1做为它底层的哈希函数。

 当用户忘记密码时如何重置密码?

这是我我的的观点:当下全部普遍使用的密码重置机制都是不安全的。若是你对高安全性有要求,如加密服务,那么就不要让用户重设密码。

大多数网站向那些忘记密码的用户发送电子邮件来进行身份认证。要作到这一点,须要随机生成一个一次性使用的令牌( token ),直接关联到用户的账号。而后将这个令牌混入一个重置密码的连接中,发送到用户的电子邮箱。当用户点击包含有效令牌的密码重置连接,就提示他们输入新密码。确保令牌只对一个账号有效,以防***者从邮箱获取到令牌后用来重置其余用户的密码。

令牌必须在15分钟内使用,且一旦使用后就当即做废。当用户登陆成功时(代表还记得本身的密码), 或者从新请求令牌时,使原令牌失效是一个好作法。若是令牌永不过时,那么它就能够一直用于***用户的帐号。电子邮件(SMTP)是一个纯文本协议,网络上有不少恶意路由在截取邮件信息。在用户修改密码后,那些包含重置密码连接的邮件在很长时间内缺少保护,所以,尽早使令牌尽快过时,来下降用户信息暴露给***者的风险。

***者可以篡改令牌,所以不要把账号信息和失效时间存储在其中。它们应该以不可猜想的二进制形式存在,而且只用来识别数据库中某条用户的记录。

千万不要经过电子邮件向用户发送新密码。记得在用户重置密码时随机生成一个新的盐值用来加密,不要重复使用已用于密码哈希加密的旧盐值。

 若是账号数据库被泄漏或***,应该怎么作?

你的首要任务是,肯定系统被暴露到什么程度,而后修复***者利用的的漏洞。若是你没有应对***的经验,我强烈建议聘请第三方安全公司来作这件事。

捂住一个漏洞并期待没人知道,是否是很省事,又诱人?可是这样作只会让你的处境变得更糟糕,由于你在用户不知情的状况下,将它们的密码和我的信息置于暴露风险之中。就算你尚未彻底发生什么事情时,你也应该尽快通知用户。例如在首页放置一个连接,指向对此问题更为详细的说明;若是可能的话经过电子邮件发送通知给每一个用户告知目前的状况。

向用户说明他们的密码到底是如何被保护的:最好是使用了加盐哈希。可是,即便用了加盐哈希,恶意***仍然可使用字典***和暴力***。若是用户在不少服务使用相同的密码,恶意***会利用他们找到的密码去尝试登录其余网站。告知用户这个风险,建议他们修改全部相似的密码,不论密码用在哪一个服务上。强制他们下次登陆你的网站时更改密码。大多数用户会尝试“修改”本身的密码为原始密码,以便记忆。您应该使用当前密码哈希值以确保用户没法作到这一点。

就算有加盐哈希的保护,也存在***者快速破解其中一些弱口令密码的可能性。为了减小***者使用这些密码的机会,应该对这些密码的账号发送认证电子邮件,直到用户修改了密码。可参考前面提到的问题:当用户忘记密码时如何重置密码?这其中有一些实现电子邮件认证的要点。

另外告诉你的用户,网站存储了哪些我的信息。若是您的数据库包括信用卡号码,您应该通知用户仔细检查近期帐单并销掉这张信用卡。

 应该使用什么样的密码策略?是否应该使用强密码?

若是您的服务没有严格的安全要求,那么不要对用户进行限制。我建议在用户输入密码时,页面显示出密码强度,由他们本身决定须要多安全的密码。若是你有特殊的安全需求,那就应该实施长度至少为12个字符的密码,而且至少须要两个字母、两个数字和两个符号。

不要过于频繁地强制你的用户更改密码,最多每半年一次,超过这个次数,用户就会感到疲劳。相反,更好的作法是教育用户,当他们感受密码可能泄露时主动修改,而且提示用户不要把密码告诉任何人。若是这是一个商业环境,鼓励员工利用工做时间熟记并使用他们的密码。

 若是***者***了数据库,他不能直接替换哈希值登录任意账号么?

是的,但若是有人***您的数据库,他们极可能已经可以访问您的服务端上的全部内容,这样他们就不须要登陆到您的账号,就能够得到他们想要的东西。密码哈希(对网站而言)的目的不是为了保护被***的网站,而是在***已经发生时保护数据库中的密码。

你能够经过给数据库链接设置两种权限,防止密码哈希在遭遇注入***时被篡改。一种权限用于建立用户,一种权限用于用户登录。“建立用户”的代码应该可以读写用户表;但“用户登录”的代码应该只可以读取用户表而不能写入。

 为何要使用一种像HMAC的特殊算法,而不是只将密钥混入密码?

如MD五、SHA一、SHA2 和 Hash 函数使用 Merkle–Damgrd,这使得它们很容易受到所谓的长度扩展***( length extension attack)。意思是给定的哈希值 H(X),对于任意的字符串 Y,***者能够计算出 H(pad(X)+Y) 的值,而无需知道 X 的值。其中, pad(X) 是哈希函数的填充函数。

这意味着,***者不知道密钥的状况下,仍然能够根据给定的哈希值 H(key+message) 计算出H(pad(key+message)+extension) 。若是该哈希值用于身份认证,并依靠其中的密钥来防止***者篡改消息,这方法已经行不通。由于***者无需知道密钥也能构造出包含 message+extension 的一个有效的哈希值。

目前尚不清楚***者如何利用这种***来快速破解密码哈希。然而,因为这种***的出现,不建议使用普通的哈希函数对密钥进行哈希加密。未来也许某个高明的密码学家有一天发现利用长度扩展***的新思路,从而更快的破解密码,因此仍是使用 HMAC 为好。

 盐值应该加到密码以前仍是以后?

无所谓,选择一个并保持风格一致便可,以避免出现互操做方面的问题。盐值加到密码以前较为广泛。

 为什么本文的哈希代码都以固定时间比较哈希值?

使用固定的时间来比较哈希值能够防止***者在在线系统使用基于时间差的***,以此获取密码的哈希值,而后进行本地破解。

比较两个字节序列(字符串)是否相同的标准作法是,从第一个字节开始,每一个字节逐一顺序比较。只要发现某个字节不一样,就能够知道它们是不一样的,当即返回false。若是遍历整个字符串没有找到不一样的字节,能够确认两个字符串就是相同的,能够返回true。这意味着比较两个字符串,若是它们相同的长度不同,花费的时间不同。开始部分相同的长度越长,花费的时间也就越长。

例如,字符串 “XYZABC” 和 “abcxyz” 的标准比较,会当即看到,第一个字符是不一样的,就不须要检查字符串的其他部分。相反,当字符串“aaaaaaaaaaB” 和 “aaaaaaaaaaZ” 进行比较时,比较算法就须要遍历最后一位前全部的“a” ,而后才能知道他们是不一样的。

假设***者试图***一个在线系统,这个系统限制了每秒只能尝试一次用户认证。还假设***者已经知道密码哈希全部的参数(盐值、哈希函数的类型等),除了密码的哈希值和密码自己。若是***者能精确测量在线系统耗时多久去比较他猜想的密码和真实密码,那么他就能使用时序***获取密码的哈希值,而后进行离线破解,从而绕过系统对认证频率的限制。

首先***者准备256个字符串,它们的哈希值的第一字节包含了全部可能的状况。他将每一个字符串发送给在线系统尝试登录,并记录系统响应所消耗的时间。耗时最长的字符串就是第一字节相匹配的。***者知道第一字节后,并能够用一样的方式继续猜想第二字节、第三字节等等。一旦***者得到足够长的哈希值片断,他就能够在本身的机器上来破解,不受在线系统的限制。

在网络上进行这种***彷佛不可能。然而,有人已经实现了,并已证实是实用的。这就是为何本文提到的代码,它利用固定时间去比较字符串,而无论有多大的字符串。

 “慢比较(slowequals)”函数如何工做?

前一个问题解释了为何“慢比较”是必要的,如今来解释代码如何工做。

private staticboolean slowEquals(byte[] a, byte[] b)

     {

         int diff =a.length ^ b.length;

         for(int i = 0; i< a.length && i < b.length; i++)

             diff|= a[i] ^ b[i];

         return diff == 0;

     }

该代码使用异或运算符“^”来比较两个整数是否相等,而不是“==”运算符。下面解释缘由。当且仅当两位相等时,异或的结果将是零。这是由于:0 XOR 0 = 0,1 XOR 1 = 0,0 XOR 1 = 1,1 XOR 0 = 1若是咱们将其应用到整数中每一位,当且仅当字节两个整数各位都相等,结果才是0。

因此,在代码的第一行中,若是a.length等于b.length ,相同的话获得0,否者获得非零值。而后使用异或比较数组中各字节,而且将结果和diff求或。若是有任何一个字节不相同,diff就会变成非零值。由于或运算没有“置0”的功能,因此循环结束后diff是0的话只有一种可能,那就是循环前两个数组长度相等(a.length == b.length),而且数组中每个字节都相同(每次异或的结果都非0)。

咱们须要使用XOR,而不是“==”运算符比较整数的缘由是,“==”一般是编译成一个分支的语句。例如,C语言代码中“ diff &= a == b”可能编译如下x86汇编:

MOV EAX, [A]

CMP [B], EAX

JZ equal

JMP done

equal:

AND [VALID], 1

done:

AND [VALID], 0

其中的分支致使代码运行的时间不固定,决定于两个整数相等的程度和CPU内部的跳转预测机制(branch prediction)。

而C语言代码“diff |= a ^ b”会被编译为下面的样子,它执行的时间和两个变量是否相等无关。

MOV EAX,[A]

XOR EAX,[B]

OR [DIFF],EAX

 为什么要进行哈希?

用户在你的网站上输入密码,是由于他们相信你能保证密码的安全。若是你的数据库遭到******,而用户的密码又不受保护,那么恶意***能够利用这些密码尝试登录其余网站和服务(大多数用户会在全部地方使用相同的密码)。这不只仅关乎你网站的安全,更关系到用户的安全。你有责任负责用户的安全。

 原文由Defuse Security安全团队撰写

相关文章
相关标签/搜索