关于iOS HTML安全的简单策略--下卷
时隔三年,终于要写下卷了,其实这个方法早就想好的了,但貌似大家不是很喜欢关注本地HTML安全的问题,主要是跨平台的手段太多了HTML不是一个优秀的“解”。
上卷说到加密的HTML放置于本地,等到App运行的时候才去解密。我们预期最终的一个方案就是用到的时候,没有用到时候就保持一个原有的加密状态。这是预期效果,思路就是获取相应的js的一个加载路径,动态去解码js文件。
1.可行性分析
1.1.NSURLProtocol
动态去加载JS的关键就是NSURLProtocol,这是个什么东西呢?简单来说一个网络层的处理代理,里面有几个关键的方法,网上相关的文章也有很多,这里就不做详细的讲解了,关于一些循环的问题,可以参考
/**
* 关键方法,防止循环调用,
* 但理论上我们只是使用本地的flie没有发起网络请求,
* 应该不会存在这个问题。,算是一个拓展了解。
* 这顺带提一下
*/
[NSURLProtocol propertyForKey:@"xxxxxx" inRequest:request]
的用法。
// 有三个类方法,两个对象方,这五个方法需要依次实现。
// 这里是判断如何是否需要处理我们的一个网络请求
//在我们这个需求里面,可以看作是否要拦截我们的请求
+ (BOOL)canInitWithRequest:(NSURLRequest *)request;
// 拦截下来的NSURLRequest收否需要额外的处理,这里是用于做重定向的
+ (NSURLRequest *) canonicalRequestForRequest:(NSURLRequest *)request;
// 这里是判断是否与缓存是同一个请求,如果是就使用缓存,如果不是,就重新请求。
+ (BOOL)requestIsCacheEquivalent:(NSURLRequest *)a toRequest:(NSURLRequest *)b;
// 开始处理请求
// 成功拦截了请求
// 系统将自动初始化一个你注册出来的NSURLProtocol子类出来处理请求。
- (void)startLoading;
// 请求结束了,这里不管是正常结束还是出错都会走这方法。
- (void)stopLoading;
看完了NSURLProtocol的关键方法,我们来看看关键属性:
/**
* client是一个代理对象,这里由网络请求的底层去管理,
* 会自动给NSURLProtocol赋值。这里无需去探究client具体是什么
* 对象
*/
@property (nullable, readonly, retain) id <NSURLProtocolClient> client;
/**
* 关于NSURLProtocolClient关键代理,
* 这里的方法系统已经实现了,
* 只需要开发者在适当的时机调用就好了
*/
/*!
* 这里是重定向,很大程度上是返回一个新的Response
*/
- (void)URLProtocol:(NSURLProtocol *)protocol wasRedirectedToRequest:(NSURLRequest *)request redirectResponse:(NSURLResponse *)redirectResponse;
/*!
* 去查找一个缓存的Response
*/
- (void)URLProtocol:(NSURLProtocol *)protocol cachedResponseIsValid:(NSCachedURLResponse *)cachedResponse;
/*!
* 返回一个已经实现的response,并添加缓存
*/
- (void)URLProtocol:(NSURLProtocol *)protocol didReceiveResponse:(NSURLResponse *)response cacheStoragePolicy:(NSURLCacheStoragePolicy)policy;
/*!
* 返回一个请data,给response
*/
- (void)URLProtocol:(NSURLProtocol *)protocol didLoadData:(NSData *)data;
/*!
* 当前请求已经结束
*/
- (void)URLProtocolDidFinishLoading:(NSURLProtocol *)protocol;
/*!
* 当前请求失败,返回一个Error
*/
- (void)URLProtocol:(NSURLProtocol *)protocol didFailWithError:(NSError *)error;
/*!
* https进行双向认证
*/
- (void)URLProtocol:(NSURLProtocol *)protocol didReceiveAuthenticationChallenge:(NSURLAuthenticationChallenge *)challenge;
/*!
* 取消https进行双向认证
*/
- (void)URLProtocol:(NSURLProtocol *)protocol didCancelAuthenticationChallenge:(NSURLAuthenticationChallenge *)challenge;
1.2.写一个小demo看看
NSURL *url = [NSURL URLWithString:@"https://www.baidu.com"];
NSURLRequest *urlRequest = [NSURLRequest requestWithURL:url
cachePolicy:NSURLRequestReloadIgnoringCacheData
timeoutInterval:20];
[[[NSURLSession sharedSession] dataTaskWithRequest:urlRequest
completionHandler:^(NSData * _Nullable data, NSURLResponse * _Nullable response, NSError * _Nullable error) {
NSString *content = [[NSString alloc] initWithData:data encoding:NSUTF8StringEncoding];
NSLog(@"我是从%@中获取数据:%@", url.absoluteString, content);
}] resume];
这是请求结果打印:
从上面我们可以看到直接把"https://www.baidu.com"的HTML文件内容给下载下来了
这里提供了一个思路,实际网络中,我们引入一个js文件,也是发出一个http请求去将这个js下载回来,简单来说我们是不是可以拦截请求,然后将我们实现解密好的data扔回去给webView自己去解析呢。
1.3.我们一块来拦截百度
创建一个NSURLProtocol的子类
// AvalanchingURLProtocol.h
#import <Foundation/Foundation.h>
NS_ASSUME_NONNULL_BEGIN
@interface AvalanchingURLProtocol : NSURLProtocol
@end
NS_ASSUME_NONNULL_END
// AvalanchingURLProtocol.m
#import "AvalanchingURLProtocol.h"
@interface AvalanchingURLProtocol ()<NSURLSessionDelegate>
@end
@implementation AvalanchingURLProtocol
+ (BOOL)canInitWithRequest:(NSURLRequest *)request {
// 官方的解释是如果协议可以处理给定的请求,则为“是”,否则为“否”。
// 这里理解成是否是处理
return YES;
}
+ (NSURLRequest *) canonicalRequestForRequest:(NSURLRequest *)request {
// 返回一个NSURLRequest
return request;
}
+ (BOOL)requestIsCacheEquivalent:(NSURLRequest *)a toRequest:(NSURLRequest *)b {
// 对比是否是相同的Request, 如果相同这使用缓存
return [super requestIsCacheEquivalent:a toRequest:b];
}
- (void)startLoading {
// 开始处理请求方法
NSString *string = @"你的百度被我偷了,我是另外一个数据";
NSData *data = [string dataUsingEncoding:NSUTF8StringEncoding];
// 返回数据
[[self client] URLProtocol:self didLoadData:data];
// 结束本次请求
[self.client URLProtocolDidFinishLoading:self];
}
- (void)stopLoading {
// 请求结束了回调放法
}
@end
// AppDelegate.m
- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions {
// 注册协议处理者
[NSURLProtocol registerClass:[AvalanchingURLProtocol class]];
NSURL *url = [NSURL URLWithString:@"https://www.baidu.com"];
NSURLRequest *urlRequest = [NSURLRequest requestWithURL:url
cachePolicy:NSURLRequestReloadIgnoringCacheData
timeoutInterval:20];
[[[NSURLSession sharedSession] dataTaskWithRequest:urlRequest
completionHandler:^(NSData * _Nullable data, NSURLResponse * _Nullable response, NSError * _Nullable error) {
NSString *content = [[NSString alloc] initWithData:data encoding:NSUTF8StringEncoding];
NSLog(@"我是从%@中获取数据:%@", url.absoluteString, content);
}] resume];
return YES;
}
请求输出的结果:
走到这一步,相信机智如你,应该都懂了吧。
2.总体的思路
2.1.0 还有些什么东西
1.要拦截WKWebView,那么我们就希望拦截WKWebView;
2.并不是所有的Request都是需要拦截的;
/**
* 通过NSClassFromString获取WKBrowsingContextController私有
* 类,这里网上有建议说要将WKBrowsingContextController拆成
* 符串数组,用的时候再拼接起来,以躲避Apple的机审,目前还没
* 有被apple拒审的情况
*/
NSString *className = @"WKBrowsingContextController";
Class cls = NSClassFromString(className);
SEL sel = NSSelectorFromString(@"registerSchemeForCustomProtocol:")
/// 拆分一个例子
#define STRNG_ENCRYPT_KEY @"kljdjgkajkdlgjlkasjdlkg"
static NSString * AES_KEY() {
unsigned char key[] = {
(STRNG_ENCRYPT_KEY ^ 'W'),
(STRNG_ENCRYPT_KEY ^ 'K'),
(STRNG_ENCRYPT_KEY ^ 'B'),
(STRNG_ENCRYPT_KEY ^ 'r'),
(STRNG_ENCRYPT_KEY ^ 'o'),
(STRNG_ENCRYPT_KEY ^ 'w'),
(STRNG_ENCRYPT_KEY ^ 's'),
(STRNG_ENCRYPT_KEY ^ 'i'),
(STRNG_ENCRYPT_KEY ^ 'n'),
(STRNG_ENCRYPT_KEY ^ 'g'),
(STRNG_ENCRYPT_KEY ^ 'C'),
(STRNG_ENCRYPT_KEY ^ 'o'),
(STRNG_ENCRYPT_KEY ^ 'n'),
(STRNG_ENCRYPT_KEY ^ 't'),
(STRNG_ENCRYPT_KEY ^ 'e'),
(STRNG_ENCRYPT_KEY ^ 'x'),
(STRNG_ENCRYPT_KEY ^ 't'),
(STRNG_ENCRYPT_KEY ^ 'C'),
(STRNG_ENCRYPT_KEY ^ 'o'),
(STRNG_ENCRYPT_KEY ^ 'n'),
(STRNG_ENCRYPT_KEY ^ 't'),
(STRNG_ENCRYPT_KEY ^ 'r'),
(STRNG_ENCRYPT_KEY ^ 'o'),
(STRNG_ENCRYPT_KEY ^ 'l'),
(STRNG_ENCRYPT_KEY ^ 'l'),
(STRNG_ENCRYPT_KEY ^ 'e'),
(STRNG_ENCRYPT_KEY ^ 'r'),
(STRNG_ENCRYPT_KEY ^ '\0')
};
unsigned char * p = key;
while (((*p) ^= STRNG_ENCRYPT_KEY != '\0')) {
p++;
}
return [NSString stringWithUTF8String:(const char *)key];
}
然后就是自定义协议了,为了和常规的协议混淆,减少不必要的操作,一般会定一个自定义的协议。所谓协议可以简单理解是"http://xxxx.xxxx.com"中的http这个协议名,这里并不是让开发者去实现类似于http的传输协议,而是用一个特殊的字符是代替"http"来标识这是我自己的协议。
SEL sel = NSSelectorFromString(@"registerSchemeForCustomProtocol:");
if (cls && sel) {
if ([(id)cls respondsToSelector:sel]) {
#pragma clang diagnostic push
#pragma clang diagnostic ignored"-Warc-performSelector-leaks"
// 注册自定义协议
[(id)cls performSelector:sel withObject:@"app"];
#pragma clang diagnostic pop
}
}
// @"app"就是我们的自定义协议,那么url就应该是app://xxxxx.xxxxxx
接下来就和上述的情况一样了这里贴上- (void)startLoading的代码作为事例。
- (void)startLoading {
// 这里模拟你在上卷学会解密后获取的字符串,.js解密了内容也是一个字符串
NSString *string = @"var i = 0;function onclickAction() {var tag1 = document.getElementById(\"myh1\");tag1.innerHTML = '我是新的内容 ' + i++;}";
// 转成Data 扔回去
NSData *data = [string dataUsingEncoding:NSUTF8StringEncoding];
// 返回数据
[[self client] URLProtocol:self didLoadData:data];
// 结束本次请求
[self.client URLProtocolDidFinishLoading:self];
}
用于存放于本地加载的index.html代码如下:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Document</title>
</head>
<body>
<div id='htag1'>
<button id='myh1' onclick="onclickAction()">
Hello world
</button>
</div>
</body>
<script src="app://diazhaotianlas.semodejfg"></script>
</html>
viewController加载WKWebView的代码
- (void)viewDidLoad {
[super viewDidLoad];
// 创建webView
WKWebView *webView = [[WKWebView alloc] init];
webView.frame = self.view.bounds;
[self.view addSubview:webView];
// 加载URL
NSString *path = [[NSBundle mainBundle] pathForResource:@"index" ofType:@"html"];
NSURL *url = [NSURL fileURLWithPath:path];
NSURLRequest *request = [NSURLRequest requestWithURL:url cachePolicy:NSURLRequestReloadIgnoringCacheData timeoutInterval:20];
[webView loadRequest:request];
}