IOS基础知识:数据安全

原创:知识点总结性文章
创作不易,请珍惜,之后会持续更新,不断完善
个人比较喜欢做笔记和写总结,毕竟好记性不如烂笔头哈哈,这些文章记录了我的IOS成长历程,希望能与大家一起进步
温馨提示:由于简书不支持目录跳转,大家可通过command + F 输入目录标题后迅速寻找到你所需要的内容

目录

  • 一、数据安全问题
    • 1、加密解密
    • 2、单向散列函数
    • 3、数字签名
    • 4、证书
    • 5、iOS 签名机制
  • 二、哈希(MD5散列)函数
    • 1、简介
    • 2、MD5 散列函数
    • 3、破解哈希(散列)函数
    • 4、加盐(Salt)
    • 5、HMAC 散列
    • 6、计算文件的MD5散列结果
  • 三、哈希(SHA散列)函数
    • 1、SHA 散列算法
    • 2、HMAC + SHA 散列算法
    • 3、文件的SHA散列结果
  • 四、对称加密算法:AES+Base64
    • 1、对称加密算法
    • 2、Base64编码
    • 3、准备工作
    • 4、使用方式
    • 5、加密解密工具类
    • 6、加解密算法
  • 五、非对称加密算法:RSA
    • 1、非对称加密算法
    • 2、RSA
    • 3、数字签名
  • 六、使用RNCryptor框架
    • 1、导入框架
    • 2、基本使用
    • 3、CXYRNCryptorTool mac端加密小工具
    • 4、PDF加解密
  • Demo
  • 参考文献

一、数据安全问题

1、加密解密

在开发应用的时候,数据的安全性至关重要。我们会要求开发者不允许明文传输用户的隐私数据,同时也不允许明文保存用户的隐私数据,这就需要我们去对敏感的数据进行加密处理。对于iOS持久化方面,无论使用userDefaultplist文件、归档、数据库等存储方式,都需要考虑一个问题,如何安全的存储数据?对于这个问题,可以对数据加密保存。

常见的加密算法
  • 哈希(散列)函数:MD5SHA
  • 对称加密算法:DESAES
  • 非对称加密算法:RSA
摘要算法或编码处理

摘要算法或编码处理只是一种把明文转为非明文的一种方式。摘要算法不可逆,数据无法还原,不符合需求。编码处理,可以反编码还原明文,实际上数据并没有变得更安全,也是不符合需求。但采用编码的方式,还是比直接使用明文要好那么一点点。

自定义加密算法

某种自定义加密算法。破解难度与算法本身设计有关,但这个一般都是比较有实力的公司才这么做。

对称和非对称加密

对称密码算法有 DESAES 等。DES是一种将 64bit 明文加密成 64bit 密文的对称密码算法,由于 DES每次只能加密 64bit的数据,遇到比较大的数据,需要对DES加密进行迭代(反复),目前已经可以在短时间内被破解,所以不建议使用 DESAES 的密钥长度有 128、192、256bit 三种,目前 AES 已经逐步取代 DES,成为首选的对称密码算法。一般来说,我们也不应该去使用任何自制的密码算法,而是应该使用 AES,它经过了全世界密码学家所进行的高品质验证工作。非对称密码算法中,目前使用最广泛的公钥密码算法是RSA

如何传输密钥

对称加密体系中,密钥不能直接传输,否则可能被截取,截获者也能破解密文。有以下几种密钥传输方式:

  • 事先共享密钥:即通过非通讯手段,私下直接给出密钥。
  • 密钥分配中心:所有密钥有分配中心管理,分配中心通过特殊安全手段下发。
  • 借助非对称密码体系:由消息的接收者,生成一对公钥、私钥,将公钥对外公开发给消息的发送者,消息的发送者使用公钥加密消息,接受者使用私钥解密。整个过程只有消息接收者具有私钥,其他任何人没有私钥不具备解密能力。
混合密码系统

对称密码不能很好地解决密钥配送问题,但是加解密速度快;非对称密码加密解密速度比较慢,但是能很好的解决密钥配送问题。所以采取对称密码和公钥密码的优势相结合的方法(即混合密码系统),可以很好的解决对称密码密钥配送问题以及加解密速度慢的问题。包括 https 中也是采用类似的混合密码系统,https 中的服务端相当于消息接收者,客服端相当于消息发送者。具体步骤如下:

加密步骤:

  1. 接收者生成私钥和公钥(非对称密码体系),并将公钥发给消息发送者;
  2. 发送者生成会话密钥(对称密码体系),使用会话密钥加密消息,使用公钥加密会话密钥;
  3. 发送者将加密消息以及机密会话密钥一并发给消息接收者;

解密步骤:

  1. 消息接收者用私钥解密出会话密钥;
  2. 再用会话密钥解密出消息;

2、单向散列函数

单向散列函数又被称为消息摘要函数(哈希函数),具备以下特点
  • 可以根据消息内容计算出固定长度散列值。散列值的长度和消息的长度无关,无论消息是1bit、10M、100G,单向散列函数都会计算出固定长度的散列值;
  • 计算速度快,能快速计算出散列值,消息不同,散列值也不同;
  • 具备单向性,即可以根据消息计算出散列值,但是根据散列值无法计算出消息;
常见单向散列函数
  • MD5:MD就是Message Digest的缩写,产生128bit的散列值,目前已经不安全。Mac 终端上默认可以使用 md5 文件名命令获取散列值。
  • SHA-2:SHA-256、SHA-384、SHA-512,散列值长度分别是 256bit、384bit、512bit。
  • SHA-3:全新标准,目前还在研发中。
单向散列函数的应用场景

1、单向散列函数可以监测文件是否被篡改,文件被篡改后的散列值和篡改前的散列值如果不同,则说明文件被篡改过。

2、用户密码加密,一般不会将用户密码明文传给服务器保存到服务器对应的数据库中,如果后端数据库被攻破,成千上百的用户账号信息直接将被泄露。所以通常做法是获取密码散列值传给后端,这样服务端也不知道用户密码是什么,所以一般用户忘记密码后就无法找回,只能通过重置密码解决。如果一个平台可以找回密码,说明该平台是不安全的。另外,为了防止用户密码相同或过于简单,增加安全性可以采取先加盐再获取散列值的策略。


3、数字签名

数字签名过程

消息接收者可以借助数字签名的方式来识别消息是否被篡改、保证消息发送者无法抵赖消息。数字签名,其实就是将公钥密码反过来使用。在非对称密码体系中,任何人都可以使用公钥进行加密,只有消息接收者才拥有私钥解密消息;在数字签名中,任何人都可以使用公钥验证签名,只有消息发送者才能拥有私钥,并借助私钥签名。

上图可以看出签名的制作过程,将消息生成对应的散列值,然后消息发送者使用私钥加密消息散列值。发送者会将消息和签名一起发送给消息接收者。消息接收者接受到消息后,借助消息发送者的公钥解密签名获取消息散列值,同时再生成另一个直接发送消息的散列值,比较两个散列值,若一样则说明消息没有被篡改,否则说明消息被篡改。

数字签名的作用不是为了保证机密性,仅仅是为了能够识别内容有没有被篡改以及防止消息发送人否认。数字签名其实就是将公钥密码反过来使用。数字签名中用私钥加密,公钥解密;非对称密码中用公钥加密,私钥解密。


4、证书

证书的作用

如果出现中间人攻击,中间人冒充消息接收者,中间人自己生成公钥和私钥,并将自己的公钥发给消息发送者。如果想防止中间人攻击,消息发送者必须确保拿到公钥的合法性。通过证书相关手段可以确保公钥的合法性,消息发送者通常要通过权威机构获取公钥。

