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

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.png

例子:

“汉”字的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

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

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

总结

  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的字节(3*8 = 4*6 = 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.png

举例:

字符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算法。

关于Base64编码后拼接到url上要注意的地方

Base64编码后会有+\,这些如果拼接在url作为参数传输的时候会被浏览器解析为空格和路径分隔符,导致参数解析异常,所以有些库会把+转义为.或下划线_
同时建议做完Base64,同时自动做一次URLEncode 。 这样+会被转义为%2B。服务端收到后先做URLDecode然后做Base64Decode。

哈希散列算法

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

散列(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算法更难破解,包含下面的算法。

/*!
    @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

//
//  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加解密算法的实现。

+ (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. 建议使用CBC模式(kCCOptionECBMode),填充采用kCCOptionPKCS7Padding。这种使用最广泛,和PHP、JAVA(AES/CBC/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的加密流程

safe_encode_sm2.png

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

  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解密流程

流程图如下:

safe_decode_sm2.png

解密步骤说明

  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国密算法官方说明
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 194,242评论 5 459
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 81,769评论 2 371
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 141,484评论 0 319
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 52,133评论 1 263
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 61,007评论 4 355
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 46,080评论 1 272
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 36,496评论 3 381
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 35,190评论 0 253
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 39,464评论 1 290
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 34,549评论 2 309
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 36,330评论 1 326
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 32,205评论 3 312
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 37,567评论 3 298
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 28,889评论 0 17
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 30,160评论 1 250
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 41,475评论 2 341
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 40,650评论 2 335

推荐阅读更多精彩内容