iOS基于CryptoKit的大文件加密解密优化方案及分块疑问
基于CryptoKit的大文件/媒体加密解密优化方案
内存优化核心思路
直接避免一次性加载全量数据到内存,采用固定块大小的流式分块处理:每次从文件读取指定大小的二进制数据块,完成加密/解密后立即写入目标文件,随后释放该块内存,循环直至处理完整个文件。
流式分块加密实现(AES-GCM为例)
AES-GCM是CryptoKit推荐的认证加密算法,支持增量式加密,完美适配流式处理:
参数选择:
- 块大小:建议选64KB(
64 * 1024字节),平衡内存占用与IO效率;若设备内存充裕,可调整为1MB - 密钥:使用
AES.GCM.Key,可通过AES.GCM.Key.generate()生成 - Nonce:每个加密会话唯一,长度12字节(CryptoKit默认推荐长度),需与密文一同存储(可写入密文文件头部)
- 块大小:建议选64KB(
代码实现:
import CryptoKit import Foundation func encryptLargeFile(at inputURL: URL, to outputURL: URL, key: AES.GCM.Key) throws { let nonce = AES.GCM.Nonce() var encryptionContext = AES.GCM.EncryptionContext(key: key, nonce: nonce) // 打开文件句柄 let inputHandle = try FileHandle(forReadingFrom: inputURL) defer { inputHandle.closeFile() } let outputHandle = try FileHandle(forWritingTo: outputURL) defer { outputHandle.closeFile() } // 先写入nonce到文件头部 try outputHandle.write(contentsOf: nonce.withUnsafeBytes { Data($0) }) let chunkSize = 64 * 1024 while true { let chunkData = inputHandle.readData(ofLength: chunkSize) guard !chunkData.isEmpty else { break } // 更新加密上下文,写入当前加密后的片段 let encryptedChunk = encryptionContext.update(data: chunkData) try outputHandle.write(contentsOf: encryptedChunk) } // 完成加密,写入认证标签到文件末尾 let sealedBox = encryptionContext.finalize() try outputHandle.write(contentsOf: sealedBox.tag) }
分块解密的关键解决方法
针对你提到的「加密块长度不一」困惑,若采用上述单会话流式加密方案,解密时无需拆分不规则块,只需按加密逻辑反向操作:
- 从密文文件头部读取存储的nonce
- 从文件末尾读取16字节的认证标签(AES-GCM标签固定16字节)
- 对剩余的密文数据分块流式解密,最后验证标签
代码实现:
func decryptLargeFile(at inputURL: URL, to outputURL: URL, key: AES.GCM.Key) throws { let fileSize = try FileManager.default.attributesOfItem(atPath: inputURL.path)[.size] as! UInt64 let nonceSize = AES.GCM.Nonce.byteCount // 12字节 let tagSize = AES.GCM.Tag.byteCount // 16字节 guard fileSize >= nonceSize + tagSize else { throw NSError(domain: "EncryptionError", code: -1, userInfo: [NSLocalizedDescriptionKey: "无效的加密文件"]) } let inputHandle = try FileHandle(forReadingFrom: inputURL) defer { inputHandle.closeFile() } let outputHandle = try FileHandle(forWritingTo: outputURL) defer { outputHandle.closeFile() } // 读取nonce let nonceData = inputHandle.readData(ofLength: nonceSize) guard let nonce = AES.GCM.Nonce(data: nonceData) else { throw NSError(domain: "EncryptionError", code: -2, userInfo: [NSLocalizedDescriptionKey: "无效的nonce"]) } // 定位读取认证标签 inputHandle.seek(toFileOffset: fileSize - UInt64(tagSize)) let tagData = inputHandle.readData(ofLength: tagSize) guard let tag = AES.GCM.Tag(data: tagData) else { throw NSError(domain: "EncryptionError", code: -3, userInfo: [NSLocalizedDescriptionKey: "无效的认证标签"]) } // 回到密文起始位置 inputHandle.seek(toFileOffset: UInt64(nonceSize)) let ciphertextEndOffset = fileSize - UInt64(tagSize) var decryptionContext = AES.GCM.DecryptionContext(key: key, nonce: nonce) let chunkSize = 64 * 1024 while inputHandle.offsetInFile < ciphertextEndOffset { let remainingBytes = ciphertextEndOffset - inputHandle.offsetInFile let readSize = min(UInt64(chunkSize), remainingBytes) let ciphertextChunk = inputHandle.readData(ofLength: Int(readSize)) let plaintextChunk = try decryptionContext.update(data: ciphertextChunk) try outputHandle.write(contentsOf: plaintextChunk) } // 验证标签并完成解密 try decryptionContext.finalize(tag: tag) }
额外注意事项
- Nonce必须唯一:同一密钥下,绝不能重复使用相同nonce,否则会导致加密安全性完全失效
- 文件IO优化:始终使用
FileHandle而非Data(contentsOf:),后者会一次性加载全量数据 - 错误处理:需完善文件读写、加密解密过程中的异常捕获,比如文件权限、数据损坏等情况
内容的提问来源于stack exchange,提问作者sudoExclamationExclamation
相关产品推荐
相关产品推荐

