使用 bcrypt 函数生成密码

在几年前,相信很多和我一样的开发者都是使用 MD5 函数对用户的密码等敏感内容进行哈希化后存储到数据库中。即便是现在,还是很多开发者是这样的做法。

但很多事实告诉我们,如今用 MD5 函数生成的值在基于 *彩虹表🌈 *和强大的 GPU 数亿次每秒的暴力破解下能较为轻松的破解。

所以对于需要保存用户密码等敏感信息的需求场景下,我们需要寻找另一种可靠安全的加密方式。

有关为什么 MD5 已经不可靠的原因可以参考我的另一篇文章《十万个为什么:别用 MD5 加密密码》。

当下推荐的方案是使用 bcrypt 函数生成密码,并不是因为它绝对安全不可破解,而是破解的成本足够高。

世上没有绝对的安全,我们能做的就是提高破解的成本。

两个关键因素

首先 bcrypt 是一个密码加密函数,由 Niels Provos 和 David Mazieres 设计,在 1999 年正式向世人提出。

这个函数由两个关键因素确保其可靠安全:

第一点:就像很多其他加密函数或者方案一样,会参杂一个 salt 进去,也就是我们常说的「加盐」,有了盐🧂,攻击者通过彩虹表就无法破解了,他必须把盐值也猜出来才有可能破解。

但是光是加盐,很多加密函数或者我们使用 MD5 搭配盐也能弄,不是使用它的强有力理由。

第二点:bcrypt 通过接受一个参数 cost 提高计算时长,换句话说,cost 数值越大,bcrypt 运行计算所需的时间就越长。

想象一个场景:

我们通过 MD5 + salt 的方案生成密码,攻击者拿到这个密码后,基于彩虹表攻击无效后,干脆直接采用暴力搜索攻击,因为执行一次 MD5 函数所需的时间在强大的计算能力面前是很短暂的,所以也不需要花多少时间就能破解出来,我们假定执行一次 MD5 函数所需时间是 1 毫秒。

现在我们换成使用 bcrypt 函数生成密码,我们生成的时候先指定这个 cost 参数值为 1,并且此时执行一次 bcrypt 函数所需时间也是 1 毫秒,但如果我们增大这个 cost 参数值,比如为 10,此时执行一次 bcrypt 函数所需时间可能是 50 毫秒,那么等于是原先平均只需要 1 小时就能破解一个密码现在需要 50 小时才能破解一个。

攻击者往往是一次破解一批用户的密码,所以可以想象这个时间成本和算力成本有多大了。

如何选取合适的 cost

一般的,我们默认取 10 作为 cost 参数的值,比如 Go 中的 bcrypt.DefaultCost 就是 10。

我们也可以根据当前服务器的性能选择一个合适的 cost 值,比如我想执行一次 bcrypt 的时间不超过 200 毫秒,这样既不会容易被破解,也不会太耗时,那么我们可以根据下面这段 Go 代码来选择出一个合适的 cost 值:

package main

import (
    "fmt"
    "time"

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

func main() {
    for cost := 10; cost <= 20; cost++ {
        start := time.Now()
        bcrypt.GenerateFromPassword([]byte("pa55w0rd"), cost)
        fmt.Printf("cost: %d, duration: %v\n", cost, time.Since(start))
    }
}

在我的本机上执行结果如下:

cost: 10, duration: 75.797197ms
cost: 11, duration: 146.597944ms
cost: 12, duration: 298.971358ms
cost: 13, duration: 610.758023ms
cost: 14, duration: 1.181615153s
cost: 15, duration: 2.433344989s
cost: 16, duration: 4.917117451s
cost: 17, duration: 9.453614867s
cost: 18, duration: 19.186913882s
cost: 19, duration: 37.79228015s
cost: 20, duration: 1m16.157706237s

那么我就可以据此选择 cost 值为 11,其实从上面的运行结果也能看出来,cost 值和运行时间之间的关系:cost 每增加一,运行耗时就会翻一倍。对具体算法有兴趣的人可以在文末的 Wikipedia 参考链接中找到更多相关信息。

也附上以下 PHP 版本的吧:

<?php
for ($cost = 10; $cost <= 15; $cost++) {
    $start = microtime(true);
    password_hash("test", PASSWORD_BCRYPT, ["cost" => $cost]);
    $end = microtime(true);
    echo "cost: " . $cost . ", duration: " . ($end - $start) * 1000 . "\n";
}
?>

示例代码

这里提供 PHP 和 Go 的两段示例代码供参考:

<?php
$pwd = "pa55w0rd";
echo "Origin Password: " . $pwd . "\n";

$hash = password_hash($pwd, PASSWORD_BCRYPT, ["cost" => 10]);
echo "Encrypted Password: " . $hash . "\n";

$match = password_verify($pwd, $hash);
echo "Match Result: " . $match;
?>
package main

import (
    "fmt"

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

func main() {
    pwd := "pa55w0rd"

    fmt.Println("Origin Password: " + pwd)

    hash, _ := bcrypt.GenerateFromPassword([]byte(pwd), bcrypt.DefaultCost)

    fmt.Println("Encrypted Password: " + string(hash))

    err := bcrypt.CompareHashAndPassword(hash, []byte(pwd))
    fmt.Println("Match Result: ", err == nil)
}

参考链接

本作品采用《CC 协议》,转载必须注明作者和本文链接
公众号:编程之谜
imxfly
讨论数量: 0
(= ̄ω ̄=)··· 暂无内容!

讨论应以学习和精进为目的。请勿发布不友善或者负能量的内容,与人为善,比聪明更重要!