密码学基础(上):对称加密、非对称加密与哈希函数

聊 TLS,绕不开密码学。很多人觉得密码学高深莫测,其实说白了,TLS 就是把这几种基础算法巧妙地组合在了一起。今天咱们就把 AES、RSA 和 SHA-256 这三个核心家伙彻底讲透。

我个人习惯,学任何密码算法,先搞清楚三个问题:它能干什么?它怎么干的?它有什么坑? 带着问题往下看,你会觉得这些东西其实挺接地气的。

1. 对称加密:AES 的实战逻辑

对称加密,顾名思义,加密和解密用同一把钥匙。就像你家里的大门,用一把钥匙锁上,也用同一把钥匙打开。

在 TLS 里,真正用来加密大量传输数据的,就是对称加密。为什么?因为快。非对称加密太慢了,不适合干这粗活。

AES 的核心参数

AES(Advanced Encryption Standard)是目前最主流的对称加密算法。我建议你重点关注这几个参数:

参数 常见值 说明
密钥长度 128 / 192 / 256 位 越长越安全,AES-256 是当前推荐标准
工作模式 GCM / CBC / CTR GCM 自带认证,TLS 1.3 首选
填充方式 PKCS#7 / NoPadding 流模式不需要填充
IV 长度 12 字节(GCM)/ 16 字节(CBC) IV 不能重复使用,这是大忌
我曾经踩过的坑: 有一次在项目中,同事把 AES-CBC 的 IV 写成了固定值。结果密文模式完全一样,攻击者一眼就能看出哪些数据块是相同的。嗯,从那以后我定了个规矩——IV 必须由安全的随机数生成器产生,每次加密都不一样。

代码示例:Go 语言实现 AES-GCM 加密

package main

import (
    "crypto/aes"
    "crypto/cipher"
    "crypto/rand"
    "encoding/hex"
    "fmt"
    "io"
)

func encryptAESGCM(key, plaintext []byte) ([]byte, error) {
    block, err := aes.NewCipher(key)
    if err != nil {
        return nil, err
    }

    // GCM 模式,nonce 长度固定为 12 字节
    aesGCM, err := cipher.NewGCM(block)
    if err != nil {
        return nil, err
    }

    nonce := make([]byte, aesGCM.NonceSize())
    if _, err := io.ReadFull(rand.Reader, nonce); err != nil {
        return nil, err
    }

    // 加密并附加认证标签
    ciphertext := aesGCM.Seal(nonce, nonce, plaintext, nil)
    return ciphertext, nil
}

func main() {
    key := []byte("0123456789abcdef0123456789abcdef") // 32 字节 = AES-256
    plaintext := []byte("Hello, TLS!")

    ciphertext, _ := encryptAESGCM(key, plaintext)
    fmt.Println("密文(hex):", hex.EncodeToString(ciphertext))
}

你看,代码其实不复杂。关键点在于:nonce 必须随机生成,而且每次加密都要换新的。GCM 模式会帮你自动校验密文是否被篡改,这也是 TLS 1.3 强制使用它的原因。

2. 非对称加密:RSA 的握手使命

非对称加密,有一对钥匙:公钥和私钥。公钥可以公开,私钥自己藏好。用公钥加密的数据,只有私钥能解开;反过来,用私钥签名的数据,公钥可以验证。

在 TLS 里,RSA 主要干两件事:密钥交换和数字签名。不过现在 TLS 1.3 已经默认禁用 RSA 密钥交换了,因为它不支持前向安全性。但 RSA 签名依然广泛使用。

RSA 的数学原理(通俗版)

你不需要记住那些复杂的数论公式,但理解核心逻辑有助于你排错:

  1. 选两个大质数 p 和 q,乘起来得到 n。n 的长度就是 RSA 的密钥长度(比如 2048 位)。
  2. 计算欧拉函数 φ(n) = (p-1)(q-1)。
  3. 选一个公钥指数 e,常用 65537。这个数有讲究,它是个质数,而且二进制只有两个 1,计算快。
  4. 计算私钥指数 d,满足 e * d ≡ 1 (mod φ(n))。d 就是你的私钥核心。

加密过程:c = m^e mod n(m 是明文,c 是密文)
解密过程:m = c^d mod n

