03、AES加密

1、AES介绍

高级加密标准(英语:Advanced Encryption Standard,缩写:AES),在密码学中又称 Rijndael 加密法,是美国联邦政府采用的一种区块加密标准。
该标准是用来替代原先的 DES,现已经被多方分析且广为全世界所使用,成为对称密钥加密中最流行的算法之一。
AES 采用对称分组密码体制,加密数据块分组长度必须为 128 比特,密钥长度可以是 128 比特、192 比特、256 比特中的任意一个(如果数据块及密钥长度不足时,会补齐)

2、AES加密模式介绍

2.1、ECB模式(电子密码本模式: Electronic codebook)

  • ECB 是最简单的块密码加密模式,加密前根据加密块大小(如 AES 为 128 位)分成若干块,之后将每块使用相同的密钥单独加密,解密同理。
  • ECB 模式由于每块数据的加密是独立的因此加密和解密都可以并行计算。
  • ECB 模式最大的缺点是相同的明文块会被加密成相同的密文块,这种方法在某些环境下不能提供严格的数据保密性。

2.2、CBC模式(密码分组链接:Cipher-block chaining)

  • CBC 模式对于每个待加密的密码块在加密前会先与前一个密码块的密文异或然后再用加密器加密。第一个明文块与一个叫初始化向量的数据块异或。

  • CBC 模式相比 ECB 有更高的保密性,但由于对每个数据块的加密依赖于前一个数据块的加密,所以加密无法并行。

  • CBC 模式与 ECB 一样在加密前需要对数据进行填充,不是很适合对流数据进行加密。

2.3、CTR 模式(计算器模式:Counter)

  • 计算器模式不常见,在 CTR 模式中, 有一个自增的算子,这个算子用密钥加密之后的输出和明文异或的结果得到密文,相当于一次一密。
  • 这种加密方式简单快速,安全可靠,而且可以并行加密。
  • 但是在计算器不能维持很长的情况下,密钥只能使用一次。

2.4、CFB 模式(密文反馈:Cipher feedback)

  • 与 ECB 和 CBC 模式只能够加密块数据不同,CFB 能够将块密文(Block Cipher)转换为流密文(Stream Cipher)。
  • CFB 的加密工作分为两部分:先将前一段加密得到的密文再加密;接着将第 1 步加密得到的数据与当前段的明文异或。
  • 由于加密流程和解密流程中被块加密器加密的数据是前一段密文,因此即使明文数据的长度不是加密块大小的整数倍也是不需要填充的,这保证了数据长度在加密前后是相同的。
  • CFB 模式非常适合对流数据进行加密,解密可以并行计算。

2.5、OFB 模式(输出反馈:Output feedback)

  • OFB 是先用块加密器生成密钥流(Keystream),然后再将密钥流与明文流异或得到密文流,解密是先用块加密器生成密钥流,再将密钥流与密文流异或得到明文,由于异或操作的对称性所以加密和解密的流程是完全一样的。
  • OFB 与 CFB 一样都非常适合对流数据的加密。
  • OFB 由于加密和解密都依赖与前一段数据,所以加密和解密都不能并行。

3、AES 加密-iOS 与 Java 的同步实现

AES是开发中常用的加密算法之一。然而由于前后端开发使用的语言不统一,导致经常出现前端加密而后端不能解密的情况出现。然而无论什么语言系统,AES的算法总是相同的, 因此导致结果不一致的原因在于加密设置的参数不一致 。于是先来看看在两个平台使用AES加密时需要统一的几个参数。

  • 密钥长度(Key Size)
  • 加密模式(Cipher Mode)
  • 填充方式(Padding)
  • 初始向量(Initialization Vector)

3.1、密钥长度

AES算法下,key的长度有三种:128、192和256 bits。由于历史原因,JDK默认只支持不大于128 bits的密钥,而128 bits的key已能够满足商用安全需求。因此本例先使用AES-128。(Java使用大于128 bits的key方法在文末提及)

3.2、加密模式

AES属于块加密(Block Cipher),块加密中有CBC、ECB、CTR、OFB、CFB等几种工作模式。本例统一使用CBC模式。

3.3、填充方式

