简介
Hash(哈希或散列)算法,是密码学中非常重要的一类算法,用于将任意长度的二进制串确定性地映射到较短的(通常是固定长度的)二进制串(哈希值)上,不同的二进制输入串很难映射到相同的输出,这种特性使得其又被称为指纹(fingerprint)或摘要(digest)算法。
一个优秀的 Hash 算法,应该具备以下特点:
- 正向快速:在给定原文后,能够在有限时间和有限资源内计算其Hash值;
- 逆向困难: 在给定Hash值后,在有限时间和有限资源内无法逆推出原文;
- 输入敏感:原文输入信息的任何轻微改变,都将造成Hash值的巨大变化;
- 避免碰撞:很难找到两段内容不同的明文,使得它们的Hash值一致(即发生碰撞);
常见算法
目前常见的 Hash 算法包括国际上的 Message Digest(MD)系列和 Secure Hash Algorithm(SHA)系列算法,以及国内的 SM3 算法。
MD 系列主要包括 MD4 和 MD5 两个算法。MD4(RFC 1320)是 MIT 的 Ronald L. Rivest 在 1990 年设计的,其输出为 128 位。MD4 已证明不够安全。Rivest在1991年对MD4进行改进发布MD5。MD5 比 MD4 更加安全,但过程更加复杂,计算速度要慢一点。MD5 已于 2004 年被成功碰撞,其安全性已不足应用于商业场景。
SHA 算法由美国国家标准与技术院(National Institute of Standards and Technology,NIST)征集制定。首个实现 SHA-0 算法于 1993 年问世,1998 年即遭破解。随后的修订版本 SHA-1 算法在 1995 年面世,它的输出为长度 160 位的 Hash 值,安全性更好。SHA-1 已于 2005 年被成功碰撞,意味着无法满足商用需求。为了提高安全性,NIST 后来制定出更安全的 SHA-224、SHA-256、SHA-384,和 SHA-512 算法(统称为 SHA-2 算法)。新一代的 SHA-3 相关算法也正在研究中。
中国密码管理局于 2010 年 12 月 17 日发布了GM/T 0004-2012 《SM3 密码杂凑算法》,建立了国内商用密码体系中的公开 Hash 算法标准,已经被广泛应用在数字签名和认证等场景中。
应用
数字摘要
利用Hash算法的“抗碰撞性”,计算出数据的Hash值作为数字摘要。当原始数据被改变时,可以通过计算摘要进行比对从而检测出数据被篡改。
最常见的用例便是文件下载,有些网站在提供下载文件的同时,会提供文件相应的数字摘要,用户在下载完成后可以在本地计算文件的摘要,通过比对来检测下载的文件是否被篡改。
数字签名
数字签名是对数据先使用Hash算法计算其数字摘要,然后使用加密算法对数字摘要进行加密,最后将数字签名附到原始数据后完成数据的签名工作。此时该数据可以在不安全的信道中传输,接收方可以检验数据的真实性。接收方检验的流程如下:
- 从接收的数据中分离出数据和签名
- 使用约定好的解密算法对签名进行解密得到数字摘要
- 使用约定好的Hash算法对数据计算数字摘要
- 如果2与3中的数字摘要一致,就意味着数据是真实有效的、未被篡改过的。
这种真实性源于下面的两个假设:
- 加密算法未被破解
- Hash算法的抗碰撞性
在加密算法未被破解的前提下,Hash算法的抗碰撞性使得在有限的时间和资源内难以找出一份篡改后的数据使得与原始数据摘要相同。加密算法未被破解,保证了对于伪造的签名将解密出不一致的摘要。
数据保护
在一些场景下不保存数据的原始信息,而保存其Hash值来减轻数据库泄露造成的危害。例如,后台数据库保存登录口令的Hash值,每次通过Hash值比对即可判断输入口令是否正确,此时即便数据库发生泄露,攻击者也无法知晓用户的登录口令。
字典攻击就是攻击者掌握了大量常见的登录密码,如password,123等等,通过对其计算Hash值,编成 口令-Hash
字典,然后通过Hash值来快速反查找到原始口令。
对这类攻击的防范可以使用加盐(Salt)的方法。保存的不是原文的直接 Hash 值,而是原文再加上一段随机字符串(即“盐”)之后的 Hash 值。Hash 结果和“盐”分别存放在不同的地方,这样只要不是两者同时泄露,攻击者很难进行破解。这种思想的高级变种包括二次验证技术。
基于内容的编址
由于Hash算法的特性,可以将任意内容映射到一个固定长度的字符串,而且不同内容映射到相同串的概率很低。因此,这就构成了一个很好的“内容 -> 索引”的生成关系。
假设,给定一个数据和存储系统。将这个数据放到存储系统中的什么位置,可以在查询时快速确定该内容是否存在于存储系统中?通过对内容进行Hash计算,然后将该哈希值作为在存储系统中地址,这样就可以实现高效的查找。
然而在物理世界中没有无限长的纸带,当存储系统越小,Hash值映射的范围越小,Hash碰撞的概率就会越大。为了提供空间利用率, Burton Howard Bloom提出使用Bloom过滤器进行Hash编址。其基本思想是:在数据上应用多个Hash函数计算多个Hash地址,然后将这些Hash地址拼接起来形成最终的地址。这样,不同数据发生地址碰撞当且仅当所有的Hash函数都碰撞,因此降低了地址碰撞的几率。
使用
下面的示例以SHA-256
为例,输出的均为Hash值的十六进制字符串
bash
# 计算字符串的SHA-256值
$ echo "hello world!" |shasum -a 256
ecf701f727d9e2d77c4aa49ac6fbbcc997278aca010bddeeb961c10cf54d435a -
# 计算一个文件的的SHA-256数字摘要
$ echo "hello world!" > hello.txt && shasum -a 256 hello.txt
ecf701f727d9e2d77c4aa49ac6fbbcc997278aca010bddeeb961c10cf54d435a hello.txt
Go
package main
import (
"fmt"
"crypto/sha256"
"encoding/hex"
"os"
"io"
)
func sha256_file(path string) (string, error) {
f, err := os.Open(path)
if err != nil{
return "", err
}
defer f.Close()
h := sha256.New()
if _, err = io.Copy(h, f); err != nil{
return "", err
}
byte_hash := h.Sum(nil)
hex_hash := hex.EncodeToString(byte_hash)
return hex_hash, nil
}
func main(){
s := "hello world!"
// 从 `sha256.New()` 创建一个新的hash对象开始
h := sha256.New()
// `Write` 需要字节输入。所以要使用 `[]byte(s)` 将字符串 `s` 转成字节切片
// 可以调用多次 `Write` 来写入数据
h.Write([]byte(s))
// 获得当前已有数据的Hash值
// `Sum` 接受一个参数可以将获得的Hash值附到一个存在的字节切片后:通常不是必需的
byte_hash := h.Sum(nil)
// 以十六进制字符串的格式进行编码
hex_hash := hex.EncodeToString(byte_hash)
fmt.Printf("%s %s\n", hex_hash, s)
// 计算文件的SHA256值
path := os.Args[1]
file_hash, err := sha256_file(path)
if err != nil {
fmt.Println("this file not exists")
} else {
fmt.Printf("%s %s\n", file_hash, path)
}
}
python
# -*- coding: utf-8 -*-
import hashlib
import sys
def sha256_file(path):
buf_size = 64*1024
sha256 = hashlib.sha256()
try:
with open(path, "rb") as f:
while True:
data = f.read(buf_size)
if not data:
break
sha256.update(data)
return sha256.hexdigest()
except:
return None
if __name__ == "__main__":
s = "hello world!"
# 创建SHA256对象
sha256 = hashlib.sha256()
# 写入要计算摘要的数据
sha256.update(s.encode())
# 计算当前数据的摘要
hex_hash = sha256.hexdigest()
print("{0} {1}".format(hex_hash, s))
path = sys.argv[1]
hex_hash = sha256_file(path)
if hex_hash == None:
print("this file not exists")
else:
print("{0} {1}".format(hex_hash, path))
注意
各个编程语言的哈希算法对于相同的二进制数据会计算出相同的Hash摘要,但有时会出现意想不到的不一致。这有可能是因为没有考虑编码的原因。例如,python中的字符串在内存中默认以utf-8编码,而bash根据每个人机器的不同配置会有不同的编码,这样表达意思相同的“你好”,在两者中将以不一致的二进制串出现,从而导致对“你好”出现不同的摘要信息。
$ python
> s = "你好" // s以utf-8标准对“你好”进行编码
$ echo "你好" // 根据机器配置对“你好”进行编码,不一定是utf-8