关键认知: RSA 的安全性建立在「大整数分解困难」这个假设上。给你 n,你很难在合理时间内算出 p 和 q。但量子计算机出现后,这个假设就不成立了。所以业界正在向 ECC(椭圆曲线)迁移。

RSA 的密钥长度选择

密钥长度 安全等级 当前状态
1024 位 80 位 已不安全,禁止使用
2048 位 112 位 当前最低要求
3072 位 128 位 推荐长期使用
4096 位 152 位 性能开销较大,但更安全

我的建议: 新系统直接上 3072 位或 4096 位。2048 位虽然还能用,但考虑到证书有效期通常是一年,明年可能就要升级了。别给自己找麻烦,一步到位。

3. 哈希函数:SHA-256 的指纹作用

哈希函数,说白了就是给数据算一个「指纹」。不管你的数据是 1 个字节还是 1 个 G,算出来的哈希值长度是固定的(SHA-256 是 256 位)。而且,只要数据改了一个比特,哈希值就完全不一样。

在 TLS 里,哈希函数无处不在:

  • 证书签名时,先对证书内容算哈希,再对哈希值做 RSA 签名。
  • TLS 握手过程中,用哈希函数派生密钥。
  • 消息认证码(HMAC)也依赖哈希函数。

SHA-256 的核心特性

一个好的哈希函数必须满足三个性质:

  1. 抗原像性: 给你哈希值,你算不出原始数据。
  2. 抗第二原像性: 给你一个数据,你找不到另一个数据跟它有相同的哈希值。
  3. 抗碰撞性: 你找不到任意两个不同的数据有相同的哈希值。

SHA-256 目前在这三个方面都还安全。但 SHA-1 已经被攻破了,Google 在 2017 年就演示了实际的碰撞攻击。所以,千万别再用 SHA-1 了。

代码示例:计算 SHA-256 哈希

package main

import (
    "crypto/sha256"
    "encoding/hex"
    "fmt"
)

func main() {
    data := []byte("Hello, TLS!")

    // 方法一:直接计算
    hash := sha256.Sum256(data)
    fmt.Println("SHA-256:", hex.EncodeToString(hash[:]))

    // 方法二:流式写入(适合大文件)
    h := sha256.New()
    h.Write([]byte("Hello, "))
    h.Write([]byte("TLS!"))
    result := h.Sum(nil)
    fmt.Println("SHA-256:", hex.EncodeToString(result))
}

两种写法结果一样。流式写入的好处是,你可以分块处理大文件,不用一次性加载到内存。

4. 三种算法的对比与协作

这三种算法不是互相替代的关系,而是各司其职。我画了一张图,帮你理清它们在 TLS 中的协作关系:

TLS 中三种密码算法的协作关系 客户端 服务端 ① 发送支持的密码套件 选择密码套件 ② 返回证书 + 公钥 (RSA) 验证证书 (SHA-256) 生成预主密钥 ③ 用 RSA 公钥加密预主密钥 RSA 私钥解密 双方派生会话密钥 AES-GCM 加密通信 RSA 只用于握手阶段 后续数据都用 AES 加密 SHA-256 用于证书验证 和密钥派生

从这张图你可以看到:

  • RSA 负责握手阶段的身份认证和密钥交换(虽然现在更推荐 ECDHE)。
  • SHA-256 负责证书指纹验证和密钥派生,确保数据完整性。
  • AES 负责握手完成后的实际数据传输,保证机密性。

一句话总结: 非对称加密解决「信任」问题,哈希函数解决「完整性」问题,对称加密解决「效率」问题。三者缺一不可。

5. 避坑指南

最后,分享几个我在实际项目中遇到的坑,希望能帮你少走弯路:

  • 不要自己实现密码算法。 永远使用标准库。自己写的 AES 实现,十有八九有侧信道漏洞。
  • 随机数生成器要用密码学安全的。 Go 的 crypto/rand,不要用 math/rand。前者是操作系统提供的真随机数,后者是伪随机数, predictability 是安全大忌。
  • 密钥不要硬编码在代码里。 我曾经接手过一个项目,RSA 私钥直接写在配置文件里,还提交到了 Git 仓库。嗯,那场面……
  • 关注算法的最新动态。 密码学是动态发展的。今天安全的算法,明天可能就被攻破了。定期关注 NIST 和 IETF 的推荐列表。