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

6,568 阅读10分钟

前言

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

一、概念

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

image.png

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

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

image.png     加密轮数越多,当然安全性越好,但也更耗费时间。迭代轮数是系统设置好的,若非自己实现算法可以不用管。

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

image.png

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

image.png

在每一个明文块加密前,会让明文块和一个值先做异或操作。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。明文分组用字节为单位的正方形矩阵描述,称为状态矩阵。在每一轮加密中,状态矩阵的内容不断发生变化,最后的结果作为密文输出。该矩阵中字节的排列顺序为从上到下、从左至右依次排列,生成状态矩阵图的过程如下图所示:

image.png

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

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

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

image.png

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

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

三、iOS中代码实现

1. 不推荐使用ECB模式

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

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

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

image.png

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

当密文被篡改时,解密后对应的明文分组也会出错,且解密者察觉不到密文被篡改了。也就是说,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_BEGIN

typedef enum : NSUInteger {
    MIUCryptorNoPadding = 0,    // 无填充
    MIUCryptorPKCS7Padding = 1, // PKCS_7 | 每个字节填充字节序列的长度。 ***此填充模式使用系统方法。***
    MIUCryptorZeroPadding = 2// 0x00 填充 | 每个字节填充 0x00
    MIUCryptorANSIX923,         // 最后一个字节填充字节序列的长度,其余字节填充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 *)originalStr
                      mode:(MIUMode)mode
                       key:(NSString *)key
                   keySize:(MIUKeySizeAES)keySize
                        iv:(NSString * _Nullable )iv
                   padding:(MIUCryptorPadding)padding;

+ (NSString *)MIUAESDecrypt:(NSString *)originalStr
                      mode:(MIUMode)mode
                       key:(NSString *)key
                   keySize:(MIUKeySizeAES)keySize
                        iv:(NSString * _Nullable )iv
                   padding:(MIUCryptorPadding)padding;

@end

NS_ASSUME_NONNULL_END

(2)MIUAES.m

//
//  MIUAES.m
//


#import "MIUAES.h"
#import "MIUGTMBase64.h"

@implementation MIUAES

+ (NSString *)MIUAESEncrypt:(NSString *)originalStr
                      mode:(MIUMode)mode
                       key:(NSString *)key
                   keySize:(MIUKeySizeAES)keySize
                        iv:(NSString * _Nullable )iv
                   padding:(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 *)originalStr
                      mode:(MIUMode)mode
                       key:(NSString *)key
                   keySize:(MIUKeySizeAES)keySize
                        iv:(NSString * _Nullable )iv
                   padding:(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 *)originalData
                 operation:(CCOperation)operation
                      mode:(CCMode)mode
                       key:(NSString *)key
                   keySize:(MIUKeySizeAES)keySize
                        iv:(NSString *)iv
                   padding:(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 *)data
                          mode:(CCMode)mode
                       padding:(MIUCryptorPadding)padding;
{
    NSMutableData *sourceData = data.mutableCopy;
    int blockSize = kCCBlockSizeAES128;         //FIXME: AES的块大小都是128bit,即16bytes
    
    switch (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作为业内最常见的对称加密模式,我们在使用的过程中,不仅仅是要会用,对其不同模式、参数区别,要有一个大概的了解。当与后端进行对接时,能根据后端制定的规则来编写客户端的代码。

参考资料:

【1】AES-GCM 加密简介 :AES加密的一些概念、参数定义、一些加密模式的简介;

【2】AES加密算法的详细介绍【面试+工作】:AES-ECB模式的基本原理;

【3】PKCS5Padding与PKCS7Padding的区别 :几种常用的填充方式的填充原理和区别;

【4】iOS中加密解密之CommonCrypto框架 :iOS中使用AES加密的框架介绍;

【5】在线AES加密解密:用于验证加解密数据是否正确;

【6】aescfb加密_iOS AES加密(主要使用CFB模式) :CFB模式下的填充方式。