证书是由权威机构认证的。密码学中的证书,全称叫公钥证书(Public-key Certificate,PKC),和现实生活中的毕业证、驾驶证等类似,里面有个人信息、此人的公钥以及权威认证机构(Certificate Authority,CA)对公钥施加的数字签名三个部分组成。CA 是指能够认定公钥确实属于此人并能够生成数字签名的个人或者组织。

证书的利用

从上图中可以看出消息接收者需要生成密钥对,并将自己的公钥给权威认证机构,权威机构会用自己私钥对消息接收者的公钥施加数字签名,制作成证书并保存在权威机构的仓库中。于此同时,消息发送者可以通过正规渠道获取拿到权威机构公钥(一般是提前内置的,如 iPhone 设备中包含 apple 的公钥),并能下载证书,此时可以用权威机构的公钥验证证书的合法性。映射的现实生活相当于:教育机构工作人员从教育机构获取到一个可以用于检测任意学历证书(证书)真伪的工具(权威机构公钥)。


5、iOS 签名机制

iOS 签名机制作用(开发、adHoc以及Inhouse)

iOS签名机制主要是保证安装到用户手机上的APP都是经过Apple 官方允许的,如果篡改了 App 本身的源码或资源文件,签名值将无法对应上,便无法安装。实际开发过程中不管是真机调试,还是发布APP,开发者都需要经过一系列复杂的步骤,这些步骤都是为了防止随意安装 App。

利用签名机制,也可以在原有项目中实现App自签名功能代码,对资源文件、源码、证书签名,在App 启动时做签名校验,当有人改动资源文件,签名校验就会失败,则直接调用 exit退出 App,为了防止逆向可将该功能相关代码通过 C 语言形式实现或者整个应用实现代码混淆。网上有不少介绍 App 自签名的实现代码,但是笔者认为自签名功能很鸡肋,因为逆向的本质是通过插件的形式更改内存中的代码,一般并非是直接更改源码或资源文件。

iOS 签名机制作流程

有了上述知识作为铺垫,签名机制流程就很好理解了,如下图:

Mac 设备具有生成公钥和私钥的能力,平时操作钥匙串工具钥匙串访问-->证书助理-->从证书颁发机构请求证书主要目的就是为了生成 Mac 公私钥,之后再将公钥上传到苹果后台,用于制作证书。Apple 本身可以认为是权威机构,Apple 后台可以生成私钥,iOS 设备中有公钥。

  1. 当运行CMD + R的时候,此时会进行代码签名,即拿 Mac 本地的私钥对应用签名生成 ipa 安装包,ipa 安装包中主要包含应用、签名、资源文件等。
  2. 将 Mac 本地生成的公钥上传到 Apple 后台,Apple 后台用自己的私钥生成证书文件,证书中包含 Mac 公钥以及签名。
  3. 选择相应的证书、devicesapp identitlements(权限),然后苹果后台用自己的私钥将这些内容签名,并生成描述文件。
  4. iOS 设备中包含苹果的公钥,使用公钥验证签名文件,如果验证通过则可以获取证书。于此同时,还会比对相应的devicesapp identitlements(权限)是否一致。
  5. 使用iOS 设备中的苹果公钥验证证书签名,如果签名验证成功则会获取到 Mac 公钥。
  6. 使用 Mac 公钥验证 ipa 安装包签名,如果验证成功则直接安装应用。
线上 App 签名机制

如果 APP 是从 AppStore 下载安装的,会发现里面是没有 mobileprovision 文件。线上 app 的验证流程简单很多,应用上传到 Apple 后台,Apple 会用自己的私钥进行签名,下载应用的设备中包含 Apple 公钥,应用下载完成,直接可以用设备中的 Apple 公钥进行验证。流程如下:


二、哈希(散列)函数

1、简介

a、哈希的用途

哈希主要用于验证,就像人的指纹一样。涉及到验证的部分一般都可以使用Md5。如:向百度云上传文件,百度云对每个文件都进行MD5加密,将你上传文件的MD5与已有的进行比对,假如已经存在则说明你上传的文件在库里已经有了,直接秒传。一般来说相同的文件库里面只会保存一份。

b、哈希破解(散列碰撞)

不同的数据MD5加密后得到的应该是不同的32位字符,但是32位字符排列组合的个数是有限的,数据却是无限的,比如自然数字。对无限的数据MD5加密得到有限个32位字符,其中肯定是有重复的,即不同的数据哈希之后可能得到相同的结果。


2、MD5 散列函数

a、特点
  • 对输入信息生成唯一的128位散列值(32个字符)。所有的数据(视频、音频、文件、只要存在于硬盘或内存中的)都是可以被MD5加密的,得到的都是32个字符
  • MD5加密后的结果是不可逆的
  • 同一个数据加密之后得到的结果是一样的

b、NSString+Hash文件中MD5算法
引入CC_MD5
#import <CommonCrypto/CommonCrypto.h>
计算MD5散列结果
/** 计算MD5散列结果
 *  @return 32个字符的MD5散列字符串
 */
- (NSString *)md5String;

// 计算MD5散列结果
- (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];
}
返回二进制 Bytes 流的字符串表示形式
/** 返回二进制 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];
}

c、运行结果
调用MD5算法
- (void)MD5Demo
{
    NSString *originString = @"123";
    NSString *MD5String = [originString md5String];
    NSLog(@"原字符串为:%@",originString);
    NSLog(@"MD5加密后的字符为:%@",MD5String);
}

输出结果为:

2020-09-09 15:19:47.719566+0800 EncryptedDemo[60660:4894670] 原字符串为:123
2020-09-09 15:19:47.719683+0800 EncryptedDemo[60660:4894670] MD5加密后的字符为:202cb962ac59075b964b07152d234b70

终端测试命令:

xiejiapei@xiejiapeis-iMac ~ % md5 -s "string"
MD5 ("string") = b45cffe084dd3d20d928bee85e7b0f21

3、破解哈希(散列)函数

a、MD5解密网站

MD5解密网站 针对md5sha1等全球通用公开的加密算法进行反向查询,通过穷举字符组合的方式,创建了明文密文对应查询数据库,创建的记录约90万亿条,占用硬盘超过500TB,查询成功率95%以上,所以哈希(散列)函数这种方式是不安全的,相对来说比较容易破解。

MD5解密网站

随着 MD5 碰撞生成器的出现,MD5 算法不应被用于任何软件完整性检查或代码签名的用途。苹果已经在iOS 13.0弃用MD5

// CC_MD5已弃用:在iOS 13.0中首次被弃用,此函数在密码上已被破坏,不应在安全上下文中使用。客户端应迁移到SHA256(或更高版本)
'CC_MD5' is deprecated: first deprecated in iOS 13.0 - This function is cryptographically broken and should not be used in security contexts. Clients should migrate to SHA256 (or stronger).

4、加盐(Salt)

传统增加安全性的方法是加盐(Salt),在明文的固定位置插入位数足够多、足够复杂的随机串,然后再进行MD5

- (void)MD5Demo
{
    NSString *originString = @"123";
    NSString *MD5String = [originString md5String];
    NSLog(@"原字符串为:%@",originString);
    NSLog(@"MD5加密后的字符为:%@",MD5String);
    
    // 加盐(Salt)
    NSString *salt = @"fdsfjf)*&*JRhuhew7HUH^&udn&&86&*";
    NSString *saltString = [originString stringByAppendingString:salt];
    NSString *MD5SaltString = [saltString md5String];
    NSLog(@"加盐(Salt)+ MD5加密后的字符为:%@",MD5SaltString);
}

输出结果为:

2020-09-09 15:39:41.667676+0800 EncryptedDemo[60865:4908204] 原字符串为:123
2020-09-09 15:39:41.667784+0800 EncryptedDemo[60865:4908204] MD5加密后的字符为:202cb962ac59075b964b07152d234b70
2020-09-09 15:39:41.667868+0800 EncryptedDemo[60865:4908204] 加盐(Salt)+ MD5加密后的字符为:3d886c749350df1977fcd2dcb237f8f9

缺点是盐是固定不变的,一旦泄露后果不堪设想。使用加盐通过MD5解密之后,很容易发现规律从而破解。

Salt

5、HMAC 散列

a、以用户账号登录注册为例
注册
  1. 客户端向服务器发起注册请求,这时服务器生成一个密钥key(并保存),然后将密钥返回给客户端。
  2. 客户端根据密钥对密码进行HMAC加密生成密文,传给服务器,服务器保存(不允许明文保存用户的隐私信息)。
  3. 服务器返回注册成功后,客户端将密钥保存到手机钥匙串(减少密钥传递次数,提高安全性)。
登录
  1. 客户端读取手机钥匙串中的密钥,对密码进行HMAC加密后,向服务器发出登录请求。
  2. 服务器根据账号读取数据库中的密文与客户端提交的进行比较。

b、计算HMAC MD5散列结果
/** 计算HMAC MD5散列结果
 *  @return 32个字符的HMAC MD5散列字符串
 */
