crypto 的用法
密码学(cryptography)是对信息及其传输的数学性研究。密码学是互联网的信任之源。密码学本质上是数学,密码学的算法多由专业的密码学研究人员设计和实现。因此大多数开发人员无须亲自设计和实现加密算法。
Go 语言在标准库 crypto 下的相关包提供了各种主流密码学算法的实现。
crypto 包概览
Go 核心团队维护的密码学包由两部分组成:一部分就是我们在标准库 crypto 下看到的包,另一部分是扩展包,位于 golang.org/x/crypto 下。 扩展包中包含了更多密码学算法实现,并且后面的扩展包很多已经成为标准包的依赖包。不同之处就在于标准库下的 crypto 是跟 Go 语言一起发布的。
标准库已经实现的密码学包大致可分如下几类:
- 分组密码(block cipher)
- cipher:实现了分组密码算法的五种标准分组模式,包括 ECB、CBC、CFB、OFB 和 CTR。
- des:实现了 DES 和 3DES 分组密码算法。
- aes:实现了 AES 分组密码算法。
- 公钥密码与数字签名
- tls:实现了 TLS1.2 和 TLS1.3 的协议。
- x509:实现了 X.509 证书的解析和生成。
- rsa:实现了 RSA 算法。
- elliptic:实现了在素数域的几个椭圆曲线密码学算法。
- dsa:实现了 DSA 算法。
- ecdsa:实现了 ECDSA 算法。
- ed25519:实现了 Ed25519 算法。
- 单向散列函数,也称消息摘要或指纹
- md5:实现了 MD5 算法。
- sha1:实现了 SHA-1 算法。
- sha256:实现了 SHA-256 算法。
- sha512:实现了 SHA-512 算法。
- 消息认真码
- hmac:实现了 HMAC 算法。
- 随机数生成
- rand:支持生成密码学安全的随机数。
分组密码算法
密码算法可以分为分组密码(block cipher)和流密码(stream cipher)两种。流密码是对数据流进行连续处理的一类算法,而我们日常使用最多的 DES、AES 加密算法则归于分组密码算法。 分组密码是一种一次仅能处理固定长度数据块的算法。
对称密码是典型的分组密码,之所以称为对称,是因为加密和解密使用的是同一个密钥(key).
我们以 AES 算法作为对称密码的代表算法进行举例说明:
在硬件支持开启 AES 扩展指令的系统上,aes 包的相关操作均可做到常熟时间的时间复杂度:
AES 标准使用的分组长度为固定 128 比特,即 16 位:
// src/crypto/aes/cipher.go
const (
BlockSize = 16
KeySize = 16
)
AES 算法支持的密钥长度有 128、192 和 256 三种,分别对应 16、24 和 32 字节。
我们使用 crypto/aes 包进行加解密,使用 CBC 模式(密码分组链接模式),下面是加密的例子:
func main() {
// 32 字节 => aes-256
key := []byte("12345678123456781234567812345678")
// 创建 aes 分组密码算法实例
aesCipher, err := aes.NewCipher(key)
if err != nil {
panic(err)
}
// 待加密的明文字符串
plaintext := []byte("Hello, Gopher!")
// 存储密文的切片,预留出在密文头部放置初始变量的空间
ciphertext := make([]byte, aes.BlockSize+len(plaintext))
// 初始化向量 IV,长度必须等于 BlockSize
iv := []byte("abcdefghijklmnop")
// 创建分组模式的实例,这里使用 CBC 模式
cbcModeEncrypter := cipher.NewCBCEncrypter(aesCipher, iv)
// 加密
cbcModeEncrypter.CryptBlocks(ciphertext[aes.BlockSize:], plaintext)
// 这里将初始变量放在密文头部
copy(ciphertext[:aes.BlockSize], iv)
fmt.Println(plaintext)
fmt.Println(ciphertext)
}
对应的解密例子:
func main() {
// 32 字节 => aes-256
key := []byte("12345678123456781234567812345678")
// 带有初始变量的密文
ciphertestWithIV, err := hex.DecodeString("6162636465666768696a6b6c6d6e6f706f7172737475767778797a2122232425262728")
if err != nil {
panic(err)
}
// 从密文中取出初始变量
iv := ciphertestWithIV[:aes.BlockSize]
// 待解密的密文数据
ciphertext := ciphertestWithIV[aes.BlockSize:]
// 创建 aes 分组密码算法实例
aesCipher, err := aes.NewCipher(key)
if err != nil {
panic(err)
}
// 解密后存放明文的字节切片
plaintext := make([]byte, len(ciphertext))
// 创建分组模式的实例,这里使用 CBC 模式
cbcModeDecrypter := cipher.NewCBCDecrypter(aesCipher, iv)
// 解密
cbcModeDecrypter.CryptBlocks(plaintext, ciphertext)
fmt.Println(ciphertestWithIV)
fmt.Println(plaintext)
}
公钥密码
在公钥密码系统中,每个通信方都会生成两把密钥:私有密钥和公共密钥。公钥也可以称为加密密钥,私钥也可以成为解密密钥。
RSA 是世界上使用最广泛的公钥密码算法, Go 语言标准库中的 rsa 包提供了 RSA 算法的实现。下面是使用 RSA 算法的第一步生成一对密钥:
func main() {
// 生成一个 2048 位的私钥
privateKey, err := rsa.GenerateKey(rand.Reader, 2048)
if err != nil {
panic(err)
}
// 从私钥中取出公钥
publicKey := &privateKey.PublicKey
fmt.Println(privateKey.Size()*8) // 2048
fmt.Println(publicKey.Size()*8) // 2048
}
RSA 加解密默认使用 PKCS#1 v1.5 填充方案,该方案对面选择密文攻击时强度不足,而使用 RSA-OAEP 则被认为是一种可信赖、满足强度需求的填充方案,下面是使用 RSA-OAEP 进行加解密的例子:
func main() {
// 生成一个 2048 位的私钥
privateKey, err := rsa.GenerateKey(rand.Reader, 2048)
if err != nil {
panic(err)
}
// 从私钥中取出公钥
publicKey := &privateKey.PublicKey
// 待加密的明文
plaintext := []byte("Hello, Gopher!")
// 加密
ciphertext, err := rsa.EncryptOAEP(sha256.New(), rand.Reader, publicKey, plaintext, nil)
if err != nil {
panic(err)
}
// 解密
decrypted, err := rsa.DecryptOAEP(sha256.New(), rand.Reader, privateKey, ciphertext, nil)
if err != nil {
panic(err)
}
fmt.Println(string(plaintext))
fmt.Println(string(ciphertext))
fmt.Println(string(decrypted))
}
单向散列函数
单向散列函数(one-way hash function)是一个接受不定长输入但产生定长输出的函数,这个定长输出被称为“摘要”或“指纹”。
单向散列函数的特点:
- 强抗碰撞性:找出散列值相同的两条不同的消息是非常困难的。
- 单向性:无法通过散列值反算出输入的消息原文。
由于上述性质,单向散列函数在密码学中有着广泛的应用,比如下载文件是否被篡改、基于口令的身份验证、数字签名、消息认证码以及随机数生成器中。
Go 语言标准库中的 crypto 包提供了多种单向散列函数的实现,包括 MD5、SHA-1、SHA-256 和 SHA-512。在扩展包中提供了更多的单向散列函数的实现,比如 SHA-3、BLAKE2 等。
SHA-256 是使用最多的单向散列函数之一,下面是使用 SHA-256 计算摘要的例子:
func sum256(data []byte) []byte {
// 创建一个新的 SHA256 散列
h := sha256.New()
// 写入数据
h.Write(data)
// 计算散列值
return h.Sum(nil)
}
func main() {
data := []byte("Hello, Gopher!")
fmt.Printf("%x\n", sum256(data))
}
消息认证码
单向散列函数虽然能辨别出数据是否被篡改,但无法防止数据被篡改。消息认证码(Message Authentication Code,MAC)是一种带有密钥的单向散列函数,它能够在数据传输过程中对数据的完整性和真实性进行验证。
Go 语言密码包中提供了 crypto/hmac 包,用于生成消息认证码。HMAC(Hash-based Message Authentication Code)是一种基于密钥的消息认证码算法。 下面是使用 HMAC-SHA256 算法生成消息认证码的例子:
func main() {
// 32 字节的密钥
key := []byte("12345678123456781234567812345678")
// 待计算 MAC 的数据
data := []byte("Hello, Gopher!")
// 创建一个新的 HMAC 实例
h := hmac.New(sha256.New, key)
h.Write(data)
// 计算 MAC
mac := h.Sum(nil)
fmt.Printf("%x\n", mac)
// 验证 MAC
h.Reset()
h.Write(data)
mac2 := h.Sum(nil)
fmt.Println(hmac.Equal(mac, mac2)) // true
}
数字签名
消息验证码虽然解决了消息发送者的身份问题,但由于采用消息认证码的通信双方共享密钥,因此对于一条消息的真实性和完整性的验证只能由通信双方进行,无法被第三方验证。
数字签名就是专为解决这个问题而被发明的密码技术。签名密钥只能由签名一方持有,它的所有通信对端将持有用于验证签名的密钥。
前面我们使用了 RSA 算法进行公钥加密和私钥解密,这里我们继续使用 RSA 算法进行私钥签名和公钥验证签名,但是这次我们使用的是 PSS 填充方案。 PSS 算法通过对消息摘要进行签名,并在计算散列值时对消息加盐的方式来提供安全性(实现对同一条消息进行多次签名得到的结果都不同)。
func main() {
// 生成一个 2048 位的私钥
privateKey, err := rsa.GenerateKey(rand.Reader, 2048)
if err != nil {
panic(err)
}
// 从私钥中取出公钥
publicKey := &privateKey.PublicKey
// 待签名的数据
data := []byte("Hello, Gopher!")
// 计算数据的 SHA256 散列值
hashed := sha256.Sum256(data)
// 使用私钥进行签名
signature, err := rsa.SingPSS(rand.Reader, privateKey, crypto.SHA256, hashed[:], nil)
if err != nil {
panic(err)
}
// 使用公钥验证签名
err = rsa.VerifyPSS(publicKey, crypto.SHA256, hashed[:], signature, nil)
if err != nil {
panic(err)
}
fmt.Println("signature verified")
}
随机数生成
随机数是密码学中一个基础而且十分重要的工具,随机数产生的随机数随机性越强,使用这些随机数的密码学算法的安全效能越好。
Go 标准库提供了 crypto/rand 包,用于生成密码学安全的随机数生成器实现 rand.Reader
,它是一个全局的密码学安全的随机数生成器。而且不同的平台使用的数据源不同。
如果我们自己要使用随机数,可以使用 rand.Read
,该函数本质是对 rand.Reader
浅封装,我们来看一个例子:
import (
"crypto/rand"
"fmt"
)
func main() {
c := 32
b := make([]byte, c)
_, err := rand.Read(b)
if err != nil {
panic(err)
}
fmt.Println(b)
}
上面例子展示了生成一个指定 32 长度的随机数。