引言
密码学是现代应用安全的基石。Zig 的密码学能力来自两个层面:标准库 std.crypto 提供了经过严格审编的纯 Zig 实现(无外部 C 依赖),社区则提供了 mbedtls-zig、bearssl-zig 等成熟 C 库绑定,补充了 TLS/X.509 等上层协议支持。
与调用 OpenSSL 不同,Zig 的 std.crypto 模块使用固定大小数组和编译期类型检查,避免了许多 C 接口中的「缓冲区大小不匹配」问题。本文覆盖对称加解密(AES-GCM、ChaCha20Poly1305)、TLS 1.3 握手、密钥派生(Argon2、Scrypt)、随机数生成与密码学编程的安全红线。
前置:/zig-memory-management/(固定数组 vs 切片)、/zig-comptime-programming/(编译期类型约束)。
目录
- 1. std.crypto 概览
- 2. AES-GCM 对称加解密
- 3. ChaCha20Poly1305:流式 AEAD
- 4. TLS 1.3 客户端与服务端
- 5. 密钥派生:Argon2 与 Scrypt
- 6. 安全随机数生成
- 7. 密码学编程安全红线
- 8. 速查表
- 9. 一句话记忆
- 相关阅读
- 延伸阅读
1. std.crypto 概览
1.1 模块组织结构
std.crypto
├── aead // AEAD 算法:GCM, ChaCha20Poly1305
├── aes // AES 分组密码
├── chacha20 // ChaCha20 流密码
├── ecc // 椭圆曲线
├── hash // 哈希:Blake3, SHA256 等
├── kdf // 密钥派生
├── pcurves // 常用椭圆曲线
├── random // CSPRNG 随机数
└── tls // TLS 1.3 客户端(实验性)
1.2 设计哲学
std.crypto 的核心设计特点:
- 无动态分配:所有接口使用固定大小的
[N]u8数组,不依赖 Allocator。 - 编译期类型安全:密钥长度、nonce 长度是类型的一部分,编译时即检查。
- 纯 Zig 实现:无外部 C 依赖,可跨平台,可审计。
- 常量时间:关键比较操作(如 MAC 验证)使用
std.crypto.utils.timingSafeEql防止时序攻击。
2. AES-GCM 对称加解密
AES-GCM(Galois/Counter Mode)是常用的 AEAD(Authenticated Encryption with Associated Data)算法,同时提供机密性和完整性。
2.1 加密
const std = @import("std");
const crypto = std.crypto;
pub fn aesGcmEncrypt(
key: [32]u8, // AES-256 密钥
nonce: [12]u8, // IV/Nonce,每个消息必须唯一
plaintext: []const u8,
associated_data: []const u8,
ciphertext: []u8,
tag: *[16]u8,
) void {
// 密钥长度决定 AES-128/192/256
var aes = crypto.aead.aes_gcm.Aes256Gcm.init(&key);
aes.encrypt(ciphertext, tag, plaintext, associated_data, nonce);
}
参数解释:
key: 32 字节(256 位)对称密钥,可用随机数生成器安全生成。nonce: 12 字节(96 位),每条消息必须唯一,重复使用会导致密钥恢复攻击。associated_data: 不加密但参与 MAC 计算的数据(如协议头、时间戳)。ciphertext: 输出缓冲区,长度 ≥plaintext.len。tag: 16 字节认证标签,解密时用来验证完整性。
2.2 解密与验证
pub fn aesGcmDecrypt(
key: [32]u8,
nonce: [12]u8,
ciphertext: []const u8,
associated_data: []const u8,
tag: [16]u8,
plaintext: []u8,
) !void {
var aes = crypto.aead.aes_gcm.Aes256Gcm.init(&key);
try aes.decrypt(plaintext, ciphertext, tag, associated_data, nonce);
// decrypt 返回 error.InvalidAuthenticationTag(验证失败)
}
关键安全点:nonce 不可重用。实践中用自增计数器或随机生成。计数器方案需持久化存储防止重启后重复。
3. ChaCha20Poly1305:流式 AEAD
ChaCha20Poly1305 由 Daniel Bernstein 设计,被 TLS 1.3 列为强制算法,在纯软件实现中通常比 AES-GCM 更快(无 AES-NI 硬件时)。
3.1 完整加解密
const ChaCha20Poly1305 = std.crypto.aead.chacha_poly.XChaCha20Poly1305;
pub fn chachaEncrypt(
key: [32]u8,
nonce: [24]u8, // XChaCha20 使用 192 位 nonce,重放风险极低
plaintext: []const u8,
ad: []const u8,
ciphertext: []u8,
tag: *[16]u8,
) void {
var ctx = ChaCha20Poly1305.init(&key);
ctx.encrypt(ciphertext, tag, plaintext, ad, nonce);
}
pub fn chachaDecrypt(
key: [32]u8,
nonce: [24]u8,
ciphertext: []const u8,
ad: []const u8,
tag: [16]u8,
plaintext: []u8,
) !void {
var ctx = ChaCha20Poly1305.init(&key);
try ctx.decrypt(plaintext, ciphertext, tag, ad, nonce);
}
3.2 XChaCha20Poly1305 vs ChaCha20Poly1305
| 变体 | Nonce 长度 | 用途 |
|---|---|---|
| ChaCha20Poly1305 | 12 字节(96 位) | 通信协议,每会话自增 nonce |
| XChaCha20Poly1305 | 24 字节(192 位) | 文件加密、随机 nonce 安全 |
XChaCha20Poly1305 的 192 位 nonce 允许安全使用随机 nonce,无需存储状态。推荐用于文件加密、数据库字段加密等「无状态」场景。
4. TLS 1.3 客户端与服务端
4.1 std.crypto.tls 客户端
Zig 标准库内置了 TLS 1.3 客户端(实验性),可与 std.net.Stream 配合使用:
const std = @import("std");
const tls = std.crypto.tls;
pub fn fetchHttps(allocator: std.mem.Allocator, hostname: []const u8) !void {
// 建立 TCP 连接
const stream = try std.net.tcpConnectToHost(allocator, hostname, 443);
defer stream.close();
// 升级为 TLS<br> var client = try tls.Client.init(stream, .{
.host = .{ .explicit = hostname },
// 证书验证设置
.ca_bundle = .{ .system = {} },
});
defer client.deinit();
// 发送 HTTP GET 请求(TLS 加密传输)
const request = "GET / HTTP/1.1\r\nHost: " ++ hostname ++ "\r\n\r\n";
_ = try client.write(request);
var buf: [4096]u8 = undefined;
const n = try client.read(&buf);
std.debug.print("{s}\n", .{buf[0..n]});
}
4.2 mbedtls-zig 绑定(生产推荐)
标准库的 std.crypto.tls 尚在发展,生产环境推荐 mbedtls-zig 或 bearssl-zig:
// 伪代码示意 mbedtls 绑定风格
const mbedtls = @cImport({
@cInclude("mbedtls/ssl.h");
@cInclude("mbedtls/net_sockets.h");
});
// 初始化 SSL 上下文
var ssl_ctx: mbedtls.mbedtls_ssl_context = undefined;
mbedtls.mbedtls_ssl_init(&ssl_ctx);
// 配置证书、握手、读写
mbedtls 提供了完整的 TLS 1.3 服务端/客户端、证书链验证、ALPN、SNI 等企业级功能,通过 @cImport 绑定后可在 Zig 中直接使用。
5. 密钥派生:Argon2 与 Scrypt
5.1 Argon2(密码哈希竞赛冠军)
Argon2 是密码哈希竞赛(Password Hashing Competition)的获胜算法,提供了抗 GPU/ASIC 攻击的能力。
const std = @import("std");
const crypto = std.crypto;
pub fn deriveKeyArgon2(
password: []const u8,
salt: [16]u8,
params: crypto.pwhash.argon2.Params,
out_key: []u8,
) !void {
try crypto.pwhash.argon2.hash(
out_key,
password,
salt,
params,
crypto.pwhash.argon2.Mode.argon2id,
);
}
// 使用示例
pub fn main() !void {
const password = "correct horse battery staple";
var salt: [16]u8 = undefined;
std.crypto.random.bytes(&salt); // 安全随机数
var key: [32]u8 = undefined;
try deriveKeyArgon2(password, salt, .{
.t = 3, // 迭代次数
.m = 65536, // 内存消耗 64 MB
.p = 4, // 并行度
}, &key);
std.debug.print("derived key: {x}\n", .{std.fmt.fmtSliceHexLower(&key)});
}
参数选择:
t(时间):迭代轮数,越高越慢但越安全。Web 登录场景通常 2-3 轮。m(内存):KB 为单位的内存消耗。64 MB(65536)是折中选择;高安全场景可用 256 MB+。p(并行度):线程数,不超过 CPU 核心数。
5.2 Scrypt(经典选择)
pub fn deriveKeyScrypt(
password: []const u8,
salt: [32]u8,
log2_n: u6, // CPU/内存成本参数(如 15 = 2^15 = 32768)
r: u30, // 块大小(通常 8)
p: u30, // 并行化因子(通常 1)
out_key: []u8,
) !void {
try crypto.pwhash.scrypt.kdf(allocator, out_key, password, salt, log2_n, r, p);
}
Scrypt 已被 Argon2 超越,但在已有系统中仍广泛使用。
6. 安全随机数生成
6.1 CSPRNG 使用
const std = @import("std");
pub fn main() !void {
var buf: [32]u8 = undefined;
// 使用 std.crypto.random(操作系统熵源 + CSPRNG)
std.crypto.random.bytes(&buf);
std.debug.print("random: {x}\n", .{std.fmt.fmtSliceHexLower(&buf)});
}
std.crypto.random 是线程安全的 CSPRNG,底层在 Linux 上读取 /dev/urandom,在 Windows 上调用 BCryptGenRandom,并可能使用 getrandom 系统调用。
6.2 密钥生成最佳实践
pub fn generateKey(comptime len: usize) [len]u8 {
var key: [len]u8 = undefined;
std.crypto.random.bytes(&key);
return key;
}
// 生成各种长度的密钥
const aes256_key = generateKey(32); // AES-256
const chacha_key = generateKey(32); // ChaCha20
const hmac_key = generateKey(64); // HMAC-SHA512
绝不使用
std.rand.DefaultPrng进行密码学操作。DefaultPrng 是伪随机数生成器,不满足密码学安全要求。
7. 密码学编程安全红线
7.1 常见错误
| 错误 | 后果 | 正确做法 |
|---|---|---|
| nonce 重用(GCM) | 密钥恢复,全密文解密 | 计数器或随机 nonce |
| 使用 DefaultPrng 生成密钥 | 密钥可预测 | 使用 std.crypto.random |
| 忽略解密错误返回值 | 接受伪造数据 | try decrypt 检查认证标签 |
| 密码直接当密钥 | 字典攻击 | Argon2/Scrypt 密钥派生 |
| 明文传输密钥 | 中间人窃取 | TLS 1.3 + 证书固定 |
7.2 常量时间比较
防止时序攻击:
const crypto = std.crypto;
pub fn verifyMac(expected: [16]u8, actual: [16]u8) bool {
return crypto.utils.timingSafeEql([16]u8, expected, actual);
}
绝不能用 std.mem.eql 比较 MAC/签名:它会提前短路,泄露差异位置信息。
8. 速查表
| 需求 | 函数/类型 |
|---|---|
| AES-256-GCM | crypto.aead.aes_gcm.Aes256Gcm |
| XChaCha20Poly1305 | crypto.aead.chacha_poly.XChaCha20Poly1305 |
| 安全随机数 | std.crypto.random.bytes(&buf) |
| Argon2 密钥派生 | crypto.pwhash.argon2.hash(...) |
| 常量时间比较 | crypto.utils.timingSafeEql |
| TLS 1.3 客户端 | std.crypto.tls.Client(实验性) |
| 生产 TLS | mbedtls-zig / bearssl-zig |
| 哈希 | crypto.hash.blake2.Blake2b256 / crypto.hash.sha2.Sha256 |
9. 一句话记忆
std.crypto 纯 Zig 无动态分配:AES-GCM/XChaCha20 做 AEAD、Argon2 派生密钥、random.bytes 生成安全随机数、timingSafeEql 防时序攻击——TLS 1.3 标注库实验性,生产选 mbedtls-zig,nonce 绝不重用是红线。
相关阅读
- /zig-memory-management/ — 固定数组与所有权
- /zig-comptime-programming/ — 编译期类型约束
- /zig-c-interoperability/ — C 库绑定(mbedtls 导入)
延伸阅读
- /zig-http-server/ — HTTPS 服务与 TLS 配置
- /zig-debugging-profiling/ — 安全审计与 Valgrind 检测
- /zig-testing-quality/ — 密码学单元测试
- [[zig]] — Zig 系统编程专题
// 完整示例:安全随机数 + XChaCha20Poly1305 加解密 + Argon2 密钥派生
const std = @import("std");
const crypto = std.crypto;
const XChaCha20Poly1305 = crypto.aead.chacha_poly.XChaCha20Poly1305;
pub fn main() !void {
// ===== 1. 生成安全随机密钥和 nonce =====
var key: [32]u8 = undefined;
var nonce: [24]u8 = undefined;
std.crypto.random.bytes(&key);
std.crypto.random.bytes(&nonce);
const plaintext = "Zig 密码学编程示例";
var ciphertext: [64]u8 = undefined;
var tag: [16]u8 = undefined;
// ===== 2. 加密 =====
var ctx = XChaCha20Poly1305.init(&key);
ctx.encrypt(
ciphertext[0..plaintext.len],
&tag,
plaintext,
"associated-data",
nonce,
);
std.debug.print("加密完成,tag = {x}\n", .{std.fmt.fmtSliceHexLower(&tag)});
// ===== 3. 解密并验证 =====
var decrypted: [64]u8 = undefined;
try XChaCha20Poly1305.init(&key).decrypt(
decrypted[0..plaintext.len],
ciphertext[0..plaintext.len],
tag,
"associated-data",
nonce,
);
std.debug.print("解密结果: {s}\n", .{decrypted[0..plaintext.len]});
// ===== 4. Argon2 密钥派生演示 =====
var salt: [16]u8 = undefined;
std.crypto.random.bytes(&salt);
var derived: [32]u8 = undefined;
try crypto.pwhash.argon2.hash(
&derived,
"my-super-secret-password",
salt,
.{ .t = 3, .m = 65536, .p = 4 },
.argon2id,
);
std.debug.print("派生密钥: {x}\n", .{std.fmt.fmtSliceHexLower(&derived)});
}
继续阅读
探索更多技术文章
浏览归档,发现更多关于系统设计、工具链和工程实践的内容。