IOS应用安全-加解密算法简述

导读
客户端经常遇到需要对数据进行加密的情况,那应该如何加密,选用什么样的加密算法,是本文想要讨论的问题。

如果把我们的数据比作笔记,那数据加密相当于给笔记本上了锁,解密相当于打开锁看到笔记。而打开锁的钥匙一定是在私人手里的,外人是打不开的。所以数据加密一定有三个关键字:

1.加密
2.解密
3.秘钥

所以有些常见的算法不是数据加密的范围,这个开发需要注意。比如Base64编码,MD5算法。

Base64只是把数据编码,通俗讲只是把原来用汉语写的笔记内容,改成用英语写的内容,只要懂转换规则的任何人都能得到数据。所以老板说把数据加下密,一定不是让你Base64一下或者用其他编码重新编码下,编码算法不涉及到数据安全。

MD5算法也是数据处理的一种方式,更多的被用在数据验证身上。用上面的例子来讲,MD5算法把整本书的内容变成了一句标题,通过标题是没办法推算出整个书讲什么的。因为根本没有解密的步骤,所以也不属于加密算法。

字符编码

计算机的所有数据,最终都是由多个二进制bit(0/1)来存储和传输的,但是怎么从0/1转化成我们可读的文字,就涉及到编码的知识了。下面是基础的编码概念。

ASCII (NSASCIIStringEncoding)

使用一个字节大小表示的128个字符。其中这些字符主要是英文字符,现在很少使用这个编码,因为不够用。ASCII字符占用一个字节ASCII码表

主要使用到的是英文字母的大小写转换。大写的A~Z编码+32等于小写的a~z。

UNICODE (NSUnicodeStringEncoding)

ASCII只能表示128个字符,对于英文国家来说足够了,对于我们中国来说,我们有几万个汉字不够啊。于是我们创造出了GB2312等等我们自己的字符集。日本也觉得我也不够啊,我也搞个字符集。这些字符集彼此是不兼容的,没办法转换,同样的字符ABCD,我们可能表示,日本就可能就表示。于是程序猿们觉得我要搞个标准,大家都按照标准来。

于是就有了UNICODE编码。它是所有字符的国际标准编码字符集。这个是为了解决ASCII字符不够的问题。同时让所有组织使用同一套编码规则,解决编码不兼容的问题。所以现在通用的编码规则都是UNICODE编码。UNICODE向下兼容ASCII编码。UNICODE最大长度可以到4个字节。不过通常只使用两个字节表示。所以通常认为UNICODE占用2字节数据

UTF-8 (NSUTF8StringEncoding)

其实UNICODE已经足够使用了,不过因为如果是ASCII表示的字符(比如英文)只需要1字节就可以了,UNICODE表示的话其中一个字节全是0,这个字节浪费了,英语国家的程序猿觉得:我靠,我又不需要那么多复杂的字符,浪费我流量和空间啊,不行!!,于是出现了对UNICODE的转换,也就是UTF-8格式,可以保证原ASCII字符依然用一个字节表示,非ASCII字符使用多个字符表示。

UNICODE到UTF-8的规则如下:

  1. 按照UNICODE编码的范围,算出需要几个字节,比如1个字节数,2个字数节,3个字节数,4个字节数。具体范围参考下面的图。
  2. 单字节和ASCII码完全相同,
  3. 对于其他字节数,字节1的前面用1填充,几个字节数就添加几个1,后面补一个0。其他字节都用10开头。
  4. 剩余的位置,按照顺序把原始数据补齐。

utf_8

例子:

“汉”字的Unicode编码是0x6C49。0x6C49在0x0800-0xFFFF之间,使用用3字节模板了:1110xxxx 10xxxxxx 10xxxxxx。将0x6C49写成二进制是:0110 1100 0100 1001, 用这个比特流依次代替模板中的x,得到:11100110 10110001 10001001,即E6 B1 89。

对于UTF-8编码的文件,会在文件头写入EF BB BF,表明是UTF-8编码。

UTF-16 (NSUTF16StringEncoding)

