MD5 or Bcrypt?

发布时间 2023-08-23 15:27:18作者: lockly

MD5 or Bcrypt?

摘要

首先是一个错误的认识观念问题,很多人觉得MD5是一个加密算法。不然,他实则是一种摘要算法,也可以叫哈希函数。他的作用是将目标文本转换成具有相同长度、不可逆的杂凑字符串。而加密算法和他恰恰相反,是将目标转换成具有不同长度、可逆的密文。

MD5简介

一般来说由于摘要算法他的单向运算,具有一定的不可逆性也就是说信息无法被还原了,所以成为加密算法中的一个构成部分。MD5他的具体原理可以看B站这个up的容易理解? 传送门

但在现在看来任何场景下都最好不要使用MD5,因为他不够安全,跟存储明文没啥两样。可以使用诸如SHA256、SHA512这类其他的摘要算法来替代。MD5的不够安全从下面说起:

MD5的不安全性

因为这种哈希算法很固定,同一个传入的字符串得到的hash字符串是相同的。以现在的手段来说,可以采用:

  1. 暴力枚举直接枚举原文并计算哈希值,然后再去比对一致的信息摘要
  2. 字典爆破,用一个巨大的字典来存储原始信息和对应哈希值的键值对,通过碰撞来比对映射关系
  3. 彩虹表,也是现在最多使用的办法(像之前的*习通信息泄露事件中)

虽然说这些破解方法的成本比较高,但在如今计算机算力势不可挡的发展下,利用分布式计算还是能很有效的进行破解。

加盐MD5

那可能会有人说了,加盐了解一下?确实加盐主要就是为了增加嗨客的计算成本,因为有了盐值要重新计算彩虹表所以在抵御彩虹表上确实有作用。但真的就安全了吗?这种情况下虽然每个加密hash字符串使用了随机的盐进行hash,但破解的方向转变成了哈希碰撞的概率。比如就算我不知道你的密码,不知道你的盐值。只要我能找到一个值经过加盐的hash后与你加盐后的密码一致照样能登录你的账号。

也就是说这还是个算力问题。除了知名的hashcat,下面是在一个靶场环境下使用 John (john the ripper)来爆破MD5

image-20230823131307999

总的来说,用MD5、MD5加盐来存密码都是不够安全的。那更换其他的加密算法呢?这需要考虑一个大问题,那就是怎么去存储加密用的密钥呢?当数据泄露时又怎么保证密钥就能独善其身呢?

Bcrypt

更加安全的解决方案是现在广泛使用的bcrypt,他是基于 eksblowfish 算法设计的加密哈希函数。他的主要特点如下:

  1. 产生随机盐:这样对于同样的原始数据得到的hash都不同。他先生成随机的盐,再拿盐和原始数据进行hash
  2. 自适应性:他可以动态调整工作因子,前面提到了算力问题,而因为他这个特性任你算力多强他会自己增加迭代次数将加密速度控制在一个范围里面,直接硬抗暴力搜索。

那盐是随机的,每次做出的菜味道都不一样,又该怎么比对内容的一致性呢?他的校验是先从hash中取出salt,再去与内容进行hash,然后去比对之前加密的hash。比如下面加密的字符串:

$2a$10$te3Yi7z4fiH2xRw6vReet.XHRbd1iz711ykk6bFAL4y/nk213QOXS

$2a$为hash算法的唯一标识,这里表示Bcrypt。10是工作因子,代表2的10次方。te3Yi7z4fiH2xRw6vReet.这部分22长的字符是16字节的salt经base64编码后得到的,最后的部分是计算后的 Base64 哈希值(24 字节)

看一个例子:

package main

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

func encryptByBcrypt(str string) string {
	bytes := []byte(str)
	hashedBytes, err := bcrypt.GenerateFromPassword(bytes, bcrypt.DefaultCost)
	if err != nil {
		fmt.Println(err)
		return ""
	}
	return string(hashedBytes)
}

func compare(format, input string) bool {
	err := bcrypt.CompareHashAndPassword([]byte(format), []byte(input))
	if err != nil {
		return false
	}
	return true
}

func main() {
	var (
		// 用户输入
		input1 = "admin666"
		input2 = "admin"
		// 数据库中存储的
		dbData = encryptByBcrypt("admin666")
	)

	fmt.Printf("用户输入%s的加密结果: %s\n", input1, encryptByBcrypt(input1))
	fmt.Printf("用户输入%s的加密结果: %s\n", input2, encryptByBcrypt(input2))
	fmt.Printf("数据库中的加密字符串: %s\n", dbData)
	for i := 2; i < 5; i++ {
		fmt.Printf("第%d次登录时对%s加密的结果: %s ", i, input1, ecryptByBcrypt(input1))
		fmt.Printf("%s是否匹配数据库中的值:%v\n", input1, compare(dbData, input1))
	}

	fmt.Printf("%s 是否匹配数据库中的值:%v", input2, compare(dbData, input2))
}

//结果:

//admin666的加密结果: $2a$10$Q1VWBHXkPxTT0oLQtvaKSOfXmz2CJIN2z7y1A9L.rcciJyxhtlMcu
//admin的加密结果: $2a$10$Z9JHgQQSEcQGIuVNhFmbpe3IdWLgR2ZjFhKQHU.2XXmWvrdXHpfZm
//数据库中的加密字符串: $2a$10$/IdAYwPf/mpnxH083nUxMuqigOuDQszgoVTjFnYMJJe3B0DFP3qNG
//第2次登录时对admin666加密的结果: $2a$10$TvHq/QzyK2mtW/VmDKEcPOp2KJ4qiKMcKzC709H0aUfqqv8dgC66u admin666是否匹配数据库中的值:true
//第3次登录时对admin666加密的结果: $2a$10$6X7DgbitO97hjDZfVfQa5.h1JNXWg0k65hc0PjhXHUnKhZhNFvoeK admin666是否匹配数据库中的值:true
//第4次登录时对admin666加密的结果: $2a$10$qtbVqqzU6V07RwMDg5.eG.SHzekcozQPeKqTewfXL9gVgt91OLXbe admin666是否匹配数据库中的值:true
//admin 是否匹配数据库中的值:false

可以看到当输入和数据库中存储加密的原始值相同时(admin666),虽然每次加密admin666的结果都不一样,但都能匹配通过,但输入不一样时(admin)一定不匹配。

再举一个?看看他如何抵御暴力搜索:

package main

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

func main() {
	for cost := 10; cost <= 20; cost++ {
		start := time.Now()
		_, err := bcrypt.GenerateFromPassword([]byte("password"), cost)
		if err != nil {
			return
		}
		end := time.Since(start)
		fmt.Printf("工作因子: %d, 耗时: %v\n", cost, end)
	}
}

//结果
//工作因子: 10, 耗时: 58.073ms
//工作因子: 11, 耗时: 119.7431ms
//工作因子: 12, 耗时: 229.9239ms
//工作因子: 13, 耗时: 473.358ms
//工作因子: 14, 耗时: 934.7008ms
//工作因子: 15, 耗时: 1.8949588s
//工作因子: 16, 耗时: 3.7853585s
//工作因子: 17, 耗时: 7.808824s
//工作因子: 18, 耗时: 23.0166827s
//工作因子: 19, 耗时: 36.2171079s
//工作因子: 20, 耗时: 1m1.0674139s

可以看到每调整了cost(工作因子),加密的时间都会不断增加。当嗨客来硬碰硬的时候就直接提高cost增加他的?成本。