密码存储中MD5的安全问题与替代方案

开发
经过各种安全事件后,很多系统在存放密码的时候不会直接存放明文密码了,大都改成了存放了 md5 加密(hash)后的密码,可是这样真的安全吗?

密码存储中MD5的安全问题与替代方案

md5安全吗?有多么地不安全?如何才能安全地存储密码?…

md5安全吗?

经过各种安全事件后,很多系统在存放密码的时候不会直接存放明文密码了,大都改成了存放了 md5 加密(hash)后的密码,可是这样真的安全吗?

这儿有个脚本来测试下MD5的速度, 测试结果:

  1. [root@f4d5945f1d7c tools]# php speed-of-md5.php 
  2. Array 
  3.     [rounds] => 100 
  4.     [times of a round] => 1000000 
  5.     [avg] => 0.23415904045105 
  6.     [max] => 0.28906106948853 
  7.     [min] => 0.21188998222351 
  8.  

有没有发现一个问题:MD5速度太快了,导致很容易进行暴力破解.

简单计算一下:

  1. > Math.pow(10, 6) / 1000000 * 0.234 
  2. 0.234 
  3. > Math.pow(36, 6) / 1000000 * 0.234 / 60 
  4. 8.489451110400001 
  5. > Math.pow(62, 6) / 1000000 * 0.234 / 60 / 60 
  6. 3.69201531296  
  1. 使用6位纯数字密码,破解只要0.234秒!
  2. 使用6位数字+小写字母密码,破解只要8.49分钟!
  3. 使用6位数字+大小写混合字母密码,破解只要3.69个小时!

当然,使用长一点的密码会显著提高破解难度:

  1. > Math.pow(10, 8) / 1000000 * 0.234 
  2. 23.400000000000002 
  3. > Math.pow(36, 8) / 1000000 * 0.234 / 60 / 60 / 24 
  4. 7.640505999359999 
  5. > Math.pow(62, 8) / 1000000 * 0.234 / 60 / 60 / 24 / 365 
  6. 1.6201035231755982  
  1. 使用8位纯数字密码,破解要23.4秒!
  2. 使用8位数字+小写字母密码,破解要7.64小时!
  3. 使用8位数字+大小写混合字母密码,破解要1.62年!

但是,别忘了,这个速度只是用PHP这个解释型语言在笔者的弱鸡个人电脑(i5-4460 CPU 3.20GHz)上跑出来的,还只是利用了一个线程一个CPU核心。若是放到最新的 Xeon E7 v4系列CPU的服务器上跑,充分利用其48个线程,并使用C语言来重写下测试代码,很容易就能提升个几百上千倍速度。那么即使用8位数字+大小写混合字母密码,破解也只要14小时!

更何况,很多人的密码都是采用比较有规律的字母或数字,更能降低暴力破解的难度… 如果没有加盐或加固定的盐,那么彩虹表破解就更easy了…

那么如何提升密码存储的安全性呢?bcrypt!

提升安全性就是提升密码的破解难度,至少让暴力破解难度提升到攻击者无法负担的地步。(当然用户密码的长度当然也很重要,建议至少8位,越长越安全)

这里不得不插播一句:PHP果然是世界上最好的语言 — 标准库里面已经给出了解决方案。

PHP 5.5 的版本中加入了 password_xxx 系列函数, 而对之前的版本,也有兼容库可以用:password_compat.

在这个名叫“密码散列算法”的核心扩展中提供了一系列简洁明了的对密码存储封装的函数。简单介绍下:

  1. password_hash 是对密码进行加密(hash),目前默认用(也只能用)bcrypt算法,相当于一个加强版的md5函数
  2. password_verify 是一个验证密码的函数,内部采用的安全的字符串比较算法,可以预防基于时间的攻击, 相当于 $hashedPassword === md5($inputPassword)
  3. password_needs_rehash 是判断是否需要升级的一个函数,这个函数厉害了,下面再来详细讲

password_hash 需要传入一个算法,现在默认和可以使用的都只有bcrypt算法,这个算法是怎么样的一个算法呢?为什么PHP标准库里面会选择bcrypt呢?

