iOS 安全性参考点

iOS本地数据存储安全

本地存储的方式主要有:Userdefault, 沙盒文件(包括DB文件),Archieve和Keychain。其中,

  • Userdefault的本质是一个plist文件,也保存在沙盒中(Library>Preference)。
  • Archieve文件也是在沙盒的自定义路径中保存一个对应的文件:
NSString *multiHomePath = [NSHomeDirectory() stringByAppendingPathComponent:@"multi.archiver"];  
NSMutableData *data = [[NSMutableData alloc]init];  
NSKeyedArchiver *archvier = [[NSKeyedArchiver alloc]initForWritingWithMutableData:data];  
  
[archvier encodeCGPoint:point forKey:@"kPoint"];  
[archvier finishEncoding];  
[data writeToFile:multiHomePath atomically:YES];  

以上这些Plist和沙盒路径文件都很容易在越狱设备甚至未越狱设备上被提取导致信息泄露。

  • Keychain是一个拥有有限访问权限的SQLite数据库(AES256加密),在iPhone上,Keychain存放在“/private/var/Keychains/keychain-2.db” SQLite数据库。
    Keychain数据库包含了一些Keychain条目,每个条目都由加密的数据和一系列未加密的描述属性组成,Keychain的条目类型(kSecClass)决定了其关联的一些描述属性。在iOS系统中,Keychain条目被分为5种类型:
这里写图片描述

其中,数字身份=证书+密钥。不同类型的加密属性支持不同的属性类型,如internet password支持设置domainserver等属性。

iOS APP的Keychain数据是存储在应用沙箱外面的,各APP的keychain数据内容为逻辑隔离,由系统进程securityd实施访问控制。为了在多个APP间能够共享Keychain信息,Apple引入了Keychain访问组概念:拥有相同Keychain访问组标识符的应用,可以共享Keychain数据。

随着iOS引入了数据保护机制,存储在Keychain中的数据被另一层与用户密码(passcode)相关联的加密机制保护着。数据保护加密密钥(保护类密钥-protection class keys)是由一个设备硬件密钥和一个由用户密码衍生的密钥共同产生的。可以通过向Keychain接口的SecItemAdd或SecItemUpdate方法的kSecAttrAccessible属性提供一个可访问常量来启用Keychain的数据保护。该可访问常量值决定一个Keychain条目在何时可被应用访问,同时也决定某个Keychain条目是否允许移动到另一台设备上。
以下是Keychain条目的数据保护可访问常量列表:
(1)kSecAttrAccessibleWhenUnlocked:Keychain条目只能在设备被解锁后被访问,此时用于解密Keychain条目的数据保护类密钥只在设备被解锁后才会被加载到内存中,并且当设备锁定后,加密密钥将在10s钟内自动清除。
(2)kSecAttrAccessibleAfterFirstUnlock:Keychain条目可以在设备第一次解锁到重启过程中被访问,此时用于解密Keychain条目的数据保护类密钥只有在用户重启并解锁设备后才会被加载到内存中,并且该密钥将一直保留在内存中,直到下一次设备重启。
(3)kSecAttrAccessibleAlways:Keychain条目即使在设备锁定时也可被访问,此时用于解密Keychain条目的数据保护类密钥一直被加载在内存中。
(4)kSecAttrAccessibleWhenUnlockedThisDeviceOnly:Keychain条目只能在设备被解锁后被访问,并且无法在设备间移动。
(5)kSecAttrAccessibleAfterFirstUnlockThisDeviceOnly:Keychain条目可在设备第一次解锁后访问,但无法在设备间移动
(6)kSecAttrAccessibleAlwaysThisDeviceOnly:Keychain条目即使在设备锁定时也可被访问,但无法再设备将移动
Keychain条目的数据保护可访问常量被映射到各Keychain表(genp、inet..)中的pdmn列(protection domain)。下表显示了keychain数据保护可访问常量和pdmn值之间的映射关系。

Keychain_dumper工具可用于导出越狱iOS设备上的所有Keychain条目,所以keychain存储也不是绝对安全的。

综合上述,如果需要本地存储敏感数据,可参考以下原则:

  • 使用Keychain,将原数据加密后存储在keychain中。如AES加密,并将KEY也存入keychain。
  • 向Keychain中存储数据时,不要使用kSecAttrAccessibleAlways,而是使用更安全的kSecAttrAccessibleWhenUnlocked或kSecAttrAccessibleWhenUnlockedThisDeviceOnly选项。

网络通信安全

HTTPS通信链路的安全

https是在http通信协议上加了一层SSL/TLS安全握手过程,以保证通信双方的合法认证保证数据以密文形式传输。本文不赘述SSL的具体过程及原理,以下是iOS端处理SSL安全认证的常见方法:
1.确认server端返回的证书是CA授权的合法证书。

