iOS 文件分割保存加密

文章讲述了使用FileMergeTool类实现的文件分割策略,通过对大于1KB的文件进行AES加密后分割为两部分,并配置可变数量的文件。分割后的文件需要特定算法组合才能还原,确保了文件安全性。之后提及了使用AES解密合并文件的功能以恢复原始内容。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

背景:

想要对沙盒内的文件做一层保护,防止有人越狱后随意读取沙盒内的关键信息,比如下载的音视频、关键的图片等信息。

思路:

把需要防护的资源分成多段,比如一个300kb的图片,分成2部分,其中第一个文件的尾部和第二个文件的头部 有若干字节重复,用户单独拿到任何一个文件都无法使用,通过拼接也需要知道重叠的部分是多少字节,组合的算法是什么。提升破解的成本。

demo只是验证想法,没有做很多异常处理

  • 默认文件是大于1KB的,对于小于1KB的没有做异常处理
  • demo中文件只能分割成2个,可以做成可配置的N个文件分割
  • 拼接还可以使用固定的二进制数据,拼接文件开头或结尾
    • 不论哪种拼法,目的都是使这个文件无法单独使用,必须组合使用,而组合的算法是只有自己知道的
  • 对纯文本的防护:
    • 思路1:单纯分割,起不到加密的作用,拿到单独的文件还是能拼接处完成内容。
      • 1.可以对文本内容进行一次AES加密
      • 2.然后在分割文件,
      • 3.合并文件完成后,
      • 4.使用时在进行AES解密,获取原始内容
    • 思路2:其实AES加密已经能做到防护了,单独使用AES加密已满足条件
      • 1.可以对文本内容进行一次AES加密
      • 2.保存到磁盘
      • 3.使用时在进行AES解密,获取原始内容

外界调用: 

let tool = FIleMergeTool(path: "/Users/zwyl/Desktop/bigImage.png")

if let data = tool.divideFile() {
    let image = UIImage.init(data: data)
    self.baseImage.image = image
}

if let data = tool.mergeFile() {
    let image = UIImage.init(data: data)
    self.imageView.image = image
}

核心实现: 

import Foundation

class FIleMergeTool: NSObject {

    var path: String
    let prePath = "/Users/zwyl/Desktop/dividerF_file1"
    let lastPath = "/Users/zwyl/Desktop/dividerF_file2"

    init(path: String) {
        self.path = path
        super.init()
    }

    struct Const {
        // firstOffset 一定要 >= lastBegin
        static let firstOffset = 1024
        static let lastBegin = 1000
    }


    func divideFile() -> Data? {
        let url = URL(filePath: self.path)

        let preUrl = URL(filePath: prePath)
        let lastUrl = URL(filePath: lastPath)

        let data = try? Data(contentsOf: url)
        guard let data else {
            return nil
        }

        try? FileManager.default.removeItem(at: preUrl)
        try? FileManager.default.removeItem(at: lastUrl)

        // 获取第一部分
        let preRange = 0 ..< Const.firstOffset
        let preData = data.subdata(in: preRange)
        try? preData.write(to: preUrl)

        // 获取第二部分
        let lastRange = Const.lastBegin ..< data.count
        let lastData = data.subdata(in: lastRange)
        try? lastData.write(to: lastUrl)

        return data
    }

    // 重新读取文件, 拼接Data
    func mergeFile() -> Data? {

        let preUrl = URL(filePath: self.prePath)
        let lastUrl = URL(filePath: self.lastPath)

        let preData = try? Data(contentsOf: preUrl)
        guard let preData else {
            return nil
        }

        let lastData = try? Data(contentsOf: lastUrl)
        guard let lastData else {
            return nil
        }

        // 拼接第一段数据
        var resultData = Data()
        resultData.append(preData)

        // 拼接第二段数据
        let begin = Const.firstOffset - Const.lastBegin
        let otherData = lastData.subdata(in: begin ..< lastData.count)
        resultData.append(otherData)
        return resultData
    }
}

删除重复部分后,继续拼接 ,即可得到原始文件。