UTF-16的编码方法是:

  • 如果二进制(流b小于0x10000,也就是十进制的0到65535之内,则直接使用两字节表示。
  • 如果二进制流b大于等于0x10000,将b-0x10000的结果中的前 10 位作为高位和0xD800进行逻辑或操作,将后10 bit作为低位和0xDC00做逻辑或操作,这样组成的4个字节就构成了b的编码。

举个例子。假设要算(U+2A6A5,四个繁体字龙)在UTF-16下的值,因为它超过 U+FFFF,所以 2A6A5-10000=0x1A6A5=。

前10位0001 1010 01 | 0xD800 = 0xD896。

后10位10 1010 0101 | 0xDC00 = 0xDEA5。

所以U+ 2A6A5 在UTF-16中的像是D8 96 DE A5。

注:上文参考:精确解释Unicode

在IOS程序里面NSUTF16StringEncoding和NSUnicodeStringEncoding是等价的。

UTF-16大端/小端(NSUTF16BigEndianStringEncoding/NSUTF16LittleEndianStringEncoding)

大小端主要表明了,系统存储数据的顺序。因为UTF-16至少两个字节,这两个字节传输过来后,接收的人需要知道哪个字节是在前,哪个字节在后。然后系统才知道改如何存取。

Unicode规范中用字节序标记字符(BOM)来标识字节序,它的编码是FEFF。这样如果接收者收到FEFF,就表明这个字节流是高位在前的;如果收到FFFE,就表明这个字节流是低位在前的。

比如“汉”字的Unicode编码是0x6C49。

对于大端的文件数据为:FE FF 6c 49
对于小端的文件数据为:FF FE 49 6c

对于大小端的概念,本人经常搞混,什么高地址存低字节的,绕一绕就晕了。下面是我的理解:

  1. 对于一个16进制数0x1234,我们知道这个数对应的是两个字节,占用16个比特。
  2. 系统中是按照字节为单位去保存数据的。一个地址空间对应1个字节。比如0x1234如果要存储在计算机里,需要占用两个地址空间。我们假设这个地址空间起始是0x00,因为需要两个字节,所以还需要一个地址空间来保存,即0x01。其中明显0x01是高地址空间。
  3. 所以问题就在于,对于0x1234这个数据保存,是0x01地址保存0x12还是保存0x24。
  4. 如果把0x1234看成字符串形式,按照正常顺序存储,先存0x12,后存0x34,对应的就是大端模式。
  5. 如果按照字节顺序,0x12是高位,0x34是低位,应该0x12存储在高位地址0x02,低位字节0x34存储在低位地址0x01。这种方式就是小端模式。
  6. 为了怕记混,可以这么记:我最大,按字符串顺序存储,我看的最舒服所以是大端。反面的就是小端的。
地址偏移 大端模式 小端模式
0x00 12 34
0x01 34 12

附:代码判断大小端的代码。

原理是生成一个两字节的数据,然后转为1字节的char数据。大端取到的是第一个高字节,小端取到的是第二个低字节。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
#include<stdio.h>
int main()
{
short x = 1; //0x0001
char *p = (char *)&x;
if(*p)
{
printf("little\n");
}
else
{
printf("large\n");
}
return 0;
}

UTF-32

详细的本人没看懂,实际中没有用到这个编码,这个编码使用4字节存储。也有大小端之分

总结

  1. 字符编码就是把可读的字符转化为二进制数据方法,字符解码就是把二进制数据转化为可读的方法。
  2. ASCII占用1个字节,只有128个字符,主要是英文字符。
  3. UNICODE是国际标准编码字符集,包含了所有已知符号。
  4. UTF-8是UNICODE编码的一种实现方式,兼容ASCII码,也就是英文字符占1个字节,汉字可能占两个字节或三个字节。
  5. UTF-16也是UNICODE编码的一种实现方式,通常和UNICODE编码一致,占用两个字节,分大小端。

Base64编码

Base64编码的作用是把非ASCII的字符转换为ASCII的字符。很多加密算法,很喜欢做一次Base64转换。原因是使用Base64编码后,所有的数据都是ASCII字符,方便在网络上传输。

设计思路是:Base64把每三个8Bit的字节转换为四个6Bit的字节(38 = 46 = 24),然后把6Bit再添两位高位0,组成四个8Bit的字节。所以Base64算法生成的数据会比原数据大1/3左右。

比如:

  1. 图片这种二进制数据就可以转换为Base64作为文本传输。
  2. 比如有中文的数据,可以通过Base64转为可以显示的ASCII数据

简单说明:

  1. 将字符按照文字编码转化为二进制字节。
  2. 每3字节化为一组(24bit),如果字节不够,最后输出结果补=。然后再把每一组拆分成4个组,每个组6bit,如果不足6bit后面补0。
  3. 将每个6bit前面补足两个0,凑够8位。
  4. 然后按照新分出来的每8位转成10进制数,按照表里面的查找,转为对应的ASCII字符。

base_64

举例:

字符bl如何转化为Base64编码:

  1. bl对应的ASCII码为: 0110001001101100,因为只有两个,所以有一个输出结果是=
  2. 按照每三个字节分组:0110001001101100
  3. 按照每个组6bit分4个组,不足6位的补0:011000,100110,110000
  4. 在前面补0,凑够8位:00011000,00100110,00110000
  5. 转为10进制:24,38,48
  6. 查表得到:Y,m,w
  7. 最后补=,所以结果为Ymw=

标准的程序实现可以参考:GTMBase64.m

说明:

Base64是一种编码算法,不是加密算法,他的作用不是加密,而是用最简的ASCII码来传输文本数据,屏蔽掉设备网络差异,是为了方便传输的一种算法。很多加密算法,最后生成的是二进制数据,不是可见字符,而传输的一般是通过字符传输,所以常见的二进制转化方式就是Base64算法。

哈希散列算法

一个萝卜一个坑这个俗语形容这个算法很贴切。官方的定义为:

散列(Hash)函数提供了这一服务,它对不同长度的输入消息,产生固定长度的输出。

安全的哈希算法要满足下面条件:

  1. 固定长度。不同长度的数据,生成的固定长度的数据
  2. 唯一性。不同的数据,生成的结果一定不同。相同的数据,每次输出的结果一定一样。
  3. 不可逆。对于生成后的数据,反推回原数据,通过算法是不可能的。
  4. 防篡改。两个输出的散列值相同,则原数据一定相同。如果两个输出的散列值不同,则原数据一定不同。

从上面的特点可以知道散列值主要使用的场景:

  1. 生成唯一的值做索引,比如哈希表
  2. 用作数据签名,校验数据完整性和有效性。
  3. 密码脱敏处理。

MD5算法

MD5算法是最常用的散列算法。

对MD5算法简要的叙述可以为:MD5以512位分组来处理输入的信息,且每一分组又被划分为十六个32位子分组,经过了一系列的处理后,算法的输出由4个32位分组组成,将这4个32位分组级联后将生成1个128位散列值。

算法有点复杂,没有看懂,放下不表。

下面是本人的简单理解:

  1. MD5算法效率是比较快的。
  2. MD5防碰撞能力比较强,只有少数的几个例子有出现碰撞的情况。但也不影响安全性。
  3. MD5生成的是固定128位,16个字节。

MD5算法安全性

目前主流看法是MD5逐渐有被攻克的风险。但是目前还没有有效算法破解。

主要的破解方法是使用数据库保存常见的字符串的MD5值,然后通过反查得到原始数据。也就是如果用户的密码很常见就很容易破解。如果用户密码是随机的,那就没什么平台可以破解了。

下面对于是用MD5的观点:

  1. MD5不是加密算法,重要的用户密码应该加密存储。做MD5只是为了脱敏,也就是不让相关人员知道原文是什么(包括内鬼)。
  2. 极重要数据是用更安全的算法:比如用户密码数据使用更安全的算法,比如SHA1算法。传输过程中也进一步加密。
  3. 如果使用MD5算法,在原始值里面加入盐值。盐值要尽量随机。因为如果加入随机值后原始值也变得随机,使用暴力破解就基本不可能了。即result = MD5(password + salt)

关于加盐

这里有个破解的网站,大家可以看下常用的策略其实都可以破解。安全性主要是盐如何选择。

  1. 盐值要是随机字符,数据尽量长一些,只有这样才能保证最后数据的随机。
  2. 盐值尽量保证每个用户不一样,增加破解的难度。
  3. 盐值的保存可以是前后端约定,固化在APP里,但是也应该和用户相关,比如salt=(固化的值+用户信息)。可以是通过一些随机值变化得来:比如用户注册时间等信息做盐值。可以是每次随机生成,当做参数带给后端,后端保存密码+盐值。安全性从低到高。还有做多次MD5的,个人觉得意义不大。
  4. 个人推荐的一个方案。result = MD5 (password + salt)。salt的计算方法是:MD5(Random(128)+ uid)。其中Random(128)表示一个随机128位字符串,两端可以一致,固化在代码里。uid是用户唯一标示,比如登陆用的用户名。这样对于破解者来说就需要先拿到这个salt值,然后对每个用户都要生成一个唯一的128位的盐值,去生成对应的库,破解成本就非常高了。

其实目前暴漏出来的是攻击者把整个数据库的内容拿到后,暴力解密出原文。但是MD5加盐也好变换也好都是可以通过前端代码查到算法的,通过算法就可以生成常用数据对应的MD5库。所以密码做MD5更重要的是脱敏处理,不能做为安全的加密使用,重要的用户密码持久化或传输过程中一定是要通过加密算法处理的。这样只要安全保存私钥就可以了。在很多金融公司,大量使用硬件加密机做加密处理,然后保存,更加大了破解难度。所以如果你的密码是使用加密再保存的,使用固定盐值的已经可以满足要求了。如果担心可以加上用户的注册时间或服务器时间戳做盐值。

SHA1

SHA1也是一种HASH算法。是MD5的替代方案。生成的数据是160位,20个字节。

目前SHA1也被认为不安全,google找到了算法进行了碰撞,所以普遍推荐使用新的SHA2代替。Google已经开始废弃这个算法了。

SHA2

  • SHA-224、SHA-256、SHA-384,和SHA-512并称为SHA-2。
  • 新的散列函数并没有接受像SHA-1一样的公众密码社区做详细的检验,所以它们的密码安全性还不被大家广泛的信任。
  • 虽然至今尚未出现对SHA-2有效的攻击,它的算法跟SHA-1基本上仍然相似;因此有些人开始发展其他替代的散列算法。

所以目前推荐使用SHA2相关的算法做散列算法。

其中SHA-256输出为256位,32字节。
SHA-512输出为512位,64字节。

HMac

HMac是秘钥相关的哈希算法。和之前的算法不同的在于需要一个秘钥,才能生成输出。主要是基于签名散列算法。可以认为是散列算法加入了加密逻辑,所以相比SHA算法更难破解,包含下面的算法。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
/*!
@enum CCHmacAlgorithm
@abstract Algorithms implemented in this module.
@constant kCCHmacAlgSHA1 HMAC with SHA1 digest
@constant kCCHmacAlgMD5 HMAC with MD5 digest
@constant kCCHmacAlgSHA256 HMAC with SHA256 digest
@constant kCCHmacAlgSHA384 HMAC with SHA384 digest
@constant kCCHmacAlgSHA512 HMAC with SHA512 digest
@constant kCCHmacAlgSHA224 HMAC with SHA224 digest
*/
enum {
kCCHmacAlgSHA1,
kCCHmacAlgMD5,
kCCHmacAlgSHA256,
kCCHmacAlgSHA384,
kCCHmacAlgSHA512,
kCCHmacAlgSHA224
};
typedef uint32_t CCHmacAlgorithm;

HMAC主要应用场景:

  1. 密码的散列存储,因为需要散列的时候需要密码,实际上相当于算法里加了盐值。使用的密码要随机和用户相关,请参考盐值的生产规则。
  2. 用于数据签名。双方使用共同的秘钥,然后做签名验证。秘钥可以固化,也可以会话开始前协商,增加签名篡改和被破解的难度。

PS:目前项目中的密码散列算法,采用的就是HMac算法。

总结

  1. 密码保存和传输需要做散列处理。但是散列算法主要是脱敏,不能替代加密算法。
  2. 如今常用的Md5算法和SHA1算法都不再安全。所以推荐使用SHA-2相关算法。
  3. 散列算法应该加入盐值即:result=HASH(password+salt)。其中盐值应该是随机字符串且每个用户不一样。
  4. HMac引入了秘钥的概念,如果不知道秘钥,秘钥不同,散列值也不同,相当于散列算法加入了盐值。可以把它当做更安全的散列算法使用。

算法实现

算法都是使用苹果自己的Security.framework框架实现的,只需要调用相关算法就可以了。推荐一个github

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
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
//
// NSData+KKHASH.m
// SecurityiOS
//
// Created by cocoa on 16/12/15.
// Copyright © 2016年 dev.keke@gmail.com. All rights reserved.
//
#import "NSData+KKHASH.h"
#include <CommonCrypto/CommonDigest.h>
#import <CommonCrypto/CommonHMAC.h>
@implementation NSData (KKHASH)
- (NSData *)hashDataWith:(CCDIGESTAlgorithm )ccAlgorithm
{
NSData *retData = nil;
if (self.length <1) {
return nil;
}
unsigned char *md;
switch (ccAlgorithm) {
case CCDIGEST_MD2:
{
md = malloc(CC_MD2_DIGEST_LENGTH);
bzero(md, CC_MD2_DIGEST_LENGTH);
CC_MD2(self.bytes, (CC_LONG)self.length, md);
retData = [NSData dataWithBytes:md length:CC_MD2_DIGEST_LENGTH];
}
break;
case CCDIGEST_MD4:
{
md = malloc(CC_MD4_DIGEST_LENGTH);
bzero(md, CC_MD4_DIGEST_LENGTH);
CC_MD4(self.bytes, (CC_LONG)self.length, md);
retData = [NSData dataWithBytes:md length:CC_MD4_DIGEST_LENGTH];
}
break;
case CCDIGEST_MD5:
{
md = malloc(CC_MD5_DIGEST_LENGTH);
bzero(md, CC_MD5_DIGEST_LENGTH);
CC_MD5(self.bytes, (CC_LONG)self.length, md);
retData = [NSData dataWithBytes:md length:CC_MD5_DIGEST_LENGTH];
}
break;
case CCDIGEST_SHA1:
{
md = malloc(CC_SHA1_DIGEST_LENGTH);
bzero(md, CC_SHA1_DIGEST_LENGTH);
CC_SHA1(self.bytes, (CC_LONG)self.length, md);
retData = [NSData dataWithBytes:md length:CC_SHA1_DIGEST_LENGTH];
}
break;
case CCDIGEST_SHA224:
{
md = malloc(CC_SHA224_DIGEST_LENGTH);
bzero(md, CC_SHA224_DIGEST_LENGTH);
CC_SHA224(self.bytes, (CC_LONG)self.length, md);
retData = [NSData dataWithBytes:md length:CC_SHA224_DIGEST_LENGTH];
}
break;
case CCDIGEST_SHA256:
{
md = malloc(CC_SHA256_DIGEST_LENGTH);
bzero(md, CC_SHA256_DIGEST_LENGTH);
CC_SHA256(self.bytes, (CC_LONG)self.length, md);
retData = [NSData dataWithBytes:md length:CC_SHA256_DIGEST_LENGTH];
}
break;
case CCDIGEST_SHA384:
{
md = malloc(CC_SHA384_DIGEST_LENGTH);
bzero(md, CC_SHA384_DIGEST_LENGTH);
CC_SHA384(self.bytes, (CC_LONG)self.length, md);
retData = [NSData dataWithBytes:md length:CC_SHA384_DIGEST_LENGTH];
}
break;
case CCDIGEST_SHA512:
{
md = malloc(CC_SHA512_DIGEST_LENGTH);
bzero(md, CC_SHA512_DIGEST_LENGTH);
CC_SHA512(self.bytes, (CC_LONG)self.length, md);
retData = [NSData dataWithBytes:md length:CC_SHA512_DIGEST_LENGTH];
}
break;
default:
md = malloc(1);
break;
}
free(md);
md = NULL;
return retData;
}
- (NSData *)hmacHashDataWith:(CCHmacAlgorithm )ccAlgorithm key:(NSString *)key {
NSData *retData = nil;
if (self.length <1) {
return nil;
}
unsigned char *md;
const char *cKey = [key cStringUsingEncoding:NSUTF8StringEncoding];
switch (ccAlgorithm) {
case kCCHmacAlgSHA1:
{
md = malloc(CC_SHA1_DIGEST_LENGTH);
bzero(md, CC_SHA1_DIGEST_LENGTH);
CC_SHA1(self.bytes, (CC_LONG)self.length, md);
CCHmac(kCCHmacAlgSHA1, cKey, strlen(cKey), self.bytes, (CC_LONG)self.length, md);
retData = [NSData dataWithBytes:md length:CC_SHA1_DIGEST_LENGTH];
}
break;
case kCCHmacAlgSHA224:
{
md = malloc(CC_SHA224_DIGEST_LENGTH);
bzero(md, CC_SHA224_DIGEST_LENGTH);
CCHmac(kCCHmacAlgSHA224, cKey, strlen(cKey), self.bytes, (CC_LONG)self.length, md);
retData = [NSData dataWithBytes:md length:CC_SHA224_DIGEST_LENGTH];
}
break;
case kCCHmacAlgSHA256:
{
md = malloc(CC_SHA256_DIGEST_LENGTH);
bzero(md, CC_SHA256_DIGEST_LENGTH);
CCHmac(kCCHmacAlgSHA256, cKey, strlen(cKey), self.bytes, (CC_LONG)self.length, md);
retData = [NSData dataWithBytes:md length:CC_SHA256_DIGEST_LENGTH];
}
break;
case kCCHmacAlgSHA384:
{
md = malloc(CC_SHA384_DIGEST_LENGTH);
bzero(md, CC_SHA384_DIGEST_LENGTH);
CCHmac(kCCHmacAlgSHA384, cKey, strlen(cKey), self.bytes, (CC_LONG)self.length, md);
retData = [NSData dataWithBytes:md length:CC_SHA384_DIGEST_LENGTH];
}
break;
case kCCHmacAlgSHA512:
{
md = malloc(CC_SHA512_DIGEST_LENGTH);
bzero(md, CC_SHA512_DIGEST_LENGTH);
CCHmac(kCCHmacAlgSHA512, cKey, strlen(cKey), self.bytes, (CC_LONG)self.length, md);
retData = [NSData dataWithBytes:md length:CC_SHA512_DIGEST_LENGTH];
}
break;
case CCDIGEST_MD5:
{
md = malloc(CC_MD5_DIGEST_LENGTH);
bzero(md, CC_MD5_DIGEST_LENGTH);
CCHmac(kCCHmacAlgMD5, cKey, strlen(cKey), self.bytes, (CC_LONG)self.length, md);
retData = [NSData dataWithBytes:md length:CC_MD5_DIGEST_LENGTH];
}
break;
default:
md = malloc(1);
break;
}
free(md);
md = NULL;
return retData;
}
- (NSString *)hexString
{
NSMutableString *result = nil;
if (self.length <1) {
return nil;
}
result = [[NSMutableString alloc] initWithCapacity:self.length * 2];
for (size_t i = 0; i < self.length; i++) {
[result appendFormat:@"%02x", ((const uint8_t *) self.bytes)[i]];
}
return result;
}
+ (NSData *)dataWithHexString:(NSString *)hexString {
NSMutableData * result;
NSUInteger cursor;
NSUInteger limit;
NSParameterAssert(hexString != nil);
result = nil;
cursor = 0;
limit = hexString.length;
if ((limit % 2) == 0) {
result = [[NSMutableData alloc] init];
while (cursor != limit) {
unsigned int thisUInt;
uint8_t thisByte;
if ( sscanf([hexString substringWithRange:NSMakeRange(cursor, 2)].UTF8String, "%x", &thisUInt) != 1 ) {
result = nil;
break;
}
thisByte = (uint8_t) thisUInt;
[result appendBytes:&thisByte length:sizeof(thisByte)];
cursor += 2;
}
}
return result;
}
@end

对称加密算法

对称加密,指双方使用的秘钥是相同的。加密和解密都使用这个秘钥。

对称加密的优点为:

  1. 加密效率高
  2. 加密速度快
  3. 可以对大数据进行加密

缺点为:

  1. 秘钥安全性无法保证,以现在的技术手段来说,默认对称秘钥的秘钥是非安全的,可以被拿到的。

加密方法

  • DES :数据加密标准。
    是一种分组数据加密技术,先将数据分成固定长度64位的小数据块,之后进行加密。
    速度较快,适用于大量数据加密。DES密钥为64位,实际使用56位。将64位数据加密成64位数据。
  • 3DES:使用三组密钥做三次加密。
    是一种基于 DES 的加密算法,使用3个不同密钥对同一个分组数据块进行3次加密,如此以使得密文强度更高。3DES秘钥为DES两倍或三倍,即112位或168位。其实就是DES的秘钥加强版。
  • AES :高级加密标准。
    是美国联邦政府采用的一种区块加密标准。
    相较于 DES 和 3DES 算法而言,AES 算法有着更高的速度和资源使用效率,安全级别也较之更高了,被称为下一代加密标准。AES秘钥长度为128、192、256位

使用到的基础数学方法:

  • 移位和循环移位
      移位就是将一段数码按照规定的位数整体性地左移或右移。循环右移就是当右移时,把数码的最后的位移到数码的最前头,循环左移正相反。例如,对十进制数码12345678循环右移1位(十进制位)的结果为81234567,而循环左移1位的结果则为23456781。
  • 置换
      就是将数码中的某一位的值根据置换表的规定,用另一位代替。它不像移位操作那样整齐有序,看上去杂乱无章。这正是加密所需,被经常应用。
  • 扩展
      就是将一段数码扩展成比原来位数更长的数码。扩展方法有多种,例如,可以用置换的方法,以扩展置换表来规定扩展后的数码每一位的替代值。
  • 压缩
      就是将一段数码压缩成比原来位数更短的数码。压缩方法有多种,例如,也可以用置换的方法,以表来规定压缩后的数码每一位的替代值。
  • 异或
      这是一种二进制布尔代数运算。异或的数学符号为⊕ ,它的运算法则如下:
    1⊕1 = 0
    0⊕0 = 0
    1⊕0 = 1
    0⊕1 = 1
      也可以简单地理解为,参与异或运算的两数位如相等,则结果为0,不等则为1。
  • 迭代
      迭代就是多次重复相同的运算,这在密码算法中经常使用,以使得形成的密文更加难以破解。

对于对称加密来说,有几个共同要点:

  1. 密钥长度;(关系到密钥的强度)
  2. 加密模式;(ecb、cbc等等)
  3. 块加密算法里的块大小和填充方式区分;

加密模式

ECB 模式

ECB :电子密本方式,最古老,最简单的模式,将加密的数据分成若干组,每组的大小跟加密密钥长度相同;
然后每组都用相同的密钥加密。OC对应的为kCCOptionECBMode

ECB的特点为:

  • 每次Key、明文、密文的长度都必须是64位;
  • 数据块重复排序不需要检测;
  • 相同的明文块(使用相同的密钥)产生相同的密文块,容易遭受字典攻击;
  • 一个错误仅仅会对一个密文块产生影响,所以支持并行计算;

CBC模式

  • CBC :密文分组链接方式。与ECB相比,加入了初始向量IV。将加密的数据分成若干组,加密时第一个数据需要先和向量异或之后才加密。后面的数据需要先和前面的数据异或,然后再加密。是OC默认的加密模式。

CBC的特点为:

  • 每次加密的密文长度为64位(8个字节);
  • 当相同的明文使用相同的密钥和初始向量的时候CBC模式总是产生相同的密文;
  • 密文块要依赖以前的操作结果,所以,密文块不能进行重新排列;
  • 可以使用不同的初始化向量来避免相同的明文产生相同的密文,一定程度上抵抗字典攻击;
  • 一个错误发生以后,当前和以后的密文都会被影响;

块大小和填充方式

对称算法的第一步就是对数据进行分组,每一个组的大小称为快大小,比如DES需要将数据分组为64位(8个字节),如果数据不够64位就需要进行补位。

PKCS7Padding填充

OC中指定的填充方法只有kCCOptionPKCS7Padding,对应JAVA的PKCS5Padding填充方式。算法为计算缺几位数,然后就补几位数,数值为下面的公式:

value=k - (l mod k) ,K=块大小,l=数据长度,如果l=8, 则需要填充额外的8个byte的8

比如块大小为8字节,数据为DD DD DD DD4个字节,带入公式,l=4,k=8,计算 8 - (4 mod 8)= 4 ,所以补充4个4,补位后得到DD DD DD DD 04 04 04 04

唯一特别的是如果最后位数是够的,也需要额外补充,比如数据是DD DD DD DD DD DD DD DD8个字节,带入公式,l=8,k=8,计算 8 - (8 mod 8)= 8,所以补位后得到DD DD DD DD DD DD DD DD 08 08 08 08 08 08 08 08。 所以如果考虑补位,实际输出buffer大小要加上快大小,防止buffer不够。

Zero Padding(No Padding)

补位的算法和PKCS7Padding一致,只不过补的位为0x00,比如数据为DD DD DD DD4个字节,带入公式,l=4,k=8,计算 8 - (4 mod 8)= 4 ,所以补充4个00,补位后得到DD DD DD DD 00 00 00 00

非常不建议用这种模式,因为解密后的数据会多出补的00。如果原始数据以00结尾(ASCII码代表空字符),就没办法区分出来了。

几种算法比较

算法 秘钥长度(字节) 分组长度(字节) 加密效率 破解难度
DES 8 8 较快(22.5MB/S) 简单
3DES 24 8 慢(12MB/S)
AES 16/24/32 16 快(51.2MB/s)

IOS 代码实现解析

下面以AES代码实现为例,说明下IOS加解密算法的实现。

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
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
+ (NSString *)AES128Encrypt:(NSString *)plainText key:(NSString *)gkey iv:(NSString *)gIv padding:(BOOL)padding
{
//先处理秘钥,如果秘钥不够算法长度,就用0填充,如果长于算法长度就截断。
char keyPtr[kCCKeySizeAES128+1]; //申请秘钥buffer,这里根据不同算法导入需要的key长度。AES128是16个字节,对应的值kCCKeySizeAES128。
memset(keyPtr, 0, sizeof(keyPtr)); //使用0填充,保证秘钥长度达到要求。
[gkey getCString:keyPtr maxLength:sizeof(keyPtr) encoding:NSUTF8StringEncoding]; //将传入的秘钥copy进秘钥buffer里
//注意这个只在模式为CBC下有效,
//处理向量值,默认模式为CBC。如果指定了kCCOptionECBMode模式,就不需要这个向量。
char ivPtr[kCCBlockSizeAES128+1]; //申请向量的buffer,长度为块长度。AES128块长度为kCCBlockSizeAES128。
memset(ivPtr, 0, sizeof(ivPtr));
[gIv getCString:ivPtr maxLength:sizeof(ivPtr) encoding:NSUTF8StringEncoding]; //将传入的值copy进向量buffer
NSData* data = [plainText dataUsingEncoding:NSUTF8StringEncoding];
NSUInteger dataLength = [data length];
//注意这个只在不指定padding的情况下有效,需要填充0,算法为num_to_fill= k - (length mod k),如果指定了kCCOptionPKCS7Padding,就不需要人为填充。
long long newSize = dataLength;
int diff = padding ? 0 : kCCKeySizeAES128 - (dataLength % kCCKeySizeAES128);
if(diff > 0) {
newSize = dataLength + diff;
}
char dataPtr[newSize];
memcpy(dataPtr, [data bytes], [data length]);
for(int i = 0; i < diff; i++) {
dataPtr[i + dataLength] = 0x00;
}
//输出的buffer
size_t bufferSize = newSize + kCCBlockSizeAES128;
void *buffer = malloc(bufferSize);
memset(buffer, 0, bufferSize);
size_t numBytesCrypted = 0;
CCOptions option = padding ? kCCOptionPKCS7Padding : 0x0000;
option = gIv.length > 0 ? option : option | kCCOptionECBMode;
CCCryptorStatus cryptStatus = CCCrypt(kCCEncrypt,
kCCAlgorithmAES128,
option,
// 0x0000, //No padding | CBC模式 需要补零且需要iv向量
// kCCOptionPKCS7Padding, // kCCOptionPKCS7Padding | CBC模式 需要iv向量
//kCCOptionPKCS7Padding | kCCOptionECBMode, // kCCOptionPKCS7Padding | kCCOptionECBMode 不需要iv向量,也不需要补零
// kCCOptionECBMode, // No padding | kCCOptionECBMode 不需要补零,不需要iv向量
keyPtr,
kCCKeySizeAES128,
ivPtr,
dataPtr,
sizeof(dataPtr),
buffer,
bufferSize,
&numBytesCrypted);
if (cryptStatus == kCCSuccess) {
NSData *resultData = [NSData dataWithBytesNoCopy:buffer length:numBytesCrypted];
resultData = [resultData base64EncodedDataWithOptions:(NSDataBase64EncodingOptions)0];
NSString *encryptedString = [[NSString alloc] initWithData:resultData encoding:NSUTF8StringEncoding];
return encryptedString;
}
free(buffer);
return nil;
}
+ (NSString *)AES128Decrypt:(NSString *)encryptText key:(NSString *)gkey iv:(NSString *)gIv padding:(BOOL)padding
{
//复制秘钥buffer
char keyPtr[kCCKeySizeAES128 + 1];
memset(keyPtr, 0, sizeof(keyPtr));
[gkey getCString:keyPtr maxLength:sizeof(keyPtr) encoding:NSUTF8StringEncoding];
//复制向量buffer
char ivPtr[kCCBlockSizeAES128 + 1];
memset(ivPtr, 0, sizeof(ivPtr));
[gIv getCString:ivPtr maxLength:sizeof(ivPtr) encoding:NSUTF8StringEncoding];
NSData *data = [[NSData alloc] initWithBase64EncodedString:encryptText options:0];
NSUInteger dataLength = [data length];
size_t bufferSize = dataLength + kCCBlockSizeAES128;
void *buffer = malloc(bufferSize);
//计算采用哪种模式和填充方式
CCOptions option = padding ? kCCOptionPKCS7Padding : 0x0000;
option = gIv.length > 0 ? option : option | kCCOptionECBMode;
size_t numBytesCrypted = 0;
//解密
CCCryptorStatus cryptStatus = CCCrypt(kCCDecrypt,
kCCAlgorithmAES128,
option,
// 0x0000, //No padding | CBC模式 需要补零且需要iv向量
// kCCOptionPKCS7Padding, // kCCOptionPKCS7Padding | CBC模式 需要iv向量
//kCCOptionPKCS7Padding | kCCOptionECBMode, // kCCOptionPKCS7Padding | kCCOptionECBMode 不需要iv向量,也不需要补零
// kCCOptionECBMode, // No padding | kCCOptionECBMode 不需要补零,不需要iv向量
keyPtr,
kCCBlockSizeAES128,
ivPtr,
[data bytes],
dataLength,
buffer,
bufferSize,
&numBytesCrypted);
if (cryptStatus == kCCSuccess) {
NSData *resultData = [NSData dataWithBytesNoCopy:buffer length:numBytesCrypted];
NSString *result = [[NSString alloc] initWithData:resultData encoding:NSUTF8StringEncoding];
if ([result length] > 0 && !padding) {
//如果是非填充模式,解析后的数据会多出填充的'\0',所以需要去掉。
long byteWithoutZero = numBytesCrypted;
const char *utf8Str = [result UTF8String];
//从后开始扫描,查到需要截断的长度
for (long i = byteWithoutZero - 1; i > 0; i --) {
if (utf8Str[i] != '\0') {
break;
}
byteWithoutZero --;
}
NSString *finalReslut = [[NSString alloc] initWithBytes:utf8Str length:byteWithoutZero encoding:NSUTF8StringEncoding];
return finalReslut;
}
return result;
}
free(buffer);
return nil;
}

建议和说明

  1. 建议使用ECB模式(kCCOptionECBMode),填充采用kCCOptionPKCS7Padding。这种使用最广泛,和PHP、JAVA(AES/ECB/PKCS5Padding)都适配。联调的时候需要注意两端是否一致,不一致是调不通的。
  2. 通常数据加密后,会做一次Base64编码进行传输,有些应用也会将数据转为二进制字符串传输。
  3. 如果不指定模式,则默认是CBC模式,需要用到向量IV。
  4. 如果不指定填充格式,则需要自行补0x00处理,在解码后也需要把补的0x00去除掉,网上很多资料解码后没有去除,会多出\0

说明和总结

  1. 建议对称加密使用AES加密。DES无论安全性和效率都不如AES算法。
  2. 加密建议用kCCOptionPKCS7Padding填充方式,对应的JAVA模式为PKCS5Padding
  3. 如果用CBC模式,需要使用初始向量,初始向量两端应该一致。如果不使用应该指定kCCOptionECBMode。也建议用这个模式,兼容性最好。
  4. 秘钥应该用随机数生成对应的位数。AES128为16个字节,也就是16个字符。不要用短密码,比如:111111,这样真的很蠢。
  5. 对称加密的安全隐患主要在于秘钥的保存。重要会话的秘钥应该随机生成,使用非对称加密来沟通交换秘钥,策略可以参考我的另一篇文章IOS应用安全-HTTP/HTTPS网络安全(一)
  6. 如果秘钥需要硬编码到程序里,应该做脱敏运算,比如做位运算进行变形等。后面会专门写怎么解决秘钥硬编码问题。

非对称加密算法

非对称秘钥加密算法的特点是:加密和解密使用不同的秘钥

非对称加密需要两个秘钥:公开秘钥和私有秘钥。两个秘钥是不同的,而且通过公钥是无法推算出私钥的,使用公钥加密的数据只有用私钥解密。

非对称算法的特点:

  1. 解决了秘钥保存的问题。公钥可以发布出去,任何人都可以使用,也不用担心被人获取到,只要保证私钥的安全就可以了。而对称加密,因为秘钥相同,客户端泄露了就不安全了。
  2. 加密和解密的效率不高,只适合加解密少量的数据。而对称加密效率要高。这里有一篇文章对比AES和RSA算法的性能对比

RSA算法

RSA是目前最常用的非对称加密算法。

算法原理可以看下这篇文章:RSA算法原理

RSA算法基于一个十分简单的数论事实:将两个大质数相乘十分容易,但是想要对其乘积进行因式分解却极其困难,因此可以将乘积公开作为加密密钥。RSA的秘钥长度在2048位,现有的技术手段是无法破解的(实际的可以暴力破解的位数为768位,也就是768位的大数才有可能暴力进行因数分解)。

RSA算法优点:

  1. 算法原理简单,我都快看懂了。
  2. 安全性也足够高,目前没有证据和方案可以破解1048位以上秘钥的RSA算法。

缺点:

  1. 安全性取决于秘钥长度,推荐的要至少1048位,但是这么高位数的秘钥生成速度很慢,所以没法做一次会话一次秘钥。
  2. 加解密的效率很低,相对于对称加密,差好几个量级,而且也不支持加密长数据。

国密算法SM2

中国特有的算法,国家强制要求金融机构使用国密算法。包括SM1/SM2/SM3/SM4。其中SM4为对称加密算法。SM3是哈希算法。SM2为非对称加密算法。但是国家只给算法原理,没有给出常用的算法实现,所以是件蛋疼的事情。

算法我也没看懂。因为项目中使用到了,所以做了一些研究。相关代码可以参考我的github,IOS SM2开源实现非常少,而且都有些问题,要么基于openSSL,代码特别大。要么基于libtommath库,但是有一些问题,SM2无法调通。所以两个结合重新整理的下代码。这个代码只保证SM2算法有效性,因为经过实际使用过,其他的项目未用到。

SM2的加密流程

抛出掉数学方法,下面是本人的一些理解:

  1. SM2需要依赖于一个曲线,一般使用国家推荐曲线。如果曲线不对,肯定是无法加解密的。曲线参数

    
    #define SM2_P     "FFFFFFFEFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF00000000FFFFFFFFFFFFFFFF"
    #define SM2_A     "FFFFFFFEFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF00000000FFFFFFFFFFFFFFFC"
    #define SM2_B     "28E9FA9E9D9F5E344D5A9E4BCF6509A7F39789F515AB8F92DDBCBD414D940E93"
    #define SM2_N     "FFFFFFFEFFFFFFFFFFFFFFFFFFFFFFFF7203DF6B21C6052B53BBF40939D54123"
    #define SM2_G_X   "32C4AE2C1F1981195F9904466A39C9948FE30BBFF2660BE1715A4589334C74C7"
    #define SM2_G_Y   "BC3736A2F4F6779C59BDCEE36B692153D0A9877CC62A474002DF32E52139F0A0"
    
  2. SM2公钥分为两部分:Pub_x和Pub_y。每个都是32字节,总共是64字节。私钥长度现在还不清楚是多少,有资料说是要32位,但是文档里面未提到。字节数如果不对说明生成秘钥算法有问题。
  3. 输出数据分为3段:C1C2C3,其中C1是64个字节,C2和原始数据大小相同,即原文是6个字节,C2就是6个字节,C3是32个字节。所以总长度是64+32+原文长度(字节)。如果长度不对,要看下是否是人为添加了其他字段。
  4. 算法涉及到哈希算法,标准是使用SM3的hash算法,SM3的Hash算法生成的字节为32字节,这个联调的时候一定要保证一致。

加密步骤说明:

  1. 第一步计算随机数,如果这个不是随机的,是固定的,那后面的结果每次输出就是唯一的。
  2. 通过随机数rank和曲线的G_x、G_y、P、A五个参数,通过ECC算法C1=[k]G = (x1,y1)生成一个点(x1,y1)。拼接起来就是C1数据。C1数据应该是64个字节。有些算法里面会在前面填充0x04,变成65个字节
  3. 通过公钥的P_x和P_y,随机数rank,A,P,通过ECC算法[k]PukeyB = [k](XB,YB) = (x2,y2)计算出(x2,y2),x2和y2的大小为分别为32字节
  4. 将上面的(x2,y2)拼接,然后做KDF(密码派生算法)计算,输出原文长度(klen)的t值。t= KDF(x2||y2, klen),KDF一般使用的是SM3的算法。结果t的大小和原文的大小一致。
  5. 然后将t和原文做异或运算,得到C2,C2的大小和原文一致。
  6. 然后将(x2,原文,x3)拼接,计算一次SM3的Hash算法,生成的数据放入C3中,C3的大小为32字节。
  7. 最后把C1C2C3拼接到一起,长度为64+原文长度+32字节。注意,老的标准为C1C3C2,有些实现的是这种模式。

注:这其中ECC算法是标准算法,大部分第三方实现的都没有问题。主要是KDF算法和Hash算法会有不同。这个联调的时候需要搞清楚。

SM2解密流程

流程图如下:

解密步骤说明

  1. 先判断C1是否在曲线上。C1长度为64字节,取数据的前64字节就可以了。所以两端一定要用同样的曲线。
  2. 使用C1的数据,曲线参数(A,P),私钥dA,使用ECC算法生成(x2,y2),dA*C1 = dA*(x2,y2) = dA*[k]*(Xg,Yg)
  3. 使用(x2,y2)和C2的长度(总长度-64-32),使用KDF计算t。
  4. 使用c2异或t,达到M’
  5. 计算(x2,M’,y2)的hash值U。
  6. 比较U和C3数据是否是一致的,如果一致就输出M’

KDF算法说明:

文档里的描述

密钥派生函数的作用是从一个共享的秘密比特串中派生出密钥数据。在密钥协商过程中,密钥派
生函数作用在密钥交换所获共享的秘密比特串上,从中产生所需的会话密钥或进一步加密所需的密钥
数据。
密钥派生函数需要调用密码杂凑函数。
设密码杂凑函数为Hv( ),其输出是长度恰为v比特的杂凑值。
密钥派生函数KDF(Z, klen):
输入:比特串Z,整数klen(表示要获得的密钥数据的比特长度,要求该值小于(232-1)v)。
输出:长度为klen的密钥数据比特串K。
a)初始化一个32比特构成的计数器ct=0x00000001;
b)对i从1到⌈klen/v⌉执行:
b.1)计算Hai=Hv(Z ∥ ct);
b.2) ct++;
c)若klen/v是整数,令Ha!⌈klen/v⌉ = Ha⌈klen/v⌉,否则令Ha!⌈klen/v⌉为Ha⌈klen/v⌉最左边的(klen −
(v × ⌊klen/v⌋))比特;
d)令K = Ha1||Ha2|| · · · ||Ha⌈klen/v⌉−1||Ha!⌈klen/v⌉。