SecTrustRef serverTrust = challenge.protectionSpace.serverTrust;
static BOOL serverTrustIsVaild(SecTrustRef trust) {
   BOOL allowConnection = NO;

// 假设验证结果是无效的
SecTrustResultType trustResult = kSecTrustResultInvalid;

// 函数的内部递归地从叶节点证书到根证书的验证
OSStatus statue = SecTrustEvaluate(trust, &trustResult);

    if (statue == noErr) {
    // kSecTrustResultUnspecified: 系统隐式地信任这个证书
    // kSecTrustResultProceed: 用户加入自己的信任锚点,显式地告诉系统这个证书是值得信任的

    allowConnection = (trustResult == kSecTrustResultProceed 
                                || trustResult == kSecTrustResultUnspecified);
    }
    return allowConnection;
}

2.验证域名
可以通过以下的代码获得当前的验证策略:

CFArrayRef policiesRef;
SecTrustCopyPolicies(trust, &policiesRef);

打印 policiesRef 后,你会发现默认的验证策略就包含了域名验证。如果想自定义域名验证过程,可参考:

    NSString *serverHostname = challenge.protectionSpace.host;
    CFRetain(serverTrust);
    
    SecPolicyRef SslPolicy = SecPolicyCreateSSL(YES, (__bridge CFStringRef)(serverHostname));
    SecTrustSetPolicies(serverTrust, SslPolicy);
    CFRelease(SslPolicy);
    serverTrustIsVaild(serverTrust)

或者取消域名验证

NSMutableArray *policies = [NSMutableArray array];
    
// BasicX509 不验证域名是否相同
SecPolicyRef policy = SecPolicyCreateBasicX509();
[policies addObject:(__bridge_transfer id)policy];
SecTrustSetPolicies(serverTrust, (__bridge CFArrayRef)policies);
serverTrustIsVaild(serverTrust)

3.验证证书的确实是自己信任的证书。
App可以将证书文件存于bundle中,也可以将证书内的一些需要验证的value存于代码中,如:PublicKey。

      SecCertificateRef serverCertificate = SecTrustGetCertificateAtIndex(trust, 0);
      NSData* serverCertificateData = (NSData* )SecCertificateCopyData(serverCertificate);
      NSString *serverCertPublicKey = (NSString* )SecCertificateCopyPublicKey(serverCertificate);//通过SecCertificateCopyPublicKey等一系列方法可以取出证书中的所有信息,以备进行比对。

注意使用:

    CFIndex certCount = SecTrustGetCertificateCount(trust);

去获取证书链上的每一级证书,必要时分别进行验证。

4.自签名的证书链验证(optional)
假设你的服务器返回:[你的自签名的根证书] -- [你的二级证书] -- [你的客户端证书],系统是不信任这个三个证书的。
所以你在验证的时候需要将这三个的其中一个设置为锚点证书,当然,多个也行。

比如将 [你的二级证书] 作为锚点后,SecTrustEvaluate() 函数只要验证到 [你的客户端证书] 确实是由 [你的二级证书] 签署的,那么验证结果为 kSecTrustResultUnspecified,表明了 [你的客户端证书] 是可信的。下面是设置锚点证书的做法:

NSMutableArray *certificates = [NSMutableArray array];

NSDate *cerData = /* 在 App Bundle 中你用来做锚点的证书数据,证书是 CER 编码的,常见扩展名有:cer, crt...*/

SecCertificateRef cerRef = SecCertificateCreateWithData(NULL, (__bridge CFDataRef)cerData);

[certificates addObject:(__bridge_transfer id)cerRef];

// 设置锚点证书。
SecTrustSetAnchorCertificates(serverTrust, (__bridge CFArrayRef)certificates);
serverTrustIsVaild(serverTrust)

只调用 SecTrustSetAnchorCertificates () 这个函数的话,那么就只有作为参数被传入的证书作为锚点证书,连系统本身信任的 CA 证书不能作为锚点验证证书链。要想恢复系统中 CA 证书作为锚点的功能,还要再调用下面这个函数:

// true 代表仅被传入的证书作为锚点,false 允许系统 CA 证书也作为锚点
SecTrustSetAnchorCertificatesOnly(trust, false);

传输数据的安全