上面的思路对于完全不知道算法的人可以起到防护效果,但是攻击者如果知道了算法,就逐个找重复的字节,然后替换掉重复的字节,还是可以获取原始信息的。

由此诞生了下面的一些思考,对整个文件的二进制数据进行加密。

但是实测之后发现对小文件还可以,但是对于几百M、1G以上的大文件在处理效率会很差,实测处理一个200M左右的视频,在iPhone6s上需要7-10秒,对于更大的文件处理时长也是按照文件大小等比例增大,这个方案的性能实在太差。

文件大小和类型

加密时长

解密时长

0.3M 图片

0.02秒

0.01秒

3.41M GIF图片

0.16秒

0.12秒

31M 视频

1.28秒

1.12秒

220M 视频

12.47秒

9.55秒

文件头部(Header)是文件起始位置存储的元数据,用于描述文件的基本属性、格式、结构等信息。不同文件类型的头部内容不同,但通常包含以下关键信息:

  1. 文件标识符(Magic Number)​用于标识文件类型,例如:

    • ZIP文件:50 4B 03 04(PK头)
    • PDF文件:%PDF-
    • PNG文件:89 50 4E 47 0D 0A 1A 0A
  2. 文件大小:文件总长度或分块大小(如音频、视频文件的时长或数据块长度)。

  3. 编码方式:文本文件的字符编码(如UTF-8、GBK),二进制文件的压缩算法(如DEFLATE、LZMA)。

对文件头部的关键信息进行加密,同时在去掉文件的格式后缀,这样的话攻击者根本无法知道文件的类似和内容。

所以最终思路是: 读取 文件的前 byteCount 字节,AES 加密 这部分数据,并将加密后的内容 覆写 回文件的同一位置,而不影响文件的其余部分。

import Foundation
import CommonCrypto

@objc public class FileCryptoTool: NSObject {

    /// 对指定文件的前 byteCount 字节进行就地加密,
    /// 仅对文件开头的 byteCount 字节进行加密操作,其余部分保持不变。
    /// - Parameters:
    ///   - filePath: 原文件路径
    ///   - byteCount: 字节长度必须为16的整数倍, 推荐1024
    @objc public static func inPlaceEncryptFirstBytesOfFile(atPath filePath: String, byteCount: Int) {
        // 打开文件用于更新操作
        guard let fileHandle = FileHandle(forUpdatingAtPath: filePath) else {
            print("无法打开文件:\(filePath)")
            return
        }
        defer { fileHandle.closeFile() }

        // 只读取文件前 byteCount 字节
        let headData = fileHandle.readData(ofLength: byteCount)
        if headData.count < byteCount {
            print("文件不足 \(byteCount) 字节用于加密")
            return
        }

        // 为保证加密后数据长度与输入相同,要求 byteCount 为 AES 块大小的整数倍
        let blockSize = kCCBlockSizeAES128
        if byteCount % blockSize != 0 {
            print("加密字节数必须是 AES 块大小(\(blockSize) 字节)的整数倍")
            return
        }

        // 设置 AES 密钥和 IV(16字节字符串,实际使用时请注意安全性)
        let keyString = "1234567890123456"
        let ivString  = "abcdefghijklmnop"
        guard let keyData = keyString.data(using: .utf8),
              let ivData = ivString.data(using: .utf8) else {
            print("密钥或IV转换失败")
            return
        }
        let keyBytes = [UInt8](keyData)
        let ivBytes = [UInt8](ivData)

        // 分配输出缓冲区(无填充模式下输出与输入长度一致)
        let buffer = UnsafeMutableRawPointer.allocate(byteCount: byteCount, alignment: MemoryLayout<UInt8>.alignment)
        defer { buffer.deallocate() }
        var numBytesEncrypted: size_t = 0

        // 进行 AES 加密(无填充模式)
        let cryptStatus = CCCrypt(CCOperation(kCCEncrypt),
                                  CCAlgorithm(kCCAlgorithmAES128),
                                  CCOptions(0), // 无填充选项
                                  keyBytes, kCCKeySizeAES128,
                                  ivBytes,
                                  (headData as NSData).bytes,
                                  byteCount,
                                  buffer,
                                  byteCount,
                                  &numBytesEncrypted)

        if cryptStatus == kCCSuccess && numBytesEncrypted == byteCount {
            // 直接利用 bytesNoCopy,避免多次内存拷贝(deallocator 设置为 .none,因为 buffer 在 defer 中释放)
            let encryptedData = Data(bytesNoCopy: buffer, count: numBytesEncrypted, deallocator: .none)
            fileHandle.seek(toFileOffset: 0)
            fileHandle.write(encryptedData)

        } else {
            print("AES加密失败,错误码:\(cryptStatus)")
        }
    }


