关于iOS HTML安全的简单策略--上卷

版权声明:本文为博主原创文章,未经博主允许不得转载。 https://blog.csdn.net/qq_27339239/article/details/77988896
最近入职一家新公司,上半年的时间都在漂泊,一直在试用期内,忙得不可开胶。新公司是做hybird开发的,HTML和原生开发。说是之前单纯使用HTML的用户体验比较差,就换了成hybird,我也就是这个时间节点入职了。忙了一段时间,终于到收尾的时刻。之前,用的是APICloud提供的解决方案,打包都是在APICloud上,平台上会将HTML,JS,CSS文件进行压缩加密,采用hybird的方案,Xcode就不会帮你加密压缩HTML相关的文件和代码,这么一来,只要下载你的ipa就能拿到你的HTML代码,很多时候HTML只是负责显示而已,没有掺杂太多的业务在里面,但是为了便捷开发,很多业务逻辑都沿用了之前的工程里面的代码,这么一来就很有必要做一个加密的处理。
首先需要明确的是,加密不是绝对的保密,加密仅仅是提高了别人获取原始代码的门槛,不让人轻易地拿到你的代码。
在开始撸代码之前,先要明确一个概念:什么是越狱?
为什么要了解什么是越狱呢?很多人都认为越狱的设备,就会移除沙盒的机制。其实,沙盒的机制依然存在,这是苹果最引以为傲的机制。沙盒也不是绝对安全的,越狱之后,开发者获取到访问/var/stash/Applications/,通过遍历去获取(因为暂时没有找到合适的设备,我没有实际去验证,这是网上普遍的说法),有兴趣的童鞋可以去了解一下,这里贴一个知乎的帖子,就当抛砖引玉了点击打开链接
  • A方案,整体思路是在工程里面放一个加密过的HMTL文件,然后在运行的时候,拷贝到沙盒里面,然后进行解密,解密完了再提给WebView去调用。(延伸的做法,使用APP进入前台就解密,APP进入后台就清除解密文件)
  • B方案,整体思路是在工程里面放一个加密过的HMTL文件,然后在运行的时候,拷贝到沙盒里面,然后对webView进行一个扩展,等到去加载页面之前去解码。(延伸做法,解密之后使用loadData:MIMEType:textEncodingName:baseURL类似的方式去加载)
这一篇,博客先来剖析方案A。

大体流程如下图:

了解的大体的思路之后,我们就开始谈一下,加密的方案。

在日常开发中,我们或多或少都会接触到数据安全,提到数据安全,就想到了数据加密这一块。

加密算法有三大类,密码学中演变过来的。第一类,哈希(散列)算法;第二类,对称加密算法;第三类,非对称加密算法。

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地址

猜你喜欢

转载自blog.csdn.net/qq_27339239/article/details/77988896
今日推荐