- (NSString *)hmacMD5StringWithKey:(NSString *)key;

// HMAC + MD5
- (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];
}

进行调用:

// HMAC + MD5
NSString *HMACMD5String = [originString hmacMD5StringWithKey:@"xiejiapei"];
NSLog(@"HMAC + MD5 加密后的字符为:%@",HMACMD5String);

输出结果为:

2020-09-09 16:01:34.220311+0800 EncryptedDemo[61099:4922578] HMAC + MD5 加密后的字符为:785c229bef809fb9eab3d2e789f82716
破解网站

终端测试命令:

xiejiapei@xiejiapeis-iMac ~ % echo -n "string" | openssl dgst -md5 -hmac "key"
38fc25be306abaee29caa95a980cd645

c、HMAC MD5散列函数的实际应用

可以利用这个做出类型于设备锁或QQ手机令牌的功能,目的就是为了在他人设备上无法登录自己的账号。这里说下大致的流程,在客户端发出登录请求时,在手机钥匙串读取密钥,当这个密钥不存在的时,就说明是在他人设备上操作的,这时可以利用推送通知或短信告诉自己,是否允许他人设备登录,只允许此次登录还是授权永久登录。如果仅登陆一次则不保存密钥到钥匙串,下次登录重复以上操作,如果授权永久登录可以在他人设备保存密钥。


d、HMAC + MD5 + 添加时间戳

只是HMAC + MD5仍然存在不安全的成分。如果在上面的第2步中,拦截传给服务器的post内容获取加密后的密文,再模拟客户端向服务器发出请求,就黑掉了!所以为了再次提高安全性,需要添加时间戳。

第一步

客户端将HMAC得到的数据拼接当前服务器时间,再用MD5加密。即(HMAC+202009101633).md5String,将加密后的数据post到服务器。202009101633 须为当前服务器时间,这样可以避免客户端时间不统一。

第二步

服务器开始验证。根据账号在数据库中读取HMAC,将当前服务器时间拼接后,再用MD5加密,得到32位字符与客户端提交的32位字符进行比较。

这里会有两种情况:
情况一:服务器拼接的时间与客户端拼接的时间一致,得到相同的密文,则通过验证。
情况二:服务器拼接的时间与客户端拼接的时间不一致,得到的密文不相同,这时候服务器就拼接上一个时间。

客服端密文是:(HMAC+202009101633).md5String
服务器第一次拼接时间的密文是:(HMAC+202009101634).md5String
两者不一致,服务器第二次拼接上一个时间段密文:(HMAC+202009101633).md5String,一致则通过。

进行这样的操作就是让客户端发出的请求的时间有效性只维持1分钟,如果还觉得不安全可以在拼接时间的时候添加秒,进一步缩短时间的有效性,以提高安全性。


6、计算文件的MD5散列结果

终端测试命令
md5 file.dat
算法
#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];
}

三、哈希(SHA散列)函数

简介

哈希值用作表示大量数据的固定大小的唯一值。数据的少量更改会在哈希值中产生不可预知的大量更改。sha256的作用就是防止数据被篡改。

  • 不可逆哈希算法:md5、sha256
  • 对称加密:des、aes
  • 非对称加密:rsa

在编写个人项目的时候,需要给字符串加密。于是就编写了以前经常使用的MD5加密算法。但是该算法在 XCode 中显示在 iOS13 以后将被废弃,系统推荐使用SHA256 算法。下面是被警告的 MD5 算法。

var md5: String {
    let utf8 = cString(using: .utf8)
    var digest = [UInt8](repeating: 0, count: Int(CC_SHA256_DIGEST_LENGTH))
    CC_MD5(utf8, CC_LONG(utf8!.count - 1), &digest)
    return digest.reduce("") { $0 + String(format:"%02x", $1) }
}

为什么废弃MD5?通过 Xcode 的警告可以了解到,MD5 在加密方面已经遭到破坏,为了数据安全不应该再使用它,应该使用更加安全的 SHA256算法。什么是SHA256SHASecure Hash Algorithm 的缩写,即安全哈希算法。

SHA 的计算流程:对数字数据进行数学运算 -> 将计算出的哈希值与已知预期的哈希值进行比较 -> 从而确定数据的完整性。哈希值可以从任意数据段生成,但该过程不能逆向,即不能从哈希值逆向生成数据。

SHA256 也成为SHA2,它是从SHA1进化而来,目前没有发现SHA256被破坏,但随着计算机计算能力越来越强大,它肯定会被破坏,所以SHA3已经在路上了。


1、SHA 散列算法

a、SHA1散列
终端测试命令
echo -n "string" | openssl sha -sha1
计算SHA1散列结果,返回40个字符的SHA1散列字符串
- (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];
}

b、SHA224散列
终端测试命令
echo -n "string" | openssl sha -sha224
计算SHA224散列结果,返回 56个字符的SHA224散列字符串
- (NSString *)sha224String
{
    const char *str = self.UTF8String;
    uint8_t buffer[CC_SHA224_DIGEST_LENGTH];
    
    CC_SHA224(str, (CC_LONG)strlen(str), buffer);
    
    return [self stringFromBytes:buffer length:CC_SHA224_DIGEST_LENGTH];
}

c、SHA256散列
终端测试命令
echo -n "string" | openssl sha -sha256
计算SHA256散列结果,返回64个字符的SHA256散列字符串
- (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];
}

d、SHA384散列
终端测试命令
echo -n "string" | openssl sha -sha384
计算SHA 384散列结果,返回96个字符的SHA 384散列字符串
- (NSString *)sha384String
{
    const char *str = self.UTF8String;
    uint8_t buffer[CC_SHA384_DIGEST_LENGTH];
    
    CC_SHA384(str, (CC_LONG)strlen(str), buffer);
    
    return [self stringFromBytes:buffer length:CC_SHA384_DIGEST_LENGTH];
}

e、SHA512散列
终端测试命令
echo -n "string" | openssl sha -sha512
计算SHA 512散列结果,返回128个字符的SHA 512散列字符串
- (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];
}

2、HMAC + SHA 散列算法

a、HMAC SHA1散列
终端测试命令
echo -n "string" | openssl sha -sha1 -hmac "key"
计算HMAC SHA1散列结果,返回40个字符的HMAC SHA1散列字符串
- (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];
}

