使用 bcrypt 函数生成密码

龚景
• 阅读 3598

在几年前,相信很多和我一样的开发者都是使用 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)
}

参考链接

点赞
收藏
评论区
推荐文章
blmius blmius
4年前
MySQL:[Err] 1292 - Incorrect datetime value: ‘0000-00-00 00:00:00‘ for column ‘CREATE_TIME‘ at row 1
文章目录问题用navicat导入数据时,报错:原因这是因为当前的MySQL不支持datetime为0的情况。解决修改sql\mode:sql\mode:SQLMode定义了MySQL应支持的SQL语法、数据校验等,这样可以更容易地在不同的环境中使用MySQL。全局s
Oracle 分组与拼接字符串同时使用
SELECTT.,ROWNUMIDFROM(SELECTT.EMPLID,T.NAME,T.BU,T.REALDEPART,T.FORMATDATE,SUM(T.S0)S0,MAX(UPDATETIME)CREATETIME,LISTAGG(TOCHAR(
Wesley13 Wesley13
4年前
MySQL部分从库上面因为大量的临时表tmp_table造成慢查询
背景描述Time:20190124T00:08:14.70572408:00User@Host:@Id:Schema:sentrymetaLast_errno:0Killed:0Query_time:0.315758Lock_
皕杰报表之UUID
​在我们用皕杰报表工具设计填报报表时,如何在新增行里自动增加id呢?能新增整数排序id吗?目前可以在新增行里自动增加id,但只能用uuid函数增加UUID编码,不能新增整数排序id。uuid函数说明:获取一个UUID,可以在填报表中用来创建数据ID语法:uuid()或uuid(sep)参数说明:sep布尔值,生成的uuid中是否包含分隔符'',缺省为
Wesley13 Wesley13
4年前
FLV文件格式
1.        FLV文件对齐方式FLV文件以大端对齐方式存放多字节整型。如存放数字无符号16位的数字300(0x012C),那么在FLV文件中存放的顺序是:|0x01|0x2C|。如果是无符号32位数字300(0x0000012C),那么在FLV文件中的存放顺序是:|0x00|0x00|0x00|0x01|0x2C。2.  
Wesley13 Wesley13
4年前
mysql中时间比较的实现
MySql中时间比较的实现unix\_timestamp()unix\_timestamp函数可以接受一个参数,也可以不使用参数。它的返回值是一个无符号的整数。不使用参数,它返回自1970年1月1日0时0分0秒到现在所经过的秒数,如果使用参数,参数的类型为时间类型或者时间类型的字符串表示,则是从1970010100:00:0
Easter79 Easter79
4年前
Twitter的分布式自增ID算法snowflake (Java版)
概述分布式系统中,有一些需要使用全局唯一ID的场景,这种时候为了防止ID冲突可以使用36位的UUID,但是UUID有一些缺点,首先他相对比较长,另外UUID一般是无序的。有些时候我们希望能使用一种简单一些的ID,并且希望ID能够按照时间有序生成。而twitter的snowflake解决了这种需求,最初Twitter把存储系统从MySQL迁移
Wesley13 Wesley13
4年前
mysql设置时区
mysql设置时区mysql\_query("SETtime\_zone'8:00'")ordie('时区设置失败,请联系管理员!');中国在东8区所以加8方法二:selectcount(user\_id)asdevice,CONVERT\_TZ(FROM\_UNIXTIME(reg\_time),'08:00','0
Stella981 Stella981
4年前
HIVE 时间操作函数
日期函数UNIX时间戳转日期函数: from\_unixtime语法:   from\_unixtime(bigint unixtime\, string format\)返回值: string说明: 转化UNIX时间戳(从19700101 00:00:00 UTC到指定时间的秒数)到当前时区的时间格式举例:hive   selec
Wesley13 Wesley13
4年前
PHP创建多级树型结构
<!lang:php<?php$areaarray(array('id'1,'pid'0,'name''中国'),array('id'5,'pid'0,'name''美国'),array('id'2,'pid'1,'name''吉林'),array('id'4,'pid'2,'n
Python进阶者 Python进阶者
2年前
Excel中这日期老是出来00:00:00,怎么用Pandas把这个去除
大家好,我是皮皮。一、前言前几天在Python白银交流群【上海新年人】问了一个Pandas数据筛选的问题。问题如下:这日期老是出来00:00:00,怎么把这个去除。二、实现过程后来【论草莓如何成为冻干莓】给了一个思路和代码如下:pd.toexcel之前把这