You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.06.13 22:33:19