b、HMAC SHA256散列
终端测试命令
echo -n "string" | openssl sha -sha512 -hmac "key"
计算HMAC SHA256散列结果,返回64个字符的HMAC SHA256散列字符串
- (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];
}

c、HMAC SHA512散列
终端测试命令
echo -n "string" | openssl sha -sha512 -hmac "key"
计算HMAC SHA512散列结果,返回128个字符的HMAC SHA512散列字符串
- (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];
}

3、文件的SHA散列结果

a、文件的SHA1散列
终端测试命令
openssl sha -sha1 file.dat
计算文件的SHA1散列结果,返回40个字符的SHA1散列字符串
- (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];
}

b、文件的SHA256散列
终端测试命令
openssl sha -sha256 file.dat
计算文件的SHA256散列结果,返回64个字符的SHA256散列字符串
- (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];
}

c、文件的SHA512散列
终端测试命令
openssl sha -sha512 file.dat
计算文件的SHA512散列结果,返回128个字符的SHA512散列字符串
- (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];
}

四、对称加密算法:AES+Base64

1、对称加密算法

a、概念

对称加密(也叫私钥加密)指加密和解密使用相同密钥的加密算法。

数据发信方将明文(原始数据)和加密密钥一起经过特殊加密算法处理后,使其变成复杂的加密密文发送出去。收信方收到密文后,若想解读原文,则需要使用加密用过的密钥及相同算法的逆算法对密文进行解密,才能使其恢复成可读明文。

明文——>加密——>密文
密文——>解密——>明文

b、特点
  • 算法公开
  • 计算量小
  • 加密速度快
  • 加密效率高
c、缺点
  • 密钥管理困难
  • 使用成本较高
  • 双方都使用同样的密钥
  • 安全性得不到保证

2、Base64编码

a、作用

为什么会有Base64编码呢?因为有些网络传送渠道并不支持所有的字节,例如传统的邮件只支持可见字符的传送,像ASCII码的控制字符就不能通过邮件传送。这样用途就受到了很大的限制,比如图片二进制流的每个字节不可能全部是可见字符,所以就传送不了。最好的方法就是在不改变传统协议的情 况下,做一种扩展方案来支持二进制文件的传送。把不可打印的字符也能用可打印字符来表示,问题就解决了。Base64编码应运而生,Base64就是一种基于64个可打印字符来表示二进制数据的表示方法。

IOS中有两种Base64编码方式,一种是NSData类中自带的编码方法。一种是Google的GTMBase64,可以用CocoaPods导入。


b、索引表

看一下Base64的索引表,字符选用了"A-Z、a-z、0-9、+、/" 64个可打印字符。数值代表字符的索引,这个是标准Base64协议规定的,不能更改。

数值 字符 数值 字符 数值 字符 数值 字符
0 A 16 Q 32 g 48 w
1 B 17 R 33 h 49 x
2 C 18 S 34 i 50 y
3 D 19 T 35 j 51 z
4 E 20 U 36 k 52 0
5 F 21 V 37 l 53 1
6 G 22 W 38 m 54 2
7 H 23 X 39 n 55 3
8 I 24 Y 40 o 56 4
9 J 25 Z 41 p 57 5
10 K 26 a 42 q 58 6
11 L 27 b 43 r 59 7
12 M 28 c 44 s 60 8
13 N 29 d 45 t 61 9
14 O 30 e 46 u 62 +
15 P 31 f 47 v 63 /

c、原理

Base64的码表只有64个字符, 如果要表达64个字符的话,使用6的bit即可完全表示(2的6次方为64)。因为Base64的编码只有6个bit即可表示,而正常的字符是使用8个bit表示, 8和6的最小公倍数是24,所以4个Base64字符可以表示3个标准的ascll字符。

如果是字符串转换为Base64码, 会先把对应的字符串转换为ascll码表对应的数字, 然后再把数字转换为2进制, 比如aascll码为97, 97的二进制是:01100001, 把8个二进制提取成6个,剩下的2个二进制和后面的二进制继续拼接, 最后再把6个二进制码转换为Base64对应的编码。当转换到最后, 最后的字符不足的话,直接在最后添加号即可。

字符串 a b c
ASCII 97 98 99
8bit 01100001 01100010 01100011
6bit 011000 010110 001001 100011
索引 24 22 9 35
对应编码 Y W J j

d、特点

说起Base64编码可能有些奇怪,因为大多数的编码都是由字符转化成二进制的过程,而从二进制转成字符的过程称为解码。而Base64的概念就恰好反了,由二进制转到字符称为编码,由字符到二进制称为解码。

Base64编码主要用在传输、存储、表示二进制等领域,还可以用来加密,但是不能叫做加密算法,因为这种加密比较简单,其算法和充当密钥的索引表都是公开的,只是一眼看上去不知道什么内容罢了。

Base64编码是从二进制到字符的过程,像一些中文字符用不同的编码转为二进制时,产生的二进制是不一样的,所以最终产生的Base64字符也不一样。例如"上网"对应utf-8格式的Base64编码是"5LiK572R", 对应GB2312格式的Base64编码是"yc/N+A=="


3、准备工作

a、导入辅助文件
  • SAMKeychain文件夹
  • GTMBase64文件夹

b、封装成工具类

考虑到加密对象和使用场景,理所当然的将加密过程放到了NSData的类别中,即下面说到的NSData+AESAES加密解密过程需要你提供一个Key,一定是和后端同学约定好的,不然是解密不了的。


4、使用方式

a、运行效果
2020-09-08 16:49:00.395366+0800 LocalEncryptDemo[45516:4298448] LocalEncryptService:秘钥重新生成
2020-09-08 16:49:00.395472+0800 LocalEncryptDemo[45516:4298448] 秘钥为:0174096234160359
2020-09-08 16:49:00.395610+0800 LocalEncryptDemo[45516:4298448] aes128加密后:{length = 16, bytes = 0x7282e38be6c799e28cacec11973c6413}
2020-09-08 16:49:00.395718+0800 LocalEncryptDemo[45516:4298448] base64编码后:coLji+bHmeKMrOwRlzxkEw==
2020-09-08 16:49:27.772725+0800 LocalEncryptDemo[45516:4298448] base64解密后:{length = 16, bytes = 0x7282e38be6c799e28cacec11973c6413}
2020-09-08 16:49:27.773796+0800 LocalEncryptDemo[45516:4298448] 加密秘钥 33 秒后过期
2020-09-08 16:49:27.773938+0800 LocalEncryptDemo[45516:4298448] 秘钥为:0174096234160359
2020-09-08 16:49:27.774069+0800 LocalEncryptDemo[45516:4298448] aes128解密后的:Hello World

2020-09-08 16:50:17.868817+0800 LocalEncryptDemo[45516:4298448] 点击了清空按钮
2020-09-08 16:50:47.524991+0800 LocalEncryptDemo[45516:4298448] 加密秘钥已过期 97 秒
2020-09-08 16:50:47.527602+0800 LocalEncryptDemo[45516:4298448] LocalEncryptService:秘钥重新生成
2020-09-08 16:50:47.527713+0800 LocalEncryptDemo[45516:4298448] 秘钥为:00396481442BD97A
2020-09-08 16:50:47.527805+0800 LocalEncryptDemo[45516:4298448] aes128加密后:{length = 16, bytes = 0xdbdf7727507e8191e4f2794480ac40d4}
2020-09-08 16:50:47.527906+0800 LocalEncryptDemo[45516:4298448] base64编码后:2993J1B+gZHk8nlEgKxA1A==
2020-09-08 16:50:57.423980+0800 LocalEncryptDemo[45516:4298448] base64解密后:{length = 16, bytes = 0xdbdf7727507e8191e4f2794480ac40d4}
2020-09-08 16:50:57.425182+0800 LocalEncryptDemo[45516:4298448] 秘钥为:00396481442BD97A
2020-09-08 16:50:57.425292+0800 LocalEncryptDemo[45516:4298448] aes128解密后的:97 Boy
运行效果

