在 Swift 中使用 Apple CryptoKit 进行密码学原语操作。适用于 SHA-2 或 SHA-3 哈希计算、生成 HMAC、AES-GCM 或 ChaChaPoly 对称加密、P256/P384/P521/Curve25519 或 ML-DSA 密钥签名、执行 ECDH、HPKE、ML-KEM 或 X-Wing 密钥协商/交换、使用 Secure Enclave 保护下的 CryptoKit 密钥,以及将 CommonCrypto 代码迁移至 CryptoKit 的场景。
CryptoKit
Apple CryptoKit 提供了 Swift 原生的密码学操作 API,涵盖哈希计算、消息认证、对称加密、公钥签名、密钥协商、HPKE、抗量子密钥封装与签名,以及基于 Secure Enclave 硬件保护的密钥管理。大部分核心原语可在 iOS 13+ 上使用;HPKE 需要 iOS 17+,而 SHA-3 及后量子密码学 API 则需要 iOS 26+。在针对 Swift 6.3+ 开发新的密码学原语代码时,推荐优先使用 CryptoKit,而非 CommonCrypto 或底层 Security 框架 API。
目录
哈希计算
在 iOS 13+ 上推荐使用 SHA256/SHA384/SHA512;SHA3_256/SHA3_384/SHA3_512 则需要 iOS 26+。所有哈希算法均遵循 HashFunction 协议。
单次哈希(One-shot hashing)
import CryptoKit
let data = Data("Hello, world!".utf8)
let digest = SHA256.hash(data: data)
let hex = digest.compactMap { String(format: "%02x", $0) }.joined()
SHA-3 可用性检查
除非项目的最低部署目标(Deployment Target)设为 iOS 26+,否则使用 SHA-3 前必须进行可用性检查:
if #available(iOS 26.0, *) {
let digest = SHA3_256.hash(data: data)
}
增量哈希(Incremental hashing)
处理大文件或流式输入时,推荐使用增量哈希:
var hasher = SHA256()
hasher.update(data: chunk1)
hasher.update(data: chunk2)
let digest = hasher.finalize()
摘要比对(Digest comparison)
对 CryptoKit 摘要值进行相等性比较时,请直接使用 ==。在安全敏感的场景下,切勿将摘要转化为字符串或数组后再做比对。
let expected = SHA256.hash(data: reference)
let actual = SHA256.hash(data: received)
if expected == actual {
// 数据完整性校验通过
}
HMAC
当协议需要基于密钥的消息认证时,请使用 HMAC;校验时应用 isValidAuthenticationCode,而不是手动序列化后比对。
计算认证码
let key = SymmetricKey(size: .bits256)
let data = Data("message".utf8)
let mac = HMAC<SHA256>.authenticationCode(for: data, using: key)
校验认证码
let isValid = HMAC<SHA256>.isValidAuthenticationCode(
mac, authenticating: data, using: key
)
增量计算 HMAC
var hmac = HMAC<SHA256>(key: key)
hmac.update(data: chunk1)
hmac.update(data: chunk2)
let mac = hmac.finalize()
对称加密
CryptoKit 提供了两种带认证的加密算法(AEAD):AES-GCM 与 ChaChaPoly。两者加密后均会输出包含随机数(Nonce)、密文和认证标签(Authentication Tag)的密封盒(Sealed Box)。
AES-GCM
对称加密的默认首选。在 Apple 芯片上享有硬件加速。
let key = SymmetricKey(size: .bits256)
let plaintext = Data("Secret message".utf8)
// 加密
let sealedBox = try AES.GCM.seal(plaintext, using: key)
let ciphertext = sealedBox.combined! // nonce + ciphertext + tag
// 解密
let box = try AES.GCM.SealedBox(combined: ciphertext)
let decrypted = try AES.GCM.open(box, using: key)
ChaChaPoly
在缺乏 AES 硬件加速的设备上,或需要与特定协议(如 TLS、WireGuard)互操作且要求 ChaCha20-Poly1305 时使用 ChaChaPoly。
let sealedBox = try ChaChaPoly.seal(plaintext, using: key)
let combined = sealedBox.combined // 对于 ChaChaPoly 而言始终非可选值
let box = try ChaChaPoly.SealedBox(combined: combined)
let decrypted = try ChaChaPoly.open(box, using: key)
附加认证数据(Authenticated data)
两种加密算法均支持附加认证数据(AAD)。AAD 只参与认证而不进行加密——适用于既需要明文传输又需要防篡改的元数据。
let header = Data("v1".utf8)
let sealedBox = try AES.GCM.seal(
plaintext, using: key, authenticating: header
)
let decrypted = try AES.GCM.open(
sealedBox, using: key, authenticating: header
)
对于 AES-256-GCM 或 ChaChaPoly,建议默认使用 .bits256 作为 SymmetricKey 的长度。从现有数据创建密钥的方法如下:
let key = SymmetricKey(data: existingKeyData)
公钥签名
CryptoKit 支持基于 NIST 曲线的 ECDSA 签名,以及通过 Curve25519 实现的 Ed25519 签名。
NIST 曲线:P256, P384, P521
let signingKey = P256.Signing.PrivateKey()
let publicKey = signingKey.publicKey
// 签名
let signature = try signingKey.signature(for: data)
// 验签
let isValid = publicKey.isValidSignature(signature, for: data)
P384 与 P521 的 API 完全一致,只需替换曲线名称即可。
NIST 密钥支持 DER、PEM、X9.63 以及 Raw(原始字节)表达形式。详见 references/cryptokit-patterns.md 了解序列化示例。
Curve25519 / Ed25519
let signingKey = Curve25519.Signing.PrivateKey()
let publicKey = signingKey.publicKey
// 签名
let signature = try signingKey.signature(for: data)
// 验签
let isValid = publicKey.isValidSignature(signature, for: data)
Curve25519 密钥仅支持 rawRepresentation(不支持 DER/PEM/X9.63)。
选择合适的曲线
| 曲线 | 签名方案 | 密钥长度 | 典型用途 |
|---|---|---|---|
| P256 | ECDSA | 256-bit | 通用场景;支持 Secure Enclave |
| P384 | ECDSA | 384-bit | 更高安全要求的场景 |
| P521 | ECDSA | 521-bit | NIST 级别的最高安全等级 |
| Curve25519 | Ed25519 | 256-bit | 高性能;API 简洁;不支持 Secure Enclave |
默认建议使用 P256。需要与基于 Ed25519 的协议互操作时请选择 Curve25519。
密钥协商
密钥协商允许通信双方使用各自的公私钥对,通过 ECDH 衍生出共同的共享对称密钥。
基于 P256 的 ECDH
// Alice
let aliceKey = P256.KeyAgreement.PrivateKey()
// Bob
let bobKey = P256.KeyAgreement.PrivateKey()
// Alice 计算共享密钥
let sharedSecret = try aliceKey.sharedSecretFromKeyAgreement(
with: bobKey.publicKey
)
// 使用 HKDF 衍生对称密钥
let symmetricKey = sharedSecret.hkdfDerivedSymmetricKey(
using: SHA256.self,
salt: Data("salt".utf8),
sharedInfo: Data("my-app-v1".utf8),
outputByteCount: 32
)
Bob 使用自己的私钥和 Alice 的公钥能够计算出相同的 sharedSecret,从而双方衍生出相同的 symmetricKey。
基于 Curve25519 的 ECDH
let aliceKey = Curve25519.KeyAgreement.PrivateKey()
let bobKey = Curve25519.KeyAgreement.PrivateKey()
let sharedSecret = try aliceKey.sharedSecretFromKeyAgreement(
with: bobKey.publicKey
)
let symmetricKey = sharedSecret.hkdfDerivedSymmetricKey(
using: SHA256.self,
salt: Data(),
sharedInfo: Data("context".utf8),
outputByteCount: 32
)
密钥衍生函数(KDF)
SharedSecret 不能直接当作 SymmetricKey 使用,必须始终通过以下方法之一衍生密钥:
| 方法 | 标准 | 用途 |
|---|---|---|
hkdfDerivedSymmetricKey |
HKDF (RFC 5869) | 推荐的默认方法 |
x963DerivedSymmetricKey |
ANSI X9.63 | 与 ANSI X9.63 系统互操作 |
请始终传入非空的 sharedInfo 字符串,以将衍生出的密钥绑定到具体的协议上下文中。
HPKE
iOS 17+ 提供了基于公钥加密工作流的 HPKE 功能。向接收方公钥加密数据时,相比手动拼装 ECDH + HKDF + AEAD 协议,更推荐使用 HPKE。
let info = Data("my-protocol-v1".utf8)
let recipientKey = Curve25519.KeyAgreement.PrivateKey()
var sender = try HPKE.Sender(
recipientKey: recipientKey.publicKey,
ciphersuite: .Curve25519_SHA256_ChachaPoly,
info: info
)
let encapsulatedKey = sender.encapsulatedKey
let ciphertext = try sender.seal(
plaintext,
authenticating: Data("metadata".utf8)
)
var recipient = try HPKE.Recipient(
privateKey: recipientKey,
ciphersuite: .Curve25519_SHA256_ChachaPoly,
info: info,
encapsulatedKey: encapsulatedKey
)
HPKE.Sender 和 HPKE.Recipient 是有状态的;请声明为 var 变量,将 encapsulatedKey 连同密文一起发送,并按加密时的相同顺序解密消息。算法套件选择和后量子 HPKE 详情参见 references/cryptokit-patterns.md。
后量子 CryptoKit
iOS 26+ 新增了抗量子密码学 API:
- 密钥封装:
MLKEM768、MLKEM1024 - 混合 HPKE:配合
.XWingMLKEM768X25519_SHA256_AES_GCM_256使用XWingMLKEM768X25519 - 数字签名:
MLDSA65、MLDSA87 - Secure Enclave 变体:
SecureEnclave.MLKEM768、SecureEnclave.MLKEM1024、SecureEnclave.MLDSA65、SecureEnclave.MLDSA87
在过渡迁移阶段,若同时关注经典密码学安全性与抗量子安全性,建议采用混合机制。注意后量子算法的公钥、密文和签名体积要远大于 P256 或 Curve25519。
Secure Enclave
Secure Enclave 提供了基于硬件的安全密钥存储,私钥永远不会离开硬件芯片。对于经典椭圆曲线 CryptoKit,Secure Enclave 支持 P256 签名与密钥协商。在支持 iOS 26+ 的硬件设备上,CryptoKit 还公开了 Secure Enclave 版本的 ML-KEM 密钥封装和 ML-DSA 数字签名类型。
可用性检查
guard SecureEnclave.isAvailable else {
// 降级使用软件密钥
return
}
创建 Secure Enclave 签名密钥
let privateKey = try SecureEnclave.P256.Signing.PrivateKey()
let publicKey = privateKey.publicKey // 标准的 P256.Signing.PublicKey
let signature = try privateKey.signature(for: data)
let isValid = publicKey.isValidSignature(signature, for: data)
访问控制
当密钥需要生物识别或锁屏密码鉴权保护时,请配合 .privateKeyUsage 使用 SecAccessControl。更加详细的 Keychain 策略决策建议放在 swift-security 领域处理。
持久化 Secure Enclave 密钥
dataRepresentation 是一个加密的数据块(blob),仅有生成它的同一台设备的 Secure Enclave 能够恢复它。推荐存储在 Keychain 中。
// 导出
let blob = privateKey.dataRepresentation
// 恢复
let restored = try SecureEnclave.P256.Signing.PrivateKey(
dataRepresentation: blob
)
Secure Enclave 密钥协商
let seKey = try SecureEnclave.P256.KeyAgreement.PrivateKey()
let peerPublicKey: P256.KeyAgreement.PublicKey = // 来自对端
let sharedSecret = try seKey.sharedSecretFromKeyAgreement(
with: peerPublicKey
)
常见误区
1. 直接将共享密钥作为加密密钥使用
// ❌ 错误写法
let badKey = sharedSecret.withUnsafeBytes { bytes in
SymmetricKey(data: Data(bytes))
}
// ✅ 正确写法 -- 使用 HKDF 衍生密钥
let goodKey = sharedSecret.hkdfDerivedSymmetricKey(
using: SHA256.self,
salt: salt,
sharedInfo: info,
outputByteCount: 32
)
2. 重用随机数(Nonce)
// ❌ 错误写法 -- 硬编码 Nonce
let nonce = try AES.GCM.Nonce(data: Data(repeating: 0, count: 12))
let box = try AES.GCM.seal(data, using: key, nonce: nonce)
// ✅ 正确写法 -- 让 CryptoKit 自动生成随机 Nonce(默认行为)
let box = try AES.GCM.seal(data, using: key)
3. 忽略认证标签校验
// ❌ 错误写法 -- 手动剥离认证标签后解密
// ✅ 正确写法 -- 始终使用 AES.GCM.open() 或 ChaChaPoly.open(),它们会自动校验认证标签
4. 为了安全性使用不安全的哈希算法
// ❌ 错误写法 -- 为了完整性或安全性使用 MD5/SHA1
import CryptoKit
let bad = Insecure.MD5.hash(data: data)
// ✅ 正确写法 -- 使用 SHA256 或更强的哈希算法
let good = SHA256.hash(data: data)
Insecure.MD5 与 Insecure.SHA1 的存在仅用于兼容旧系统(如校验和比对、旧协议互操作)。切勿在全新的安全敏感业务中使用它们。
5. 将对称密钥直接保存在 UserDefaults 中
// ❌ 错误写法
UserDefaults.standard.set(rawKeyData, forKey: "en...




