Hash 简介
Hash(哈希 / 散列)算法:把任意长度的输入通过散列算法变换成固定长度的输出,该输出就是散列值。这种转换是一种压缩映射,也就是说,散列值的空间通常远小于输入的空间,不同的输入可能会散列成相同的输出。所以,不可能从散列值来唯一确定输入值。简单地说,哈希函数就是一种将任意长度的消息压缩到某一固定长度的 消息摘要 的函数。计算哈希值的常用方法有(关键字 K = 输入,散列地址 = 哈希地址 = 散列值 = 哈希值):
哈希碰撞:两个不同的输入,根据同一哈希函数计算出相同的哈希值的现象。衡量一个哈希函数的好坏的重要指标就是发生碰撞的概率以及发生碰撞的解决方案。任何哈希函数基本都无法彻底避免碰撞,常见的解决碰撞的方法有以下几种:
常见的 Hash 函数
MD5(Message-Digest Algorithm 5):信息摘要算法,用于生成信息摘要,确保信息传输完整一致。曾广泛运用于信息安全领域,但是随着近年来密码学领域实质性的研究进展,使本算法不再适合当前的安全环境。目前,MD5 广泛应用于错误检查。例如,在一些 BitTorrent 下载中,软件通过计算 MD5 检验下载到的碎片的完整性。MD5 的输出结果长度为 128 bit(32 Hex,16 Byte)
SHA(Secure Hash Algorithm):安全散列算法,用于生成信息摘要,确保信息传输完整一致。SHA 是一个算法系列的统称,包括以下几个大版本:
SHA - 1:输出结果长度为 160 bit(40 Hex,20 Byte),在 2005 年的时候,出现了 SHA - 1 的破解方法,因此 SHA - 1 也在逐渐被淘汰
SHA - 2 包括以下几个子算法:
SHA - 3:又叫 Keccak 算法,可以生成任意长度的哈希值,但为了配合 SHA - 2 的哈希值长度,SHA - 3 标准中规定了 SHA3 - 224、SHA3 - 256、SHA3 - 384、SHA3 - 512这 4 种版本。
SHA - 3 并没有取代 SHA - 2 的趋势,因为就目前来说,SHA - 2 并没有被发现明显的缺点。SHA - 3 更多的是做为 SHA - 2 的补充。
Question:为什么 SHA - 2 要有这么多的版本呢?只使用最长的一种不行吗?
Answer:信息摘要越长,发生碰撞的几率就越低,破解的难度就越大。但同时,耗费的性能和占用的空间也就越高。 SHA - 2 有不同输出长度的子算法是为了适应不同的应用场景,从而对安全、性能、空间等因素做出权衡。比如说过我的需求仅仅是验证数据完整性,使用 SHA - 512 显然是浪费的。
Hash 的特点
所有哈希函数都有以下基本特性:
因此,Hash 算法可以看出是一种单向的加密算法。
Hash 的应用
无论是 MD5 还是 SHA 系列算法,本质上都是:把任意长度的输入通过散列算法变换成固定长度的输出,即对信息进行摘要(信息摘要 有时也叫 信息指纹)。Hash 的应用,其实就是对 Hash 函数的特点进行扬长避短:
Hash 与 搜索引擎分词搜索
Question:用户使用搜索引擎进行搜索时,不同用户拥有不同的搜索习惯。比如,当需要搜索2020年英雄联盟世界赛的信息时,以下分词的组合都是有可能的:2020 LOL 世界赛,LOL 世界赛 2020,LOL 2020 世界赛,… 。那么,搜索引擎是如何发现不同用户的同一搜索需求,进而提供相同的搜索结果的呢?
Answer:对所有分词取 Hash 值,再将得到的所有 Hash 值相加,最终得到一个特征值。因为每个分词的 Hash 值是固定的,所以不论分词的组合如何变化,最终所有分词的 Hash 值相加的结果也是固定的。以对分词取 MD5 举例:
MD5(2020) = 7B7A53E239400A13BD6BE6C91C4F6C4E
MD5(LOL) = AEE4BD941F8B4D9E39210C06C44FCB71
MD5(世界赛) = D594C223B8D03B1B85B62514286F2E06
MD5(2020) + MD5(LOL) + MD5(世界赛) = 固定的特征结果
注意:所有类型的 Hash 值,本质上都是一个固定长度的数值。如,MD5 是一个 128 bit 长的数值,SHA1 是一个 160 bit 长的数值 …
HMAC 算法简介
HMAC(Hash-based Message Authentication Code):哈希消息认证码,一种使用单向散列函数来构造消息认证码的算法。HMAC 以一个密钥和一个消息作为输入,生成一个消息摘要作为输出。HMAC 算法用数学公式表示如下:
H M A C ( K , M ) = H ( ( K ′ ⊕ o p a d ) ∣ H ( ( K ′ ⊕ i p a d ) ∣ M ) ) H\!M\!A\!C(K,M) = H(~(K'⊕opad)~∣~H(~(K'⊕ipad)~∣~M)~) HMAC(K,M)=H( (K′⊕opad) ∣ H( (K′⊕ipad) ∣ M) )
其中:
K K K 表示秘钥(Key)
M M M 表示消息(Message)
K ′ K' K′ 表示被处理成 哈希函数分组长度 的秘钥
i p a d ipad ipad 表示固定常数,值为 0x5C(1 Byte,二进制为:01011100)循环 哈希函数分组长度 所形成的比特序列,i 是 inner 的意思
o p a d opad opad 表示固定常数,值为 0x36(1 Byte,二进制为:00110110)循环 哈希函数分组长度 所形成的比特序列,o 是 outer 的意思
HMAC 算法流程
HMAC 中所使用的单向散列函数并不局限于一种,任何高强度的单向散列函数都可以被用于 HMAC,如果将来设计出的新的单向散列函数,也同样可以使用。如:
HMAC + SHA1 = HMAC - SHA1
HMAC + SHA224 = HMAC - SHA224
HMAC + SHA256 = HMAC - SHA256
HMAC + SHA384 = HMAC - SHA384
HMAC + SHA512 = HMAC - SHA512
这里以 HMAC - SHA1 举例说明 HMAC 的算法流程,其中 SHA1 的分组长度为 512 bit(64 Byte),输出结果为 160 bit(20 Byte):
密钥填充,将 K K K 转换为 K ′ K' K′
如果密钥 K K K 比单向散列函数分组长度要短,就需要在末尾填充0,直到其长度达到单向散列函数的分组长度为止
如果密钥 K K K 比单向散列函数分组长度要长,则需要用单向散列函数求出密钥的散列值,然后将这个散列值用作为 K ′ K' K′
将填充后的密钥 K ′ K' K′ 与 i p a d ipad ipad 进行异或运算,得到比特序列 i p a d K e y ipadK\!ey ipadKey
将填充后的密钥 K ′ K' K′ 与被称为 i p a d ipad ipad 的比特序列进行 XOR 运算
i p a d ipad ipad 是将 00110110 这一比特序列不断循环反复直到达到散列函数分组长度所形成的比特序列( i p a d ipad ipad 的 i i i 是 inner 的意思)
XOR 运算所得到的值,就是一个和单向散列函数的分组长度相同,且和密钥相关的比特序列
这里将这个比特序列称为 i p a d K e y ipadK\!ey ipadKey
组合 i p a d K e y ipadK\!ey ipadKey 与消息 M M M,也就是将 i p a d K e y ipadK\!ey ipadKey 附加在消息 M M M 的开头
将步骤 3 的结果输入单向散列函数,并计算出散列值
将填充后的密钥 K ′ K' K′ 与 o p a d opad opad 进行异或运算,得到比特序列 o p a d K e y opadK\!ey opadKey
将填充后的密钥 K ′ K' K′ 与被称为 o p a d opad opad 的比特序列进行 XOR 运算
o p a d opad opad 是将 01011100 这一比特序列不断循环反复直到达到散列函数分组长度所形成的比特序列( o p a d opad opad 的 o o o 是 outer 的意思)
XOR 运算所得到的值,也是一个和单向散列函数的分组长度相同,且和密钥相关的比特序列
这里将这个比特序列称为 o p a d K e y opadK\!ey opadKey
组合 o p a d K e y opadK\!ey opadKey 与步骤 4 得到的散列值,也就是将 o p a d K e y opadK\!ey opadKey 附加在步骤 4 得到的散列值的开头
将步骤 6 的结果输入单向散列函数,并计算出散列值,这个散列值就是最终的 HMAC 值
Objective - C 中 HMAC 的使用
#import
#import
-(void)HMAC_Demo {
// 用户密码
NSString* userPwd = @"123456";
// HMAC 运算使用的 Key,一般从服务器获取,一个用户对应一个不同的 Key
NSString* hmacKey = @"RandomString";
// 转成 C 字符串
const char* userPwdData = userPwd.UTF8String;
const char* hmackeyData = hmacKey.UTF8String;
// 开辟用于接收 HMAC 运算结果的缓冲区
uint8_t buffer[CC_MD5_DIGEST_LENGTH];
// 进行 HMAC 运算
CCHmac(kCCHmacAlgSHA1, hmackeyData, strlen(hmackeyData), userPwdData, strlen(userPwdData), buffer);
// 将 HMAC 运算结果转成 16 进制字符串
NSMutableString* hmacStr = [NSMutableString string];
for (int i = 0; i < CC_MD5_DIGEST_LENGTH; i++) {
[hmacStr appendFormat:@"%02x", buffer[i]];
}
// 打印结果
NSLog(@"hmacStr = %@", hmacStr);
/** 输出
hmacStr = 318df6b14983a3f8dd29f7191c1e1b9b
*/
}
用户登录流程 && 注意点
一般情况下,用户登录的业务流程为:
关于 Client 端的风险:
Client 直接暴露在外界,容易遭受逆向分析,需要做到,即使某一客户端被黑客攻陷,也只有被攻陷的客户端的安全受影响,没有被逆向攻陷的客户端,仍然是安全的。这是什么意思呢?比如,在做 Client 开发的时候,用户账号、密码的存储与传输,需要进行加密,但不能将加密的算法与加密的秘钥全部写死在客户端里面。即,虽然所有 Client 可以采用同一套加密算法与流程,但是每个 Client 加密算法的秘钥需要不同。这样子,即使黑客攻陷了某一个客户端,也只能获取到所有客户端的加密算法和被攻陷的客户端的秘钥,黑客不能获取到其他没有被攻陷的客户端的秘钥,因此其他没被攻陷的客户端相对来说是安全的。
关于网络传输的风险:
虽然涉及到用户隐私的数据请求,一般都使用 HTTPS 进行传输,但是不安全的网络环境下,HTTPS 中的内容,仍然可能被监听和拦截,从而进行重放攻击。因此,Client 和 Server 之间的 身份验证请求 和 身份验证结果,需要做到具有短暂的时效性。即黑客在拦截到 身份验证请求 或者 身份验证结果 的时候,如果不能在短时间内进行破解或者使用,身份验证请求 或者 身份验证结果 就会失效。
关于 Server 端的风险:
Server 保存着所有用户的账号、密码,需要做到即使 Server 的源码与数据库都遭到了泄漏,黑客也不能通过 Server 源码中的算法和数据库,反推出用户的明文密码。
因此,我们在进行开发时,需要注意:
常见登录方法分析
① 直接使用 Hash 对登录密码进行不可逆加密:
Client 直接将用户输入的密码进行 Hash 运算,将得到结果发送给 Server 进行验证。同时,Server 在用户注册的时候,数据库中保存的就是用户密码的 Hash 值,而不是密码本身。这样就算 Server 被攻克,用户的隐私信息也能起到一定的保护。
这样做存在的隐患:
② 为了防止对登录密码的 Hash 值进行反向查询,在方法 ① 的基础上 Client 可以对登录密码加盐后,再进行 Hash 运算。使用这种方式,对于反向查询来说就比较困难了,安全系数也相对较高,但是依然存在隐患:
③ Client 使用 HMAC 计算登录密码的 Hash 值。用户的登录密码即为 HMAC 的 M M M,并且 Client 的 HMAC. K e y K\!ey Key 通过服务器获取,服务器上一个用户对应一个不同的 HMAC. K e y K\!ey Key 。这种加密方案,可以很好的保护用户的隐私信息。因为就算 Client 泄露了 K e y K\!ey Key 值,这个 K e y K\!ey Key 值也只是一个用户的,不会污染整个项目。再者,就算 Server 被黑客攻陷,Server 的加密算法与数据库全部遭到了泄露,因为每个用户使用的 HMAC. K e y K\!ey Key 都是不一样的,黑客要破解所有用户的账号、密码,成本会趋近于无穷大。其实,HMAC 算法当中的 K e y K\!ey Key 值,可以看成是一种特殊的盐值,此种做法可以很好地解决方法 ② 中,盐值和代码耦合度高的问题。但是这种做法,依然无法防止重放攻击。
④ 从 方法① 演变到 方法③ 的过程,虽然已经可以很好地保护用户的真实密码,但是 方法① ~ 方法③ 都有一个致命的缺陷:无论 Client 和 Server 如何复杂地对密码进行 Hash 运算,在用户不更改密码的情况下,任何时候,Client 发送给 Server 的数据请求中,用户密码都是一个不变的 Hash 值。此时,黑客只要拿到用户密码的 Hash 值,那么模拟用户登录,再简单不过了。
重放攻击本质上针对的是没有时效性或者时效性很长的数据请求,要解决重放攻击,需要让数据请求变得具有较短的时效性,或者让数据请求只在当次有效。
为防止重放攻击,可以用拼接时间戳的方式对方法 ③ 的流程进行升级:
关于注册:
关于登录:
注意点:
⑤ 其实,上面的方法 ④ 还是存在漏洞,即用户在注册时,Client 需要将 HMAC.Result 发送给 Server,如果黑客在用户注册时,监听到了 HMAC.Result,并且知道服务器拼接时间戳的规则,还是可以轻易拿到用户的登录权限。网络安全就是这么有趣,总是在道高一尺魔高一丈的对抗中,旧的漏洞不断被修复,新的漏洞被不断发现。我们在进行开发时,往往需要基于业务场景,在安全程度和开发效率上,做一个平衡。作为刚刚启程我们,需要保持严谨且谦卑的心态。
一些细节
大多数用户都有一个特点:不同平台,不同应用的账号、密码喜欢使用重复的。如果某款应用泄露了用户的手机号、账号、密码,那么很有可能,黑客会利用用户的手机号码加上密码,套出用户的支付信息,这种后果是非常严重的!
在以前,很多的平台、应用,关于用户的密码,都会提供一个功能:找回密码。但是现在,找回密码 这个功能被越来越多的大型平台和应用所淡化,取而代之的是:重置密码。如果某个平台或者应用提供了找回密码的功能,那么就意味着用户的真实密码,会以某种形式存储在该平台或者用户的数据库里面,就现在的安全形式而言,这是一个非常危险的操作。
在进行开发的时候,需要谨记两个原则:
1.用户的隐私信息不允许在网络上明文传递
2.用户的隐私信息不允许在本地(Client / Server)明文保存
macOS、iOS 中,钥匙串采用的是 AES(高级密码标准) 加密。
NSString+Hash.h
#import
@interface NSString (Hash)
#pragma mark - 散列函数
// 计算 MD5 散列结果
// 终端测试命令:
// echo -n "string" | openssl dgst -md5
// 提示:随着 MD5 碰撞生成器的出现,MD5 算法不应被用于任何软件完整性检查或代码签名的用途。
// @return 32 个十六进制字符的 MD5 散列字符串
-(NSString *)md5String;
// 计算 SHA1 散列结果
// 终端测试命令:
// echo -n "string" | openssl dgst -sha1
// @return 40 个十六进制字符的 SHA1 散列字符串
-(NSString *)sha1String;
// 计算 SHA256 散列结果
// 终端测试命令:
// echo -n "string" | openssl dgst -sha256
// @return 64个十六进制字符的 SHA256 散列字符串
-(NSString *)sha256String;
// 计算 SHA512 散列结果
// 终端测试命令:
// echo -n "string" | openssl dgst -sha512
// @return 128 个十六进制字符的 SHA512 散列字符串
-(NSString *)sha512String;
#pragma mark - HMAC 散列函数
// 计算 HMAC MD5 散列结果
// 终端测试命令:
// echo -n "string" | openssl dgst -md5 -hmac "key"
// @return 32 个十六进制字符的 HMAC MD5 散列字符串
-(NSString *)hmacMD5StringWithKey:(NSString *)key;
// 计算 HMAC SHA1 散列结果
// 终端测试命令:
// echo -n "string" | openssl dgst -sha1 -hmac "key"
// @return 40 个十六进制字符的 HMAC SHA1 散列字符串
-(NSString *)hmacSHA1StringWithKey:(NSString *)key;
// 计算 HMAC SHA256 散列结果
// 终端测试命令:
// echo -n "string" | openssl dgst -sha256 -hmac "key"
// @return 64 个十六进制字符的 HMAC SHA256 散列字符串
-(NSString *)hmacSHA256StringWithKey:(NSString *)key;
// 计算 HMAC SHA512 散列结果
// 终端测试命令:
// echo -n "string" | openssl dgst -sha512 -hmac "key"
// @return 128 个十六进制字符的 HMAC SHA512 散列字符串
-(NSString *)hmacSHA512StringWithKey:(NSString *)key;
#pragma mark - 文件散列函数
// 计算文件的 MD5 散列结果
// 终端测试命令:
// md5 file.dat
// @return 32 个十六进制字符的 MD5 散列字符串
-(NSString *)fileMD5Hash;
// 计算文件的 SHA1 散列结果
// 终端测试命令:
// openssl dgst -sha1 file.dat
// @return 40 个十六进制字符的 SHA1 散列字符串
-(NSString *)fileSHA1Hash;
// 计算文件的 SHA256 散列结果
// 终端测试命令:
// openssl dgst -sha256 file.dat
// @return 64 个十六进制字符的 SHA256 散列字符串
-(NSString *)fileSHA256Hash;
// 计算文件的 SHA512 散列结果
// 终端测试命令:
// openssl dgst -sha512 file.dat
// @return 128 个十六进制字符的 SHA512 散列字符串
-(NSString *)fileSHA512Hash;
@end
NSString+Hash.m
#import "NSString+Hash.h"
#import
@implementation NSString (Hash)
#pragma mark - 散列函数
-(NSString *)md5String {
const char *str = self.UTF8String;
uint8_t buffer[CC_MD5_DIGEST_LENGTH];
CC_MD5(str, (CC_LONG)strlen(str), buffer);
return [self stringFromBytes:buffer length:CC_MD5_DIGEST_LENGTH];
}
-(NSString *)sha1String {
const char *str = self.UTF8String;
uint8_t buffer[CC_SHA1_DIGEST_LENGTH];
CC_SHA1(str, (CC_LONG)strlen(str), buffer);
return [self stringFromBytes:buffer length:CC_SHA1_DIGEST_LENGTH];
}
-(NSString *)sha256String {
const char *str = self.UTF8String;
uint8_t buffer[CC_SHA256_DIGEST_LENGTH];
CC_SHA256(str, (CC_LONG)strlen(str), buffer);
return [self stringFromBytes:buffer length:CC_SHA256_DIGEST_LENGTH];
}
-(NSString *)sha512String {
const char *str = self.UTF8String;
uint8_t buffer[CC_SHA512_DIGEST_LENGTH];
CC_SHA512(str, (CC_LONG)strlen(str), buffer);
return [self stringFromBytes:buffer length:CC_SHA512_DIGEST_LENGTH];
}
#pragma mark - HMAC 散列函数
-(NSString *)hmacMD5StringWithKey:(NSString *)key {
const char *keyData = key.UTF8String;
const char *strData = self.UTF8String;
uint8_t buffer[CC_MD5_DIGEST_LENGTH];
CCHmac(kCCHmacAlgMD5, keyData, strlen(keyData), strData, strlen(strData), buffer);
return [self stringFromBytes:buffer length:CC_MD5_DIGEST_LENGTH];
}
-(NSString *)hmacSHA1StringWithKey:(NSString *)key {
const char *keyData = key.UTF8String;
const char *strData = self.UTF8String;
uint8_t buffer[CC_SHA1_DIGEST_LENGTH];
CCHmac(kCCHmacAlgSHA1, keyData, strlen(keyData), strData, strlen(strData), buffer);
return [self stringFromBytes:buffer length:CC_SHA1_DIGEST_LENGTH];
}
-(NSString *)hmacSHA256StringWithKey:(NSString *)key {
const char *keyData = key.UTF8String;
const char *strData = self.UTF8String;
uint8_t buffer[CC_SHA256_DIGEST_LENGTH];
CCHmac(kCCHmacAlgSHA256, keyData, strlen(keyData), strData, strlen(strData), buffer);
return [self stringFromBytes:buffer length:CC_SHA256_DIGEST_LENGTH];
}
-(NSString *)hmacSHA512StringWithKey:(NSString *)key {
const char *keyData = key.UTF8String;
const char *strData = self.UTF8String;
uint8_t buffer[CC_SHA512_DIGEST_LENGTH];
CCHmac(kCCHmacAlgSHA512, keyData, strlen(keyData), strData, strlen(strData), buffer);
return [self stringFromBytes:buffer length:CC_SHA512_DIGEST_LENGTH];
}
#pragma mark - 文件散列函数
#define FileHashDefaultChunkSizeForReadingData 4096
-(NSString *)fileMD5Hash {
NSFileHandle *fp = [NSFileHandle fileHandleForReadingAtPath:self];
if (fp == nil) {
return nil;
}
CC_MD5_CTX hashCtx;
CC_MD5_Init(&hashCtx);
while (YES) {
@autoreleasepool {
NSData *data = [fp readDataOfLength:FileHashDefaultChunkSizeForReadingData];
CC_MD5_Update(&hashCtx, data.bytes, (CC_LONG)data.length);
if (data.length == 0) {
break;
}
}
}
[fp closeFile];
uint8_t buffer[CC_MD5_DIGEST_LENGTH];
CC_MD5_Final(buffer, &hashCtx);
return [self stringFromBytes:buffer length:CC_MD5_DIGEST_LENGTH];
}
-(NSString *)fileSHA1Hash {
NSFileHandle *fp = [NSFileHandle fileHandleForReadingAtPath:self];
if (fp == nil) {
return nil;
}
CC_SHA1_CTX hashCtx;
CC_SHA1_Init(&hashCtx);
while (YES) {
@autoreleasepool {
NSData *data = [fp readDataOfLength:FileHashDefaultChunkSizeForReadingData];
CC_SHA1_Update(&hashCtx, data.bytes, (CC_LONG)data.length);
if (data.length == 0) {
break;
}
}
}
[fp closeFile];
uint8_t buffer[CC_SHA1_DIGEST_LENGTH];
CC_SHA1_Final(buffer, &hashCtx);
return [self stringFromBytes:buffer length:CC_SHA1_DIGEST_LENGTH];
}
-(NSString *)fileSHA256Hash {
NSFileHandle *fp = [NSFileHandle fileHandleForReadingAtPath:self];
if (fp == nil) {
return nil;
}
CC_SHA256_CTX hashCtx;
CC_SHA256_Init(&hashCtx);
while (YES) {
@autoreleasepool {
NSData *data = [fp readDataOfLength:FileHashDefaultChunkSizeForReadingData];
CC_SHA256_Update(&hashCtx, data.bytes, (CC_LONG)data.length);
if (data.length == 0) {
break;
}
}
}
[fp closeFile];
uint8_t buffer[CC_SHA256_DIGEST_LENGTH];
CC_SHA256_Final(buffer, &hashCtx);
return [self stringFromBytes:buffer length:CC_SHA256_DIGEST_LENGTH];
}
-(NSString *)fileSHA512Hash {
NSFileHandle *fp = [NSFileHandle fileHandleForReadingAtPath:self];
if (fp == nil) {
return nil;
}
CC_SHA512_CTX hashCtx;
CC_SHA512_Init(&hashCtx);
while (YES) {
@autoreleasepool {
NSData *data = [fp readDataOfLength:FileHashDefaultChunkSizeForReadingData];
CC_SHA512_Update(&hashCtx, data.bytes, (CC_LONG)data.length);
if (data.length == 0) {
break;
}
}
}
[fp closeFile];
uint8_t buffer[CC_SHA512_DIGEST_LENGTH];
CC_SHA512_Final(buffer, &hashCtx);
return [self stringFromBytes:buffer length:CC_SHA512_DIGEST_LENGTH];
}
#pragma mark - 助手方法
// 返回二进制 Bytes 流的字符串表示形式
// @param bytes 二进制 Bytes 数组
// @param length 数组长度
// @return 字符串表示形式
-(NSString *)stringFromBytes:(uint8_t *)bytes length:(int)length {
NSMutableString *strM = [NSMutableString string];
for (int i = 0; i < length; i++) {
[strM appendFormat:@"%02x", bytes[i]];
}
return [strM copy];
}
@end