b、点击加密按钮触发的事件
- (void)encodeAction
{
    ...
}
❶ 文本框为空
// sku为空
if (self.skuTextField.text.length == 0)
{
    [self showInfo:@"请输入sku" title:@"提示"];
    return;
}

// 加密内容为空
if (self.encryptionTextView.text.length == 0)
{
    [self showInfo:@"请输入加密内容" title:@"提示"];
    return;
}
❷ 有效期存在
if (self.validitySecondsTextField.text)
{
    // 绑定标识和有效期
     [[LocalEncryptService shareInstance] bindSku:self.skuTextField.text validityPeriod:@(self.validitySecondsTextField.text.integerValue)];
}
❸ 进行加密并保存
// 进行加密并保存
NSString *encodeString =  [[LocalEncryptService shareInstance] encryptAndSaveInfo:self.encryptionTextView.text SkuNum:self.skuTextField.text];

// 展示加密后的内容
NSString *encodeInfo = [NSString stringWithFormat:@"\n\n加密后的内容\n%@",encodeString];
self.encryptionTextView.text = [self.encryptionTextView.text stringByAppendingString:encodeInfo];
❹ 倒计时
[LocalEncryptViewController dispatchSourceTimerWithSeconds:self.validitySecondsTextField.text.integerValue inProgressBlock:^(NSInteger sec) {
    if (sec == 0)
    {
        self.timeLabel.text = @"加密秘钥已过期";
        return ;
    }
    self.timeLabel.text = [NSString stringWithFormat:@"加密秘钥%@s秒后过期", @(sec)];
} periodOverBlock:^{
}];

c、点击解密按钮触发的事件
- (void)decodeAction
{
    // sku为空
    if (self.skuTextField.text.length == 0)
    {
        [self showInfo:@"请输入sku" title:@"提示"];
        return;
    }
    
    // 进行解密并获取
    NSError *error;
    NSString *decodeString = [[LocalEncryptService shareInstance] decryptAndQueryWithSkuNum:self.skuTextField.text error:&error];
    
    
    if (error) //解密出错
    {
        [self showInfo:error.domain title:@"提示"];
    }
    else
    {
        // 展示解密后的内容
        NSString *decodeInfo = [NSString stringWithFormat:@"解密后的内容\n%@",decodeString];
        self.decryptionTextView.text = decodeInfo;
    }
}

d、点击关闭键盘和清空内容按钮
关闭键盘
- (void)endEditing
{
    [self.view endEditing:YES];
}
清空内容
- (void)clearTextField
{
    NSLog(@"点击了清空按钮");
    self.encryptionTextView.text = nil;
    self.decryptionTextView.text = nil;
    _timeLabel.text = nil;
}

e、用于计算加密秘钥失效的倒计时
+ (void)dispatchSourceTimerWithSeconds:(NSInteger)seconds  inProgressBlock:(void (^)(NSInteger))inProgressBlock periodOverBlock:(void (^)(void))periodOverBlock
{
    NSInteger timeOut = seconds; //default:60
    __block NSInteger second = timeOut;
    
    dispatch_queue_t quene = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);
    
    dispatch_source_t timer = dispatch_source_create(DISPATCH_SOURCE_TYPE_TIMER, 0, 0, quene);
    
    dispatch_source_set_timer(timer, DISPATCH_TIME_NOW, 1 * NSEC_PER_SEC, 0 * NSEC_PER_SEC);
    
    dispatch_source_set_event_handler(timer, ^{
        dispatch_async(dispatch_get_main_queue(), ^{
            if (second == 0)
            {
                if(periodOverBlock)
                {
                    periodOverBlock();
                }
                second = timeOut;
                dispatch_cancel(timer);
            }
            else
            {
                second--;
                if(inProgressBlock)
                {
                    inProgressBlock(second);
                }
            }
        });
    });
    dispatch_resume(timer);
}

5、加密解密工具类

a、提供的接口属性和方法
@interface LocalEncryptService : NSObject

/** 单例 */
+ (instancetype)shareInstance;

/** 绑定标识和有效期 */
- (void)bindSku:(NSString *)sku validityPeriod:(NSNumber *)seconds;
/** 强制清除加密秘钥 */
- (void)forceClearPriEncryptKeyWithSkuNum:(NSString *)sku;
/** 加密并保存 */
- (NSString *)encryptAndSaveInfo:(NSString *)info SkuNum:(NSString *)sku;
/** 解密并获取 */
- (NSString *)decryptAndQueryWithSkuNum:(NSString *)sku error:(NSError **)error;

@end
b、导入框架并声明属性
#import "LocalEncryptService.h"
#import "SAMKeychain.h"
#import "NSData+AES.h"

// 添加到sku前面的前缀
static NSString *AESPriPerfix = @"AES";

@interface LocalEncryptService ()

// 绑定标识和有效期
@property (nonatomic, strong) NSMutableDictionary *validityPeriods;

@end
c、绑定标识和有效期
- (void)bindSku:(NSString *)sku validityPeriod:(NSNumber *)seconds
{
    if ([sku isKindOfClass:NSString.class] && [seconds isKindOfClass:NSNumber.class] && seconds.integerValue != 0)
    {
        [self.validityPeriods setValue:seconds forKey:sku];
    }
}
d、强制清除加密秘钥
- (void)forceClearPriEncryptKeyWithSkuNum:(NSString *)sku
{
    [SAMKeychain deletePasswordForService:sku account:sku];
}
e、加密并保存
- (NSString *)encryptAndSaveInfo:(NSString *)info SkuNum:(NSString *)sku;
{
    // 加密
    NSString *encryptInfo = [self encodeWithString:info sku:sku];
    
    if (encryptInfo)
    {
        // 保存
        [[NSUserDefaults standardUserDefaults] setObject:encryptInfo forKey:[AESPriPerfix stringByAppendingString:sku]];
    }
    
    return encryptInfo;
}
f、解密并获取
- (NSString *)decryptAndQueryWithSkuNum:(NSString *)sku error:(NSError **)error;
{
    // 获取
    NSString *decryptInfo = [[NSUserDefaults standardUserDefaults] objectForKey:[AESPriPerfix stringByAppendingString:sku]];
    
    if (decryptInfo)
    {
        // 解密
        return [self decodeWithString:decryptInfo sku:sku error:error];
    }
    
    return nil;
}

6、加解密算法

a、加密算法
- (NSString*)encodeWithString:(NSString *)string sku:(NSString *)sku
{
    // 1.将内容转化为数据
    NSData *data = [string dataUsingEncoding:NSUTF8StringEncoding];
    
    // 2.获取秘钥
    NSString *key = [self codeKeyWithSuk:sku error:nil];
    
    // 3.根据秘钥对内容进行aes加密
    NSData *aesData = [data AES128EncryptWithKey:key];
    NSLog(@"aes128加密后:%@",aesData);
    
    // 4.再进行base64编码
    NSString *base64Data = [NSData encodeBase64Data:aesData];
    NSLog(@"base64编码后:%@",base64Data);
    return base64Data;
}