由于块加密只能对特定长度的数据块进行加密,因此CBC、ECB模式需要在最后一数据块加密前进行数据填充。(CFB,OFB和CTR模式由于与key进行加密操作的是上一块加密后的密文,因此不需要对最后一段明文进行填充)
在iOS SDK中提供了PKCS7Padding,而JDK则提供了PKCS5Padding。原则上PKCS5Padding限制了填充的Block Size为8 bytes,而Java实际上当块大于该值时,其PKCS5Padding与PKCS7Padding是相等的:每需要填充χ个字节,填充的值就是χ。

3.4、初始向量

使用除ECB以外的其他加密模式均需要传入一个初始向量,其大小与Block Size相等(AES的Block Size为128 bits),而两个平台的API文档均指明当不传入初始向量时,系统将默认使用一个全0的初始向量。

有了上述的基础之后,可以开始分别在两个平台进行实现了。

3.5、iOS实现

先定义一个初始向量的值。

NSString *const kInitVector = @"16-Bytes--String";

确定密钥长度,这里选择 AES-128。

size_t const kKeySize = kCCKeySizeAES128;

加密操作:

(NSString *)encryptAES:(NSString *)content key:(NSString *)key {

    NSData *initVector = [kInitVector dataUsingEncoding:NSUTF8StringEncoding];
    NSData *keyData = [key dataUsingEncoding:NSUTF8StringEncoding];
    NSData *contentData = [content dataUsingEncoding:NSUTF8StringEncoding];
    NSUInteger dataLength = contentData.length;
    
    // 密文长度 <= 明文长度 + BlockSize
    size_t encryptSize = dataLength + kCCBlockSizeAES128;
    void *encryptedBytes = malloc(encryptSize);
    size_t actualOutSize = 0;
    
    CCCryptorStatus cryptStatus = CCCrypt(kCCEncrypt,
                                          kCCAlgorithmAES,
                                          kCCOptionPKCS7Padding,  // 系统默认使用 CBC,然后指明使用 PKCS7Padding
                                          keyData.bytes,
                                          kKeySize,
                                          initVector.bytes,
                                          contentData.bytes,
                                          dataLength,
                                          encryptedBytes,
                                          encryptSize,
                                          &actualOutSize);
    
    if (cryptStatus == kCCSuccess) {
        // 对加密后的数据进行 base64 编码
        return [[NSData dataWithBytesNoCopy:encryptedBytes length:actualOutSize] base64EncodedStringWithOptions:NSDataBase64EncodingEndLineWithLineFeed];
    }
    free(encryptedBytes);
    return nil;

3.6、Java实现

同理先在类中定义一个初始向量,需要与iOS端的统一。

private static final String IV_STRING = "16-Bytes--String";

另 Java 不需手动设置密钥大小,系统会自动根据传入的 Key 进行判断。
加密操作:

public static String encryptAES(String content, String key) 
			throws InvalidKeyException, NoSuchAlgorithmException, 
			NoSuchPaddingException, UnsupportedEncodingException, 
			InvalidAlgorithmParameterException, IllegalBlockSizeException, BadPaddingException {

    byte[] byteContent = content.getBytes("UTF-8");

    // 注意,为了能与 iOS 统一
    // 这里的 key 不可以使用 KeyGenerator、SecureRandom、SecretKey 生成
    byte[] enCodeFormat = key.getBytes();
    SecretKeySpec secretKeySpec = new SecretKeySpec(enCodeFormat, "AES");
		
    byte[] initParam = IV_STRING.getBytes();
    IvParameterSpec ivParameterSpec = new IvParameterSpec(initParam);
		
    // 指定加密的算法、工作模式和填充方式
    Cipher cipher = Cipher.getInstance("AES/CBC/PKCS5Padding");
    cipher.init(Cipher.ENCRYPT_MODE, secretKeySpec, ivParameterSpec);
	
    byte[] encryptedBytes = cipher.doFinal(byteContent);
	
    // 同样对加密后数据进行 base64 编码
    Encoder encoder = Base64.getEncoder();
    return encoder.encodeToString(encryptedBytes);
}

注意以上实现的是 AES-128,因此方法传入的 key 需为长度为 16 的字符串。

3.7、关于Java使用大于128 bits的key

到Oracle官网下载对应Java版本的 JCE ,解压后放到 JAVA_HOME/jre/lib/security/ ,然后修改 iOS 端的 kKeySize 和两端对应的 key 即可。

4、CryptoSwift中的AES的用法

4.1、关于密钥长度

(1)在进行 AES 加密时,CryptoSwift 会根据密钥的长度自动选择对应的加密算法(AES128, AES192, AES256)

  • AES-128 = 16 bytes
  • AES-192 = 24 bytes
  • AES-256 = 32 bytes

(2)这里我们以 ECB 模式为例。由于密钥为 16 个字符(字节),则自动采用 aes128 加密。

do {
    let str = "欢迎访问 hangge.com"
    print("原始字符串:\(str)")
     
    let key = "hangge.com123456"
    print("key密钥:\(key)")
     
    //使用AES-128-ECB加密模式
    let aes = try AES(key: key.bytes, blockMode: ECB())
     
    //开始加密
    let encrypted = try aes.encrypt(str.bytes)
    let encryptedBase64 = encrypted.toBase64() //将加密结果转成base64形式
    print("加密结果(base64):\(encryptedBase64!)")
     
    //开始解密1(从加密后的字符数组解密)
    let decrypted1 = try aes.decrypt(encrypted)
    print("解密结果1:\(String(data: Data(decrypted1), encoding: .utf8)!)")
     
    //开始解密2(从加密后的base64字符串解密)
    let decrypted2 = try encryptedBase64?.decryptBase64ToString(cipher: aes)
    print("解密结果2:\(decrypted2!)")
} catch { }

(3)如果密钥长度不够 16、24 或 32 字节,可以使用 zeroPadding 将其补齐至 blockSize 的整数倍。

zeroPadding 补齐规则:
将长度补齐至 blockSize 参数的整数倍。比如我们将 blockSize 设置为 AES.blockSize(16)。

  • 如果长度小于 16 字节:则尾部补 0,直到满足 16 字节。
  • 如果长度大于等于 16 字节,小于 32 字节:则尾部补 0,直到满足 32 字节。
  • 如果长度大于等于 32 字节,小于 48 字节:则尾部补 0,直到满足 48 字节。 * 以此类推…

这里还是以 ECB 模式为例,假设我们密钥只有 9 个字节,通过 zeroPadding 补齐到 16 个字节。

do {
    let str = "欢迎访问 hangge.com"
    print("原始字符串:\(str)")
     
    let key = "hangge666"
    print("key密钥:\(key)")
     
    //使用AES-128-ECB加密模式
    let aes = try AES(key: Padding.zeroPadding.add(to: key.bytes, blockSize: AES.blockSize),
                      blockMode: ECB())
     
    //开始加密
    let encrypted = try aes.encrypt(str.bytes)
    let encryptedBase64 = encrypted.toBase64() //将加密结果转成base64形式
    print("加密结果(base64):\(encryptedBase64!)")
     
    //开始解密1(从加密后的字符数组解密)
    let decrypted1 = try aes.decrypt(encrypted)
    print("解密结果1:\(String(data: Data(decrypted1), encoding: .utf8)!)")
     
    //开始解密2(从加密后的base64字符串解密)
    let decrypted2 = try encryptedBase64?.decryptBase64ToString(cipher: aes)
    print("解密结果2:\(decrypted2!)")
} catch { }

4.2、CBC 模式的便捷写法

(1)我们知道使用 CBC 模式加密时,除了提供一个密钥(key)外,还需要一个密钥偏移量(iv)。这个 Cipher 的完整写法如下:

do {
    let str = "欢迎访问 hangge.com"
    print("原始字符串:\(str)")
     
    let key = "hangge.com123456"
    print("key密钥:\(key)")
     
    let iv = "1234567890123456"
    print("密钥偏移量:\(iv)")
     
    //使用AES-128-CBC加密模式
    let aes = try AES(key: key.bytes, blockMode: CBC(iv: iv.bytes))
     
    //开始加密
    let encrypted = try aes.encrypt(str.bytes)
    let encryptedBase64 = encrypted.toBase64() //将加密结果转成base64形式
    print("加密结果(base64):\(encryptedBase64!)")
     
    //开始解密1(从加密后的字符数组解密)
    let decrypted1 = try aes.decrypt(encrypted)
    print("解密结果1:\(String(data: Data(decrypted1), encoding: .utf8)!)")
     
    //开始解密2(从加密后的base64字符串解密)
    let decrypted2 = try encryptedBase64?.decryptBase64ToString(cipher: aes)
    print("解密结果2:\(decrypted2!)")
} catch { }

(2)CBC 模式的 Cipher 还可以这么写,效果同上面的那个是一样的。

do {
    let str = "欢迎访问 hangge.com"
    print("原始字符串:\(str)")
     
    let key = "hangge.com123456"
    print("key密钥:\(key)")
     
    let iv = "1234567890123456"
    print("密钥偏移量:\(iv)")
     
    //使用AES-128-CBC加密模式
    let aes = try AES(key: key, iv: iv)
     
    //开始加密
    let encrypted = try aes.encrypt(str.bytes)
    let encryptedBase64 = encrypted.toBase64() //将加密结果转成base64形式
    print("加密结果(base64):\(encryptedBase64!)")
     
    //开始解密1(从加密后的字符数组解密)
    let decrypted1 = try aes.decrypt(encrypted)
    print("解密结果1:\(String(data: Data(decrypted1), encoding: .utf8)!)")
     
    //开始解密2(从加密后的base64字符串解密)
    let decrypted2 = try encryptedBase64?.decryptBase64ToString(cipher: aes)
    print("解密结果2:\(decrypted2!)")
} catch { }

4.3、随机生成密钥偏移量

除了 ECB 模式外,其它模式不仅需要提供密钥(key),还需要一个密钥偏移量(iv)。如果觉得自己定义 iv 麻烦,可以通过 AES.randomIV() 方法来自动生成。

//创建一个16字节的随机密钥偏移量
let iv = AES.randomIV(AES.blockSize)
print(iv)

4.4、String 的加密与解密

(1)上面的样例中,我们都是先定义一个 Cipher,然后使用这个 Cipher 的 encrypt 和 decrypt 方法对字符串进行加密与解密。但这过程中我们还需要手动进行一些数据转换工作:

  • 加密时需要先将字符串转为字节数组([UInt8])。加密后又需要将结果从字节数组转为 base64 的字符串形式。
  • 解密后同样需要将结果从字节数组转为字符串形式。
//使用AES-128-CBC加密模式的Cipher
let aes = try AES(key: key, iv: iv)
 
//方式一
let str = "欢迎访问 hangge.com"
             
let encrypted = try aes.encrypt(str.bytes)
print("加密结果(base64):\(encrypted.toBase64()!)")
 
let decrypted = try aes.decrypt(encrypted)
print("解密结果:\(String(data: Data(decrypted), encoding: .utf8)!)")

(2)而 CryptoSwift 其实对 String 做了扩展,增加了许多加密解密的相关方法。我们只需要传入 Cipher 即可,不再需要进行这些数据转换工作了。

//使用AES-128-CBC加密模式的Cipher
let aes = try AES(key: key, iv: iv)
 
//方式二
let str = "欢迎访问 hangge.com"
 
let encrypted = try str.encryptToBase64(cipher: aes)!
print("加密结果(base64):\(encrypted)")
             
let decrypted = try encrypted.decryptBase64ToString(cipher: aes)
print("解密结果:\(decrypted)")

4.5、增量更新

使用 Cryptor 实例可以进行增量操作,即每次只加密或解密一部分数据,这样对于大文件可以有效地节省内存占用。

do {
    //创建一个用于增量加密的Cryptor实例
    var encryptor = try AES(key: "hangge.com123456", iv: "drowssapdrowssap").makeEncryptor()
     
    var ciphertext = Array<UInt8>()
    //合并每个部分的结果
    ciphertext += try encryptor.update(withBytes: Array("欢迎访问".utf8))
    ciphertext += try encryptor.update(withBytes: Array(" ".utf8))
    ciphertext += try encryptor.update(withBytes: Array("hangge.com".utf8))
    //结束
    ciphertext += try encryptor.finish()
     
    //输出完整的结果(base64字符串形式)
    print(ciphertext.toBase64()!)
} catch {
    print(error)
}

4.6 补码方式(padding)

(1)默认情况下,CryptoSwift 使用 PKCS7 进行填充。比如下面两个定义方式是一样的。

let aes1 = try AES(key: key, blockMode: CBC(iv: iv))
         
let aes2 = try AES(key: key, blockMode: CBC(iv: iv), padding: .pkcs7)

(2)我们也可以根据需求改成其它的填充方式。具体有:

pkcs5
pkcs7
zeroPadding
noPadding

参考文章

AES 加密 - iOS 与 Java 的同步实现
Swift - 第三方加密库CryptoSwift使用详解3(AES加密与解密)

  • 0
    点赞
  • 4
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值