iOS-AES加解密各模式(ECB、CBC、CFB、OFB)的实现

前言

最近和服务器同学对接口进行数据加解密时用到了AES加密。原本以为AES就一种加密形式,对接过程中才学习到AES不同模式、不同填充方式下,结果都不相同。因此去学习了一下AES加密的基本概念、实现原理,以及各种模式下的区别与实现。

一、概念

AES加密是对称加密的一种,全称是Advanced Encryption Standard(高级加密标准)。常用于网络传输中的数据加解密。

这是一个AES在线加密工具。通过网站上的内容可以可以看出,加解密除了需要秘钥(Key)之外,AES还有多种模式,不同的模式加密的方式和结果都不相同。同时还有秘钥长度、初始向量、填充方式等参数,结果也是不尽相同。下面简单介绍一下AES加密的一些概念和参数:

  • 分组(或者叫块) :AES是一种分组加密技术,即把明文分成一组一组的,每组长度相等,每次加密一组数据,直到加密完整个明文。在AES标准规范中,分组长度只能是128 bits,也就是每个分组为16个bytes(16bytes = 128bits / 8)。
  • 密钥长度:AES支持的密钥长度可以是128 bits、192 bits或256 bits。密钥的长度不同,推荐加密轮数也不同,如下表:

  • 加密模式:因为分组加密只能加密固定长度的分组,而实际需要加密的明文可能超过分组长度,此时就要对分组密码算法进行迭代,以完成整个明文加密,迭代的方法就是加密模式。它有很多种,常见的工作模式如下图:

  • 初始向量(IV,Initialization Vector) :目的是防止同样的明文块,始终加密成同样的密文块,以CBC模式为例:

在每一个明文块加密前,会让明文块和一个值先做异或操作。IV作为初始化变量,参与第一个明文块的异或,后续的每一个明文块和它前一个明文块所加密出的密文块相异或,从而保证加密出的密文块都不同。

  • 填充方式(Padding) :由于密钥只能对确定长度的数据块进行处理,而数据的长度通常是可变的,因此需要对最后一块做额外处理,在加密前进行数据填充。常用的模式有PKCS5, PKCS7等。
填充方式 说明 示例(假定块长度为8,数据长度为9)
None 不填充
PKCS7 填充字符串由一个字节序列组成,每个字节填充该字节序列的长度。 填充用八位字节数,等于7:数据: FF FF FF FF FF FF FF FF FFPKCS7 填充: FF FF FF FF FF FF FF FF FF 07 07 07 07 07 07 07
PKCS5 通常与PKCS7通用。区别在于PKCS5明确定义Block的大小是8位,而PKCS7不确定
ANSIX923 填充字符串由一个字节序列组成,此字节序列的最后一个字节填充字节序列的长度,其余字节均填充数字零 数据: FF FF FF FF FF FF FF FF FFX923 填充: FF FF FF FF FF FF FF FF FF 00 00 00 00 00 00 07
ISO10126 填充字符串由一个字节序列组成,此字节序列的最后一个字节填充字节序列的长度,其余字节填充随机数据。 数据: FF FF FF FF FF FF FF FF FFISO10126 填充: FF FF FF FF FF FF FF FF FF 7D 2A 75 EF F8 EF 07
Zeros 填充字符串由设置为零的字节组成

二、原理简述

AES加密函数中,会执行一个轮函数,并且执行10次这个轮函数,这个轮函数的前9次执行的操作是一样的,只有第10次有所不同。也就是说,一个明文分组会被加密10轮。

AES的处理单位是字节,128位的输入明文分组P被分成16个字节。

假设明文分组为P = abcdefghijklmnop。明文分组用字节为单位的正方形矩阵描述,称为状态矩阵。在每一轮加密中,状态矩阵的内容不断发生变化,最后的结果作为密文输出。该矩阵中字节的排列顺序为从上到下、从左至右依次排列,生成状态矩阵图的过程如下图所示:

上图中,0x61为字符a的十六进制表示,其他同理。

明文经过AES加密后,已经面目全非。

