Go 语言
标准库、反射和 cgo
crypto 的用法

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 长度的随机数。