对称加密算法
-
对称加密算法简介
对称加密算法是指加密和解密使用同一个密钥的加密算法。
发送方使用密钥将明文数据加密成密文,然后发送出去。
接收方收到密文后,使用同一个密钥将密文解密成明文数据。优点:
- 算法公开
- 加密计算量小、加密速度快,适合对大量数据进行加解密的场景
缺点:
- 密钥传输安全性问题。
由于对称加密算法的加密和解密使用的是同一个密钥,所以对称加密的安全性就不仅仅取决于加密算法本身的强度,更取决于密钥是否被安全的保管。
因此加密者如何把密钥安全的传递到解密者手里,就成了对称加密面临的关键问题。(比如在实际开发中,客户端肯定不能直接存储对称加密的密钥,因为客户端被反编译之后,密钥就泄露了,数据安全性得不到保障。所以一般都是客户端通过非对称加密算法向服务端请求对称加密的密钥) - 密钥的管理问题。
随着密钥数量的增多,密钥的管理问题会逐渐显现出来。(比如在实际开发中,加密用户的信息时,不可能所有用户都用同一个密钥加密解密。因为这样的话,一旦密钥泄漏,就相当于泄露了所有用户的信息。因此需要为每一个用户单独的生成一个密钥并且管理,这样密钥管理的代价会非常大。)
-
常用的对称加密算法
-
DES(Data Encryption Standard):由 IBM 公司研发,美国国家标准局于1977 年公布把它作为非机要部门使用的数据加密标准。
DES 是以 64 bit (8 Byte)的明文为一个分组单位来进行加密的
DES 使用的密钥长度为 64 bit(8 Byte),但由于每隔 7 bit 设置一个奇偶校验位,因此其密钥长度实际为 56 bit。(奇偶校验是最简单的错误检测码,即根据一组二进制代码中1的个数是奇数或偶数来检测错误) -
3DES(Triple DES):由于计算机运算能力的增强,原版 DES 的密钥长度变得容易被暴力破解。3DES 相当于是对每个数据块应用 3 次 DES 加密算法,即通过增加 DES 的密钥长度来避免暴力破解。(3DES 是 DES 向 AES 过渡的加密算法)
3DES 是以 64 bit x 3 = 192 bit(24 Byte) 的明文为一个分组单位来进行加密的
3DES 使用的密钥长度为 64 bit x 3 = 192 bit(24 Byte),但由于每隔 7 bit 设置一个奇偶校验位,因此其密钥长度实际为 56 bit x 3 = 168 bit(21 Byte)。3DES 的加密过程为( E E E 代表加密, D D D 代表解密, k 1 k_1 k1、 k 2 k_2 k2、 k 3 k_3 k3 代表秘钥):
C i p h e r = E ( k 3 , D ( k 2 , E ( k 1 , P l a i n ) ) ) Cipher=E(k_3,~D(k_2,~E(k_ 1,~Plain))) Cipher=E(k3, D(k2, E(k1, Plain)))
先使用 k 1 k_1 k1 加密,再使用 k 2 k_2 k2 对 k 1 k_1 k1 加密的结果解密,最后使用 k 3 k_3 k3 对 k 2 k_2 k2 解密的结果再加密,第三次的结果作为最终的密文3DES 的解密过程为( E E E 代表加密, D D D 代表解密, k 1 k_1 k1、 k 2 k_2 k2、 k 3 k_3 k3 代表秘钥):
P l a i n = D ( K 1 , E ( k 2 , D ( k 3 , C i p h e r ) ) ) Plain=D(K_1,~E(k_2,~D(k_3,~Cipher))) Plain=D(K1, E(k2, D(k3, Cipher)))
先使用 k 3 k_3 k3 解密,再使用 k 2 k_2 k2 对 k 3 k_3 k3 解密的结果加密,最后使用 k 1 k_1 k1 对 k 2 k_2 k2 加密的结果再解密,第三次的结果作为最终的明文实际使用过程中,常常使用 k 1 k_1 k1 代替 k 3 k_3 k3 ,这样密钥的长度就是16 字节。
-
AES(Advanced Encryption Standard)):高级加密标准,具有安全性好、效率高、灵活可变的特点,是目前主流的对称加密算法。美国政府于 1997 年开始公开征集的新的数据加密标准算法,经过三轮筛选,最终于 2000 年 10 月 02 日正式宣布选中密码学家 Joan Daemen 和 Vincent Rijmen 提出的 RINJDAEL 算法作为 AES。AES 算法是一个数据块长度和密钥长度都可变的分组加密算法,其数据块长度和密钥长度都可 独立地 选定为 >= 128 bit(16 Byte) && <= 256 bit(32 Byte) 的 32 bit(4 Byte) 的任意倍数。
在 AES 标准规范中,分组长度只能是128 bit(16 Byte)。密钥的长度可以使用 128 bit(16 Byte)、192 bit(24 Byte) 或 256 bit(32 Byte)。密钥的长度不同,推荐加密轮数也不同,如下表所示:
加密算法 密钥长度 分组长度 加密轮数 AES - 128 128 bit(16 Byte) 128 bit(16 Byte) 10 AES - 192 192 bit(24 Byte) 128 bit(16 Byte) 12 AES - 256 256 bit (32 Byte) 128 bit(16 Byte) 14
对称加密算法常用的工作模式
-
对称加密算法工作模式简介
对称加密算法的加密方式分为:流加密 和 块加密。
流加密(Stream Cyphers),也叫序列密码,一次加密明文中的一个位。是指利用少量的密钥(制乱元素)通过某种复杂的运算(密码算法)产生大量的伪随机位流,用于对明文位流的加密。
块加密(Block Cyphers),也叫分组密码,一次加密明文中的一个块。是将明文按一定的位长分组,明文组经过加密运算得到密文组,密文组经过解密运算还原成明文组。
分组加密方式中,有以下 5 种常用的工作模式:
-
ECB(Electronic Code Book):电子密码本模式,是最简单的加密模式,明文消息被分成固定大小的块(分组),并且每个块被单独加密。 每个块的加密和解密都是独立的,且使用相同的方法进行加密,所以可以进行并行计算,但是这种方法一旦有一个块被破解,使用相同的方法可以解密所有的明文数据,安全性比较差。 适用于数据较少的情形,加密前需要把明文数据填充到块大小的整倍数。
使用 ECB 模式加密时,相同的明文分组会被转换为相同的密文分组。也就是说,可以将 ECB 模式理解为是一个巨大的 明文分组 到 密文分组 的对应表,因此 ECB 模式也称为电子密码本模式。当最后一个明文分组的内容长度小于分组长度时,需要用一特定的数据进行填充(padding),让最后一个分组的内容长度等于分组长度。
ECB 模式是所有模式中最简单的一种。ECB 模式中,明文分组与密文分组是一一对应的关系。因此,如果明文中存在多个相同的明文分组,则这些明文分组最终都将被转换为相同的密文分组。这样一来,只要观察一下密文,就可以知道明文中存在怎样的重复组合,并能以此为线索来破译密码,因此ECB模式是存在一定风险的。
-
CBC(Cipher Block Chaining):密文分组链接模式,每一个分组要先和前一个分组加密后的数据进行异或运算(XOR),然后再进行加密。 这样每个密文块依赖于该块之前的所有明文块,为了保持每条消息都具有唯一性,第一个数据块进行加密之前需要用初始化向量 IV 进行异或运算(XOR)。 CBC 模式是一种最常用的加密模式,它主要缺点是加密是连续的,不能并行处理,并且加密前需要把明文数据填充到块大小的整倍数。
初始化向量(Initialization Vector):当加密第一个明文分组时,由于不存在 前一个密文分组,因此需要事先准备一个长度为一个分组长度的比特序列来代替 前一个密文分组,这个比特序列称为初始化向量,通常缩写为 IV 。一般来说,每次分组加密时都会随机产生一个不同的比特序列来作为初始化向量。
-
CFB(Cipher FeedBack):密文反馈模式,前一个分组的密文加密后和当前分组的明文进行异或运算(XOR)生成当前分组的密文。所谓反馈,这里指的就是加密算法的输出结果返回作为加密算法的输入参数的意思,即前一个分组的密文会作为输入参数被送回到加密算法的输入端。
CFB 模式是通过将 明文分组 与 密码算法的输出 进行异或运算(XOR)来生成 密文分组 的。
在 CFB 模式中,密码算法的输出相当于一个随机比特序列。由于密码算法的输出是通过计算得到的,并不是真正的随机数,因此 CFB 模式不可能具有理论上不可破译的性质。
CFB 模式中由密码算法所生成的比特序列称为密钥流(Key Stream)。在 CFB 模式中,密码算法就相当于用来生成密钥流的伪随机数生成器,而初始化向量相当于伪随机数生成器的种子。
-
OFB(Output FeedBack):输出反馈模式,加密算法的输出会反馈到加密算法的输入中, 即上一个分组密码算法的输出是当前分组密码算法的输入。OFB 模式并不是通过密码算法对明文直接进行加密的,而是通过将 明文分组 和 密码算法的输出 进行异或运算(XOR)来产生 密文分组 的。
在 OFB 模式中,异或运算(XOR)所需要的比特序列(密钥流)可以事先通过密码算法生成,和明文分组无关。只要提前准备好所需的密钥流,则在实际从明文生成密文的过程中,就完全不需要动用密码算法了。只要将明文与密钥流进行异或运算(XOR)就可以了。异或运算(XOR)的速度是非常快的,这就意味着只要提前准备好密钥流就可以快速完成加密。换个角度来看,生成密钥流的操作和进行异或运算(XOR)的操作是可以并行的。
-
CTR(Counter):计数器模式,是一种通过将逐次累加的计数器进行加密来生成密钥流的分组模式。在 CTR 模式中,每个分组对应一个逐次累加的计数器,并通过对计数器进行加密来生成密钥流。也就是说,最终的密文分组是通过将 计数器加密得到的比特序列 与 明文分组 进行异或运算(XOR)而得到的。
计数器的生成方法:每次加密时都会生成一个不同的值(nonce)来作为计数器的初始值。假设当分组长度为 128 bit(16 Byte)时,计数器的初始值可能是像下面这样的形式:
其中前 8 个 Byte 为 nonce(随机数),这个值在每次加密时必须都是不同的,后 8 个 Byte 为分组序号,这个部分是会逐次累加的。按照上述生成方法,可以保证计数器的值每次都不同。因为计数器的值每次都不同,所以每个分组中将计数器进行加密所得到的密钥流也是不同的。
-
各种分组工作模式的比较:
对称加密算法常用的填充模式
-
对称加密算法填充模式简介
在使用对称加密算法的分组工作模式时:
CFB 模式、OFB 模式、CTR 模式,可以进行流加密,此时无需对明文数据进行分组加密。
ECB 模式、CBC 模式,不能进行流加密,需要对明文数据进行分组加密,最后一个不满足分组长度要求的块,就需要进行填充。有以下 5 种常用的填充模式:
-
ANSI X.923 填充模式
ANSIX923 填充模式,在填充时首先获取需要填充的字节长度,填充的字节序列的最后一个字节为需要填充的字节长度的值,填充的字节序列的其余字节均填充 0x00
假设分组长度为 8 Byte 数据: 11 22 33 44 55 66 77 88 99 AA ANSIX923 填充后: 11 22 33 44 55 66 77 88|99 AA 00 00 00 00 00 06 数据: 11 22 33 44 55 66 77 88 ANSIX923 填充后: 11 22 33 44 55 66 77 88|00 00 00 00 00 00 00 08
-
ISO 10126 填充模式
ISO 10126 填充模式,在填充时首先获取需要填充的字节长度,填充的字节序列的最后一个字节为需要填充的字节长度的值,填充的字节序列的其余字节填充随机值。
假设分组长度为 8 Byte 数据: 11 22 33 44 55 66 77 88 99 AA ISO 10126 填充后: 11 22 33 44 55 66 77 88|99 AA 15 26 37 9A CF 06 数据: 11 22 33 44 55 66 77 88 ISO 10126 填充后: 11 22 33 44 55 66 77 88|82 49 15 26 37 9A CF 08
-
PKCS7 填充模式
PKCS7 填充模式,在填充时首先获取需要填充的字节长度,填充的所有字节序列均为 需要填充的字节长度的值
假设分组长度为 8 Byte 数据: 11 22 33 44 55 66 77 88 99 AA PKCS7 填充后: 11 22 33 44 55 66 77 88|99 AA 06 06 06 06 06 06 数据: 11 22 33 44 55 66 77 88 PKCS7 填充后: 11 22 33 44 55 66 77 88|08 08 08 08 08 08 08 08
-
Zero 填充模式
Zero 填充模式,在填充时首先获取需要填充的字节长度,填充的所有字节序列均为 0x00
Zero 填充模式,在数据最后字节为零的时候不可逆假设分组长度为 8 Byte 数据: 11 22 33 44 55 66 77 88 99 AA Zero 填充后: 11 22 33 44 55 66 77 88|99 AA 00 00 00 00 00 00 // 这种情况下,填充后,无法区分哪些是填充的 0x00,哪些是原数据的 0x00 数据: 11 22 33 44 55 66 77 88 00 00 Zero 填充后: 11 22 33 44 55 66 77 88|00 00 00 00 00 00 00 00 数据: 11 22 33 44 55 66 77 88 Zero 填充后: 11 22 33 44 55 66 77 88|00 00 00 00 00 00 00 00
-
nopaddig 填充模式
nopaddig 填充模式,即不进行数据的填充。EBC 分组模式、CBC 分组模式中,如果使用 nopaddig 填充模式,那么明文的长度必须等于 分组长度 的倍数,否则会报错。
对称加密算法演示
-
macOS 下使用 OpenSSL 命令行进行 AES 加解密
AES 在 ECB 工作模式下(电子密码本工作模式),相同的明文分组会被转换为相同的密文分组(AES 的分组长度为 16 Byte)
// 进入 AES_Test 目录,之后所有的操作和生成结果都在此目录下 cd AES_Test // 创建用于加解密测试的文本:32个字符 'a' 与 32个字符 'b' vim message.txt cat message.txt aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaabbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb // 使用 OpenSSL 进行 AES 加密 openssl enc -aes-128-ecb -K 123456 -nosalt -in message.txt -out cipher.bin // 查看加密后的结果 xxd cipher.bin 00000000: 4a37 d61b 92db a28d cb03 d42a 5248 d511 J7.........*RH.. 00000010: 4a37 d61b 92db a28d cb03 d42a 5248 d511 J7.........*RH.. 00000020: be51 7377 f0c7 9e03 fdd2 ad6c 3f59 b598 .Qsw.......l?Y.. 00000030: be51 7377 f0c7 9e03 fdd2 ad6c 3f59 b598 .Qsw.......l?Y.. 00000040: b571 99b5 1f0b 5c14 3e1b 926e 5aab d2b4 .q....\.>..nZ... // 使用 OpenSSL 进行 AES 解密 openssl enc -aes-128-ecb -K 123456 -nosalt -in cipher.bin -out plain.txt -d // 查看解密后的结果 cat plain.txt aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaabbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb
AES 在 CBC 工作模式下(密文分组链接工作模式),因为当前分组的加密结果依赖于上一分组的加密结果,所以相同的明文分组会被转换为不同的密文分组(AES 的分组长度为 16 Byte,因此,初始化向量的长度也为 16 Byte)
// 进入 AES_Test 目录,之后所有的操作和生成结果都在此目录下 cd AES_Test // 创建用于加解密测试的文本:32个字符 'a' 与 32个字符 'b' vim message.txt cat message.txt aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaabbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb // 使用 OpenSSL 进行 AES 加密 openssl enc -aes-128-cbc -iv 0102030405060708 -k 123456 -nosalt -in message.txt -out cipher.bin // 查看加密后的结果 xxd cipher.bin 00000000: d519 01c2 8266 37f5 3dac ab9c ee0b 9980 .....f7.=....... 00000010: 31a6 acf2 dc84 98a3 4796 e93f 20c3 c5b3 1.......G..? ... 00000020: 2374 81a7 2c2e f275 e72b bcb3 16f7 e1d1 #t..,..u.+...... 00000030: eaf8 d4e1 0968 dd2d 7b56 d468 64b8 6a33 .....h.-{V.hd.j3 00000040: bd04 567c 2e27 cd13 a054 c868 80b5 5597 ..V|.'...T.h..U. // 使用 OpenSSL 进行 AES 解密 openssl enc -aes-128-cbc -iv 0102030405060708 -k 123456 -nosalt -in cipher.bin -out plain.txt -d // 查看解密后的结果 cat plain.txt aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaabbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb
-
Objective-C 的 CCCrypt 函数
CCCrypt 函数是 Objective-C 中对称加密算法的核心。
Objective-C 中支持的所有对称加密算法的 加密和解密 都可以通过 CCCrypt 函数实现。
通过前面对 对称加密算法 的介绍,不难发现:
所有对称加密算法的 加密和解密 所需要的参数个数是一致的。
因此,Objective-C 对其支持的所有对称加密算法的 加密和解密 过程做了一层封装,对外提供了一个统一的调用接口。-(void)CCCryptDemo { // 函数解释: // 无状态地执行一次加密或解密操作 // 这是对 CCCrytorCreate()、CCCryptorUpdate()、 CCCryptorFinal()、CCCryptorRelease() 一系列操作的封装 // param0.操作类型。枚举值,有两个选项:kCCEncrypt(对称加密)、kCCDecrypt(对称解密) CCOperation op = kCCEncrypt; // param1.加密算法类型。枚举值,包含常见的对称加密算法 // AES 分组长度固定为:128 bit(16 Byte) // AES 可选的秘钥长度为:128 bit(16 Byte)、192 bit(24 Byte)、256 bit(32 Byte) // 这里 kCCAlgorithmAES128 中的 128 指的是分组的长度(秘钥长度在参数 keyLength 中指定) CCAlgorithm alg = kCCAlgorithmAES128; // param2.附加选项。枚举值,有两个选项: // kCCOptionPKCS7Padding(PKCS7 填充模式) // kCCOptionECBMode(ECB 工作模式/电子密码本工作模式) // 如果工作模式缺省,则默认的工作模式为:CBC 工作模式(密文分组链接模式) // CCOptions options = kCCOptionPKCS7Padding | kCCOptionECBMode; // ECB 工作模式 // CCOptions options = kCCOptionPKCS7Padding; // CBC 工作模式 CCOptions options = kCCOptionPKCS7Padding; // param3.秘钥序列,需要传入字节数组 // 注意: // 1.如果传入的秘钥长度不够,函数内部会自动补全;如果传入的秘钥长度超出,函数内部会自动截取 // 2.养成良好的编程习惯:应该从 kCCKeySize 枚举值中选择秘钥长度,而不是手动赋值秘钥长度的常量 // 3.养成良好的编程习惯:在使用字节数组前,应先使用 bzero() 清空字节数组 NSString* keyStr = @"123456"; NSData* keyData = [keyStr dataUsingEncoding:NSUTF8StringEncoding]; uint8_t key[kCCKeySizeAES128]; bzero(key, sizeof(key)); [keyData getBytes:key length:sizeof(key)]; // param4.秘钥长度(这里真正决定秘钥长度)(Byte) // 养成好的编程习惯:应该从 kCCKeySize 枚举值中选择对应的秘钥长度,而不是手动赋值秘钥长度的常量 size_t keyLength = kCCKeySizeAES128; // param5.初始化向量,必须与分组长度一致 // CBC 工作模式下会使用到该值,ECB 工作模式下会忽略该值 // 传 nil 则初始化向量默认被设置为 0 // 养成好的编程习惯:应该从 kCCBlockSize 枚举值中选择对应的分组长度,而不是手动赋值分组长度的常量 NSString* ivStr = @"0102030405060708"; NSData* ivData = [ivStr dataUsingEncoding:NSUTF8StringEncoding]; uint8_t iv[kCCBlockSizeAES128]; bzero(iv, sizeof(iv)); [ivData getBytes:iv length:sizeof(iv)]; // param6.需要加密的数据,需要转换成字节数组 NSString* msg = @"hello, I am hcg!"; NSData* msgData = [msg dataUsingEncoding:NSUTF8StringEncoding]; const void* dataIn = msgData.bytes; // param7.需要加密的数据长度(Byte) size_t dataInLength = msgData.length; // param8.用于接收加密结果的缓冲区 // 因为 AES 的 CBC 工作模式为分组加密,存在自动补全机制 // 所以这里申请的接收缓冲区的长度要比输入数据的长度多出一个分组的长度(Byte) // 养成良好的编程习惯:应该从 kCCBlockSize 枚举值中选择分组长度,而不是手动赋值分组长度的常量 size_t dataOutBufferSize = dataInLength + kCCBlockSizeAES128; void* dataOut = malloc(dataOutBufferSize); // param9.用于接收加密结果的缓冲区的长度(Byte) size_t dataOutAvailable = dataOutBufferSize; // param10.操作成功后,被写入接收缓冲区的数据长度(Byte) size_t dataOutSize = 0; size_t* dataOutMoved = &dataOutSize; // 调用 CCCrypt 函数执行加密 CCCryptorStatus encStatus = CCCrypt(op, alg, options, key, keyLength, iv, dataIn, dataInLength, dataOut, dataOutAvailable, dataOutMoved); // 以 Base64 编码打印加密后的数据 NSData* cipherData = nil; if (kCCSuccess == encStatus) { cipherData = [NSData dataWithBytes:dataOut length:dataOutSize]; NSString* base64CipherText = [cipherData base64EncodedStringWithOptions:NSDataBase64Encoding64CharacterLineLength]; NSLog(@"base64CipherText = %@", base64CipherText); /* 输出结果: sEC/YRH5ft/feSs5Ye7Yw/SME9+YTcFZOgXgseKuamg= */ } // 释放申请的堆空间 free(dataOut); dataOut = NULL; // 打印需要加密的数据长度(Byte) NSLog(@"dataInLength = %ld", dataInLength); // 打印加密操作成功后,写入接收缓冲区的数据长度(Byte) NSLog(@"dataOutSize = %ld", dataOutSize); /* 输出结果: dataInLength = 16 dataOutSize = 32 注意: 需要加密的数据长度为 16 Byte 加密操作成功后,写入接收缓冲区的数据长度为 32 Byte 说明 CBC 工作模式存在填充机制 -> 申请接收缓冲区的时候需要多申请一个分组的长度 */ // 调用 CCCrypt 函数执行解密 op = kCCDecrypt; const void* decDataIn = cipherData.bytes; size_t decDataInLength = cipherData.length; void* decDataOut = malloc(decDataInLength); size_t decDataOutAvailable = decDataInLength; size_t decDataOutSize = 0; size_t* decDataOutMoved = &decDataOutSize; CCCryptorStatus decStatus = CCCrypt(op, alg, options, key, keyLength, iv, decDataIn, decDataInLength, decDataOut, decDataOutAvailable, decDataOutMoved); // 打印解密后的数据 NSData* plainData = nil; if (kCCSuccess == decStatus) { plainData = [NSData dataWithBytes:decDataOut length:decDataOutSize]; NSString* plainText = [[NSString alloc] initWithData:plainData encoding:NSUTF8StringEncoding]; NSLog(@"plainText = %@", plainText); /* 输出结果: plainText = hello, I am hcg! */ } // 释放申请的堆空间 free(decDataOut); decDataOut = NULL; }
CCCrypt 函数中用到的枚举值如下所示:
// CCCryptor 可以执行的操作 enum { kCCEncrypt = 0, // 对称加密 kCCDecrypt, // 对称解密 }; typedef uint32_t CCOperation; // CCCryptor 实现的加密算法 enum { kCCAlgorithmAES128 = 0, // 由于名称在分组长度和秘钥长度上产生歧义,已废弃 kCCAlgorithmAES = 0, kCCAlgorithmDES, kCCAlgorithm3DES, kCCAlgorithmCAST, kCCAlgorithmRC4, kCCAlgorithmRC2, kCCAlgorithmBlowfish }; typedef uint32_t CCAlgorithm; // 附加选项 enum { /* 块加密的选项 */ kCCOptionPKCS7Padding = 0x0001, // 执行 PKCS7 填充模式 kCCOptionECBMode = 0x0002 // 执行 ECB 工作模式(缺省为 CBC 工作模式) /* 流加密目前没有选项 */ }; typedef uint32_t CCOptions; // CCCryptor 支持的对称加密算法的秘钥长度(Byte) // 建议使用这些常量来赋值代表秘钥长度的变量 // DES 和 3DES 的秘钥长度是固定的 // AES 有三个可选的秘钥长度 // CAST、RC4、RC2、Blowfish 有可变的秘钥长度 enum { kCCKeySizeAES128 = 16, kCCKeySizeAES192 = 24, kCCKeySizeAES256 = 32, kCCKeySizeDES = 8, kCCKeySize3DES = 24, kCCKeySizeMinCAST = 5, // CAST 最小秘钥长度 kCCKeySizeMaxCAST = 16, // CAST 最大秘钥长度 kCCKeySizeMinRC4 = 1, // RC4 最小秘钥长度 kCCKeySizeMaxRC4 = 512, // RC4 最大秘钥长度 kCCKeySizeMinRC2 = 1, // RC2 最小秘钥长度 kCCKeySizeMaxRC2 = 128, // RC2 最大秘钥长度 kCCKeySizeMinBlowfish = 8, // Blowfish 最小秘钥长度 kCCKeySizeMaxBlowfish = 56, // Blowfish 最大秘钥长度 }; // CCCryptor 支持的对称加密算法的分组长度(Byte) // 建议使用这些常量来赋值代表分组长度的变量 enum { kCCBlockSizeAES128 = 16, // 这里的 128 指的是分组长度(bit) kCCBlockSizeDES = 8, kCCBlockSize3DES = 8, kCCBlockSizeCAST = 8, kCCBlockSizeRC2 = 8, kCCBlockSizeBlowfish = 8, }; // CCCryptor 支持的工作模式 enum { kCCModeECB = 1, kCCModeCBC = 2, kCCModeCFB = 3, kCCModeCTR = 4, kCCModeOFB = 7, kCCModeRC4 = 9, kCCModeCFB8 = 10, }; typedef uint32_t CCMode; // CCCryptor 支持的(分组的)填充模式 enum { ccNoPadding = 0, ccPKCS7Padding = 1, }; typedef uint32_t CCPadding; // CommonCryptor 操作结果的返回值 enum { kCCSuccess = 0, // 操作正常完成 kCCParamError = -4300, // 非法参数值 kCCBufferTooSmall = -4301, // 指示需要为指定的操作提供足够的缓冲区 kCCMemoryFailure = -4302, // 内存分配失败 kCCAlignmentError = -4303, // 输入的 size 没有正确对齐 kCCDecodeError = -4304, // 输入数据没有正确解码或解密 kCCUnimplemented = -4305, // 系统未为当前指定的算法实现相应的函数 kCCOverflow = -4306, // 内存溢出 kCCRNGFailure = -4307, // 生成随机数失败 kCCUnspecifiedError = -4308, // 未知的错误 kCCCallSequenceError= -4309, // 调用流程出错 kCCKeySizeError = -4310, // 秘钥长度错误 kCCInvalidKey = -4311, // 秘钥无效 }; typedef int32_t CCStatus; typedef int32_t CCCryptorStatus;
-
直接调用 CCCrypt 函数进行加密的安全隐患
在实际开发中,如果需要在 APP 本地持久化存储重要的数据,或者需要在网络上传输重要的数据,那么在数据进行持久化存储或者网络传输之前,一般需要先对数据进行加密。而 CCCrypt 是 iOS 系统中对称加密的核心函数。
在调用 CCCrypt 函数进行加解密之前,需要先导入 iOS 系统的通用加密库:
#import <CommonCrypto/CommonCrypto.h>
在 Objective-C 的语法中,有一个细节:
#import <Aaa/Bbb.h> // 表示该头文件属于系统,该头文件所在的库由系统提供 #import "Aaa/Bbb.h" // 表示该头文件属于第三方,该头文件所在的库由第三方提供
这就意味着,当真机调试或者发布程序时,CCCrypt 函数的相关实现并没有随代码一起打包在 APP 的 Mach-O 文件里面,而是位于 iOS 系统中。
那么,当在 APP 中调用 CCCrypt 函数进行加解密时,实际上调用的是系统提供的功能。这就意味着需要将调用参数传递给位于系统中的通用加密库 <CommonCrypto/CommonCrypto.h> 的 CCCrypt 函数,当然传递的参数中也包括了需要执行加密的重要数据。
如果对 iOS 系统进行越狱,对位于系统通用加密库 <CommonCrypto/CommonCrypto.h> 中的 CCCrypt 函数进行 Hook,那么就可以轻而易举地拿到 APP 需要加密的数据。
鉴于存在以上风险,当在 APP 中需要执行加密操作时,好的做法是:
- APP 集成第三方加密库(如 OpenSSL)代替 iOS 系统提供的加密库,将加密算法集成到 APP 中。由于加密算法被集成到 APP 中,代码的可控制性变强,在 APP 中可以为加密算法提供更多的保护,从而增加加密算法被 Hook 的难度(例如,支付宝 APP 就内置了 OpenSSL 用于代替对 iOS 系统加密算法的调用,并且在调用 OpenSSL 加密算法之前做了混淆)。
- 为了防止加密函数被 Hook(不论是 iOS 系统提供的加密函数,还是内置的第三方的加密函数),在代码中调用加密函数之前,对需要加密的明文数据进行一次混淆(即,不能将需要加密的明文数据原原本本地传递给加密函数)。比如:
明文数据 + 异或运算 = 混淆数据
混淆数据 + 加密函数 = 密文数据
密文数据 + 解密函数 = 混淆数据
混淆数据 + 异或运算 = 明文数据
其中,对数据进行异或运算的部分,可以写成静态函数。这样在程序编译时,这部分代码会变成内联汇编,从而减少了数据混淆过程被发现的可能。
在游戏里面,很多本地数据的加密保存,使用的只是简单的异或运算
CommonCryto(常用加密算法库里面。)
#import 导入的函数相关的实现,不在 mach o 文件里面,而是在 iOS系统里面。
直接调用 #import 里面的加密函数,是有安全隐患的
原理上安全,使用方式上不安全。
对称加密算法在工作中用的很多,很多关键的数据,都是使用对称加密对其进行保护。
直接调用 #import 里面的加密函数,是有安全隐患的
原理上安全,使用方式上不安全。
拿到 app 后加一个符号断点:CCCrypt
符号断点:断点调试 - 加号 - Symbolic BreakPoint
只要是系统的,都可以拦截到。(这符号是系统的,系统级的函数的符号 ,你是去不掉的!!!!)
如果是第三方的,在没有去符号的情况下,也是可以拦截到。
模拟拦截到 加密字符串的过程!!!!
register read x6
p (char *) 0x地址
百度一下去符号是 什么意思??
所以说,一旦我们有了逆向的基础知识后,我们就知道 ,很多核心的加密方法,不能直接去调用(调用系统、调用第三方,尤其是调用系统的,因为调用系统的可以被符号断点)
好的方法是在工程中内置加密的第三方库(比如支付宝,就内置了openssl,这里有个注意点,内置的 openssl 的符号是可以去掉的),关键数据在调用加密方法之前,先对其进行形变(自己再写一点加密的代码,不如对加密的数据先进行一次或几次异或)
tips:static 的函数,在汇编的时候,会变成内联函数,反而看不到函数的名称了。
不管调用系统的加密库,还是调用三方的加密库,在做加密之前,自己再写一点加密的代码(对加密的数据进行异或)
XCode 符号断点
-
XCode 符号断点的简介
XCode 内置的符号断点(Symbolic Breakpoint)是一个全局断点,可以对指定的函数或者方法进行拦截,结合寄存器名称和 LLDB 命令,还可以打印方法或者函数的调用参数和线程调用栈。在调试时,无需编写 Hook 代码,使用符号断点就可以拦截方法或者函数的调用并获取其上下文信息。
-
XCode 符号断点的使用
① 将左侧导航视图切换到 Breakpoint Navigator
② 点击导航栏左下角的 + 号
③ 在弹出的菜单中选择 Symbolic Breakpoint…
④ 在符号断点输入框中根据提示输入对应的函数符号、模块名、断点条件等信息
⑤ 运行 XCode,当程序执行到 Symbol 字段所标识的函数或方法时,程序将暂停在该方法处
符号断点输入框中,各个字段的含义如下:- Name:符号断点的名称
- Symbo:要拦截的符号(即,想设置断点的方法)
- Module:要拦截的符号所在的模块,留空表示拦截所有模块下的该符号
- Condition:断点生效的条件
- Ignore:跳过前面的若干次拦截
- Action:可在程序断点执行后增加额外动作
- AppleScript
- Capture GPU Frame
- Debugger Command
- Log Message
- Shell Command
- Sound - Option(Automatically continue after evaluating actions):执行 Action 后自动继续
注意
- 现代密码学中的很多算法都是基于几何学的,因为几何学中,很多描述图形的公式的变量关系是有规律的但是同时公式的结果也是多变的(例如:圆、椭圆、球)。
- iOS、macOS 的钥匙串使用的加密方式就是 AES
- 关于安全攻防的一点体会:
世界上没有绝对安全的系统,很多时候我们做安全防护,是在破解成本与防护成本之间做一个相对平衡的选择。
就像非对称加密算法,其在数学原理上是可以通过暴力枚举进行破解的。但是在实际应用中,破解非对称加密算法所需要花费的代价 远远大于 破解非对称加密算法所获得的收益,所以我们可以认为在现有技术环境下使用非对称加密算法是安全的。
同理,做 APP 安全防护时,我们需要养成一个意识:做 APP 的安全防护,并不是要从技术上让这个 APP 变得不可破解和绝对安全,而是要杜绝别有用心的人对这个 APP 的逆向破解的动机。什么意思呢?世界上没有绝对安全的 APP,APP 一旦发布出去,任何人都可以下载安装,当然也包括别有用心的人。所以我们在发布 APP 时,就应该做好 APP 会被逆向攻破的心理准备。因此,APP 安全防护所要做的是:让逆向破解 APP 所花费的成本 远远大于 逆向破解 APP 所能获得的收益,这样逆向破解 APP 就失去了意义,从而从动机上杜绝对 APP 的逆向破解。
所以 APP 的安全防护的关键在于:让逆向工程师逆向破解 APP 拿到其所需要的东西的时间成本大大增加。常用安全防护手段(隐藏函数的调用、保护核心的数据),其核心都是让逆向破解变得更加复杂和困难
在一个优秀的逆向工程师眼里:APP 里面的所有东西都是可以拿得到的。同时,逆向工程师在分析汇编代码的时候,一般是一段一段进行分析的,这主要是为了摸清楚函数的调用流程。所以,对于一个逆向工程师而言,最可怕的事情就是要找一个东西时,需要逐句分析汇编代码,这将会耗费他巨大的精力。