有时候我们会把资源文件放在本地,但是又不想让这些资源被其它利用,那么如何加密iOS本地的资源文件呢?
此方案适合小型资源文件加密,包括但不限于plist、html、txt、jpg、png、xml等,不适合视频资源加密,如果你有好的方案欢迎交流。
一、此方案优势
我的提供的方案具有以下优点:
- 一劳永逸。传统的做法是在外部把资源文件加密后在拖入工程内,如果修改了资源文件(比如plist配置文件可能会经常修改)需要重新加密后导入,很麻烦。我的这个方案配置一次后,可以直接在工程中修改资源文件,无需再次加密和导入。
- 静默无感知。加密过程在Xcode编译阶段自动进行,程序员无感知。
- 支持多种格式。理论上支持所有的资源文件格式,包括但不限于plist、html、txt、jpg、png、xml等。由于原理局限性,不适合过大的文件和流文件,比如视频。
- 安全。采用AES加密,相对比较安全。
二、原理
Xcode编译阶段可以运行脚本,我们用脚本加密原始资源文件,生成的密文存放到.app目录下,运行时用[NSBundle mainBulde]读取加密后的资源文件并解密。
因为是在编译阶段自动生成密文,所以工程中我们可以直接操作原始文件,从而实现了“一劳永逸”和无感知。不过需要解决以下几个问题:
- 寻找一套脚本语言和原生语言匹配的加密库。
- 如何保护原始资源文件的安全?
- 脚本加密后的文件存放在哪里能够在运行时读取到?
带着上述问题,接下来讲述如何实现这个方案。
三、方案
- 寻找一套脚本语言和原生语言匹配的加密库。
我们需要在编译阶段用脚本语言加密,运行时用原生语言解密,所以我们需要寻找一套脚本语言和原生语言匹配的加密库。
这里我们采用shell脚本,因为Mac自带shell脚本,无需额外安装,Xcode默认的脚本语言也是Shell,这点很方便。
原生语言加密库采用RNOpenSSLCryptor。请注意,这个库的作者没有在维护了,而是推出了新的RNCryptor,但是新的库里面并不包含openssl的加解密,没法和shell脚本配合使用,因此我们这里采用旧的RNOpenSSLCryptor库。
RNCryptor这个新的库很强大,利用这个新的库可以实现传统的资源加密方案,即先在外部加密资源文件,在拖进工程里面。有人写过这种方案的文章,甚至提供了一个Mac小工具方便大家在外部加密资源文件。**如果你的资源文件不经常修改,你可以使用传统的资源加密方式。**
2023.6.15更新
更新到Xcode14以后,RNOpenSSLCryptor库和openssl库不再匹配了,导致解密失败。
楼主已经找到了解决方案,新的库和shell脚本见文末Demo。
- 如何保护原始资源文件的安全?
原始资源文件我们可以拖到工程中,但是不要勾选target,这样打包的时候这个资源文件是不会被打包进ipa包里的,只是关联到你的工程中而已。所以只要你的源码不泄露,原始资源文件是安全的。拖入工程里的文件,Xcode编译时shell脚本是可以读取到的,利用这一点我们可以实现既保护了原始资源文件,又可以通过脚本把资源文件加密后导入ipa包中。
- 脚本加密后的文件存放在哪里能够在运行时读取到?
先看下面这段shell脚本
#$TARGET_BUILD_DIR 和 $UNLOCALIZED_RESOURCES_FOLDER_PATH 是Xcode的两个环境变量,拼起来对应.app的目录
openssl enc -e -aes-256-cbc -K $key -iv $iv -nosalt -in EncrypNativeResourceDemo/myPlist.plist -out $TARGET_BUILD_DIR/$UNLOCALIZED_RESOURCES_FOLDER_PATH/myPlist
这个脚本是将工程中myPlist.plist文件用AES-256-CBC模式加密,把生成的加密后的文件myPlist.enlist(文件名和后缀格式随便取,不要后缀也可以),放到MyApp.ipa包的MyApp.app目录下,.app目录下的资源是可以通过[[NSBundle mainBundle] pathForResource:fileName ofType:nil]
方法来读取的。
看到这里,你应该明白了整个过程——编译时我们通过shell脚本加密myPlist.plist文件并把密文myPlist.enlist保存在Build产生的.app目录下,运行时通过[NSBundle mainBundle]读取myPlist.enlist文件并解密。
四、具体实现
- 下载Demo,将CCOpenSSLHelper目录拖到你的工程中。
- 把待加密的资源文件拖到你的工程里面,不要勾选target!
- 在Xcode配置中添加(菜单栏 - Editor - Add Build Phase - Add Run Script Build Phase)如下脚本,文件路劲换成你自己的,这里的文件路径是相对Project的。
- Xcode - Build Phases - Run Script,添加脚本,如果没有Run Script这一栏,请按下图方式添加
注意:修改脚本引擎为 /bin/bash,如下图,Xcode默认是 /bin/sh。不然脚本生成的密钥key会和代码生成的不匹配,导致解密失败,至于为什么楼主也没找到原因。
添加如下脚本(请根据自己的情况修改)
#password需要和代码中保持一致
password="这里填你的密码"
#根据password,利用sha257和md5,生成密钥key和初始向量iv
key=`echo -n $password | shasum -a 256 | awk -F' ' '{print $1}' | tr 'a-z' 'A-Z'`
iv=`md5 -s $password | awk -F'=' '{print $2}' | sed -e 's/^[[:space:]]*//' -e 's/[[:space:]]*$//' | tr 'a-z' 'A-Z'`
#每个需要加密的文件都写在这里
openssl enc -e -aes-256-cbc -K $key -iv $iv -nosalt -in EncrypNativeResourceDemo/myPlist.plist -out $TARGET_BUILD_DIR/$UNLOCALIZED_RESOURCES_FOLDER_PATH/myPlist
openssl enc -e -aes-256-cbc -K $key -iv $iv -nosalt -in EncrypNativeResourceDemo/myImage.jpg -out $TARGET_BUILD_DIR/$UNLOCALIZED_RESOURCES_FOLDER_PATH/myImage
openssl enc -e -aes-256-cbc -K $key -iv $iv -nosalt -in EncrypNativeResourceDemo/baidu.html -out $TARGET_BUILD_DIR/$UNLOCALIZED_RESOURCES_FOLDER_PATH/baidu
openssl enc -e -aes-256-cbc -K $key -iv $iv -nosalt -in EncrypNativeResourceDemo/button.mp3 -out $TARGET_BUILD_DIR/$UNLOCALIZED_RESOURCES_FOLDER_PATH/button
- 代码中使用CCOpenSSLHelper类进行解密
#import "CCOpenSSLHelper.h"
//获取加密的文件
NSString *filePath = [[NSBundle mainBundle] pathForResource:@"myPlist.enlist" ofType:nil];
NSData *data = [[NSData alloc] initWithContentsOfFile:filePath];
//开始解密
NSData *dataDecrypted = [CCOpenSSLHelper decryptData:data password:PASS_WORD];
//TODO: 再将NSData转成对应的文件即可
...
解密出来是一个NSData对象,根据你的文件类型转成你需要的类型即可。我简单地写了一个方法,仅供参考。
//解密资源文件
- (id)decodeResourceFilePath:(NSString *)filePath fileType:(NSString *)fileType
{
if (!filePath) {
return nil;
}
NSData *data = [[NSData alloc] initWithContentsOfFile:filePath];
NSError *error;
NSData *dataDecrypted = [CCOpenSSLHelper decryptData:data password:PASS_WORD];
if ([fileType isEqualToString:@"plist"]) {
id plist = [NSPropertyListSerialization propertyListWithData:dataDecrypted options:(NSPropertyListImmutable) format:nil error:&error];
return plist;
}
else if ([fileType isEqualToString:@"string"]) {
NSString *decodeStr = [[NSString alloc] initWithData:dataDecrypted encoding:(NSUTF8StringEncoding)];
return decodeStr;
}
else if ([fileType isEqualToString:@"image"]) {
UIImage *image = [UIImage imageWithData:dataDecrypted];
return image;
}
return dataDecrypted;
}
Demo已上传到Github,大家可以参考。
常见问题
1.我都配置好了,为什么运行时读取文件路径返回nil?
检查一下你读取的文件名是否和脚本里的密文文件名一致;可能是你修改了脚本中的文件名,脚本生成的文件有缓存,clean一下试试。
参考链接:
how-to-protect-ios-bundle-files-like-plist-image-sqlite-media-files
iOS中使用RNCryptor对资源文件加密(先加密后拖进项目中)
Xcode环境变量
OpenSSL中文手册之命令行详解
Encrypt data using AES and 256-bit keys