關于iOS HTML安全的簡單策略--下卷
時隔三年,終于要寫下卷了,其實這個方法早就想好的了,但貌似大家不是很喜歡關注本地HTML安全的問題,主要是跨平臺的手段太多了HTML不是一個優秀的“解”。
上卷說到加密的HTML放置于本地,等到App運行的時候才去解密。我們預期最終的一個方案就是用到的時候,沒有用到時候就保持一個原有的加密狀態。這是預期效果,思路就是獲取相應的js的一個加載路徑,動態去解碼js文件。
1.可行性分析
1.1.NSURLProtocol
動態去加載JS的關鍵就是NSURLProtocol,這是個什么東西呢?簡單來說一個網絡層的處理代理,里面有幾個關鍵的方法,網上相關的文章也有很多,這里就不做詳細的講解了,關于一些循環的問題,可以參考
/**
* 關鍵方法,防止循環調用,
* 但理論上我們只是使用本地的flie沒有發起網絡請求,
* 應該不會存在這個問題。,算是一個拓展了解。
* 這順帶提一下
*/
[NSURLProtocol propertyForKey:@"xxxxxx" inRequest:request]
的用法。
NSURLProtocol的作用.png
// 有三個類方法,兩個對象方,這五個方法需要依次實現。
// 這里是判斷如何是否需要處理我們的一個網絡請求
//在我們這個需求里面,可以看作是否要攔截我們的請求
+ (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];
這是請求結果打印:
直接下載了百度的HTML
從上面我們可以看到直接把"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;
}
請求輸出的結果:篡改之后的結果.png
走到這一步,相信機智如你,應該都懂了吧。
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];
}
讓我們跑起來
讓我們跑起來.gif