而这10轮加密到底做了什么呢?主要包括4个操作:字节代换、行位移、列混合和轮密钥加。最后一轮迭代不执行列混合。另外,在第一轮迭代之前,先将明文和原始密钥进行一次异或加密操作。

同样,AES解密过程仍为10轮,每一轮的操作是加密操作的逆操作。同加密操作类似,最后一轮不执行逆列混合,在第1轮解密之前,要执行1次密钥加操作。

AES加密的具体操作,可以在文章 AES加密算法的详细介绍 找到详细的阐述。这里只简单介绍,不展开说明。

三、iOS中代码实现

1. 不推荐使用ECB模式

一般情况下,iOS开发者若没有详细接触过AES加密,当后端同事告诉你客户端需要AES加解密时,下意识去网上直接找代码copy。现在网上最常见、也是大家copy使用最多的,实际上是 AES128(即秘钥长度为128)、ECB模式、PKCS7填充 的加密方式。

而ECB模式却是AES加密中最不推荐的加密模式!

下图是ECB模式的分组密码算法加密过程:

上图可以看出,明文中重复的排列会反映在密文中(即明文分组是什么顺序,密文分组就是什么顺序)。

当密文被篡改时,解密后对应的明文分组也会出错,且解密者察觉不到密文被篡改了。也就是说,ECB不能提供对密文的完整性校验。因此,在任何情况下都不推荐使用ECB模式。

2. iOS实现各种模式下的AES加解密

iOS开发中,官方的CommonCrypto.framework提供了常用的加密方式的实现,其中就包括了AES加密算法(除此之外还有DES、blowfish等)。

对于AES加密来说,苹果官方有提供了三种函数接口,它们分别是CCCryptorcreate()、CCCryptorCreateFromData()、以及CCCryptorCreateWithMode()。下面使用CCCryptorCreateWithMode()来实现AES加密的4种常用模式:ECB、CBC、CFB、OFB。

(1)支持的模式

因为框架中有个CCMode的宏,里面就包含了ECB、CBC、CFB、OFB这4种模式,而这个宏只有在CCCryptorCreateWithMode()中才有参数。而为了对比加密数据的正确性,我使用 在线AES加密解密 的结果来对比,网站里只有ECB、CBC、CFB、OFB这4种模式,所以我代码也暂时只实现这4种模式。

(2)支持的秘钥长度

系统默认对128、192、256三种长度都支持。

(3)支持的填充方式

系统只提供了PKCS7Pading和NoPading(不填充)。这里借鉴大佬的博客 aescfb加密_iOS AES加密(主要使用CFB模式) ,实现PKCS7Pading、ZeroPadding 、ANSIX923、ISO10126四种填充方式。

直接Show Code:

(1)MIUAES.h

//
//  MIUAES.h
//#import <Foundation/Foundation.h>
#import <CommonCrypto/CommonCryptor.h>NS_ASSUME_NONNULL_BEGINtypedef enum : NSUInteger {MIUCryptorNoPadding = 0,    // 无填充MIUCryptorPKCS7Padding = 1, // PKCS_7 | 每个字节填充字节序列的长度。 ***此填充模式使用系统方法。***MIUCryptorZeroPadding = 2,  // 0x00 填充 | 每个字节填充 0x00MIUCryptorANSIX923,       // 最后一个字节填充字节序列的长度,其余字节填充0x00。MIUCryptorISO10126          // 最后一个字节填充字节序列的长度,其余字节填充随机数据。
}MIUCryptorPadding;typedef enum {MIUKeySizeAES128          = 16,MIUKeySizeAES192          = 24,MIUKeySizeAES256          = 32,
}MIUKeySizeAES;typedef enum {MIUModeECB        = 1,MIUModeCBC        = 2,MIUModeCFB        = 3,MIUModeOFB        = 7,
}MIUMode;@interface MIUAES : NSObject+ (NSString *)MIUAESEncrypt:(NSString *)originalStrmode:(MIUMode)modekey:(NSString *)keykeySize:(MIUKeySizeAES)keySizeiv:(NSString * _Nullable )ivpadding:(MIUCryptorPadding)padding;+ (NSString *)MIUAESDecrypt:(NSString *)originalStrmode:(MIUMode)modekey:(NSString *)keykeySize:(MIUKeySizeAES)keySizeiv:(NSString * _Nullable )ivpadding:(MIUCryptorPadding)padding;@endNS_ASSUME_NONNULL_END 

