0%

如何正确存储密码

1. 哈希还是加密?

哈希(Hash)是将目标文本转换成具有相同长度的、不可逆的杂凑字符串(或叫做消息摘要)而加密(Encrypt)是将目标文本转换成具有不同长度的、可逆的密文。

哈希算法往往被设计成生成具有相同长度的文本,而加密算法生成的文本长度与明文本身的长度有关。哈希算法是不可逆的,而加密算法是可逆的。

哈希函数并不是专门用来设计存储用户密码的,不论如何,使用 MD5、MD5 加盐或者其他哈希的方式来存储密码都是不安全的.

使用加密的方式存储密码相比于哈希加盐的方式,在一些安全意识和能力较差的公司和网站反而更容易导致密码的泄露和安全事故。

哈希加盐的方式确实能够增加攻击者的成本,但是今天来看还远远不够,我们需要一种更加安全的方式来存储用户的密码,这也就是今天被广泛使用的慢哈希算法.

慢哈希算法是为哈希密码而专门设计的,所以它是一个执行相对较慢的算法, 自己计算起来都慢, 那么破解起来也会非常慢.

2. 破解哈希

  • 暴力枚举法:简单粗暴地枚举出所有原文,并计算出它们的哈希值,看看哪个哈希值和给定的信息摘要一致。

  • 字典法:黑客利用一个巨大的字典,存储尽可能多的原文和对应的哈希值。破解时通过密文直接反查明文。但存储一个这样的数据库,空间成本是惊人的。

  • 彩虹表(rainbow)法:在字典法的基础上改进,以时间换空间。是现在破解哈希常用的办法。

虽然彩虹表有着如此惊人的破解效率,但网站的安全人员仍然有办法防御彩虹表。最有效的方法就是“加盐”,即在密码的特定位置插入特定的字符串,这个特定字符串就是“盐(Salt)”,加盐后的密码经过哈希加密得到的哈希串与加盐前的哈希串完全不同,黑客用彩虹表得到的密码根本就不是真正的密码。即使黑客知道了“盐”的内容、加盐的位置,还需要对H函数和R函数进行修改,彩虹表也需要重新生成,因此加盐能大大增加利用彩虹表攻击的难度。

3. Bcrypt加密

Bcrypt内部自己实现了随机加盐处理。使用Bcrypt,每次加密后的密文是不一样的。对一个密码,Bcrypt每次生成的hash都不一样,那么它是如何进行校验的?

虽然对同一个密码,每次生成的hash不一样,但是hash中包含了salt(hash产生过程:先随机生成salt,salt跟password进行hash);

在下次校验时,从hash中取出salt,salt跟password进行hash;得到的结果跟保存在DB中的hash进行比对。

