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的规则如下:
- 按照UNICODE编码的范围,算出需要几个字节,比如1个字节数,2个字数节,3个字节数,4个字节数。具体范围参考下面的图。
- 单字节和ASCII码完全相同,
- 对于其他字节数,字节1的前面用1填充,几个字节数就添加几个
1
,后面补一个0。其他字节都用10
开头。 - 剩余的位置,按照顺序把原始数据补齐。
例子:
“汉”字的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个字节就构成了对应的编码。
举个例子。假设要算(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
对于大小端的概念,本人经常搞混,什么高地址存低字节的,绕一绕就晕了。下面是我的理解:
- 对于一个16进制数0x1234,我们知道这个数对应的是两个字节,占用16个比特。
- 系统中是按照字节为单位去保存数据的。一个地址空间对应1个字节。比如0x1234如果要存储在计算机里,需要占用两个地址空间。我们假设这个地址空间起始是0x00,因为需要两个字节,所以还需要一个地址空间来保存,即0x01。其中明显0x01是高地址空间。
- 所以问题就在于,对于0x1234这个数据保存,是0x01地址保存0x12还是保存0x34。
- 如果把0x1234看成字符串形式,按照正常顺序存储,先存0x12,后存0x34,对应的就是大端模式。
- 如果按照字节顺序,0x12是高位,0x34是低位,应该0x12存储在高位地址0x02,低位字节0x34存储在低位地址0x01。这种方式就是小端模式。
- 为了怕记混,可以这么记:我最大,按字符串顺序存储,我看的最舒服所以是大端。反面的就是小端的。
地址偏移 | 大端模式 | 小端模式 |
---|---|---|
0x00 | 12 | 34 |
0x01 | 34 | 12 |
附:代码判断大小端的代码。
原理是生成一个两字节的数据,然后转为1字节的char数据。大端取到的是第一个高字节,小端取到的是第二个低字节。
#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字节存储。也有大小端之分
总结
- 字符编码就是把可读的字符转化为二进制数据方法,字符解码就是把二进制数据转化为可读的方法。
- ASCII占用1个字节,只有128个字符,主要是英文字符。
- UNICODE是国际标准编码字符集,包含了所有已知符号。
- UTF-8是UNICODE编码的一种实现方式,兼容ASCII码,也就是英文字符占1个字节,汉字可能占两个字节或三个字节。
- UTF-16也是UNICODE编码的一种实现方式,通常和UNICODE编码一致,占用两个字节,分大小端。
Base64编码
Base64编码的作用是把非ASCII的字符转换为ASCII的字符。很多加密算法,很喜欢做一次Base64转换。原因是使用Base64编码后,所有的数据都是ASCII字符,方便在网络上传输。
设计思路是:Base64把每三个8Bit的字节转换为四个6Bit的字节(3*8 = 4*6 = 24),然后把6Bit再添两位高位0,组成四个8Bit的字节。所以Base64算法生成的数据会比原数据大1/3左右。
比如:
- 图片这种二进制数据就可以转换为Base64作为文本传输。
- 比如有中文的数据,可以通过Base64转为可以显示的ASCII数据
简单说明:
- 将字符按照文字编码转化为二进制字节。
- 每3字节化为一组(24bit),如果字节不够,最后输出结果补
=
。然后再把每一组拆分成4个组,每个组6bit,如果不足6bit后面补0。 - 将每个6bit前面补足两个0,凑够8位。
- 然后按照新分出来的每8位转成10进制数,按照表里面的查找,转为对应的ASCII字符。
举例:
字符bl
如何转化为Base64编码:
- bl对应的ASCII码为:
01100010
和01101100
,因为只有两个,所以有一个输出结果是=
- 按照每三个字节分组:
0110001001101100
- 按照每个组6bit分4个组,不足6位的补0:
011000
,100110
,110000
- 在前面补
0
,凑够8位:00011000
,00100110
,00110000
- 转为10进制:
24
,38
,48
。 - 查表得到:
Y
,m
,w
- 最后补
=
,所以结果为Ymw=
标准的程序实现可以参考:GTMBase64.m
。
说明:
Base64是一种编码算法,不是加密算法,他的作用不是加密,而是用最简的ASCII码来传输文本数据,屏蔽掉设备网络差异,是为了方便传输的一种算法。很多加密算法,最后生成的是二进制数据,不是可见字符,而传输的一般是通过字符传输,所以常见的二进制转化方式就是Base64算法。
关于Base64编码后拼接到url上要注意的地方
Base64编码后会有+
和\
,这些如果拼接在url作为参数传输的时候会被浏览器解析为空格和路径分隔符,导致参数解析异常,所以有些库会把+
转义为.
或下划线_
。
同时建议做完Base64,同时自动做一次URLEncode 。 这样+
会被转义为%2B
。服务端收到后先做URLDecode然后做Base64Decode。
哈希散列算法
一个萝卜一个坑
这个俗语形容这个算法很贴切。官方的定义为:
散列(Hash)函数提供了这一服务,它对不同长度的输入消息,产生固定长度的输出。
安全的哈希算法要满足下面条件:
- 固定长度。不同长度的数据,生成的固定长度的数据
- 唯一性。不同的数据,生成的结果一定不同。相同的数据,每次输出的结果一定一样。
- 不可逆。对于生成后的数据,反推回原数据,通过算法是不可能的。
- 防篡改。两个输出的散列值相同,则原数据一定相同。如果两个输出的散列值不同,则原数据一定不同。
从上面的特点可以知道散列值主要使用的场景:
- 生成唯一的值做索引,比如哈希表
- 用作数据签名,校验数据完整性和有效性。
- 密码脱敏处理。
MD5算法
MD5算法是最常用的散列算法。
对MD5算法简要的叙述可以为:MD5以512位分组来处理输入的信息,且每一分组又被划分为十六个32位子分组,经过了一系列的处理后,算法的输出由4个32位分组组成,将这4个32位分组级联后将生成1个128位散列值。
算法有点复杂,没有看懂,放下不表。
下面是本人的简单理解:
- MD5算法效率是比较快的。
- MD5防碰撞能力比较强,只有少数的几个例子有出现碰撞的情况。但也不影响安全性。
- MD5生成的是固定128位,16个字节。
MD5算法安全性
目前主流看法是MD5逐渐有被攻克的风险。但是目前还没有有效算法破解。
主要的破解方法是使用数据库保存常见的字符串的MD5值,然后通过反查得到原始数据。也就是如果用户的密码很常见就很容易破解。如果用户密码是随机的,那就没什么平台可以破解了。
下面对于是用MD5的观点:
- MD5不是加密算法,重要的用户密码应该加密存储。做MD5只是为了脱敏,也就是不让相关人员知道原文是什么(包括内鬼)。
- 极重要数据是用更安全的算法:比如用户密码数据使用更安全的算法,比如SHA1算法。传输过程中也进一步加密。
- 如果使用MD5算法,在原始值里面加入盐值。盐值要尽量随机。因为如果加入随机值后原始值也变得随机,使用暴力破解就基本不可能了。即
result = MD5(password + salt)
关于加盐
这里有个破解的网站,大家可以看下常用的策略其实都可以破解。安全性主要是盐如何选择。
- 盐值要是随机字符,数据尽量长一些,只有这样才能保证最后数据的随机。
- 盐值尽量保证每个用户不一样,增加破解的难度。
- 盐值的保存可以是前后端约定,固化在APP里,但是也应该和用户相关,比如salt=(固化的值+用户信息)。可以是通过一些随机值变化得来:比如用户注册时间等信息做盐值。可以是每次随机生成,当做参数带给后端,后端保存密码+盐值。安全性从低到高。还有做多次MD5的,个人觉得意义不大。
- 个人推荐的一个方案。
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算法更难破解,包含下面的算法。
/*!
@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主要应用场景:
- 密码的散列存储,因为需要散列的时候需要密码,实际上相当于算法里加了盐值。使用的密码要随机和用户相关,请参考盐值的生产规则。
- 用于数据签名。双方使用共同的秘钥,然后做签名验证。秘钥可以固化,也可以会话开始前协商,增加签名篡改和被破解的难度。
PS:目前项目中的密码散列算法,采用的就是HMac算法。
总结
- 密码保存和传输需要做散列处理。但是散列算法主要是脱敏,不能替代加密算法。
- 如今常用的Md5算法和SHA1算法都不再安全。所以推荐使用SHA-2相关算法。
- 散列算法应该加入盐值即:
result=HASH(password+salt)
。其中盐值应该是随机字符串且每个用户不一样。 - HMac引入了秘钥的概念,如果不知道秘钥,秘钥不同,散列值也不同,相当于散列算法加入了盐值。可以把它当做更安全的散列算法使用。
算法实现
算法都是使用苹果自己的Security.framework框架实现的,只需要调用相关算法就可以了。推荐一个github
//
// 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
对称加密算法
对称加密,指双方使用的秘钥是相同的。加密和解密都使用这个秘钥。
对称加密的优点为:
- 加密效率高
- 加密速度快
- 可以对大数据进行加密
缺点为:
- 秘钥安全性无法保证,以现在的技术手段来说,默认对称秘钥的秘钥是非安全的,可以被拿到的。
加密方法
- 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。 - 迭代
迭代就是多次重复相同的运算,这在密码算法中经常使用,以使得形成的密文更加难以破解。
对于对称加密来说,有几个共同要点:
- 密钥长度;(关系到密钥的强度)
- 加密模式;(ecb、cbc等等)
- 块加密算法里的块大小和填充方式区分;
加密模式
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 DD
4个字节,带入公式,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 DD
8个字节,带入公式,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 DD
4个字节,带入公式,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加解密算法的实现。
+ (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;
}
建议和说明
- 建议使用CBC模式(kCCOptionECBMode),填充采用kCCOptionPKCS7Padding。这种使用最广泛,和PHP、JAVA(AES/CBC/PKCS5Padding)都适配。联调的时候需要注意两端是否一致,不一致是调不通的。
- 通常数据加密后,会做一次Base64编码进行传输,有些应用也会将数据转为二进制字符串传输。
- 如果不指定模式,则默认是CBC模式,需要用到向量IV。
- 如果不指定填充格式,则需要自行补
0x00
处理,在解码后也需要把补的0x00
去除掉,网上很多资料解码后没有去除,会多出\0
。
说明和总结
- 建议对称加密使用AES加密。DES无论安全性和效率都不如AES算法。
- 加密建议用
kCCOptionPKCS7Padding
填充方式,对应的JAVA模式为PKCS5Padding
- 如果用CBC模式,需要使用初始向量,初始向量两端应该一致。如果不使用应该指定
kCCOptionECBMode
。也建议用这个模式,兼容性最好。 - 秘钥应该用随机数生成对应的位数。AES128为16个字节,也就是16个字符。不要用短密码,比如:
111111
,这样真的很蠢。 - 对称加密的安全隐患主要在于秘钥的保存。重要会话的秘钥应该随机生成,使用非对称加密来沟通交换秘钥,策略可以参考我的另一篇文章IOS应用安全-HTTP/HTTPS网络安全(一)。
- 如果秘钥需要硬编码到程序里,应该做脱敏运算,比如做位运算进行变形等。后面会专门写怎么解决秘钥硬编码问题。
非对称加密算法
非对称秘钥加密算法的特点是:加密和解密使用不同的秘钥。
非对称加密需要两个秘钥:公开秘钥和私有秘钥。两个秘钥是不同的,而且通过公钥是无法推算出私钥的,使用公钥加密的数据只有用私钥解密。
非对称算法的特点:
- 解决了秘钥保存的问题。公钥可以发布出去,任何人都可以使用,也不用担心被人获取到,只要保证私钥的安全就可以了。而对称加密,因为秘钥相同,客户端泄露了就不安全了。
- 加密和解密的效率不高,只适合加解密少量的数据。而对称加密效率要高。这里有一篇文章对比AES和RSA算法的性能对比。
RSA算法
RSA是目前最常用的非对称加密算法。
算法原理可以看下这篇文章:RSA算法原理。
RSA算法基于一个十分简单的数论事实:将两个大质数相乘十分容易,但是想要对其乘积进行因式分解却极其困难,因此可以将乘积公开作为加密密钥。RSA的秘钥长度在2048位,现有的技术手段是无法破解的(实际的可以暴力破解的位数为768位,也就是768位的大数才有可能暴力进行因数分解)。
RSA算法优点:
- 算法原理简单,我都快看懂了。
- 安全性也足够高,目前没有证据和方案可以破解1048位以上秘钥的RSA算法。
缺点:
- 安全性取决于秘钥长度,推荐的要至少1048位,但是这么高位数的秘钥生成速度很慢,所以没法做一次会话一次秘钥。
- 加解密的效率很低,相对于对称加密,差好几个量级,而且也不支持加密长数据。
国密算法SM2
中国特有的算法,国家强制要求金融机构使用国密算法。包括SM1/SM2/SM3/SM4。其中SM4为对称加密算法。SM3是哈希算法。SM2为非对称加密算法。但是国家只给算法原理,没有给出常用的算法实现,所以是件蛋疼的事情。
算法我也没看懂。因为项目中使用到了,所以做了一些研究。相关代码可以参考我的github,IOS SM2开源实现非常少,而且都有些问题,要么基于openSSL,代码特别大。要么基于libtommath库,但是有一些问题,SM2无法调通。所以两个结合重新整理的下代码。这个代码只保证SM2算法有效性,因为经过实际使用过,其他的项目未用到。
SM2的加密流程
除掉数学方法,下面是本人的一些理解:
-
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"
SM2公钥分为两部分:Pub_x和Pub_y。每个都是32字节,总共是64字节。私钥长度现在还不清楚是多少,有资料说是要32位,但是文档里面未提到。字节数如果不对说明生成秘钥算法有问题。
输出数据分为3段:
C1C2C3
,其中C1是64个字节,C2和原始数据大小相同,即原文是6个字节,C2就是6个字节,C3是32个字节。所以总长度是64+32+原文长度(字节)。如果长度不对,要看下是否是人为添加了其他字段。算法涉及到哈希算法,标准是使用SM3的hash算法,SM3的Hash算法生成的字节为32字节,这个联调的时候一定要保证一致。
加密步骤说明:
- 第一步计算随机数,如果这个不是随机的,是固定的,那后面的结果每次输出就是唯一的。
- 通过随机数rank和曲线的G_x、G_y、P、A五个参数,通过ECC算法
C1=[k]G = (x1,y1)
生成一个点(x1,y1)。拼接起来就是C1数据。C1数据应该是64个字节。有些算法里面会在前面填充0x04
,变成65个字节 - 通过公钥的P_x和P_y,随机数rank,A,P,通过ECC算法
[k]PukeyB = [k](XB,YB) = (x2,y2)
计算出(x2,y2),x2和y2的大小为分别为32字节 - 将上面的(x2,y2)拼接,然后做KDF(密码派生算法)计算,输出原文长度(klen)的t值。
t= KDF(x2||y2, klen)
,KDF一般使用的是SM3的算法。结果t的大小和原文的大小一致。 - 然后将t和原文做异或运算,得到C2,C2的大小和原文一致。
- 然后将(x2,原文,x3)拼接,计算一次SM3的Hash算法,生成的数据放入C3中,C3的大小为32字节。
- 最后把
C1C2C3
拼接到一起,长度为64+原文长度+32字节。注意,老的标准为C1C3C2
,有些实现的是这种模式。
注:这其中ECC算法是标准算法,大部分第三方实现的都没有问题。主要是KDF算法和Hash算法会有不同。这个联调的时候需要搞清楚。
SM2解密流程
流程图如下:
解密步骤说明
- 先判断C1是否在曲线上。C1长度为64字节,取数据的前64字节就可以了。所以两端一定要用同样的曲线。
- 使用C1的数据,曲线参数(A,P),私钥dA,使用ECC算法生成(x2,y2),
dA*C1 = dA*(x2,y2) = dA*[k]*(Xg,Yg)
- 使用(x2,y2)和C2的长度(总长度-64-32),使用KDF计算t。
- 使用c2异或t,达到M’
- 计算(x2,M’,y2)的hash值U。
- 比较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⌉。
简化下说明:
- 先分组,分组的大小为
klen/v
,向上取整,其中klen是数据长度,v是HASH算法输出长度。SM3的输出长度为32字节。 - 然后每一组循环,把原始数据Z和计数器ct拼接,做SM3_Hash运算得到Hai。然后计数器ct+1。
- 最终生成的数据Ha1,Ha2…拼接起来,然后截断到klen长度也就是数据长度。
HASH算法说明
官方使用的是SM3密码杂凑算法,输入为小于2的64次方bit,输出为256bit(32字节)。
总结:
- 国密算法的基础是使用曲线计算。曲线应该使用官方推荐的曲线,曲线不同加解密肯定失败。
- 国密算法生成的数据为
C1C2C3
,其中C1为固定的64字节,c2和原始数据一样长,C3为固定的32字节。有些要求数据前面加上’0x04’,旧的版本输出是C3C1C2
,这两点要注意。 - 公钥分为P_x和P_y,都是32字节长度。私钥长度从资料上看没有限制,是一个随机数[1,N-2]。N为曲线参数。
- 加密过程中使用了SM3的散列算法(官方叫杂凑算法),这个算法输出为32字节的数据。如果对端没有用这个算法,两端也无法加解密成功。
总结
- 字符编码是为了把可见字符和二进制之间做一层转化。其中UNICODE编码是国际编码标准。UTF-8是这种编码格式的实现方式。特点是ASCII码的字符占用一个字节,其他的比如中文字符占用两到三个字符。
- Base64也是一种编码方式,主要用于把二进制数据转化为ASCII字符,方便传输。现在很多加密算法习惯在加密后把二进制数做一次Base64进行传输。相对于原文,长度会多出1/3。也有把二进制转为字符串的形式,不过长度是原文的2倍。
- 哈希散列算法,主要用于脱敏处理和信息签名防篡改,做哈希运算应该加盐处理。盐值应该是随机值,而且和用户相关,建议使用(随机数 + 用户名)。
- 对称加密两端秘钥相同,加密速度快,可以加密大数据,但是秘钥保存一直是个难题。
- 非对称加密分为公钥和私钥,公钥可以公开。加密速度慢,只能加密小数据,但是只需要妥善保存私钥就可以了。
通常一个信息加密传输流程为:
- 双方约定好使用的编码格式。通常常用的是UTF-8编码。
- 客户端随机生成对称秘钥作为会话秘钥。使用非对称加密传输给后端,后端保存这个对称秘钥用于之后的加解密过程。
- 用户使用对称加密(通常为AES)加密整个数据,结果通常使用Base64做编码(通常还要做一次URLEncode操作),整个相关数据按照规则使用Hash算法(通常为SHA256算法)做数据签名。最后做传输
- 如果是用户密码的话建议用HMac做Hash脱敏处理,然后单独使用非对称加密进一步加强安全性。