iOS应用安全2 — Hash概念及用途,对称加密

概述

本篇文章主要讲解的内容有:

  • hash的基本概念。
  • hash在密码这一块的常见用法。
  • 常用的对称加密。

如果是你感兴趣的,那么咱们就接着往下看,如果不是就可以cmd+w离开了。

hash概念

hash表

哈希(hash)表原称散列表,音译为哈希。它是一种可以根据键(key)直接访问对应值(value)的数据结构。

hash表数学原理

hash表是基于数组的,因此我们来先看下面这个数组(上面是index,下面是value)// 表格有点宽,显示不完,可以左右滑动查看

0 1 2 3 4 5 6 7
28 16 97 45 9 76 33 47

这个数组的值是随便写的,现在假设我们要判断33是否在这个数组中,那么我们就可以通过for循环遍历这个数组,一一对比数组中的元素是否等于33,然后就可以得出结果,但是这样做的时间复杂度是O(n),也就是说最坏的情况是我们需要把数组中的元素都对照一遍才能得出结论。那么如何能够更快找出33呢?

我们都知道在数组中,通过下标取值是非常快的,而hash表就是以这个思想为基础进行实现的。下面我们改变一下思路,将上面数组中的每个元素都对10进行整除并且把计算结果作为数组的下标,然后就可以得到下面的表:

0 1 2 3 4 5 6 7 8 9
9 16 28 33 45 76 97
47

这样,当我们再想找33的时候我们就可以直接把33对10进行整除得出3,然后对比数组中下标是3的那个元素就可以了。这样做的时间复杂度是O(1)。

哈希冲突/散列冲突/散列碰撞

看了上面的那个哈希表我们可以发现,45和47通过对10整除这个算法的映射,结果都是4,这种情况被称为哈希冲突/散列冲突,也叫散列碰撞,反正不管怎么叫,是那个意思就对了。
在理想情况下,哈希函数(在这里就是对10整除这个算法)设计合理的话,可以让不同的值映射成不同的结果,时间复杂度真正为O(1)。但是事实证明无论设计的哈希函数多么好,哈希冲突总是不可避免的,因此就需要我们来解决哈希冲突。

解决哈希冲突最常见的方法有线性探测法(也叫开放定址法)链地址法
其实上面那个表中解决哈希冲突的方法有点链地址法的影子。下面还是以上面的那个表为例子分别讲解一下这两种方法。

线性探测法

在上面的例子中,在插入45时通过哈希函数计算的映射值为4,然后查看下标4对应的地方是否有值,没有,所以将45插入到4的位置;在插入47时映射值也是4,查看下标为4的地方是否有值,有值,此时会向后面一个地址(或多个地址,具体视情况而定)进行探测,当探测到下标5没有值,则将47插入到5的位置,若是5的地方也有值,则继续向后探测。因此如果用线性探测法,上面那个哈希表应该是:

0 1 2 3 4 5 6 7 8 9
9 16 28 33 45 47 76 97

链地址法

为了让这个解释更形象,这次我们换一组数据。将32、88、65、72、3、18、99、37、48、42、56、81、12、8、24、28、92、69 用链地址法解决冲突插入到哈希表中。在这假设哈希函数为对6求余,则哈希表应为下:

哈希表


当然,这里只是为了更加形象才将哈希函数设置为对6求余,所以才会有这么多的碰撞。在这种情况下,我们如果要查询24,则可以将24对6求余得到0,然后到下标0对应的链表中去一个个找,在这里我们仍然需要找很多次,所以这个哈希函数设计的挺失败的。

hash函数的设计

在正常用法中,hash函数的设计主要有两个作用

  1. 将其他数据类型转成数字。在有些时候我们存储的数据并不是数字,例如:@"张三"、@"李四"、@"王五"。
  2. 在哈希表大小合适的基础上尽量减少哈希冲突。

要做到以上两点其实并不容易,尤其是第2点。hash 表的空间如果远大于实际存储数据的数量,则造成空间浪费;如果过小,又容易造成哈希冲突。针对hash表的大小一般有以下两种解决思路:

  1. 如果最初知道存储的数据量,则可以根据存储个数和数据的分布特点来确定hash 表的大小。
  2. 如果不知道最终需要存储的数据数量,则需要动态维护hash表的容量,此时可能涉及到重新计算hash地址,计算量较大。

