AES-256-GCM加密内存过载求助:Swift大文件加密内存过高崩溃
解决Swift中AES-256-GCM大文件加密/解密的内存过载问题
你的问题核心是一次性将整个文件加载到内存中处理,CryptoKit其实提供了增量式的AES-GCM加密/解密接口,不需要依赖第三方库就能实现流式处理,以下是针对2024/2025 Swift版本的高效实现方案:
流式加密实现
通过AES.GCM.Sealer进行增量加密,分块读取源文件,每处理一块就写入到目标文件,内存仅占用单块数据的大小:
import CryptoKit import Foundation func encryptFileStreaming(sourceURL: URL, destinationURL: URL, thumbnailData: Data?, keyString: String) throws { guard let key = getKey(from: keyString) else { throw NSError(domain: "EncryptionError", code: -1, userInfo: [NSLocalizedDescriptionKey: "Invalid key"]) } // 处理缩略图加密(单独加密,和内容用相同密钥) let finalThumbnailData = thumbnailData ?? generatePlaceholderThumbnail().jpegData(compressionQuality: 0.7) ?? Data() let thumbnailSealedBox = try AES.GCM.seal(finalThumbnailData, using: key) guard let thumbnailEncryptedData = thumbnailSealedBox.combined else { throw NSError(domain: "EncryptionError", code: -2, userInfo: [NSLocalizedDescriptionKey: "Failed to encrypt thumbnail"]) } // 写入头部:缩略图长度(小端序) let thumbnailLength = UInt32(thumbnailEncryptedData.count).littleEndian let headerData = withUnsafeBytes(of: thumbnailLength) { Data($0) } let fileManager = FileManager.default try fileManager.createDirectory(at: destinationURL.deletingLastPathComponent(), withIntermediateDirectories: true) // 打开目标文件用于写入 let destinationFileHandle = try FileHandle(forWritingTo: destinationURL) defer { destinationFileHandle.closeFile() } // 写入头部和缩略图加密数据 try destinationFileHandle.write(contentsOf: headerData) try destinationFileHandle.write(contentsOf: thumbnailEncryptedData) // 初始化内容加密的Sealer let nonce = AES.GCM.Nonce() var sealer = try AES.GCM.Sealer(key: key, nonce: nonce) // 先写入nonce到文件(解密时需要用到) try destinationFileHandle.write(contentsOf: nonce.data) // 分块读取源文件,推荐块大小64KB-1MB,根据需求调整 let blockSize = 64 * 1024 // 64KB let sourceFileHandle = try FileHandle(forReadingFrom: sourceURL) defer { sourceFileHandle.closeFile() } while true { let blockData = sourceFileHandle.readData(ofLength: blockSize) guard !blockData.isEmpty else { break } // 增量更新加密 try sealer.update(with: blockData) // 写入加密后的块数据 if let encryptedBlock = sealer.output { try destinationFileHandle.write(contentsOf: encryptedBlock) } } // 完成加密,获取最终的tag并写入 let sealedBox = try sealer.finalize() try destinationFileHandle.write(contentsOf: sealedBox.tag) }
流式解密实现
对应加密的格式,分块读取加密文件,用AES.GCM.OpeningUpdater增量解密:
func decryptFileStreaming(sourceURL: URL, destinationURL: URL, keyString: String) throws -> Data? { guard let key = getKey(from: keyString) else { throw NSError(domain: "DecryptionError", code: -1, userInfo: [NSLocalizedDescriptionKey: "Invalid key"]) } let sourceFileHandle = try FileHandle(forReadingFrom: sourceURL) defer { sourceFileHandle.closeFile() } // 读取头部:缩略图长度 let headerData = sourceFileHandle.readData(ofLength: MemoryLayout<UInt32>.size) guard headerData.count == MemoryLayout<UInt32>.size else { throw NSError(domain: "DecryptionError", code: -2, userInfo: [NSLocalizedDescriptionKey: "Invalid header"]) } let thumbnailLength = headerData.withUnsafeBytes { $0.load(as: UInt32.self) }.littleEndian // 读取缩略图加密数据并解密 let thumbnailEncryptedData = sourceFileHandle.readData(ofLength: Int(thumbnailLength)) let thumbnailSealedBox = try AES.GCM.SealedBox(combined: thumbnailEncryptedData) let thumbnailData = try AES.GCM.open(thumbnailSealedBox, using: key) // 读取nonce let nonceData = sourceFileHandle.readData(ofLength: AES.GCM.Nonce.byteCount) guard let nonce = try? AES.GCM.Nonce(data: nonceData) else { throw NSError(domain: "DecryptionError", code: -3, userInfo: [NSLocalizedDescriptionKey: "Invalid nonce"]) } // 读取tag(最后16字节) sourceFileHandle.seekToEndOfFile() let tagOffset = sourceFileHandle.offsetInFile - AES.GCM.Tag.byteCount sourceFileHandle.seek(toFileOffset: tagOffset) let tagData = sourceFileHandle.readData(ofLength: AES.GCM.Tag.byteCount) guard let tag = try? AES.GCM.Tag(data: tagData) else { throw NSError(domain: "DecryptionError", code: -4, userInfo: [NSLocalizedDescriptionKey: "Invalid tag"]) } // 回到内容加密数据的起始位置 sourceFileHandle.seek(toFileOffset: UInt64(headerData.count + thumbnailEncryptedData.count + nonceData.count)) // 初始化解密Updater var updater = try AES.GCM.OpeningUpdater(key: key, nonce: nonce, tag: tag) let fileManager = FileManager.default try fileManager.createDirectory(at: destinationURL.deletingLastPathComponent(), withIntermediateDirectories: true) let destinationFileHandle = try FileHandle(forWritingTo: destinationURL) defer { destinationFileHandle.closeFile() } let blockSize = 64 * 1024 // 和加密时一致 while sourceFileHandle.offsetInFile < tagOffset { let remainingBytes = tagOffset - sourceFileHandle.offsetInFile let readLength = min(Int(remainingBytes), blockSize) let blockData = sourceFileHandle.readData(ofLength: readLength) guard !blockData.isEmpty else { break } // 增量更新解密 try updater.update(with: blockData) // 写入解密后的块数据 if let decryptedBlock = updater.output { try destinationFileHandle.write(contentsOf: decryptedBlock) } } // 完成解密,验证tag try updater.finalize() return thumbnailData }
关键优化点
- 块大小选择:推荐64KB到1MB,太小会增加IO次数,太大则占用更多内存,可根据设备性能调整。
- 避免内存拷贝:直接用
FileHandle进行读写,减少Data的中间拷贝。 - 错误处理:完善的错误抛出逻辑,便于定位问题。
- 兼容性:基于CryptoKit官方API,适配最新Swift版本(包括2024/2025的iOS/macOS版本)。
不需要依赖第三方库,这套方案可以处理任意大小的文件,内存占用稳定在块大小的范围内,彻底解决内存过载问题。
内容的提问来源于stack exchange,提问作者Tthecodedog
相关产品推荐
相关产品推荐