简化下说明:

  1. 先分组,分组的大小为klen/v,向上取整,其中klen是数据长度,v是HASH算法输出长度。SM3的输出长度为32字节。
  2. 然后每一组循环,把原始数据Z和计数器ct拼接,做SM3_Hash运算得到Hai。然后计数器ct+1。
  3. 最终生成的数据Ha1,Ha2…拼接起来,然后截断到klen长度也就是数据长度。

HASH算法说明

官方使用的是SM3密码杂凑算法,输入为小于2的64次方bit,输出为256bit(32字节)

总结:

  1. 国密算法的基础是使用曲线计算。曲线应该使用官方推荐的曲线,曲线不同加解密肯定失败。
  2. 国密算法生成的数据为C1C2C3,其中C1为固定的64字节,c2和原始数据一样长,C3为固定的32字节。有些要求数据前面加上’0x04’,旧的版本输出是C3C1C2,这两点要注意。
  3. 公钥分为P_x和P_y,都是32字节长度。私钥长度从资料上看没有限制,是一个随机数[1,N-2]。N为曲线参数。
  4. 加密过程中使用了SM3的散列算法(官方叫杂凑算法),这个算法输出为32字节的数据。如果对端没有用这个算法,两端也无法加解密成功。

总结

  1. 字符编码是为了把可见字符和二进制之间做一层转化。其中UNICODE编码是国际编码标准。UTF-8是这种编码格式的实现方式。特点是ASCII码的字符占用一个字节,其他的比如中文字符占用两到三个字符。
  2. Base64也是一种编码方式,主要用于把二进制数据转化为ASCII字符,方便传输。现在很多加密算法习惯在加密后把二进制数做一次Base64进行传输。相对于原文,长度会多出1/3。也有把二进制转为字符串的形式,不过长度是原文的2倍。
  3. 哈希散列算法,主要用于脱敏处理和信息签名防篡改,做哈希运算应该加盐处理。盐值应该是随机值,而且和用户相关,建议使用(随机数 + 用户名)。
  4. 对称加密两端秘钥相同,加密速度快,可以加密大数据,但是秘钥保存一直是个难题。
  5. 非对称加密分为公钥和私钥,公钥可以公开。加密速度慢,只能加密小数据,但是只需要妥善保存私钥就可以了。