b、解密算法
- (NSString*)decodeWithString:(NSString *)string sku:(NSString *)sku error:(NSError **)error;
{
    // 1.解开base64编码
    NSData *decodeBase64Data = [NSData decodeBase64String:string];
    NSLog(@"base64解密后:%@",decodeBase64Data);
    
    // 2.获取秘钥
    NSError *keyError;
    NSString *key = [self codeKeyWithSuk:sku error:&keyError];
    if(keyError)
    {
        if (error != NULL)
        {
            *error = [[NSError alloc] initWithDomain:keyError.domain code:keyError.code userInfo:keyError.userInfo];
        }
        return nil;
    }
    
    // 3.根据秘钥解开aes加密内容
    NSData *decryptAesData = [decodeBase64Data AES128DecryptWithKey:key];
    
    // 4.将数据转化为字符串
    NSString *resultString = [[NSString alloc] initWithData:decryptAesData encoding:NSUTF8StringEncoding];
    NSLog(@"aes128解密后的:%@",resultString);
    return resultString;
}

c、获取秘钥算法
- (NSString*)codeKeyWithSuk:(NSString *)sku error:(NSError **)error;
{
    ...
    return key;
}
❶ 根据绑定标识创建秘钥
NSString *key = [SAMKeychain passwordForService:sku account:sku];
❷ 根据绑定标识获取有效期
NSInteger validityPeriod = [[self.validityPeriods valueForKey:sku] integerValue];
❸ 判断是否是最近生成的本地秘钥,即判断秘钥是否过期
if (validityPeriod && key.length >= 10)
{
    // 取以秒为单位时间作比较
    NSString *keyTime = [key substringWithRange:NSMakeRange(0, 10)];
    long long last = keyTime.longLongValue;
    long long now = (long long)[NSDate date].timeIntervalSince1970;
    
    if (now - last > validityPeriod || last > now) //秘钥失效
    {
        key = nil;
        NSLog(@"加密秘钥已过期 %lld 秒",now - last - validityPeriod);
        
        if (error != NULL)
        {
            *error = [[NSError alloc] initWithDomain:@"加密秘钥过期" code:-1 userInfo:nil];
        }
    }
    else// 尚未失效
    {
        NSLog(@"加密秘钥 %lld 秒后过期",validityPeriod - (now - last));
    }
}
❹ key不存在
// 首次随机生成用于信息加密KEY,确保不同手机的加密key是不同的
// 可以凭借时间戳设定key的时效性
if (!key.length)
{
    // 4.1 当前时间
    NSString *time = [NSString stringWithFormat:@"%lld",(long long)[NSDate date].timeIntervalSince1970 * 1000];
    
    // 4.2 随机生成用于信息加密KEY
    key = [time stringByAppendingString:[NSString stringWithFormat:@"%ld",(long)arc4random()]];
    
    // 4.3 添加salt
    NSString *salt = nil;
    if ([NSUUID UUID].UUIDString.length >= 2)
    {
        salt = [[NSUUID UUID].UUIDString substringWithRange:NSMakeRange(0, 5)];
    }
    else
    {
        salt = @"xjpjm";
    }
    key = [key stringByAppendingString:salt];
    
    // 4.4 存储加密秘钥
    [SAMKeychain setPassword:key forService:sku account:sku];
    NSLog(@"LocalEncryptService:秘钥重新生成");
}
❺ 16字节128位秘钥
if (key.length >= 16)
{
    // aes128
    key = [key substringWithRange:NSMakeRange(key.length - 16, 16)];
    NSLog(@"秘钥为:%@",key);
}

d、base64编码和解码
使用GTMBase64
#import "GTMBase64.h"
GTMBase64编码
+ (NSString*)encodeBase64Data:(NSData *)data
{
    // 进行GTMBase64编码
    data = [GTMBase64 encodeData:data];
    
    // 转为字符串 
    NSString *base64String = [[NSString alloc] initWithData:data encoding:NSUTF8StringEncoding];
    return base64String;
}
GTMBase64解码
+ (NSData*)decodeBase64String:(NSString * )input
{
    // 转为数据
    // usedLossyConversion:是否使用有损转化
    NSData *data = [input dataUsingEncoding:NSUTF8StringEncoding allowLossyConversion:YES];
    
    // 进行GTMBase64解码
    data = [GTMBase64 decodeData:data];
    return data;
}

e、AES加密
- (NSData *)AES128EncryptWithKey:(NSString *)key
{
    char keyPtr[kCCKeySizeAES128 + 1];
    memset(keyPtr, 0, sizeof(keyPtr));
    [key getCString:keyPtr maxLength:sizeof(keyPtr) encoding:NSUTF8StringEncoding];

    NSUInteger dataLength = [self length];

    size_t bufferSize = dataLength + kCCBlockSizeAES128;
    void *buffer = malloc(bufferSize);

    size_t numBytesEncrypted = 0;
    // keyPtr加解密的密钥
    // 加密模式kCCOptionECBMode
    // 补码方式kCCOptionPKCS7Padding
    // iv向量,此属性可选,但只能用于CBC模式
    // kCCEncrypt: 编码 or 解码
    CCCryptorStatus cryptStatus = CCCrypt(kCCEncrypt,
                                          kCCAlgorithmAES,
                                          kCCOptionPKCS7Padding|kCCOptionECBMode,
                                          keyPtr,
                                          kCCBlockSizeAES128,
                                          NULL /* initialization vector (optional) */,
                                          [self bytes],
                                          dataLength, /* input */
                                          buffer,
                                          bufferSize, /* output */
                                          &numBytesEncrypted);
    if (cryptStatus == kCCSuccess)
    {
        //the returned NSData takes ownership of the buffer and will free it on deallocation
        return [NSData dataWithBytesNoCopy:buffer length:numBytesEncrypted];
    }

    free(buffer); //free the buffer;
    return nil;
}

f、AES解密
- (NSData *)AES128DecryptWithKey:(NSString *)key
{
    // 'key' should be 32 bytes for AES256, will be null-padded otherwise
    char keyPtr[kCCKeySizeAES128+1]; // room for terminator (unused)
    bzero(keyPtr, sizeof(keyPtr)); // fill with zeroes (for padding)

    // fetch key data
    [key getCString:keyPtr maxLength:sizeof(keyPtr) encoding:NSUTF8StringEncoding];

    NSUInteger dataLength = [self length];

    //See the doc: For block ciphers, the output size will always be less than or
    //equal to the input size plus the size of one block.
    //That's why we need to add the size of one block here
    size_t bufferSize = dataLength + kCCBlockSizeAES128;
    void *buffer = malloc(bufferSize);

    size_t numBytesDecrypted = 0;
    CCCryptorStatus cryptStatus = CCCrypt(kCCDecrypt,
                                          kCCAlgorithmAES128,
                                          kCCOptionPKCS7Padding|kCCOptionECBMode,
                                          keyPtr,
                                          kCCBlockSizeAES128,
                                          NULL /* initialization vector (optional) */,
                                          [self bytes],
                                          dataLength, /* input */
                                          buffer,
                                          bufferSize, /* output */
                                          &numBytesDecrypted);

    if (cryptStatus == kCCSuccess) {
        //the returned NSData takes ownership of the buffer and will free it on deallocation
        return [NSData dataWithBytesNoCopy:buffer length:numBytesDecrypted];
    }

    free(buffer); //free the buffer;
    return nil;
}

五、非对称加密算法:RSA

1、非对称加密算法

概念

非对称加密算法需要两个密钥:公开密钥(publickey)和私有密钥(privatekey)。

公开密钥与私有密钥是一对,如果用公开密钥对数据进行加密,只有用对应的私有密钥才能解密。如果用私有密钥对数据进行加密,那么只有用对应的公开密钥才能解密。因为加密和解密使用的是两个不同的密钥,所以这种算法叫作非对称加密算法。