原则只有一个,不管是https还是http,不要明文传输敏感数据,防止数据泄露。并且要充分验证传输数据的完整性,保证数据不被篡改。

  • 通常情况下,对称加密算法如AES加密可以高效加密数据,但key一旦泄露,服务器端不容易更改,所以更安全的加密方式为RSA非对称加密,更严格者可以使用双向RSA加密,保证request和response的数据都只有拥有private key的接收端可以正确解密。
  • 有时为了使后台也不能得到明文信息,如用户密码。就需要在客户端进行MD5或者HASH不可逆加密。服务端存储的也应是同样算法加密后的值。由于目前MD5可以通过一些工具和网站解密出一些原文,我们有必要对md5进行一些二次加工,如加盐加盐后乱序重复多次md5加密等手段加大破解的难度。
  • 通过计算form/body的hash/md5值,将其和数据一起发送出去,接收端自行计算接收data的hash/md5值,并和接收到的值进行比对,已验证数据完整性。(文件传输尤其需要)
  • 有时,为了保证service api的访问安全,服务器会设置访问密码.

        NSURLCredential *defaultCredential = [NSURLCredential credentialWithUser:@"username" password:@"password" persistence:NSURLCredentialPersistenceForSession];
        NSURL *APIURL = [NSURL URLWithString:@"www.test.com"];
        NSString *host = [APIURL host];
        NSInteger port = [APIURL port];
        NSString *scheme = [APIURL scheme];
        NSURLProtectionSpace *protectionSpace = [[NSURLProtectionSpace alloc] initWithHost:host port:port protocol:scheme realm:nil authenticationMethod:NSURLAuthenticationMethodHTTPBasic];
        NSURLCredentialStorage *credentials = [NSURLCredentialStorage sharedCredentialStorage];
        [credentials setDefaultCredential:defaultCredential forProtectionSpace:protectionSpace];
        
        NSURLSessionConfiguration *sessionConfiguration = [NSURLSessionConfiguration defaultSessionConfiguration];
        [sessionConfiguration setURLCredentialStorage:credentials];
        //sessionConfiguration.HTTPAdditionalHeaders =@{@"Authorization":[NSString stringWithFormat:@"Basic %@=%@",@"username",@"password"]};
        //不需要在手动设置header
        NSURLSession *sesson = [NSURLSession sessionWithConfiguration:sessionConfiguration];

应用快照缓存

当一个应用在后台被挂起时,iOS会生成一个当前屏幕的快照,当应用被重新唤起时,可以快速还原该APP之前的内容,以提高用户的使用体验。
然而,这样做也可能会导致一些应用数据的泄露。应用快照保存在“/var/mobile/Containers/Data/Application/XXXXXXX-XXXXXXXXX-XXXXXXXXX/Library/Caches/Snapshots/”目录下。恶意APP可通过读取该文件并发送至远程服务端,从而获得其快照内容信息。

措施

要防止这种信息泄露途径,屏幕内容就必须在iOS系统进行屏幕快照之前进行隐藏、模糊化或覆盖其他图片处理,而iOS系统也提供了许多回调方法来提示程序将被挂起。例如以下两个方法:

  • (void)applicationWillResignActive:(UIApplication *)application
    应用程序将要入非活动状态执行,在此期间,应用程序不接收消息或事件
  • (void)applicationDidEnterBackground:(UIApplication *)application
    程序将被推送到后台。
    方案一:
    一个简单方法就是设置关键窗口的hidden属性为YES,这样当前在屏幕上显示的内容将被隐藏,返回一个空白的快照来替代任何内容。
[UIApplication sharedApplication].keyWindow.hidden=YES;

方案二:在两个挂起回调内,在window上覆盖一个新图片或对当前界面模糊化处理。
获取当前视图的参考代码:


 UIWindow *window = [[UIApplication sharedApplication] keyWindow];
    if (window.windowLevel != UIWindowLevelNormal) {
        NSArray *windows = [[UIApplication sharedApplication] windows];
        for(UIWindow *tmpWin in windows) {
            if (tmpWin.windowLevel == UIWindowLevelNormal) {
                window = tmpWin;
                break;
            }
        }
    }
[window addSubview:blurView];

键盘缓存

为提供自动填充和纠正的功能,iOS系统的自带键盘会缓存用户的输入信息。其会保存一个接近600个单词的列表,存放在“Library/Keyboard/ en_GB-dynamic-text.dat”或“/private/var/mobile/Library/Keyboard/dynamic-text.dat”文件中(iOS版本不同,位置及文件名会略有不同),该文件存储的是明文信息,有存入敏感信息的可能性。

措施

1, 首先,对于以下位置或方式的输入,iOS不会对其输入内容进行缓存:

  • 在标记为secure的字段、passwords字段内输入的内容不会缓存;
  • 输入只包括数字的字符串不会被缓存,这也即意味着银行卡号、信用卡号是安全的,(iOS 5之前的版本会缓存);
  • 非常短的输入,如只有1或者2个字母组成的单词不会被缓存;
  • 禁用了自动纠正功能的文本框会阻止输入内容被缓存;

2, 可以在不需要缓存的文本框处禁用自动纠正功能。如下代码所示:

UITextField *textField = [[UITextField alloc] initWithFrame: frame ]; 
textField.autocorrectionType = UITextAutocorrectionTypeNo;

