密码学基础(上):对称加密、非对称加密与哈希函数
聊 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 不能重复使用,这是大忌 |
代码示例: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 的数学原理(通俗版)
你不需要记住那些复杂的数论公式,但理解核心逻辑有助于你排错:
- 选两个大质数 p 和 q,乘起来得到 n。n 的长度就是 RSA 的密钥长度(比如 2048 位)。
- 计算欧拉函数 φ(n) = (p-1)(q-1)。
- 选一个公钥指数 e,常用 65537。这个数有讲究,它是个质数,而且二进制只有两个 1,计算快。
- 计算私钥指数 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 的核心特性
一个好的哈希函数必须满足三个性质:
- 抗原像性: 给你哈希值,你算不出原始数据。
- 抗第二原像性: 给你一个数据,你找不到另一个数据跟它有相同的哈希值。
- 抗碰撞性: 你找不到任意两个不同的数据有相同的哈希值。
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 中的协作关系:
从这张图你可以看到:
- RSA 负责握手阶段的身份认证和密钥交换(虽然现在更推荐 ECDHE)。
- SHA-256 负责证书指纹验证和密钥派生,确保数据完整性。
- AES 负责握手完成后的实际数据传输,保证机密性。
一句话总结: 非对称加密解决「信任」问题,哈希函数解决「完整性」问题,对称加密解决「效率」问题。三者缺一不可。
5. 避坑指南
最后,分享几个我在实际项目中遇到的坑,希望能帮你少走弯路:
- 不要自己实现密码算法。 永远使用标准库。自己写的 AES 实现,十有八九有侧信道漏洞。
- 随机数生成器要用密码学安全的。 Go 的
crypto/rand,不要用math/rand。前者是操作系统提供的真随机数,后者是伪随机数, predictability 是安全大忌。 - 密钥不要硬编码在代码里。 我曾经接手过一个项目,RSA 私钥直接写在配置文件里,还提交到了 Git 仓库。嗯,那场面……
- 关注算法的最新动态。 密码学是动态发展的。今天安全的算法,明天可能就被攻破了。定期关注 NIST 和 IETF 的推荐列表。