草庐IT

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

GJ_ia 2023-09-12 原文

前言

最近和服务器同学对接口进行数据加解密时用到了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_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作为业内最常见的对称加密模式,我们在使用的过程中,不仅仅是要会用,对其不同模式、参数区别,要有一个大概的了解。当与后端进行对接时,能根据后端制定的规则来编写客户端的代码。

有关iOS-AES加解密各模式(ECB、CBC、CFB、OFB)的实现的更多相关文章

  1. ruby-on-rails - Rails - 子类化模型的设计模式是什么? - 2

    我有一个模型:classItem项目有一个属性“商店”基于存储的值,我希望Item对象对特定方法具有不同的行为。Rails中是否有针对此的通用设计模式?如果方法中没有大的if-else语句,这是如何干净利落地完成的? 最佳答案 通常通过Single-TableInheritance. 关于ruby-on-rails-Rails-子类化模型的设计模式是什么?,我们在StackOverflow上找到一个类似的问题: https://stackoverflow.co

  2. ruby - 解析 RDFa、微数据等的最佳方式是什么,使用统一的模式/词汇(例如 schema.org)存储和显示信息 - 2

    我主要使用Ruby来执行此操作,但到目前为止我的攻击计划如下:使用gemsrdf、rdf-rdfa和rdf-microdata或mida来解析给定任何URI的数据。我认为最好映射到像schema.org这样的统一模式,例如使用这个yaml文件,它试图描述数据词汇表和opengraph到schema.org之间的转换:#SchemaXtoschema.orgconversion#data-vocabularyDV:name:namestreet-address:streetAddressregion:addressRegionlocality:addressLocalityphoto:i

  3. ruby - 如何在续集中重新加载表模式? - 2

    鉴于我有以下迁移:Sequel.migrationdoupdoalter_table:usersdoadd_column:is_admin,:default=>falseend#SequelrunsaDESCRIBEtablestatement,whenthemodelisloaded.#Atthispoint,itdoesnotknowthatusershaveais_adminflag.#Soitfails.@user=User.find(:email=>"admin@fancy-startup.example")@user.is_admin=true@user.save!ende

  4. ruby - 如何验证 IO.copy_stream 是否成功 - 2

    这里有一个很好的答案解释了如何在Ruby中下载文件而不将其加载到内存中:https://stackoverflow.com/a/29743394/4852737require'open-uri'download=open('http://example.com/image.png')IO.copy_stream(download,'~/image.png')我如何验证下载文件的IO.copy_stream调用是否真的成功——这意味着下载的文件与我打算下载的文件完全相同,而不是下载一半的损坏文件?documentation说IO.copy_stream返回它复制的字节数,但是当我还没有下

  5. Ruby 文件 IO 定界符? - 2

    我正在尝试解析一个文本文件,该文件每行包含可变数量的单词和数字,如下所示:foo4.500bar3.001.33foobar如何读取由空格而不是换行符分隔的文件?有什么方法可以设置File("file.txt").foreach方法以使用空格而不是换行符作为分隔符? 最佳答案 接受的答案将slurp文件,这可能是大文本文件的问题。更好的解决方案是IO.foreach.它是惯用的,将按字符流式传输文件:File.foreach(filename,""){|string|putsstring}包含“thisisanexample”结果的

  6. ruby - 是否有用于序列化和反序列化各种格式的对象层次结构的模式? - 2

    给定一个复杂的对象层次结构,幸运的是它不包含循环引用,我如何实现支持各种格式的序列化?我不是来讨论实际实现的。相反,我正在寻找可能会派上用场的设计模式提示。更准确地说:我正在使用Ruby,我想解析XML和JSON数据以构建复杂的对象层次结构。此外,应该可以将该层次结构序列化为JSON、XML和可能的HTML。我可以为此使用Builder模式吗?在任何提到的情况下,我都有某种结构化数据-无论是在内存中还是文本中-我想用它来构建其他东西。我认为将序列化逻辑与实际业务逻辑分开会很好,这样我以后就可以轻松支持多种XML格式。 最佳答案 我最

  7. 区块链之加解密算法&数字证书 - 2

    目录一.加解密算法数字签名对称加密DES(DataEncryptionStandard)3DES(TripleDES)AES(AdvancedEncryptionStandard)RSA加密法DSA(DigitalSignatureAlgorithm)ECC(EllipticCurvesCryptography)非对称加密签名与加密过程非对称加密的应用对称加密与非对称加密的结合二.数字证书图解一.加解密算法加密简单而言就是通过一种算法将明文信息转换成密文信息,信息的的接收方能够通过密钥对密文信息进行解密获得明文信息的过程。根据加解密的密钥是否相同,算法可以分为对称加密、非对称加密、对称加密和非

  8. Get https://registry-1.docker.io/v2/: net/http: request canceled while waiting - 2

    1.错误信息:Errorresponsefromdaemon:Gethttps://registry-1.docker.io/v2/:net/http:requestcanceledwhilewaitingforconnection(Client.Timeoutexceededwhileawaitingheaders)或者:Errorresponsefromdaemon:Gethttps://registry-1.docker.io/v2/:net/http:TLShandshaketimeout2.报错原因:docker使用的镜像网址默认为国外,下载容易超时,需要修改成国内镜像地址(首先阿里

  9. ruby - Ruby 中的单 block AES 解密 - 2

    我需要尝试一些AES片段。我有一些密文c和一个keyk。密文已使用AES-CBC加密,并在前面加上IV。不存在填充,纯文本的长度是16的倍数。所以我这样做:aes=OpenSSL::Cipher::Cipher.new("AES-128-CCB")aes.decryptaes.key=kaes.iv=c[0..15]aes.update(c[16..63])+aes.final它工作得很好。现在我需要手动执行CBC模式,所以我需要单个block的“普通”AES解密。我正在尝试这个:aes=OpenSSL::Cipher::Cipher.new("AES-128-ECB")aes.dec

  10. ruby - 使用 AES 的 Rails 加密,过于复杂 - 2

    我在加密来self正在使用的第三方供应商的值时遇到问题。他们的指令如下:1)Converttheencryptionpasswordtoabytearray.2)Convertthevaluetobeencryptedtoabytearray.3)Theentirelengthofthearrayisinsertedasthefirstfourbytesontothefrontofthefirstblockoftheresultantbytearraybeforeencryption.4)EncryptthevalueusingAESwith:1.256-bitkeysize,2.25

随机推荐