3, 输入框也可以被标记为密码输入类型,使得输入变得更加安全,防止缓存。如:

textField.secureTextEntry = YES;

4,在所有敏感信息输入处均使用自定义键盘(尤其是银行App和Hybird App)
下面是一段自定义键盘收起和展开的事件响应:

@interface WQSafeKeyboard : UIWindow  

@property (nonatomic, weak, setter = focusOnTextFiled:) UITextField *textFiled;  
+ (WQSafeKeyboard *)deploySafeKeyboard;  
@end  

@interface WQSafeKeyboard()  

@property (nonatomic, strong)WQInterKeyboard *keyboard;  
@end  

@implementation WQSafeKeyboard  

+ (WQSafeKeyboard *)deploySafeKeyboard  
{  
    WQSafeKeyboard *kb = [[WQSafeKeyboard alloc]init];  
    [kb addObserver];  
    return kb;  
}  

- (instancetype)init  
{  
    if (self = [super init]) {  
        self.windowLevel = UIWindowLevelAlert;  
        self.frame = CGRectZero;  
        self.rootViewController = self.keyboard;  
    }  
    return self;  
}  

- (void)dealloc  
{  
    [[NSNotificationCenter defaultCenter] removeObserver:self];  
}  

- (WQInterKeyboard *)keyboard  
{  
    if (!_keyboard) {  
        _keyboard = [[WQInterKeyboard alloc]init];  
    }  
    return _keyboard;  
}  

- (void)focusOnTextFiled:(UITextField *)textFiled  
{  
    _textFiled = textFiled;  
    self.keyboard.textField = _textFiled;  
}  

- (void)addObserver  
{  
    [[NSNotificationCenter defaultCenter]addObserver:self  
                                            selector:@selector(keyboardWillShow:)  
                                                name:UIKeyboardWillShowNotification  
                                              object:nil];  
    [[NSNotificationCenter defaultCenter]addObserver:self  
                                            selector:@selector(keyboardWillHide:)  
                                                name:UIKeyboardWillHideNotification  
                                              object:nil];  
}  

- (void)keyboardWillShow:(NSNotification *)notification  
{  
    if (![self.textFiled isFirstResponder]) {  
        return;  
    }  
    [self keyboardAnimationWithNotification:notification];  
}  

- (void)keyboardWillHide:(NSNotification *)notification  
{  
    if (![self.textFiled isFirstResponder]) {  
        return;  
    }  
    [self keyboardAnimationWithNotification:notification];  
}  

- (void)keyboardAnimationWithNotification:(NSNotification *)notification  
{  
    [self makeKeyAndVisible];  
    NSDictionary *userInfo = [notification userInfo];  
    CGRect kbFrame_end,kbFrame_begin;  
    NSTimeInterval animationDuration;  
    UIViewAnimationCurve animationCurve;  
    [userInfo[UIKeyboardFrameEndUserInfoKey] getValue:&kbFrame_end];  
    [userInfo[UIKeyboardFrameBeginUserInfoKey] getValue:&kbFrame_begin];  
    [userInfo[UIKeyboardAnimationCurveUserInfoKey] getValue:&animationCurve];  
    [userInfo[UIKeyboardAnimationDurationUserInfoKey] getValue:&animationDuration];  

    self.frame = [self resizeFrameToAdjust:kbFrame_begin];  
    [UIView animateWithDuration:animationDuration  
                          delay:0  
                        options:(animationCurve<<16)  
                     animations:^{  
                         self.frame = [self resizeFrameToAdjust:kbFrame_end];  
                     }completion:^(BOOL finished) {  

                     }];  
    if ([notification.name isEqualToString:UIKeyboardWillHideNotification]) {  
        [self resignKeyWindow];  
    }  
}  

- (CGRect)resizeFrameToAdjust:(CGRect)frame  
{  
    if ([[UIApplication sharedApplication] isStatusBarHidden] )  
        return frame;  

    if (SYSTEM_VERSION_LESS_THAN(@"7.0")) {  
        frame = CGRectMake(frame.origin.x,  
                           frame.origin.y - STATUSBAR_HEIGHT,  
                           frame.size.width,  
                           frame.size.height);  
    }  
    return frame;  
}  

@end

参考:
iOS本地数据存储安全
iOS 中对 HTTPS 证书链的验证

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 194,524评论 5 460
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 81,869评论 2 371
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 141,813评论 0 320
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 52,210评论 1 263
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 61,085评论 4 355
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 46,117评论 1 272
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 36,533评论 3 381
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 35,219评论 0 253
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 39,487评论 1 290
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 34,582评论 2 309
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 36,362评论 1 326
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 32,218评论 3 312
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 37,589评论 3 299
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 28,899评论 0 17
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 30,176评论 1 250
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 41,503评论 2 341
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 40,707评论 2 335

推荐阅读更多精彩内容