bcrypt是基于 Blowfish 算法的一种专门用于密码哈希的算法,由 Niels Provos 和 David Mazieres 设计的。这个算法的特别之处在于,别的算法都是追求快,这个算法中有一个至关重要的参数:cost. 正如其名,这个值越大,耗费的时间越长,而且是指数级增长 — 其加密流程中有一部分是这样的:

  1. EksBlowfishSetup(cost, salt, key
  2.     state <- InitState() 
  3.     state <- ExpandKey(state, salt, key
  4.     repeat (2^cost)                         // "^"表示指数关系 
  5.         state <- ExpandKey(state, 0, key
  6.         state <- ExpandKey(state, 0, salt) 
  7.     return state  

比如下面是笔者的一次测试结果(个人弱机PC, i5-4460 CPU 3.20GHz) :

  1. cost       time 
  2.          8   0.021307 
  3.          9   0.037150 
  4.         10   0.079283 
  5.         11   0.175612 
  6.         12   0.317375 
  7.         13   0.663080 
  8.         14   1.330451 
  9.         15   2.245152 
  10.         16   4.291169 
  11.         17   8.318790 
  12.         18  16.472902 
  13.         19  35.146999  

附:测试代码

这个速度与md5相比简直是蜗牛与猎豹的差别 — 即使按照cost=8, 一个8位的大小写字母+数字的密码也要14万年才能暴力破解掉,更何况一般服务器都会至少设置为10或更大的值(那就需要54万年或更久了)。

显然,cost不是越大越好,越大的话会越占用服务器的CPU,反而容易引起DOS攻击。建议根据服务器的配置和业务的需求设置为10~12即可。最好同时对同一IP同一用户的登录尝试次数做限制,预防DOS攻击。

一个安全地存储密码的方案

总上所述,一个安全地存储密码的方案应该是这样子的:(直接放代码吧)

  1. class User extends BaseModel 
  2.     const PASSWORD_COST = 11; // 这里配置bcrypt算法的代价,根据需要来随时升级 
  3.     const PASSWORD_ALGO = PASSWORD_BCRYPT; // 默认使用(现在也只能用)bcrypt 
  4.  
  5.     /** 
  6.     * 验证密码是否正确 
  7.     * 
  8.     * @param string $plainPassword 用户密码的明文 
  9.     * @param bool  $autoRehash    是否自动重新计算下密码的hash值(如果有必要的话) 
  10.     * @return bool 
  11.     */ 
  12.     public function verifyPassword($plainPassword, $autoRehash = true
  13.     { 
  14.         if (password_verify($plainPassword, $this->password)) { 
  15.             if ($autoRehash && password_needs_rehash($this->password, self::PASSWORD_ALGO, ['cost' => self::PASSWORD_COST])) { 
  16.                 $this->updatePassword($plainPassword); 
  17.             } 
  18.  
  19.             return true
  20.         } 
  21.  
  22.         return false
  23.     } 
  24.  
  25.     /** 
  26.     * 更新密码 
  27.     * 
  28.     * @param string $newPlainPassword 
  29.     */ 
  30.     public function updatePassword($newPlainPassword) 
  31.     { 
  32.         $this->password = password_hash($newPlainPassword, self::PASSWORD_ALGO, ['cost' => self::PASSWORD_COST]); 
  33.         $this->save(); 
  34.     } 
  35.  

这样子,在用户注册或修改密码的时候就调用 $user->updatePassword() 来设置密码,而登录的时候就调用 $user->verifyPassword() 来验证下密码是否正确。

当硬件性能提升到一定程度,而cost=11无法满足安全需求的时候,则修改下 PASSWORD_COST 的值即可无缝升级,让存放的密码更安全。 

责任编辑:庞桂玉 来源: Android开发中文站
相关推荐

2015-03-23 11:21:08

2022-10-18 22:21:51

2009-11-03 13:46:56

Oracle密码

2016-12-15 09:26:53

MD5加密

2010-04-02 13:53:47

2021-12-06 18:16:14

SQLCRCMD5

2009-05-05 17:52:48

系统安全密码安全Windows

2009-04-27 10:39:47

视频会议存储安全

2019-06-14 05:00:05

2020-10-15 08:20:52

MD5算法加密的过程

2009-06-06 18:57:47

MD5加密类Java Bean

2010-05-04 17:43:50

Unix系统

2011-12-28 13:14:39

2010-03-16 09:27:31

2013-04-02 13:06:20

BYODBYOD安全

2012-05-30 16:25:30

密码安全

2012-11-20 10:47:16

2013-08-14 09:11:43

云数据存储云存储云安全

2011-07-14 14:21:11

2014-07-28 11:18:30

件测试云计算云测试
点赞
收藏

51CTO技术栈公众号