举个栗子,假如一个密文是 $2a$10$vI8aWBnW3fID.ZQ4/zo1G.q1lRps.9cGLcZEiGDMVr5yUP1KUOYTa, 那么通过 $ 分隔符我们可以得到下面三个信息:

  1. 2a 表示的是用于此次计算的 bcrypt 算法版本;
  2. 10 表示的是 log_rounds 值;
  3. vI8aWBnW3fID.ZQ4/zo1G.q1lRps.9cGLcZEiGDMVr5yUP1KUOYTa 是 salt 和加密文本的拼接值 (经过了 base 64 编码,前面 22 个字母是 salt 的十六进制值。

4. PBKDF2,Scrypt,Bcrypt 和 ARGON2对比

  • PBKDF2

PBKDF2 被设计的很简单,它的基本原理是通过一个伪随机函数(例如 HMAC 函数),把明文和一个盐值作为输入参数,然后按照设置的计算强度因子重复进行运算,并最终产生密钥。

这样的重复 hash 已经被认为足够安全,但也有人提出了不同意见,此类算法对于传统的 CPU 来说的确是足够安全,使用GPU阵列、或FPGA来破解PBKDF2仍相对容易。注意这里说的是相对,为了比较接下来提到的另外两种算法。

  • BCrypt

BCrypt 在1999年发明,由于使用GPU、FPGA的破解是基于它们相对于CPU的并行计算优势,因此BCrypt算法不仅设计为CPU运算密集,而且是内存IO密集。

然而随着时间迁移,目前新的FPGA已经集成了很大的RAM(类型CPU缓存、大约几十兆),解决了内存密集IO的问题。

  • Scrypt

Scrypt 于2009年产生,弥补了BCrypt的不足。它将CPU计算与内存使用开销提升了一个层次,不仅CPU运算需要指数时间开销,还需要指数内存IO开销。

  • Argon2

Argon2 有两个主要的版本:Argon2i 是对抗侧信道攻击的最安全选择,而 Argon2d 是抵抗 GPU 破解攻击的最安全选择。

在 2019 年,我建议你以后不要使用PBKDF2 或 BCrypt,并强烈建议将 Argon2(最好是 Argon2id)用于最新系统。

Scrypt 是当 Argon2 不可用时的不二选择,但要记住,它在侧侧信道泄露方面也存在相同的问题。

5. 代码实现

  • pbkdf2 不推荐

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    package main

    import (
    "crypto/sha256"
    "fmt"

    "golang.org/x/crypto/pbkdf2"
    )

    func main() {

    passwd := "levonfly"
    salt := "salt"

    res1 := pbkdf2.Key([]byte(passwd), []byte(salt), 10, 20, sha256.New)
    fmt.Println(string(res1)) //'J!85|LU@

    // 加密后一样
    res2 := pbkdf2.Key([]byte(passwd), []byte(salt), 10, 20, sha256.New)
    fmt.Println(string(res2)) //'J!85|LU@

    }
  • bcrypt 推荐

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    package main

    import (
    "fmt"

    "golang.org/x/crypto/bcrypt"
    )

    func main() {

    passwd := "levonfly"

    // cost默认是10,不要太小
    res1, _ := bcrypt.GenerateFromPassword([]byte(passwd), 10)
    fmt.Println(string(res1)) //$2a$10$Y85p96ZRD1Sa5iU7M/ngku9MIFNkmAwEI38FvPT9dj628E8hPOU0K

    // 加密结果不一样
    res2, _ := bcrypt.GenerateFromPassword([]byte(passwd), 10)
    fmt.Println(string(res2)) //$2a$10$7xUWgmWB3te5OipBYx4aheUFz7dCcj7JLIpQW6D/Me1R4qljEIFy2

    err1 := bcrypt.CompareHashAndPassword(res1, []byte(passwd))
    fmt.Println(err1) //nil

    err2 := bcrypt.CompareHashAndPassword(res1, []byte("random"))
    fmt.Println(err2) //crypto/bcrypt: hashedPassword is not the hash of the given password
    }
  • scrypt 推荐

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    package main

    import (
    "fmt"

    "golang.org/x/crypto/scrypt"
    )

    func main() {

    passwd := "levonfly"
    salt := []byte{0xc8, 0x28, 0xf2, 0x58, 0xa7, 0x6a, 0xad, 0x7b}

    res1, _ := scrypt.Key([]byte(passwd), salt, 1<<15, 8, 1, 32)
    fmt.Println(string(res1)) //TCoi[DRt;IALuw}

    res2, _ := scrypt.Key([]byte(passwd), salt, 1<<15, 8, 1, 32)
    fmt.Println(string(res2)) //TCoi[DRt;IALuw}
    }
  • argon2 推荐

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    package main

    import (
    "encoding/base64"
    "fmt"

    "golang.org/x/crypto/argon2"
    )

    func main() {

    passwd := "levonfly"
    salt := "salt"

    res1 := argon2.IDKey([]byte(passwd), []byte(salt), 3, 32, 4, 32)
    fmt.Println(base64.StdEncoding.EncodeToString(res1)) //uEZgAbCSfDyd8VAMbcmSSZKpH/TQ9hh9VsblPFGuDjM

    res2 := argon2.IDKey([]byte(passwd), []byte(salt), 3, 32, 4, 32)
    fmt.Println(base64.StdEncoding.EncodeToString(res2)) //uEZgAbCSfDyd8VAMbcmSSZKpH/TQ9hh9VsblPFGuDjM
    }

6. 数据库存储

如果存储慢哈希的密码, 一般都是存储定长的. 如char(60)

参考: https://stackoverflow.com/questions/247304/what-data-type-to-use-for-hashed-password-field-and-what-length/

7. 参考资料

给作者打赏,可以加首页微信,咨询作者相关问题!