掘金 后端 ( ) • 2024-04-14 11:04

前言

在系统开发中,为了保护用户的密码安全,不被恶意窃取,我们需要对密码进行加密和验证。本文将介绍Go语言如何使用bcrypt库来实现安全的密码加密。

首先,为什么要加密?试想,如果服务器的数据库被盗,那攻击者就可以拿着明文的密码进行登入篡改、信息泄露等。

一些基础

  • bcrypt: 是专门为密码存储而设计的算法,基于Blowfish加密算法变形而来,由Niels Provos和David Mazières发表于1999年的USENIX。
  • SALT值(盐值): 属于随机值.用户注册时,系统用来和用户密码进行组合而生成的随机数值,称作salt值,通称为加盐值。
  • 彩虹表: 预先计算出所有可能的hash值,比如6位密码的所有hash值(大小写加数字,只有62^6个)

加密过程与原理

当用户首次提供密码时(通常是注册时),由系统自动添加随机生成的salt值,然后再散列。而当用户登录时,系统为用户提供的代码撒上同样的加盐值,然后散列,再比较散列值,以确定密码是否正确。

为用户密码添加Salt值,使得加密的得到的密文更加冷僻,不宜查询。即使黑客有密文查询到的值,也是加了salt值的密码,而非用户设置的密码。salt值是随机生成的一组字符串,可以包括随机的大小写字母、数字、字符,位数可以根据要求而不一样。

bcrypt

bcrypt 包实现了 Provos 和 Mazières 的 bcrypt 自适应哈希算法。

加密过程与源码

  1. 生成盐值:在对密码进行哈希之前,需要生成一个随机的盐值。 源码参考:
unencodedSalt := make([]byte, maxSaltSize)
_, err = io.ReadFull(rand.Reader, unencodedSalt)
if err != nil {
    return nil, err
} 
  1. 使用生成的盐值和密码进行哈希运算。源码参考:
p.salt = base64Encode(unencodedSalt)
hash, err := bcrypt(password, p.cost, p.salt)
if err != nil {
    return nil, err
}
  1. 可选修改迭代次数来增加哈希函数的计算复杂度, 增加迭代次数会导致哈希函数的计算时间增加,从而增加攻击者破解密码的难度。源码参考:
func expensiveBlowfishSetup(key []byte, cost uint32, salt []byte) (*blowfish.Cipher, error) {
 csalt, err := base64Decode(salt)
 if err != nil {
    return nil, err
 }

 // Bug compatibility with C bcrypt implementations. They use the trailing
 // NULL in the key string during expansion.
 // We copy the key to prevent changing the underlying array.
 ckey := append(key[:len(key):len(key)], 0)

 c, err := blowfish.NewSaltedCipher(ckey, csalt)
 if err != nil {
    return nil, err
 }

 var i, rounds uint64
 rounds = 1 << cost
 for i = 0; i < rounds; i++ {
    blowfish.ExpandKey(ckey, c)
    blowfish.ExpandKey(csalt, c)
 }

 return c, nil
}

使用Bcrypt进行密码加密的一个例子:

first 安装

go get golang.org/x/crypto/bcrypt

Example:

package main

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

func main() {
    var (
       password = "123456"
    )

    hashedPassword, err := HashPassword(password)
    if err != nil {
       fmt.Println("Error hashing password:", err)
       return
    }

    fmt.Println("未加密:", password)
    fmt.Println("加密后:", hashedPassword)

    if err := CheckPasswordHash("202020", hashedPassword); err != nil {
       fmt.Println("密码不匹配:", err)
    } else {
       fmt.Println("密码匹配")
    }

    if err := CheckPasswordHash(password, hashedPassword); err != nil {
       fmt.Println("密码不匹配:", err)
    } else {
       fmt.Println("密码匹配!")
    }
}

func CheckPasswordHash(password, hash string) error {
    return bcrypt.CompareHashAndPassword([]byte(hash), []byte(password))
}

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

执行输出:

未加密: 123456
加密后: $2a$10$EvmU.zY9qGx7QntUFG9Qf.G6SRytkf8GMtFMsn1IYjnIMbZ6gUd4O
密码不匹配: crypto/bcrypt: hashedPassword is not the hash of the given password
密码匹配!

注意:

  • bcrypt接受最长密码的长度 72 字节,在使用时注意密码长度
  • 迭代次数 Cost 如果给定的成本小于 MinCost(4),则成本将改为 DefaultCos(10)

小结

为了保护用户的密码不被恶意攻击者窃取,如服务器数据库被盗、 碰撞、彩虹表等,可以通过使用Bcrypt算法加密。同时介绍了加密的过程和Go Bcrypt源码,包括生成随机盐值、安全哈希值和修改迭代次数。文中通过一个示例,演示了如何使用进行加密、校验的过程。

参考

  • Documentation https://pkg.go.dev/golang.org/x/crypto/bcrypt
  • 密码安全技术 https://zhuanlan.zhihu.com/p/379281284
  • Gin 框架之用户密码加密 https://juejin.cn/post/7324891390668636201?searchId=20240402095159E8CCCDF45C4004809A10
  • 为什么加盐值再加密能降低彩虹表攻击? https://www.zhihu.com/question/602069900