    /// 对指定文件的前 byteCount 字节进行就地解密,
    /// 恢复原始数据,其余部分保持不变。
    @objc public static func inPlaceDecryptFirstBytesOfFile(atPath filePath: String, byteCount: Int) {
        // 打开文件用于更新
        guard let fileHandle = FileHandle(forUpdatingAtPath: filePath) else {
            print("无法打开文件:\(filePath)")
            return
        }
        defer { fileHandle.closeFile() }

        // 读取文件前 byteCount 字节
        let encryptedData = fileHandle.readData(ofLength: byteCount)
        if encryptedData.count < byteCount {
            print("文件不足 \(byteCount) 字节用于解密")
            return
        }

        let blockSize = kCCBlockSizeAES128
        if byteCount % blockSize != 0 {
            print("解密字节数必须是 AES 块大小(\(blockSize) 字节)的整数倍")
            return
        }

        // 与加密时相同的密钥和 IV
        let keyString = "1234567890123456"
        let ivString  = "abcdefghijklmnop"
        guard let keyData = keyString.data(using: .utf8),
              let ivData = ivString.data(using: .utf8) else {
            print("密钥或IV转换失败")
            return
        }
        let keyBytes = [UInt8](keyData)
        let ivBytes = [UInt8](ivData)

        // 分配输出缓冲区(无填充模式下输入和输出长度一致)
        let buffer = UnsafeMutableRawPointer.allocate(byteCount: byteCount, alignment: MemoryLayout<UInt8>.alignment)
        defer { buffer.deallocate() }
        var numBytesDecrypted: size_t = 0

        // 执行 AES 解密
        let cryptStatus = CCCrypt(CCOperation(kCCDecrypt),
                                  CCAlgorithm(kCCAlgorithmAES128),
                                  CCOptions(0), // 无填充选项
                                  keyBytes, kCCKeySizeAES128,
                                  ivBytes,
                                  (encryptedData as NSData).bytes,
                                  byteCount,
                                  buffer,
                                  byteCount,
                                  &numBytesDecrypted)

        if cryptStatus == kCCSuccess && numBytesDecrypted == byteCount {
            let decryptedData = Data(bytesNoCopy: buffer, count: numBytesDecrypted, deallocator: .none)
            fileHandle.seek(toFileOffset: 0)
            fileHandle.write(decryptedData)
        } else {
            print("AES解密失败,错误码:\(cryptStatus)")
        }
    }
}



其中提升效率的关键部分是:

let encryptedData = Data(bytesNoCopy: buffer, count: numBytesEncrypted, deallocator: .none)

fileHandle.seek(toFileOffset: 0)

fileHandle.write(encryptedData)

  • seekToFileOffset:0 将文件指针定位到文件起始位置

  • writeData:encryptedData 从当前位置开始写入数据

    • encryptedData.length == 1024,则 会覆盖原文件的 0-1023 字节,而 不会影响 1024 字节之后的内容

    • writeData: 不会清空文件的其余部分,仅仅是 替换相同长度的数据

这样就避免了对整个文件的操作,并且是就地修改,仅操作固定长度字节数,和单个文件的大小无关,执行效率得到了极大提升。

文件大小和类型

加密时长

解密时长

0.3M 图片

0.003秒

0.003秒

3.41M GIF 图片

0.001秒

0.002秒

30.91M 视频

0.009秒

0.003秒

228M 视频

0.018秒

0.003秒

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值