下面说一下常见的hash函数设计方法

  1. 直接定址法:根据数据中的某些线性特征,如学生的学号,公司的工号等,直接拿来作为hash的地址。
  2. 平方取中法:可以将数据的某些数字进行平方运算,再取结果的中间的1~2位数作为hash地址。
  3. 折叠法:可以将数据中某串数字进行叠加作为hash地址,如18位身份证号,每两位当作一个数字,进行加法运算,运算结果取最后两位为hash地址。
  4. 除留取余法:如果知道了hash表的最大长度,则可以取不大于最大长度的最大质数作为除数,即hash(key) = key % x,这里x很关键,如果取的好能够最大程度的减少冲突的几率,一般情况下是取不大于hash表长度的最大质数。

hash算法特点

目前著名的哈希算法有很多,比如:MD5,SHA1/256/512(加密强度不一样),它们都是将任意长度的二进制数据映射成固定长度的二进制串。hash算法应具有以下一些特点:

  1. 单向映射。原始值通过哈希算法的映射能够得到一个结果哈希值,但是通过哈希值并不能逆向推导出原始值,即哈希算法不可逆。
  2. 哈希冲突发生的几率要很小。对于不同的原始值,哈希值相同的概率应该非常小。
  3. 数据敏感。对于原始值,哪怕只有一个二进制位不一样,计算出的哈希值也要大不相同。
  4. 算法效率高。哈希算法应具有高效计算的特点。对于很长的数据也应该快速计算出哈希值。

hash用途

hash算法的用途有很多,iOS系统就有很多地方用到了hash算法。这里简单说几个:weak的底层原理、关联对象(也就是常说的分类添加属性)底层、字典NSDictionary(通过上面就能发现hash和字典真的很像,快速查找),还有其他很多,有兴趣的可以看看这篇文章---->搞iOS的,面试官问Hash干嘛?。
上面这些都是iOS系统中使用的hash,但是今天我要讲的不是这方面的,我要讲讲hash在加密这一块的用途。

用户密码加密

在用户进行登录注册操作时,在网络中直接传输明文账号密码是非常危险的,黑客可以使用Charles等工具在网络拦截http请求,能够很轻易的就获取到用户账号密码信息。为了保护用户的密码信息,我们不得不和黑客进行一场攻防战。

开发者:

我们为了不让明文传输账号密码,可以在登录注册的时候将用户密码的md5值传输给服务器,服务器保存账号和密码的md5值,这样黑客在拦截的时候他看到密码就是一串32个字符的16进制字符串。

黑客:

好,我拦截到了一串不知道的md5值,我自己算不出来,可以找其他人算,只要用户的密码强度不高,一样能够破解出来。

开发者:

就算用户自己设置的密码强度不高,我也一样可以让它变成强度高的密码。加盐,在用户密码后面拼接一串很乱的字符串,如:a1df/H&OI)HF@,这样计算的md5值就算你请其他人也无法逆算出来。

黑客:

既然你加盐了,那我就分析你代码的二进制文件,找到那个字符串盐,只要有这个盐,我一样可以破解出密码。

开发者:

既然如此,那这个盐我不保存到程序中了,我在注册的时候就让服务器给我这个账号分配一个盐(这种方案叫:Hmac),即给我这个设备授权。换设备登录时我让服务器向已授权的设备询问是否允许给新设备授权。(说到这,大家应该都很熟悉了,QQ、微信登录时都有类似这样的操作)。

黑客:

好吧,既然我获取不到用户的账号密码,那么我摊牌了。我直接拦截你的登录请求,我拦截到你发送的md5(密码+授权盐),我也不管你密码是啥,我就拿我拦截到的这个东西来登录,一样可以登录这个账号。

开发者:

还能这样?既然如此,那我再加点东西,我在登录之前向服务器要一下时间戳,我发送一个md5(md5(密码+授权盐)+202003131118)的东西(当然,时间戳不长这个样,为了更形象),服务器判断的时候只计算最近的两分钟的时间戳,以防止网络延时导致的问题。

黑客:

wc,一个登录都搞的这么麻烦,我拦截到了数据还要破解,还要在两分钟内登录?登录之后我还不知道要花多少时间才能破解到其他的东西,不干了。碰、碰、哗啦(砸电脑、摔手机的声音)。