(2)MIUAES.m

//
//  MIUAES.m
//#import "MIUAES.h"
#import "MIUGTMBase64.h"@implementation MIUAES+ (NSString *)MIUAESEncrypt:(NSString *)originalStrmode:(MIUMode)modekey:(NSString *)keykeySize:(MIUKeySizeAES)keySizeiv:(NSString * _Nullable )ivpadding:(MIUCryptorPadding)padding;
{NSData *data = [originalStr dataUsingEncoding:NSUTF8StringEncoding];data = [self MIUAESWithData:data operation:kCCEncrypt mode:mode key:key keySize:keySize iv:iv padding:padding];//base64加密(可自己去实现)return [MIUGTMBase64 stringByEncodingData:data];
}+ (NSString *)MIUAESDecrypt:(NSString *)originalStrmode:(MIUMode)modekey:(NSString *)keykeySize:(MIUKeySizeAES)keySizeiv:(NSString * _Nullable )ivpadding:(MIUCryptorPadding)padding
{//base64解密(可自己去实现)NSData *data = [MIUGTMBase64 decodeData:[originalStr dataUsingEncoding:NSUTF8StringEncoding]];data = [self MIUAESWithData:data operation:kCCDecrypt mode:mode key:key keySize:keySize iv:iv padding:padding];return [[NSString alloc] initWithData:data encoding:NSUTF8StringEncoding];
}+ (NSData *)MIUAESWithData:(NSData *)originalDataoperation:(CCOperation)operationmode:(CCMode)modekey:(NSString *)keykeySize:(MIUKeySizeAES)keySizeiv:(NSString *)ivpadding:(MIUCryptorPadding)padding
{NSAssert((mode != kCCModeECB && iv != nil && iv != NULL) || mode == kCCModeECB, @"使用 CBC 模式,initializationVector(即iv,填充值)必须有值");CCCryptorRef cryptor = NULL;CCCryptorStatus status = kCCSuccess;NSMutableData * keyData = [[key dataUsingEncoding: NSUTF8StringEncoding] mutableCopy];NSMutableData * ivData = [[iv dataUsingEncoding: NSUTF8StringEncoding] mutableCopy];#if !__has_feature(objc_arc)[keyData autorelease];[ivData autorelease];
#endif[keyData setLength:keySize];[ivData setLength:keySize];//填充模式(系统API只提供了两种)CCPadding paddingMode = (padding == ccPKCS7Padding) ? ccPKCS7Padding : ccNoPadding ;NSData *sourceData = originalData;if (operation == kCCEncrypt) {sourceData =  [self bitPaddingWithData:originalData mode:mode padding:padding];    //FIXME: 实际上的填充模式}status = CCCryptorCreateWithMode(operation, mode, kCCAlgorithmAES, paddingMode, ivData.bytes, keyData.bytes, keyData.length, NULL, 0, 0, 0, &cryptor);if ( status != kCCSuccess ){NSLog(@"Encrypt Error:%d",status);return nil;}//确定处理给定输入所需的输出缓冲区大小尺寸。size_t bufsize = CCCryptorGetOutputLength( cryptor, (size_t)[sourceData length], true );void * buf = malloc( bufsize );size_t bufused = 0;size_t bytesTotal = 0;//处理(加密,解密)一些数据。如果有结果的话,写入提供的缓冲区.status = CCCryptorUpdate( cryptor, [sourceData bytes], (size_t)[sourceData length],buf, bufsize, &bufused );if ( status != kCCSuccess ){NSLog(@"Encrypt Error:%d",status);free( buf );return nil;}bytesTotal += bufused;if (padding == MIUCryptorPKCS7Padding) {status = CCCryptorFinal( cryptor, buf + bufused, bufsize - bufused, &bufused );if ( status != kCCSuccess ){NSLog(@"Encrypt Error:%d",status);free( buf );return nil;}bytesTotal += bufused;}NSData *result = [NSData dataWithBytesNoCopy:buf length: bytesTotal];if (operation == kCCDecrypt) {//解密时移除填充result = [self removeBitPaddingWithData:result mode:mode operation:operation andPadding:padding];}CCCryptorRelease(cryptor);return result;
}// 填充需要加密的字节
+ (NSData *)bitPaddingWithData:(NSData *)datamode:(CCMode)modepadding:(MIUCryptorPadding)padding;
{NSMutableData *sourceData = data.mutableCopy;int blockSize = kCCBlockSizeAES128;       //FIXME: AES的块大小都是128bit,即16bytesswitch (padding) {case MIUCryptorPKCS7Padding:{if (mode == kCCModeCFB || mode == kCCModeOFB) {//MARK: CCCryptorCreateWithMode方法在这两个模式下,并不会给块自动填充,所以需要手动去填充NSUInteger shouldLength = blockSize * ((sourceData.length / blockSize) + 1);NSUInteger diffLength = shouldLength - sourceData.length;uint8_t *bytes = malloc(sizeof(*bytes) * diffLength);for (NSUInteger i = 0; i < diffLength; i++) {// 补全缺失的部分bytes[i] = diffLength;}[sourceData appendBytes:bytes length:diffLength];}}break;case MIUCryptorZeroPadding:{int pad = 0x00;int diff = blockSize - (sourceData.length % blockSize);for (int i = 0; i < diff; i++) {[sourceData appendBytes:&pad length:1];}}break;case MIUCryptorANSIX923:{int pad = 0x00;int diff = blockSize - (sourceData.length % blockSize);for (int i = 0; i < diff - 1; i++) {[sourceData appendBytes:&pad length:1];}[sourceData appendBytes:&diff length:1];}break;case MIUCryptorISO10126:{int diff = blockSize - (sourceData.length % blockSize);for (int i = 0; i < diff - 1; i++) {int pad  = arc4random() % 254 + 1;      //FIXME: 因为是随机填充,所以相同参数下,每次加密都是不一样的结果(除了分段后最后一个分段的长度为15bytes的时候加密结果相同)[sourceData appendBytes:&pad length:1];}[sourceData appendBytes:&diff length:1];}break;default:break;}return sourceData;
}+ (NSData *)removeBitPaddingWithData:(NSData *)sourceData mode:(CCMode)mode operation:(CCOperation)operation andPadding:(MIUCryptorPadding)padding
{int correctLength = 0;int blockSize = kCCBlockSizeAES128;Byte *testByte = (Byte *)[sourceData bytes];char end = testByte[sourceData.length - 1];if (padding == MIUCryptorPKCS7Padding) {if ((mode == kCCModeCFB || mode == kCCModeOFB) && (end > 0 && end < blockSize + 1)) {correctLength = (short)sourceData.length - end;}else{return sourceData;}}else if (padding == MIUCryptorZeroPadding && end == 0) {for (int i = (short)sourceData.length - 1; i > 0 ; i--) {if (testByte[i] != end) {correctLength = i + 1;break;}}}else if ((padding == MIUCryptorANSIX923 || padding == MIUCryptorISO10126) && (end > 0 && end < blockSize + 1)){correctLength = (short)sourceData.length - end;}NSData *data = [NSData dataWithBytes:testByte length:correctLength];return data;
}@end 

需要注意的是,ISO10126填充标准, 每次是随机填充的。所以除了最后一个分段长度是15bits以外(因为15bits长度只需要填充一个bit,而这个bit内容是固定的,即长度01)的情况,其他情况每次加密结果是不一样的。因为最后一个块的解密是先把填充删除了再解密的,所以不影响解密。

其他没什么好解释的,代码里有注释。加解密的详细过程不用实现,CCCryptorCreateWithMode()内都实现好了。

Demo源码:

码云:gitee.com/ztfiso/MIUA…

Github:github.com/Ztfiso/MIUA…

总结

AES作为业内最常见的对称加密模式,我们在使用的过程中,不仅仅是要会用,对其不同模式、参数区别,要有一个大概的了解。当与后端进行对接时,能根据后端制定的规则来编写客户端的代码。

猜你喜欢

转载自blog.csdn.net/u013712343/article/details/132472634