背景:
想要对沙盒内的文件做一层保护,防止有人越狱后随意读取沙盒内的关键信息,比如下载的音视频、关键的图片等信息。
思路:
把需要防护的资源分成多段,比如一个300kb的图片,分成2部分,其中第一个文件的尾部和第二个文件的头部 有若干字节重复,用户单独拿到任何一个文件都无法使用,通过拼接也需要知道重叠的部分是多少字节,组合的算法是什么。提升破解的成本。
demo只是验证想法,没有做很多异常处理
- 默认文件是大于1KB的,对于小于1KB的没有做异常处理
- demo中文件只能分割成2个,可以做成可配置的N个文件分割
- 拼接还可以使用固定的二进制数据,拼接文件开头或结尾
- 不论哪种拼法,目的都是使这个文件无法单独使用,必须组合使用,而组合的算法是只有自己知道的
- 对纯文本的防护:
- 思路1:单纯分割,起不到加密的作用,拿到单独的文件还是能拼接处完成内容。
- 1.可以对文本内容进行一次AES加密
- 2.然后在分割文件,
- 3.合并文件完成后,
- 4.使用时在进行AES解密,获取原始内容
- 思路2:其实AES加密已经能做到防护了,单独使用AES加密已满足条件
- 1.可以对文本内容进行一次AES加密
- 2.保存到磁盘
- 3.使用时在进行AES解密,获取原始内容
- 思路1:单纯分割,起不到加密的作用,拿到单独的文件还是能拼接处完成内容。
外界调用:
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)是文件起始位置存储的元数据,用于描述文件的基本属性、格式、结构等信息。不同文件类型的头部内容不同,但通常包含以下关键信息:
-
文件标识符(Magic Number)用于标识文件类型,例如:
- ZIP文件:
50 4B 03 04
(PK头) - PDF文件:
%PDF-
- PNG文件:
89 50 4E 47 0D 0A 1A 0A
- ZIP文件:
-
文件大小:文件总长度或分块大小(如音频、视频文件的时长或数据块长度)。
-
编码方式:文本文件的字符编码(如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秒 |