当然,如果黑客不计代价的想破解的话,他总是能找到其他办法破解的,但是也不能一直没完没了下去啊。我们要做的就是让黑客花更多的时间破解,做不到绝对安全,但我们可以做到相对安全。

这里我想到之前看的一个例子:有两个屋子,放着同样的东西,一个屋子全部用水泥盖的,门都是防盗门;另一个屋子全是烂木头搭起来的,甚至连门都没安。你说如果你是黑客你选择哪个屋子?

数字签名

签名的作用是什么?老外喜欢用支票,他们在支票上签上自己的名字,那么就代表着这张支票就是他的,是有效的。签名的意义就不言而喻了吧?那么数字签名类推下来就是用来鉴别数字信息(二进制数据)的。

当我们向服务器发送一条很关键的数据(特别是有关钱的)时,为了防止黑客在网络传输过程中拦截篡改数据,我们通常会对数据进行数字签名操作。如下:我们将原始数据进行md5哈希得到一个32个字符的16进制字符串,再将这个字符串进行RSA加密计算,最后和原始数据拼接起来一起发送到服务器。服务器拿到这个之后解密RSA得到原始数据的md5值,对比收到的原始数据计算的md5值,来验证原始数据是否在传输过程中被篡改。
有关RSA加解密原理不懂的可以看我上一篇文章。

数字签名

应用签名

我们都知道在app打包上架的过程中需要配置证书和签名,其实原理也和上面类似(具体方案要比上面的那些复杂些),只是原始数据成了app打包的二进制数据。

对称加密

上一篇文章说RSA非对称加密的时候提到过一些对称加密的特点,如加密和解密使用的密钥是同一个,因此被称为对称加密。下面说一下现在比较经典的对称加密算法。

  • Des,数据加密标准,现在用的少了,原因是加密强度不够。
  • 3Des,相对与Des来说他使用3个密钥,强度增加,但本质没变。
  • Aes,高级密码标准,目前最流行的对称加密算法。

加密模式

  • ECB:电子密码本模式。把数据分块加密,每一块数据加密结果不影响其他数据的加密结果。
  • CBC:密码分组链式模式。使用一个密钥和一个初始化向量进行加密,数据分块加密,但后一块的数据加密前需要与前面加密好的数据进行异或运算(第一块与初始化向量运算)。因此只要选择不同的初始化向量,那么相同的明文和密钥加密出的结果也不一样。另外CBC模式还能够保证数据的完整性,在传输的过程中密文丢失或者被篡改了一段,那么这一段后面的数据都讲无法解密。CBC具有以上特点所以被称为链式。

CCCrypt参数介绍

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
/**
     CCCrypt函数的参数说明
     1、加密或解密,加密传kCCEncrypt,解密传kCCDecrypt
     2、加密算法,AES/DES
     3、ECB/CBC模式,kCCOptionPKCS7Padding为CBC模式,kCCOptionPKCS7Padding | kCCOptionECBMode为ECB模式
     4、加密密钥的字节数组
     5、加密密钥的大小
     6、初始化向量IV的字节数组
     7、原始数据的字节数组
     8、原始数据的大小
     9、加密结果要存放的地方
     10、分块加密分的块的大小
     11、加密完成后的密文大小
     */
    CCCryptorStatus cryptStatus = CCCrypt(kCCEncrypt,
                                          self.algorithm,
                                          option,
                                          cKey,
                                          self.keySize,
                                          cIv,
                                          [data bytes],
                                          [data length],
                                          buffer,
                                          bufferSize,
                                          &encryptedSize);
                                         
    NSData *result = nil;
    if (cryptStatus == kCCSuccess) {
        result = [NSData dataWithBytesNoCopy:buffer length:encryptedSize];
    } else {
        free(buffer);
        NSLog(@"[错误] 加密失败|状态编码: %d", cryptStatus);
    }

总结

本篇文章主要讲述了Hash(哈希)的概念、hash的数学原理、哈希冲突及解决方法、hash算法和算法的特点、hash的用途。用户密码加密方案HMAC(若不是太理解可以搜一下相关文章看看)、数字签名原理,应用签名。

另外因为对称加密内容不多,懒得再开一篇文章,就直接把常用对称加密Des,Aes的特点、加密模式、以及iOS系统提供的加密方法参数解释写到了这里。

本文地址。