写在前面:任何应用的开发中安全都是重中之重,在信息交互异常活跃的现在,信息加密技术显得尤为重要。在app应用开发中,我们需要对应用中的多项数据进行加密处理,从而来保证应用上线后的安全性,给用户一个安全保障。这篇文章就介绍在iOS开发中最常用的数据加密方式。
文中证书锁定内容部分参考了博客
http://blog.csdn.net/dd864140130/article/details/52625666。
iOS中数据加密有下面几种方式
1、使用数字证书锁定来保证不被中间人拦截,将服务器返回的数据和我的当地证书进行对比,确保是从服务器返回回来的。证书有ca证书,也可以自己给自己签发证书。像12306购票。
2、使用https协议请求网页,post来请求网页数据,保证用户的账号密码不被被人获取到。
3、使用苹果自己的SSKeyChain钥匙串,将用户的账号密码保存在钥匙串中。钥匙串拱了错误处理,如果保存出错,会在判断后打印出出错的信息。
4、最保险的加密算法是非对称加密。非对称加密公钥加密私钥解密。缺点是要耗费时间。
1、证书锁定
当我们上网浏览网页,从网上获取数据的时候,我们知道,不管是http还是https协议,都是服务端被动,客户端主动。所以,客户端第一次发出请求之后,通常无法确定服务端是不是合法。就很可能就会出现以下情景,正常情况下,我们想要根据文章aid查看某篇文章内容,其流程如下:
但如果遭受黑客攻击,流程就会这样的:
此时恶意服务端完全可以发起双向攻击:对上可以欺骗服务端,对下可以欺骗客户端,更严重的是客户端段和服务端完全感知不到已经被攻击了。这就是中间人攻击。
关于中间人攻击维基百科上有更深入的定义:
中间人攻击(Man-in-the-middle attack,缩写:MITM)是指攻击者与通讯的两端分别创建独立的联系,并交换其所收到的数据,使通讯的两端认为他们正在通过一个私密的连接与对方直接对话,但事实上整个会话都被攻击者完全控制。在中间人攻击中,攻击者可以拦截通讯双方的通话并插入新的内容。在许多情况下这是很简单的(例如,在一个未加密的Wi-Fi无线接入点的接受范围内的中间人攻击者,可以将自己作为一个中间人插入这个网络)。
一个中间人攻击能成功的前提条件是攻击者能将自己伪装成每一个参与会话的终端,并且不被其他终端识破。中间人攻击是一个(缺乏)相互认证的攻击。大多数的加密协议都专门加入了一些特殊的认证方法以阻止中间人攻击。例如,SSL协议可以验证参与通讯的一方或双方使用的证书是否是由权威的受信任的数字证书认证机构颁发,并且能执行双向身份认证。
那么我们来看下证书锁定是怎么样提高安全性,避免中间人攻击的,用一张简单的流程图来说明:
不难看出,通过证书锁定能有有效的避免中间人攻击。
证书锁定的缺点
证书锁定尽管带了较高的安全性,但是这种安全性的提高却牺牲了灵活性。一旦当证书发生变化时,我们的客户端也必须随之升级,除此之外,我们的服务端不得不为了兼容以前的客户端而做出一些妥协或者说直接停用以前的客户端,这对开发者和用户来说并不是那么的友好。
但实际上,极少情况下我们才会变动证书。因此,如果产品安全性要求比较高还是启动证书锁定吧。
在iOS开发中,我们可以自己给自己签发数字证书,就类似于12306购票网站。从而保证了数据的安全性。下面是生成证书的过程。
// 生成1024位私钥
openssl genrsa -out private_key.pem 1024
// 根据私钥生成CSR文件
openssl req -new -key private_key.pem -out rsaCertReq.csr
// 根据私钥和CSR文件生成crt文件 openssl x509 -req -days 3650 -in rsaCertReq.csr -signkey private_key.pem -out rsaCert.crt
// 为IOS端生成公钥der文件
openssl x509 -outform der -in rsaCert.crt -out public_key.der
// 将私钥导出为这p12文件
openssl pkcs12 -export -out private_key.p12 -inkey private_key.pem -in rsaCert.crt
2、使用post
下面是两段post和get的代码的对比。
//GET请求的URL
http://localhost/php/login/login.php?username=zhangsan&password=zhang
//POST请求的URL
http://localhost/php/login/login.php
我们可以很清楚地看到使用GET方式发起请求的URL中包含了用户的账号和密码信息。如果使用GET发起请求,如果有人使用Charles拦截我们的请求,就很容易地从我们发起请求的URL中获取到我们的账号信息。所以发起数据请求的时候避免使用GET,而使用POST。
3、SSKeyChain
使用苹果自己的SSKeyChain钥匙串,我们也能保证用户的数据安全,我们将用户的账号信息保存到钥匙串中能保证数据安全的原因是因为只有苹果公司才知道钥匙串保存在内存中的哪个位置。
使用SSKeyChain我们进行下面两步骤操作:
1、 在工程中加入Security.framework框架。
2、 把SSKeychain.h和SSKeychain.m加到项目文件夹。
加入了需要的文件夹后,SSKeyChain的作者samsoffes在实例代码中给出了使用SSKeyChain的方法。
我们通过下面方法来使用SSKeyChain。
//获取所有账号
+ (NSArray *)allAccounts;
//通过账号名字获取服务名
+ (NSArray *)accountsForService:(NSString *)serviceName;
//通过服务名和账号获取密码
+ (NSString *)passwordForService:(NSString*)serviceNameaccount:(NSString *)account;
//通过服务名和账号删除密码
+ (BOOL)deletePasswordForService:(NSString*)serviceNameaccount:(NSString *)account;
//通过服务名和账号设置密码
+ (BOOL)setPassword:(NSString *)passwordforService:(NSString*)serviceName account:(NSString *)account;
下面是具体的使用方法,通过上面几个方法,我们可以很方便地将用户账号保存到钥匙串,或者从钥匙串中取出来。
项目地址https://github.com/samsoffes/sskeychain
#import <SenTestingKit/SenTestingKit.h>
#import "SSKeychain.h"
//用变量接受服务名,账号和密码
static NSString *kSSToolkitTestsServiceName = @"SSToolkitTestService";
static NSString *kSSToolkitTestsAccountName = @"SSToolkitTestAccount";
static NSString *kSSToolkitTestsPassword = @"SSToolkitTestPassword";
@interface SSKeychainTests : SenTestCase
//判断钥匙串所有账号中是否包含一个指定的账号
- (BOOL)_accounts:(NSArray *)accounts containsAccountWithName:(NSString *)name;
@end
@implementation SSKeychainTests
- (void)testAll {
// Getting & Setings Passwords
[SSKeychain setPassword:kSSToolkitTestsPassword forService:kSSToolkitTestsServiceName account:kSSToolkitTestsAccountName];
NSString *password = [SSKeychain passwordForService:kSSToolkitTestsServiceName account:kSSToolkitTestsAccountName];
STAssertEqualObjects(password, kSSToolkitTestsPassword, @"Password reads and writes");
// Getting Accounts
NSArray *accounts = [SSKeychain allAccounts];
STAssertTrue([self _accounts:accounts containsAccountWithName:kSSToolkitTestsAccountName], @"All accounts");
accounts = [SSKeychain accountsForService:kSSToolkitTestsServiceName];
STAssertTrue([self _accounts:accounts containsAccountWithName:kSSToolkitTestsAccountName], @"Account for service");
// Deleting Passwords
[SSKeychain deletePasswordForService:kSSToolkitTestsServiceName account:kSSToolkitTestsAccountName];
password = [SSKeychain passwordForService:kSSToolkitTestsServiceName account:kSSToolkitTestsAccountName];
STAssertNil(password, @"Password deletes");
}
- (BOOL)_accounts:(NSArray *)accounts containsAccountWithName:(NSString *)name {
for (NSDictionary *dictionary in accounts) {
if ([[dictionary objectForKey:@"acct"] isEqualToString:name]) {
return YES;
}
}
return NO;
}
上面的方法是用来保存用户的账号信息,将账号信息保存到钥匙串中,因为钥匙串的不可见性,就已经足够地保证了用户的账号信息安全。如果我们还想让用户的账号信息得到更安全的保证,我们可以先将用户信息进行MD5加密,然后加盐。再将加密后的账号信息保存到钥匙串中。因为MD5编码的不可逆性,就更进一步地保证了用户信息的安全。
关于MD5加密我在我之前写的一篇MD5加密的博客中有详细的说明。
4、非对称加密
刚才介绍方法是用来保证用户信息的安全。在金融类app中,要保证财务数据的安全,就需要使用到更加安全的非对称加密(维基百科上叫公开密钥加密)。维基百科对于非对称加密的定义是:一种密码学算法类型,在这种密码学方法中,需要一对密钥,一个是私人密钥,另一个则是公开密钥。这两个密钥是数学相关,用某用户密钥加密后所得的信息,只能用该用户的解密密钥才能解密。如果知道了其中一个,并不能计算出另外一个。因此如果公开了一对密钥中的一个,并不会危害到另外一个的秘密性质。称公开的密钥为公钥;不公开的密钥为私钥。
使用公开加密方式中的公钥和私钥可以进行数字签名,原理是这样子的:用私钥加密的信息,可以用公钥对其解密,用于客户验证持有私钥一方发布的数据或文件是完整准确的,接收者由此可知这条信息确实来自于拥有私钥的某人,这被称作数字签名,公钥的形式就是数字证书。例如,从网上下载的安装程序,一般都带有程序制作者的数字签名,可以证明该程序的确是该作者(公司)发布的而不是第三方伪造的且未被篡改过(身份认证/验证)。
常见的公钥加密算法有:RSA、ElGamal、背包算法、Rabin(RSA的特例)、迪菲-赫尔曼密钥交换协议中的公钥加密算法、椭圆曲线加密算法(英语:Elliptic Curve Cryptography, ECC)。使用最广泛的是RSA算法(由发明者Rivest、Shmir和Adleman姓氏首字母缩写而来)是著名的公开秘钥加密算法。在这里我们也是使用RSA来进行公钥加密。
通过下面的代码,我们能够自己生成一个数字证书。
// 生成1024位私钥 openssl genrsa -out private_key.pem 1024
// 根据私钥生成CSR文件 openssl req -new -key private_key.pem -out rsaCertReq.csr
// 根据私钥和CSR文件生成crt文件 openssl x509 -req -days 3650 -in rsaCertReq.csr -signkey private_key.pem -out rsaCert.crt
// 为IOS端生成公钥der文件 openssl x509 -outform der -in rsaCert.crt -out public_key.der
// 将私钥导出为这p12文件 openssl pkcs12 -export -out private_key.p12 -inkey private_key.pem -in rsaCert.crt
得到公钥和私钥后就可以将数据进行加密了。我们把一个字符串用RAS算法加密后查看加密后的字符串,再反编码看到解密后的字符串,查看加密和解密的效果。
NSString *encryptString = [self rsaEncryptText:@"123456好哇好哇哈"];
NSLog(@"加密:123456好哇好哇哈:%@", encryptString);
NSLog(@"解密结果为:%@", [self rsaDecryptWithText:encryptString]);
然后我们自己定义加密和解密的方法,等下我们就要使用加密和解密的方法来进行数据的加密和解密。
插入自定义加密和解密的方法。
#import <Foundation/Foundation.h>
@interface HYBRSAEncrypt : NSObject
// 加密相关
- (void)loadPublicKeyWithPath:(NSString *)derFilePath;
- (void)loadPublicKeyWithData:(NSData *)derData;
- (NSString *)rsaEncryptText:(NSString *)text;
- (NSData *)rsaEncryptData:(NSData *)data;
// 解密相关
- (void)loadPrivateKeyWithPath:(NSString *)p12FilePath password:(NSString *)p12Password;
- (void)loadPrivateKeyWithData:(NSData *)p12Data password:(NSString *)p12Password;
- (NSString *)rsaDecryptText:(NSString *)text;
- (NSData *)rsaDecryptData:(NSData *)data;
@end
加密过程中的思路是:
1、定义一个方法,把证书文件需要加密的数据传入一个方法中,生成一个公钥。
2、创建一个能够将数据进行base64加密的方法。将需要加密的文本通过base加密,加密完成后再调用公钥加密的方法对base加密后的数据进行二次加密。加密时是讲二进制数据分段,切片后进行加密再拼接到二进制数据的变量中。
#import "HYBRSAEncrypt.h"
@interface HYBRSAEncrypt () {
SecKeyRef _publicKey;
SecKeyRef _privateKey;
}
@end
@implementation HYBRSAEncrypt
- (void)dealloc {
if (nil != _publicKey) {
CFRelease(_publicKey);
}
if (nil != _privateKey) {
CFRelease(_privateKey);
}
}
#pragma mark - 加密相关
//用本地证书加载公钥
- (void)loadPublicKeyWithPath:(NSString *)derFilePath {
NSData *derData = [[NSData alloc] initWithContentsOfFile:derFilePath];
if (derData.length > 0) {
[self loadPublicKeyWithData:derData];
} else {
NSLog(@"load public key fail with path: %@", derFilePath);
}
}
//加载公钥方法
- (void)loadPublicKeyWithData:(NSData *)derData {
SecCertificateRef myCertificate = SecCertificateCreateWithData(kCFAllocatorDefault, (__bridge CFDataRef)derData);
SecPolicyRef myPolicy = SecPolicyCreateBasicX509();
SecTrustRef myTrust;
OSStatus status = SecTrustCreateWithCertificates(myCertificate,myPolicy,&myTrust);
SecTrustResultType trustResult;
if (status == noErr) {
status = SecTrustEvaluate(myTrust, &trustResult);
}
SecKeyRef securityKey = SecTrustCopyPublicKey(myTrust);
CFRelease(myCertificate);
CFRelease(myPolicy);
CFRelease(myTrust);
_publicKey = securityKey;
}
//将文本内容加密
- (NSString *)rsaEncryptText:(NSString *)text {
NSData *encryptedData = [self rsaEncryptData:[text hdf_toData]];
NSString *base64EncryptedString = [NSString hdf_base64StringFromData:encryptedData
length:encryptedData.length];
return base64EncryptedString;
}
//分段再加密数据
- (NSData *)rsaEncryptData:(NSData *)data {
SecKeyRef key = _publicKey;
size_t cipherBufferSize = SecKeyGetBlockSize(key);
uint8_t *cipherBuffer = malloc(cipherBufferSize * sizeof(uint8_t));
size_t blockSize = cipherBufferSize - 11;
size_t blockCount = (size_t)ceil([data length] / (double)blockSize);
NSMutableData *encryptedData = [[NSMutableData alloc] init] ;
for (int i = 0; i < blockCount; i++) {
size_t bufferSize = MIN(blockSize,[data length] - i * blockSize);
NSData *buffer = [data subdataWithRange:NSMakeRange(i * blockSize, bufferSize)];
OSStatus status = SecKeyEncrypt(key,
kSecPaddingPKCS1,
(const uint8_t *)[buffer bytes],
[buffer length],
cipherBuffer,
&cipherBufferSize);
if (status == noErr) {
NSData *encryptedBytes = [[NSData alloc] initWithBytes:(const void *)cipherBuffer
length:cipherBufferSize];
[encryptedData appendData:encryptedBytes];
} else {
if (cipherBuffer) {
free(cipherBuffer);
}
return nil;
}
}
if (cipherBuffer){
free(cipherBuffer);
}
return encryptedData;
}
然后我们可以通过私钥解密。解密思路和加密过程相同。
#pragma mark - 解密相关
- (void)loadPrivateKeyWithPath:(NSString *)p12FilePath password:(NSString *)p12Password {
NSData *data = [NSData dataWithContentsOfFile:p12FilePath];
if (data.length > 0) {
[self loadPrivateKeyWithData:data password:p12Password];
} else {
NSLog(@"load private key fail with path: %@", p12FilePath);
}
}
//生成私钥
- (void)loadPrivateKeyWithData:(NSData *)p12Data password:(NSString *)p12Password {
SecKeyRef privateKeyRef = NULL;
NSMutableDictionary * options = [[NSMutableDictionary alloc] init];
[options setObject:p12Password forKey:(__bridge id)kSecImportExportPassphrase];
CFArrayRef items = CFArrayCreate(NULL, 0, 0, NULL);
OSStatus securityError = SecPKCS12Import((__bridge CFDataRef)p12Data,
(__bridge CFDictionaryRef)options,
&items);
if (securityError == noErr && CFArrayGetCount(items) > 0) {
CFDictionaryRef identityDict = CFArrayGetValueAtIndex(items, 0);
SecIdentityRef identityApp = (SecIdentityRef)CFDictionaryGetValue(identityDict,
kSecImportItemIdentity);
securityError = SecIdentityCopyPrivateKey(identityApp, &privateKeyRef);
if (securityError != noErr) {
privateKeyRef = NULL;
}
}
_privateKey = privateKeyRef;
// CFRelease(items);
}
//调用下面方法进行解密,最后返回一个字符串
- (NSString *)rsaDecryptText:(NSString *)text {
NSData *data = [NSData hdf_base64DataFromString:text];
NSData *decryptData = [self rsaDecryptData:data];
NSString *result = [[NSString alloc] initWithData:decryptData encoding:NSUTF8StringEncoding];
return result;
}
//用私钥解密的方法,被上面方法调用
- (NSData *)rsaDecryptData:(NSData *)data {
SecKeyRef key = _privateKey;
size_t cipherLen = [data length];
void *cipher = malloc(cipherLen);
[data getBytes:cipher length:cipherLen];
size_t plainLen = SecKeyGetBlockSize(key) - 12;
void *plain = malloc(plainLen);
OSStatus status = SecKeyDecrypt(key, kSecPaddingPKCS1, cipher, cipherLen, plain, &plainLen);
if (status != noErr) {
return nil;
}
NSData *decryptedData = [[NSData alloc] initWithBytes:(const void *)plain length:plainLen];
return decryptedData;
}
@end
上面就是iOS开发中常用的几种数据加密方式。