密码加密 | 青训营笔记

237 阅读5分钟

这是我参与「第三届青训营 -后端场」笔记创作活动的第6篇笔记。

由于参与青训营的抖音项目,在开发用户注册和登录时,需要将密码加密传入数据库,提升用户的安全性能,于是调研了密码加密的方法,并对选型和使用做了介绍。

攻击密码的主要方法

我们需要防御的两种主要的密码攻击方式是:

  • 字典攻击
  • 暴力攻击

它们的工作方式非常简单:使用预先生成的密码哈希列表并进行简单的比较,以找到创建所需哈希的字符串。对于未加盐的密码,可以下载大量预先生成的密码列表。然后只需要简单查找即可。这种列表称被为彩虹表

如果密码使用了盐,但没有为每个密码使用唯一的盐,那么攻击者要做的就是手动生成彩虹表,对每个组合使用盐,然后进行查找。如果你对每个密码使用唯一的盐,则攻击者需要针对每个密码为每个组合生成一个列表再去查找。这是另外一种类型的攻击,即暴力攻击

暴力攻击通过反复尝试猜测密码来破解密码。与慢速哈希(例如bcryptscryptpbkdf2)不同,MD5SHA-1(或SHA-256SHA-512等)是消息摘要算法,它们旨在快速验证给定消息是否未被篡改。

MD5SHA-1不推荐使用的原因:

一个常见的示例是验证下载的文件。下载文件后,在文件上运行MD5,然后将哈希与原始站点提供的哈希进行比较验证文件是否被篡改过。所以MD5SHA-1这些算法不是为密码设计的。因为这些算法的哈希速度会随着硬件计算能力的提升一起提升,如果你使用了这些算法做密码哈希,那么不论密码加没加盐,加盐后做了几次哈希,破解密码的的时间都会随着计算机硬件的提升越来越短。

在2012年的演讲中,Jeremi M. Gosney使用商业级硬件测试比对了各种哈希算法的性能。该测试使用五台服务器,并带有18个消费级显卡:

  • 10x HD 7970
  • 4x HD 5970 (dual GPU)
  • 3x HD 6990 (dual GPU)
  • 1x HD 5870

得出的结果如下:

image.png

鉴于硬件的进步速度,我们应该期望今天使用相同的硬件的花费会大大降低,或者使用今天的硬件,性能会比2012年时提高大约6-8倍。

上面的图表里的结果显示如果你(2012年)仍在使用未加盐(或非唯一)的MD5哈希值作为密码,那么2012年使用此配置硬件的的攻击者每秒可以进行180,000,000,000次哈希。即使是最复杂的SHA算法(SHA-512),其速度比MD5慢49,000,000倍,但每秒也可以计算36.4万次哈希。

另一方面,bcrypt每秒仅能执行71,000次哈希。这比MD5慢253,500,000%,比SHA-1慢88,700,000%,甚至比SHA-512慢412%。

很多系统中在存储用户的密码前会对密码加盐并增加密码哈希的次数,以减慢密码暴力攻击的速度。例如执行五次MD5迭代,但即使是这样仍然只能使尝试破解次数减少到每秒36,000,000,000次,是单个SHA-1的两倍,并且仍然比SHA-512快99.99898%,而使用bcrypt则要慢443,000,000%。所以bcrypt是一种很好的散列密码解决方案,可以有效地防止暴力破解和字典攻击。

使用bcrpyt哈希用户密码

bcrypt哈希字符串的组成

bcrypt哈希由多个部分组成。这些部分用于确定创建哈希的设置,从而可以在不需要任何其他信息的情况下对其进行验证。

image.png

上图是一个bcrypt哈希的示例图,其由四部分组成:

  • Prefix说明了使用的bcrypt的版本
  • Cost是进行哈希的次数-数字越大生成bcrypt的速度越慢,成本越大。同样也意味着如果密码库被盗,攻击者想通过暴力破解的方法猜测出用户密码的成本变得越昂贵。
  • Salt是添加到要进行哈希的字符串中的随机字符(21.25个字符),所以使用bcrypt时不需要我们在表里单独存储Salt
  • Hashed Text是明文字符串最终被bcrypt应用这些设置哈希后的哈希文本。

另外无论什么方法:每个密码加单独的盐进行哈希,使用bcrypt进行哈希等等,如果用户使用非常简单的密码例如password123456,还是能被猜测出来的,所以在用户设置密码时应该禁止他们输入简单的密码。

Go语言使用bcrypt

bcrypt的原理和实现都非常复杂,不过常用的编程语言都有实现bcrypt的包让我们直接使用,在Go语言里是通过golang.org/x/crypto/bcrypt包提供bcrypt相关功能给开发者使用的。

接下来我们在http_demo项目里演示一下使用bcrypt做密码哈希和验证的方法,首先我们需要安装一下bcrypt

$ go get golang.org/x/crypto/bcrypt

bcrypt包只提供了三个函数:

  • CompareHashAndPassword 用于比对bcrypt哈希字符串和提供的密码明文文本是否匹配。
  • GenerateFromPassword以给定的Cost返回密码的bcrypt哈希。如果给定的成本小于MinCost,则将成本设置为DefaultCost(10)。
  • Cost返回用于创建给定bcrypt哈希的哈希成本。将来密码系统为了应对更大的计算能力而增加哈希成本时,该功能可以用于确定哪些密码需要更新。

我们创建一个处理请求的Handler程序,演示bcrypt库三个函数的功能

// ./handler/password_hashing.go
package handler

import (
    "fmt"
    "golang.org/x/crypto/bcrypt"
    "net/http"
)


func HashPassword(password string) (string, error) {
    bytes, err := bcrypt.GenerateFromPassword([]byte(password), 14)
    return string(bytes), err
}

func CheckPasswordHash(password, hash string) bool {
    err := bcrypt.CompareHashAndPassword([]byte(hash), []byte(password))
    return err == nil
}

func GetHashingCost(hashedPassword []byte) int {
    cost, _ := bcrypt.Cost(hashedPassword) // 为了简单忽略错误处理
    return cost
}

func PassWordHashingHandler(w http.ResponseWriter, r *http.Request) {
    password := "secret"
    hash, _ := HashPassword(password) // 为了简单忽略错误处理

    fmt.Fprintln(w,"Password:", password)
    fmt.Fprintln(w, "Hash:    ", hash)

    match := CheckPasswordHash(password, hash)
    fmt.Fprintln(w,"Match:   ", match)

    cost := GetHashingCost([]byte(hash))
    fmt.Fprintln(w,"Cost:    ", cost)

}

增加Handler程序的路由:

func RegisterRoutes(r *mux.Router) {
  ...
  indexRouter := r.PathPrefix("/index").Subrouter()
  indexRouter.HandleFunc("/password_hashing", handler.PassWordHashingHandler)
  ...
}

重启http_demo服务器后访问http://localhost:8000/index/password_hashing即可得到如下结果:

Password: secret
Hash:     $2a$14$Ael8nW7UF/En/iI7LGdyBuaIO8VREbL2CAShRN0EUQHqtmOHXh.XK
Match:    true
Cost:     14