流程
  1. A要向B发送信息,B产生一对用于加密和解密的公钥和私钥
  2. B的私钥保密,B的公钥告诉A
  3. A要给B发送信息时,因为A知道B的公钥,A便用B的公钥加密信息
  4. A将这个消息发给B(已经用B的公钥加密消息)
  5. B收到这个消息后,B用自己的私钥解密A的消息。其他所有收到这个报文的人都无法解密,因为只有B才有B的私钥
特点
  • 由于其算法复杂,而使得加密解密速度没有对称加密解密的速度快
  • 安全性较高

2、RSA

概念

RSA算法基于一个十分简单的数论事实:将两个大质数(素数)相乘十分容易,但是想要对其乘积进行因式分解却极其困难,因此可以将乘积公开作为加密密钥。

举例

取两个简单的质数:89、97,得到两者乘积很简单8633,但是要想对8633进行因式分解,其工作量成几何增加。


3、数字签名

a、场景

早上买杯10元的奶茶,得知使用店家的app支付可以打5折,于是你用店家的app支付了5元,马上收到消费5元的通知。看似一切正常,但是黑客拦截并获取客户端发送的请求支付的信息,将需要支付的5元改成50元并发送,终端收到支付50元的请求,扣款50元并将扣款信息返回给客户端,黑客再次拦截扣款信息,将已扣款50元改成已扣款5元后发送给客户端,哈哈哈一来一回你已经在不知不觉中损失了45元。移动支付、金融流通越来越频繁,如何避免这样的问题,这就需要对敏感数据进行数字签名。

数字签名

b、概念

数字签名(又称公钥数字签名、电子签章)是一种类似写在纸上的普通的物理签名,但是使用了公钥加密领域的技术实现,用于鉴别数字信息的方法。一套数字签名通常定义两种互补的运算,一个用于签名,另一个用于验证。

将摘要信息用发送者的私钥加密,与原文一起传送给接收者。接收者只有用发送者的公钥才能解密被加密的摘要信息,然后用HASH函数对收到的原文产生一个摘要信息,与解密的摘要信息对比。如果相同,则说明收到的信息是完整的,在传输过程中没有被修改,否则说明信息被修改过,因此数字签名能够验证信息的完整性。


c、流程

数字签名是个加密的过程,数字签名验证是个解密的过程。

客户端
  • 将敏感信息进行对称或非对称加密得到加密过的数据报文
  • 将数据报文再进行一下HASH算法加密得到32位的HSAH
  • 将32位HASH值进行RSA算法加密得到签名
  • 将签名和数据报文打包组合发送给服务端
服务端
  • 将接收到的签名用私钥解密得到32位的HASH
  • 将接收到的数据报文按照客户端相同的操作进行HASH算法,得到32位HASH
  • 判断两个HASH是否相同,如相同则表示数据报文是安全的
  • 将数据报文解密得到需要的原始数据
流程图

六、使用RNCryptor框架

1、导入框架

❶ 将框架从RNCryptor-objc下载到本地后,将RNCryptorRNCryptor iOS文件夹导入到工程中。

导入文件夹

❷ 需要在Build PhasesLink Binary With Libraries中添加Security.framework

Security.framework

RNCryptor iOS.h文件中,需要将

#import <RNCryptor/RNCryptor.h">
#import <RNCryptor/RNDecryptor.h>
#import <RNCryptor/RNEncryptor.h>
#import <RNCryptor/RNCryptorEngine.h>

替换为:

#import "RNCryptor.h"
#import "RNDecryptor.h"
#import "RNEncryptor.h"
#import "RNCryptorEngine.h"

这样才可以正常编译。编译成功后,在需要使用的地方#import "RNCryptor iOS.h"即可。


2、基本使用

#import "RNCryptor iOS.h"

- (void)viewDidLoad
{
    [super viewDidLoad];

    // 获得需要加密的数据
    NSString *str = @"谢佳培";
    NSLog(@"加密内容为:%@",str);
    NSData *data = [str dataUsingEncoding:NSUTF8StringEncoding allowLossyConversion:YES];
    // 加密秘钥
    NSString *privateKey = @"123456";
    NSError *error;
    
    // RNCryptor加密
    NSData *encryptedData = [RNEncryptor encryptData:data withSettings:kRNCryptorAES256Settings password: privateKey error:&error];
    
    // RNCryptor解密
    NSData *decryptedData = [RNDecryptor decryptData:encryptedData withSettings:kRNCryptorAES256Settings password: privateKey error:&error];
    // 转为字符串
    NSString *resultStr = [[NSString alloc] initWithData:decryptedData encoding:NSUTF8StringEncoding];
    
    NSLog(@"解密内容为:%@",resultStr);
}

输出结果为:

2020-09-09 10:38:53.132186+0800 RNCryptorDemo[56693:4712005] 加密内容为:谢佳培
2020-09-09 10:38:53.159468+0800 RNCryptorDemo[56693:4712005] 解密内容为:谢佳培

3、CXYRNCryptorTool mac端加密小工具

a、用途

CXYRNCryptorTool是基于RNCryptorMac端加密小工具。开发中出于安全考虑需要对一些资源(pngmp3)加密,就可以使用RNCryptorCXYRNCryptorTool能够帮你对资源文件先进行加密。


b、使用步骤

CXYRNCryptorTool下载好工具,然后用Xcode运行,在弹出的窗口中放入图片,指定加密密码、格式以及存放地址。

CXYRNCryptorTool

c、加密效果
运行效果

d、使用RNCryptor进行解密

运行效果如下:

2020-09-09 14:00:57.643081+0800 RNCryptorDemo[59004:4830056] ======解开苹果!=====
2020-09-09 14:00:57.666878+0800 RNCryptorDemo[59004:4830056] ======解开瑞幸咖啡!=====
使用RNCryptor进行解密

