加密算法
首先了解一下加密算法,常见的加密算法可以分成三类,对称密钥加密、公开密钥加密、散列函数。
对称密钥加密
对称密钥加密又称为对称加密、私钥加密、共享密钥加密,是密码学中的一类加密算法。这类算法在加密和解密时使用相同的密钥,或是使用两个可以简单地相互推算的密钥。与公开密钥加密相比,要求双方获取相同的密钥是对称密钥加密的主要缺点之一。
常见的对称加密算法有:DES、3DES、AES、Blowfish、IDEA、RC5、RC6
。
对称加密的速度比公钥加密快很多,在很多场合都需要对称加密。
公开密钥加密
公开密钥加密,也称为非对称加密,是密码学的一种算法,它需要两个密钥,一个是公开密钥,另一个是私有密钥;一个用作加密的时候,另一个则用作解密。
使用最广泛的是RSA算法
。
公开密钥加密的速度比对称密钥速度慢。
RSA 加密算法具体实现思路
我们发现服务器判断用户是否登录, 完全依赖于sessionId
, 一旦其被截获, 黑客就能够模拟出用户的请求。于是我们需要引入token
的概念: 用户登录成功后, 服务器不但为其分配了sessionId
, 还分配了token
, token
是维持登录状态的关键秘密数据。在服务器向客户端发送的token
数据,也需要加密。于是一次登录的细节再次扩展。
- 客户端向服务器第一次发起登录请求(不传输用户名和密码)。
- 服务器利用RSA算法产生一对公钥和私钥。并保留私钥, 将公钥发送给客户端。
- 客户端收到公钥后, 加密用户密码,向服务器发送用户名和加密后的用户密码; 同时另外产生一对公钥和私钥,自己保留私钥, 向服务器发送公钥; 于是第二次登录请求传输了用户名和加密后的密码以及客户端生成的公钥。
- 服务器利用保留的私钥对密文进行解密,得到真正的密码。 经过判断, 确定用户可以登录后,生成
sessionId
和token
, 同时利用客户端发送的公钥,对token
进行加密。最后将sessionId
和加密后的token
返还给客户端。
客户端利用自己生成的私钥对token
密文解密, 得到真正的token
。
登录保持(也就是http数据请求阶段)
引入token后,http请求被获取问题便可得到解决。 服务器将token和其它的一些变量, 利用散列加密算法得到签名后,连同sessionId一并发送给服务器; 服务器取出保存于服务器端的token,利用相同的法则生成校验签名, 如果客户端签名与服务器的校验签名一致, 就认为请求来自登录的客户端。(支付宝一样的机制)
注:token失效的两种情况:
- 用户登录出系统
- token在后台的规定时间内失效(每个token都是有时间效应的)
失效原理:在服务器端的redis中删除相应key为session的键值对。
散列函数
散列函数(Hash function)又称散列算法、哈希函数,是一种从任何一种数据中创建小的数字“指纹”的方法。散列函数把消息或数据压缩成摘要,使得数据量变小,将数据的格式固定下来。该函数将数据打乱混合,重新创建一个叫做散列值(hash values,hash codes,hash sums,或hashes)的指纹。散列值通常用一个短的随机字母和数字组成的字符串来代表。
目前流行的 Hash 算法包括 MD5、SHA-1 和 SHA-2
哈希算法生成的密文不可逆,因此可以用来判断数据的完整性。
存储加密
为了不明文存储数据,我们需要选择一种加密算法,将明文加密后存储,在需要的时候再读取密文解密。由于哈希算法不可逆,排除,由于不需要将缓存的数据共享到服务器,并且非对称加密算法加密解密速度慢,因此在项目中我选用了对称加密AES来加密缓存的数据。
使用AES加密缓存的数据,重要的是保存好密钥,目前我使用的办法是将密钥分成4部分隐藏在项目中。
// 获取分散存储的密钥
+ (NSString *)getAESKey {
NSString *key = [NSString stringWithFormat:@"%@%@%@%@", TDKeyA, TDKeyB, TDKeyC, TDKeyD];
return key;
}
// AES加密
+ (NSData *)aes128EncryptData:(NSData *)data {
if (data == nil) {
return nil;
}
NSString *key = [self getAESKey];
char keyPtr[kCCKeySizeAES128+1];
memset(keyPtr, 0, sizeof(keyPtr));
[key getCString:keyPtr maxLength:sizeof(keyPtr) encoding:NSUTF8StringEncoding];
NSUInteger dataLength = [data length];
size_t bufferSize = dataLength + kCCBlockSizeAES128;
void *buffer = malloc(bufferSize);
size_t numBytesEncrypted = 0;
CCCryptorStatus cryptStatus = CCCrypt(kCCEncrypt,
kCCAlgorithmAES128,
kCCOptionPKCS7Padding|kCCOptionECBMode,
keyPtr,
kCCBlockSizeAES128,
NULL,
[data bytes],
dataLength,
buffer,
bufferSize,
&numBytesEncrypted);
if (cryptStatus == kCCSuccess) {
NSData *resultData = [NSData dataWithBytesNoCopy:buffer length:numBytesEncrypted];
return resultData;
}
free(buffer);
return nil;
}
// AES解密
+ (NSData *)aes128DecryptData:(NSData *)data {
NSString *key = [self getAESKey];
char keyPtr[kCCKeySizeAES128 + 1];
memset(keyPtr, 0, sizeof(keyPtr));
[key getCString:keyPtr maxLength:sizeof(keyPtr) encoding:NSUTF8StringEncoding];
NSUInteger dataLength = [data length];
size_t bufferSize = dataLength + kCCBlockSizeAES128;
void *buffer = malloc(bufferSize);
size_t numBytesCrypted = 0;
CCCryptorStatus cryptStatus = CCCrypt(kCCDecrypt,
kCCAlgorithmAES128,
kCCOptionPKCS7Padding|kCCOptionECBMode,
keyPtr,
kCCBlockSizeAES128,
NULL,
[data bytes],
dataLength,
buffer,
bufferSize,
&numBytesCrypted);
if (cryptStatus == kCCSuccess) {
NSData *resultData = [NSData dataWithBytesNoCopy:buffer length:numBytesCrypted];
return resultData;
}
free(buffer);
return nil;
}
别忘记在文件开头导入#import
数据完整性验证
想要检查你的文件数据是否被篡改,这时候哈希算法就可以登场啦,由于哈希算法生成文件的唯一标志信息是与文件的每一个字节都相关,并且不可逆。因此只要你的文件被篡改,那么生成的这个唯一标志信息也会跟随改变。这里我使用了MD5算法:
+ (NSString *)md5FromString:(NSString *)string {
const char *cStr = [string UTF8String];
unsigned char result[CC_MD5_DIGEST_LENGTH];
CC_MD5(cStr, strlen(cStr), result);
return [[NSString stringWithFormat:@"%02X%02X%02X%02X%02X%02X%02X%02X%02X%02X%02X%02X%02X%02X%02X%02X",
result[0], result[1], result[2], result[3],
result[4], result[5], result[6], result[7],
result[8], result[9], result[10], result[11],
result[12], result[13], result[14], result[15]
] lowercaseString];
}
为了保证项目中缓存数据的完整性以及密文存储,我做了以下处理:
MD5方式
存储文件
- 通过MD5算法获取文件数据的散列值并存储(以便后续检查存储的文件数据的完整性)。
- 将该文件数据通过AES加密并存储加密后的密文。
获取文件数据
- 读取存储的密文数据并进行AES解密得到明文数据。
验证文件数据完整性
- 通过MD5算法获取解密后的数据的散列值hashA。
- 读取存储的散列值hashB。
- 将存储的散列值hashB与hashA比较,如果相同,则文件数据未被篡改。
加盐哈希
加盐哈希是目前业界最常见的做法。
加盐哈希的步骤如下:
- 用户注册时,给他随机生成一段字符串,这段字符串就是盐(Salt)
- 把用户注册输入的密码和盐拼接在一起,叫做加盐密码
- 对加盐密码进行哈希,并把结果和盐都储存起来
- 在登陆时,先取出盐,再同样进行拼接、计算哈希,就能判断密码的合法性。
加盐哈希的做法,既保证了储存数据的不可逆,又防止了上一章的彩虹表攻击方式。这种方式下,黑客拿到数据库后,如果再要用遍历所有常用的密码组合的方式做彩虹表,那他需要对所有常用密码+盐值进行哈希运算。而每个用户的盐值都不相同,之前彩虹表的「一次运算无数次使用」变成了「一次运算一次使用」。这样的成本是难以接受的,由于前提3(攻击成本远高于收益,系统达到相对安全),所以这是一个比较安全的做法。