首先需要明确的是,加密不是绝对的保密,加密仅仅是提高了别人获取原始代码的门槛,不让人轻易地拿到你的代码。
在开始撸代码之前,先要明确一个概念:什么是越狱?
为什么要了解什么是越狱呢?很多人都认为越狱的设备,就会移除沙盒的机制。其实,沙盒的机制依然存在,这是苹果最引以为傲的机制。沙盒也不是绝对安全的,越狱之后,开发者获取到访问/var/stash/Applications/,通过遍历去获取(因为暂时没有找到合适的设备,我没有实际去验证,这是网上普遍的说法),有兴趣的童鞋可以去了解一下,这里贴一个知乎的帖子,就当抛砖引玉了点击打开链接。
- A方案,整体思路是在工程里面放一个加密过的HMTL文件,然后在运行的时候,拷贝到沙盒里面,然后进行解密,解密完了再提给WebView去调用。(延伸的做法,使用APP进入前台就解密,APP进入后台就清除解密文件)
- B方案,整体思路是在工程里面放一个加密过的HMTL文件,然后在运行的时候,拷贝到沙盒里面,然后对webView进行一个扩展,等到去加载页面之前去解码。(延伸做法,解密之后使用loadData:MIMEType:textEncodingName:baseURL类似的方式去加载)
大体流程如下图:
了解的大体的思路之后,我们就开始谈一下,加密的方案。
在日常开发中,我们或多或少都会接触到数据安全,提到数据安全,就想到了数据加密这一块。
加密算法有三大类,密码学中演变过来的。第一类,哈希(散列)算法;第二类,对称加密算法;第三类,非对称加密算法。
1.哈希算法,常用的有MD5,SHA1(256/512)。
2.对称算法,常用的DES, 3DES和AES。
3.非对称算法,RSA。
这里不做太多的说明了,想了解的童鞋们就自己去自行补习一下,毕竟相关的博客都很多,资料也比较充足。
现在上代码,第一步加密,加密算法的选择。
iOS系统中自带了各种的加密算法,很多时候,都不需要从网上去寻找加密的算法,现在每一台计算机上都有加密的算法,系统都有相应的接口,而iOS的加密算法主要是用这个头文件来实现:
#import <CommonCrypto/CommonCrypto.h>
CCCryptorStatus CCCrypt(
CCOperation op, /* kCCEncrypt, etc. */
CCAlgorithm alg, /* kCCAlgorithmAES128, etc. */
CCOptions options, /* kCCOptionPKCS7Padding, etc. */
constvoid*key,
size_tkeyLength,
constvoid *iv, /* optional initialization vector */
constvoid *dataIn, /* optional per op and alg */
size_tdataInLength,
void*dataOut, /* data RETURNED here */
size_tdataOutAvailable,
size_t*dataOutMoved)
__OSX_AVAILABLE_STARTING(__MAC_10_4, __IPHONE_2_0);
我这边举一个AES的CBC加密的方法,所以要用到上面的API。
这里做一个特别说明,AES是属于对称加密算法里面的一种,Apple的NSKeyedArchiver也是使用同一种的加密方式。相比之下,比DES和3DES的加密更合理一些。在对称加密算法中,常用两个类:一个是ECB和CBC两类。
ECB:Electronic Codebook,电码本模式是分组密码的一种最基本的工作模式。简单来说,就是每个内容独立加密,加密方式一致,相互独立,只要破解其中一个加密的内容,以同样的方式就可以破解其他的内容。
CBC:加密块链模式,加密的内容划分成块状,进行加密,加密的内容之间会有密码链将上下的加密块关联的一起,当上面的内容块发生改变,其后的内容也随之改变。
出于安全性的问题,这里就选用了CBC的模式。(这里想了解更多的童鞋可以自己去学习和查阅相关的书籍,资料)
我们需要创建一个工具工程去处理我们文件,起初有一个想法,就是使用lua或者python去做这件事情,但是跟我们的游戏开发的同事去沟通了一下,同事表示lua不太好去操作二进制的文件,另外我对python也不是很熟悉,避免装逼失败,能有Objective-C解决的还是用OC好了。
首先创建一个名为QYJKeyChainTool(工程名字随便起了)。然后创建一个cocospod的Podfile的文件
在终端,CD到你的工程目录下,
1.输入:touch Podfile;
2.输入:vim Podfile ;(打开文件)
3.在文件中输入:
platform:ios, '版本号'
target '工程名字' do
pod 'ZipArchive', '~> 1.4.0'
end
// 例子
platform:ios, '8.0' // 最低支持iOS 8.0
target 'QYJKeyChainTool' do
pod 'ZipArchive', '~> 1.4.0' // 第三方的解压库
end
// 这里我习惯用cocospod,如果不喜欢的同学可以到github上面下载 下载地址,星星很多的
4.ESC键,输入 :wq(冒号, w,q);
5.在终端输入:pod install --repo-update (一般人输入pod install 就结束了,后面那一段是保留现有的第三方库,能更新的更新,不能更新的就跳过,新加入的就新增,指定版本号的就无法更新)
做好上述的准备,就开始写代码:
#import "QYJFileManager.h"
#import "EncryptionTools.h"
#import "ZipArchive.h"
// 文件 NSFileManager
#define QYJFileSingle [NSFileManager defaultManager]
// 沙盒中的Document路径
#define QYJPaths NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, YES)
// 沙盒中Document路径具体的路径
#define QYJDocumentPath ([QYJPaths count] > 0) ? [QYJPaths objectAtIndex:0] : @""
// NSUserDefaulst 存取类 这里用来标识,app是否进行了解码,app进行热更之后需要重新解码
#define QYJUserDefaults [NSUserDefaults standardUserDefaults]
// 解密的文件夹 存放在沙盒中的路径
#define QYJSuffixPath @"/Customer"
#define QYJWidgetPath QYJSuffixPath@"/widget"
// 这两个是用于判断设备是否越狱了
#define ARRAY_SIZE(a) sizeof(a)/sizeof(a[0])
const char* jailbreak_tool_pathes[] = {
"/Applications/Cydia.app",
"/Library/MobileSubstrate/MobileSubstrate.dylib",
"/bin/bash",
"/usr/sbin/sshd",
"/etc/apt"
};
// app是否热更的标识
static NSString *const QYJResetSymbolKey = @"AvalanchingSymbolKey";
/**
* 测试的时候发现不满足16个字符的长度,也是可以成功的,
* 但是Android那边只能是16个字符,故这里都写成16个字符。
* 偏移量同理。
*/
// 加密的密码 这里需要16个字符
static NSString *const key = @"qwertyuiopasdfgh";
// 数据的偏移量(CBC所谓的链条)这里需要16个字符
static NSString *const iv = @"0102030405060708";
@implementation QYJFileManager
/**
* load 方法是先于main函数加载的,是app启动,资源文件和相关代码加入内存
* 时候调用的,系统自动调用。将这里东西全部打包到.a库中,它会自动去执行,无需外部去调用和引用。
*/
+ (void)load {
[super load];
NSLog(@"sandbox Path:%@", QYJDocumentPath);
[self handleKeyChainFile];
// // 解压缩解码
// [self zipArchive];
// [self dencrytionFileBySandBox];
}
+ (void)handleKeyChainFile {
[self fileMoveToSandBox];
[self encrytionFileBySandBox];
[self zipArchiveToFile];
}
+ (void)handleHtmlFile {
if ([self isResetting]) {
// 移动文件夹到沙盒子
[self fileMoveToSandBox];
// copy成功->解密
[self dencrytionFileBySandBox];
// 保存标字符
[self saveSymbol:NO];
}
}
// 工程里面的文件移动到沙盒里面
+ (BOOL)fileMoveToSandBox {
NSString *appLib = [QYJDocumentPath stringByAppendingString:QYJSuffixPath];
// 判断是否存在 Customer 文件夹
BOOL flag = [QYJFileSingle fileExistsAtPath:appLib];
if (flag) {
// 存在且不需要升级
flag = [QYJFileSingle isDeletableFileAtPath:appLib];
if (flag) {
// 删除重新拷贝文件
[self cleanSandBoxFile];
} else {
return NO;
}
}
// 将加密的文件 copy到沙盒
/**
* - (nullable NSString *)pathForResource:(nullable NSString *)name ofType:(nullable NSString *)ext;
* 获取工程中一个"文件"的路径
*/
/**
* - (nullable NSString *)pathForAuxiliaryExecutable:(NSString *)executableName;
* 获取工程中一个"文件夹"的路径
*/
NSString *path = [[NSBundle mainBundle] pathForAuxiliaryExecutable:@"widget"];
// 创建文件夹
[QYJFileManager createFolder:appLib];
// 将项目中的文件加copy到沙盒
BOOL filesPresent = [self copyMissingFile:path toPath:appLib];
// 这里判断是否成功了。添加额外的操作
if (filesPresent) {
return YES;
} else {
return NO;
}
}
// 拷贝文件夹到指定目录 传入两个文件夹路径
+ (BOOL)copyMissingFile:(NSString *)sourcePath toPath:(NSString *)toPath {
BOOL retVal = YES;
NSString * finalLocation = [toPath stringByAppendingPathComponent:[sourcePath lastPathComponent]];
if (![QYJFileSingle fileExistsAtPath:finalLocation]) {
retVal = [QYJFileSingle copyItemAtPath:sourcePath toPath:finalLocation error:NULL];
}
return retVal;
}
// 传入了一个文件路径(包含文件夹名字)
+ (BOOL)createFolder:(NSString *)createDir {
BOOL isDir = NO;
BOOL existed = [QYJFileSingle fileExistsAtPath:createDir isDirectory:&isDir];
if (!(YES == isDir && YES == existed)) {
[QYJFileSingle createDirectoryAtPath:createDir withIntermediateDirectories:YES attributes:nil error:nil];
}
return isDir;
}
// 获取需要加解密的文件路径 只能获取到文件下下的路径,完整的路径需要再拼接
+ (NSArray *)getWidgetFinderAllFile {
NSString* widgetPath = [QYJDocumentPath stringByAppendingString:QYJWidgetPath];
NSError *err = nil;
NSArray *files = [QYJFileSingle subpathsOfDirectoryAtPath:widgetPath error:&err];
NSMutableArray *results = @[].mutableCopy;
for (NSString *name in files) {
// 选择要操作文件
if ([name hasSuffix:@".png"] ||
[name hasSuffix:@".pubxml"] ||
[name hasSuffix:@".p12"] ||
[name hasSuffix:@".TTF"] ||
[name hasSuffix:@".csproj"] ||
[name hasSuffix:@".project"] ||
[name rangeOfString:@"."].length == 0 ||
[name hasSuffix:@".gif"] ||
[name hasSuffix:@".jpg"] ||
[name hasSuffix:@".xml"]) {
continue;
} else {
[results addObject:name];
}
}
return results;
}
#pragma mark - decrytion 解密 begin
// 解密的入口
+ (void)dencrytionFileBySandBox {
NSArray *array = [self getWidgetFinderAllFile];
if (array) {
[self dencryptionHTMLFileWithFiles:array];
} else {
return;
}
}
// 解密
+ (NSString *)dencrytionFileWithNSString:(NSString *)content {
EncryptionTools *tool = [EncryptionTools sharedEncryptionTools];
// AES -- CBC
NSData *data = [iv dataUsingEncoding:NSUTF8StringEncoding];
NSString *result = [tool decryptString:content keyString:key iv:data];
return result;
}
// 拼接解密路径
+ (void)dencryptionHTMLFileWithFiles:(NSArray *)names {
for (NSString *name in names) {
NSString *suffix = [NSString stringWithFormat:@"%@/%@", QYJWidgetPath, name];
NSString *path = [QYJDocumentPath stringByAppendingString:suffix];
NSError *error = nil;
NSString *content = [[NSString alloc] initWithContentsOfFile:path encoding:NSUTF8StringEncoding error:&error];
if (!error) {
// 解密
content = [self dencrytionFileWithNSString:content];
if (!content || content.length == 0) {
NSLog(@"解密失败了!!!!content is nil");
continue;
}
// 重新写入文件
[content writeToFile:path atomically:YES encoding:NSUTF8StringEncoding error:&error];
if (error) {
NSLog(@"%@", error);
}
} else {
NSLog(@"解密失败");
}
}
}
#pragma mark - Dencrytion 解密 end
#pragma mark - Encrytion 加密 begin
+ (void)encrytionFileBySandBox {
NSArray *array = [self getWidgetFinderAllFile];
if (array) {
[self encryptionHTMLFileWithFiles:array];
} else {
return;
}
}
+ (void)encryptionHTMLFileWithFiles:(NSArray *)names {
for (NSString *name in names) {
NSString *suffix = [NSString stringWithFormat:@"/Customer/widget/%@", name];
NSString *path = [QYJDocumentPath stringByAppendingString:suffix];
NSError *error = nil;
NSString *content = [[NSString alloc] initWithContentsOfFile:path encoding:NSUTF8StringEncoding error:&error];
if (!error) {
// 加密
content = [self encrytionFileWithNSString:content];
// 写入文件
[content writeToFile:path atomically:YES encoding:NSUTF8StringEncoding error:&error];
} else {
NSLog(@"加密失败");
}
}
}
+ (NSString *)encrytionFileWithNSString:(NSString *)content {
EncryptionTools *tool = [EncryptionTools sharedEncryptionTools];
// AES -- CBC
NSData *data = [iv dataUsingEncoding:NSUTF8StringEncoding];
NSString *result = [tool encryptString:content keyString:key iv:data];
return result;
}
#pragma mark - Encrytion 加密 end
// 保存是否重新导入
+ (BOOL)isResetting {
id object = [QYJUserDefaults objectForKey:QYJResetSymbolKey];
if (object) {
return [object boolValue];
} else {
return YES;
}
}
// 设置热更的标识,判断是否需要更新
+ (void)saveSymbol:(BOOL)flag {
[QYJUserDefaults setObject:@(flag) forKey:QYJResetSymbolKey];
[QYJUserDefaults synchronize];
}
// 判断手机是否是越狱的
+ (BOOL)authorityJudgment {
for (int i = 0; i < ARRAY_SIZE(jailbreak_tool_pathes); i++) {
if ([[NSFileManager defaultManager] fileExistsAtPath:[NSString stringWithUTF8String:jailbreak_tool_pathes[i]]]) {
// 越狱了
return YES;
}
}
// 没有越狱
return NO;
}
// 清除沙盒里面的文件
+ (void)cleanSandBoxFile {
NSString *appLib = [QYJDocumentPath stringByAppendingString:QYJSuffixPath];
NSError *error = nil;
[QYJFileSingle removeItemAtPath:appLib error:&error];
[self saveSymbol:YES];
}
// 这里是热更需要的,这里简单滴做了一次热更新,从服务器下载文件,然后将下载的文件拷贝到相应的目录下,再解码
+ (void)copyUpdateFileToCustomPath {
// 将更新的文件拷贝到指定的文件目录下
NSString *filePath = [QYJDocumentPath stringByAppendingString:@"XXXXXXXXX"];
[self cleanSandBoxFile];
NSString *appLib = [QYJDocumentPath stringByAppendingString:QYJSuffixPath];
[QYJFileManager createFolder:appLib];
BOOL flag = [self copyMissingFile:filePath toPath:appLib];
if (flag) {
[self dencrytionFileBySandBox];
}
}
#pragma mark - ZipArchive 压缩成zip包
+ (void)zipArchiveToFile {
// zip包的路径
NSString* zipFile = [QYJDocumentPath stringByAppendingString:@"/Customer/widget.zip"] ;
// 解压的目标路径
NSString* sourcePath = [QYJDocumentPath stringByAppendingString:@"/Customer/"] ;
ZipArchive * zipArchive = [ZipArchive new];
[zipArchive CreateZipFile2:zipFile];
NSArray *subPaths = [QYJFileSingle subpathsAtPath:sourcePath];// 关键是subpathsAtPath方法
for(NSString *subPath in subPaths){
NSString *fullPath = [sourcePath stringByAppendingPathComponent:subPath];
BOOL isDir;
if([QYJFileSingle fileExistsAtPath:fullPath isDirectory:&isDir] && !isDir)// 只处理文件
{
[zipArchive addFileToZip:fullPath newname:subPath];
}
}
[zipArchive CloseZipFile2];
}
#pragma mark - ZipArchive 解压成文件夹
+ (void)zipArchive {
ZipArchive* zip = [[ZipArchive alloc] init];
// zip包的路径
NSString* zipFile = [QYJDocumentPath stringByAppendingString:@"/Customer/widget.zip"] ;
// 解压的目标路径
NSString* unZipTo = [QYJDocumentPath stringByAppendingString:@"/Customer/"] ;
if( [zip UnzipOpenFile:zipFile] ) {
BOOL result = [zip UnzipFileTo:unZipTo overWrite:YES];
if(NO == result) {
//添加代码
}
[zip UnzipCloseFile];
}
}
@end
1.将文件拷贝到沙盒,解密并且压缩
2.解密,先删除之前分widget文件,解压缩,解密。(这里就手动删除就好了,需要代码删除上诉的clean方法已经实现)
这样子简单的方案就实现了,这里主要依赖的是沙盒的安全机制。这里上面的图片暴露的时间,可能有些混乱,我在写博客的时候,被其他事情打断了,所以截图时间会有相距很长时间,不用在意这些细节。github上的Demo地址