❶ 将加密好的cxy文件导入项目中,注意对于Xcode无法识别格式的文件,无法自动导入资源到mainBundle中,所以需要我们自己手动添加到Bundle Resource,否则[[NSBundle mainBundle] pathForResource......返回的结果为nil

❷ 使用之前约定的密码xiejiapei对加密数据通过RNCryptor工具进行解密。

// 针对 CXYRNCryptorTool mac端加密小工具 进行解密Demo
- (void)decryptCXYRNCryptorTool
{
    NSData *encryptedAppleData = [NSData dataWithContentsOfFile:[[NSBundle mainBundle] pathForResource:@"apple" ofType:@"cxy"]];    
    NSData *encryptedLuckcoffeeData = [NSData dataWithContentsOfFile:[[NSBundle mainBundle] pathForResource:@"luckcoffee" ofType:@"cxy"]];
    
    NSError *appleError;
    NSError *luckcoffeeError;

    NSData *decryptedAppleData = [RNDecryptor decryptData:encryptedAppleData withPassword:@"xiejiapei" error:&appleError];
    NSData *decryptedLuckcoffeeData = [RNDecryptor decryptData:encryptedLuckcoffeeData withPassword:@"xiejiapei" error:&luckcoffeeError];

    if (!appleError)
    {
        NSLog(@"======解开苹果!=====");
        UIImage *image = [UIImage imageWithData:decryptedAppleData];
        self.appleImageView.image = image;
    }
    else
    {
        NSLog(@"======苹果坏了!=====");
    }
    
    if (!luckcoffeeError)
    {
        NSLog(@"======解开瑞幸咖啡!=====");
        UIImage *image = [UIImage imageWithData:decryptedLuckcoffeeData];
        self.luckcoffeeImageView.image = image;
    }
    else
    {
        NSLog(@"======瑞幸咖啡喝光了!=====");
    }
}

4、PDF加解密

a、注意事项
  • 考虑到PDF文件可能较大的原因,这里在加解密时使用了子线程,以避免加解密过程耗时
  • PDF查看需要提供路径

b、准备工作
❶ 导入框架
#import "RNCryptor iOS.h"
#import "ReaderViewController.h"

#define Password  @"xiejiapei"

@interface RootViewController ()<ReaderViewControllerDelegate>

@property(copy,nonatomic) NSString *filepath;// PDF文件存储路径

@end
❷ 导入PDFReaderSources文件夹和PDF文件

c、PDF加密

将网络请求下来的数据流(NSData)直接进行加密,加密成功后存入沙盒目录中。

// PDF加密
- (void)PDFEncryption
{
    __block NSData *encryptedData;// 用于加密的数据
    __block NSError *error;// 出错
    ...
}
❶ 获取待加密PDF
NSString *filePath = [[NSBundle mainBundle] pathForResource:@"天域苍穹小说.pdf" ofType:nil];
❷ 用于加密成功后存入的沙盒目录
NSString *fileEncryPath = [NSHomeDirectory()stringByAppendingPathComponent:@"/Documents/novel.xiejiapei"];
❸ 判断是否已存在加密文件,若存在直接执行解密过程
NSFileManager *fileManager = [NSFileManager defaultManager];
if ([fileManager fileExistsAtPath:fileEncryPath])
{
    // PDF解密
    [self PDFDecryptedData:[NSData dataWithContentsOfFile:fileEncryPath]];
    return;
}
❹ 异步去加密,防止占用太多内存
dispatch_async(dispatch_get_global_queue(0, 0), ^{
    // 将PDF转化为数据
    NSData *data = [NSData dataWithContentsOfFile:filePath];
    
    // 对数据进行加密
    encryptedData = [RNEncryptor encryptData:data withSettings:kRNCryptorAES256Settings password:Password error:&error ];
    
    // 加密成功
    if (!error)
    {
        NSLog(@"^_^ PDF加密成功 ……——(^_^)\n");
        NSLog(@"加密后数据为 == %@",encryptedData);
    }
    
    ...
}
❺ 在主线程上写入加密后的文件到沙盒目录
dispatch_sync(dispatch_get_main_queue(), ^{
    BOOL isSuccessfullyStored = [encryptedData writeToFile:fileEncryPath atomically:NO];
   
    if (isSuccessfullyStored)
    {
        NSLog(@"加密文件写入到沙盒目录成功");

    }
    else
    {
        NSLog(@"加密文件写入到沙盒目录失败");
    }
    
    NSLog(@"写入PDF的路径:%@",fileEncryPath);
    
    // 6.PDF解密
    [self PDFDecryptedData:encryptedData];
});

d、PDF解密

在查看PDF时,先对加密的PDF进行解密,再将解密的PDF存入沙盒目录中(区分加解密PDF文件)。这里加解密时并没有具体实现网络请求模块,只是简单的对本地文件进行了实践,但大体实现过程已经实现。

- (void)PDFDecryptedData:(NSData *)encryptedData
{
    // 1.用于解密成功后存入的沙盒目录
    NSString *fileDecryPath = [NSHomeDirectory() stringByAppendingPathComponent:@"/Documents/novel"];
    
    // 2.异步去解密,防止占用太多内存
    dispatch_async(dispatch_get_global_queue(0, 0), ^{
        NSError *error;// 出错
        
        if (encryptedData != nil || Password != nil)
        {
            // 解密
            NSData *decryptedData = [RNDecryptor decryptData:encryptedData withPassword:Password error:&error];
            
            // 3.在主线程上写入解密后的文件到沙盒目录
            dispatch_sync(dispatch_get_main_queue(), ^{
                 BOOL isSuccessfullyStored = [decryptedData writeToFile:fileDecryPath atomically:NO];
                
                if (isSuccessfullyStored)
                {
                    NSLog(@"解密文件写入成功");
                    NSLog(@"解密PDF写入的路径为:%@",fileDecryPath);
                    
                    self.filepath = fileDecryPath;
                    [self pushReaderVC];
                }
                else
                {
                    NSLog(@"解密文件写入失败");
                }
            });
        }
        else
        {
            NSLog(@"加密数据为空");
        }
    });
}

e、查看PDF

获取解密的PDF文件路径,查看PDF文件。

// 跳转到阅读器视图
-(void)pushReaderVC
{
    // 1.实例化控制器
    NSString *phrase = nil;
    ReaderDocument *document = [ReaderDocument withDocumentFilePath:self.filepath password:phrase];
    
    // 2.跳转到阅读器视图
    if (document != nil)
    {
        ReaderViewController *readerViewController = [[ReaderViewController alloc] initWithReaderDocument:document];
        readerViewController.delegate = self;
        
        readerViewController.modalTransitionStyle = UIModalTransitionStyleCrossDissolve;
        readerViewController.modalPresentationStyle = UIModalPresentationFullScreen;
        
        [self presentViewController:readerViewController animated:YES completion:NULL];
        
    }
    else// 3.没有PDF文件
    {
        NSLog(@"%s [ReaderDocument withDocumentFilePath:'%@' password:'%@'] failed.", __FUNCTION__, self.filepath, phrase);
        NSLog(@"没有PDF文件");
    }

}

f、删除PDF

退出查看当前的PDF文件时,删除解密后的PDF文件缓存,保留加密的PDF缓存。

// ReaderViewControllerDelegate
- (void)dismissReaderViewController:(ReaderViewController *)viewController
{
    // 退出查看PDF时删除解密存储文件
    NSFileManager *fileManager = [NSFileManager defaultManager];
    [fileManager removeItemAtPath:self.filepath error:nil];
    [self dismissViewControllerAnimated:YES completion:nil];
}

g、运行效果
2020-09-10 11:08:20.578064+0800 RNCryptorDemo[72221:5403187] ^_^ PDF加密成功 ……——(^_^)
2020-09-10 11:08:20.578226+0800 RNCryptorDemo[72221:5403187] 加密后数据为 == {length = 21371410, bytes = 0x0301c6c9 244d913d 765b3348 42f22bc9 ... af608d91 e43bd8ad }
2020-09-10 11:08:20.590865+0800 RNCryptorDemo[72221:5402550] 加密文件写入到沙盒目录成功
2020-09-10 11:08:20.591008+0800 RNCryptorDemo[72221:5402550] 写入PDF的路径:/Users/xiejiapei/Library/Developer/CoreSimulator/Devices/82067702-CBE3-4F02-986F-160872522121/data/Containers/Data/Application/4BC50446-6428-4D5B-B106-A9CC9A453625/Documents/novel.xiejiapei
2020-09-10 11:08:20.724884+0800 RNCryptorDemo[72221:5402550] 解密文件写入成功
2020-09-10 11:08:20.725016+0800 RNCryptorDemo[72221:5402550] 解密PDF写入的路径为:/Users/xiejiapei/Library/Developer/CoreSimulator/Devices/82067702-CBE3-4F02-986F-160872522121/data/Containers/Data/Application/4BC50446-6428-4D5B-B106-A9CC9A453625/Documents/novel.pdf
PDF加解密

Demo

Demo在我的Github上,欢迎下载。
BasicsDemo

参考文献

iOS加密详解

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
【社区内容提示】社区部分内容疑似由AI辅助生成,浏览时请结合常识与多方信息审慎甄别。
平台声明:文章内容(如有图片或视频亦包括在内)由作者上传并发布,文章内容仅代表作者本人观点,简书系信息发布平台,仅提供信息存储服务。
禁止转载,如需转载请通过简信或评论联系作者。

相关阅读更多精彩内容

友情链接更多精彩内容