通常一个信息加密传输流程为:

  1. 双方约定好使用的编码格式。通常常用的是UTF-8编码。
  2. 客户端随机生成对称秘钥作为会话秘钥。使用非对称加密传输给后端,后端保存这个对称秘钥用于之后的加解密过程。
  3. 用户使用对称加密(通常为AES)加密整个数据,结果通常使用Base64做编码(通常还要做一次URLEncode操作),整个相关数据按照规则使用Hash算法(通常为SHA256算法)做数据签名。最后做传输
  4. 如果是用户密码的话建议用HMac做Hash脱敏处理,然后单独使用非对称加密进一步加强安全性。

参考:

  1. 字符编码笔记:ASCII,Unicode和UTF-8
  2. 百度百科-ASCII
  3. 深入浅出大小端
  4. Base64 编码
  5. MD5+Salt安全浅析
  6. 哈希加密算法 MD5,SHA-1,SHA-2,SHA-256,SHA-512,SHA-3,RIPEMD-160 - aTool
  7. DES加密模式详解
  8. DES加密算法原理
  9. 关于PKCS5Padding与PKCS7Padding的区别
  10. 各种加密算法比较
  11. AES在线加解密
  12. iOS - Safe iOS 加密安全
  13. RSA算法原理
  14. SM2国密算法官方说明
坚持原创技术分享,您的支持将鼓励我继续创作!