Tenloy's Blog

数据加密 — 对称加密(以AES为例)

Word count: 3.3kReading time: 13 min
2020/12/22 Share

一、数据加密简述

数据加密(Encryption)是指将明文信息(Plaintext)采取数学方法进行函数转换成密文(Ciphertext),只有特定接受方才能将其解密(Decryption)还原成明文的过程。

构成:

  • 明文(Plaintext):加密前的原始信息
  • 密文(Ciphertext):明文被加密后的信息
  • 密钥(Key):控制加密算法和解密算法得以实现的关键信息,分为加密密钥和解密密钥(必然,密钥不同,由明文生成的密文的结果也不同)
  • 加密(Encryption):将明文通过数学算法转换成密文的过程
  • 解密(Decryption):将密文还原成明文的过程

1.1 密码系统的一般模型

20
  • 如果不论截取者获得了多少密文,但在密文中都没有足够的信息来唯一地确定出对应的明文,则这一密码体制称为无条件安全的,或称为理论上是不可破的(这种方法是不太容易获得的,因此在现实生活中,更多是追求计算上安全即可。)
  • 如果密码体制中的密码不能被可使用的计算资源 破译,则这一密码体制称为在计算上是安全的(利用已有的最好方法破译某个密码系统所需要的代价超出了破译者的能力(如时间、空间、资金等资源))

二、对称密钥加密(单钥加密)

20

常用的对称加密算法:

  • DES/3DES(3重DES)
  • IDEA
  • RC5
  • AES(Rijndael)

DES早期用的很多,但是由于相对比较简单,加密的安全性偏低,所以现在一般都使用3DES或 AES来替代。

2.1 对称加密的优缺点

优点:

  1. 算法公开(往往是标准算法,是可以公开的)
  2. 计算量小
  3. 加密速度快(核心是替换和移位,可以用硬件来实现)
  4. 加密效率高。

缺点:

  1. 交易双方都使用同样钥匙,安全性得不到保证。
  2. 每对用户每次使用对称加密算法时,都需要使用其他人不知道的唯一钥匙,使得发收双方所拥有的钥匙数量呈几何级数增长,密钥管理成为用户负担。
  3. 密钥分发比较困难,尤其网络环境中安全难以保障,易成为瓶颈。

三、AES(Rijndael)

下面摘自 一篇写的非常全面的博客

AES, Advanced Encryption Standard,其实是一套标准:FIPS 197,而我们所说的AES算法其实是Rijndael算法。

20

Rijndael算法是基于代换-置换网络(SPN,Substitution-permutation network)的迭代算法。明文数据经过多轮次的转换后方能生成密文,每个轮次的转换操作由轮函数定义。轮函数任务就是根据密钥编排序列(即轮密码)对数据进行不同的代换及置换等操作。

补充代换-置换网络SPN是一系列被应用于分组密码中相关的数学运算,高级加密标准(AES)、3-Way、Kuznyechik、PRESENT、SAFER、SHARK、Square都有涉用。这种加密网络使用明文块和密钥块作为输入,并通过交错的若干“轮”(或“层”)代换操作和置换操作产生密文块。代换(Substitution)和置换(Permutation)分别被称作S盒(替换盒/S-boxes)和P盒(排列盒/P-boxes)。由于其实施于硬件的高效性,SPN的应用十分广泛。

四、iOS中的AES

部分摘自 简书博客1,加了一些个人理解,有兴趣可以直接移步原文

iOS中的AES:iOS SDK中的Security.framework库,非openssl库

4.1 四个参数

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

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

4.1.1 密钥长度

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

4.1.2 加密模式

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

4.1.3 填充方式

由于块加密只能对特定长度的数据块进行加密,因此CBC、ECB模式需要在最后一数据块加密前进行数据填充,解密后删除掉填充的数据。(CFB,OFB和CTR模式由于与key进行加密操作的是上一块加密后的密文,因此不需要对最后一段明文进行填充)

  • NoPadding
    • 顾名思义,不填充,自己对长度不足block size的部分进行填充
  • ZeroPadding
    • 数据长度不对齐时使用0填充,否则不填充(当原数据尾部也存在0时,在unpadding时可能会存在问题)。
  • PKCS7Padding
    • 如果数据长度需要填充n(n>0)个字节才对齐,那么填充n个字节,每个字节都是n;
    • 如果数据本身就已经对齐了,则填充一块长度为块大小的数据,每个字节都是块大小。
  • PKCS5Padding
    • PKCS7Padding的子集,块大小固定为8字节,其它一致(即PKCS5Padding是限制块大小的PKCS7Padding)。
  • PKCS1Padding
    • 与RSA算法一起使用,这里不再赘述

附上文档链接:

使用PKCS7Padding/PKCS5Padding填充时,最后一个字节肯定为填充数据的长度,所以在解密后,取最后一位,就可以准确删除填充的数据。

在iOS SDK中提供了PKCS7Padding,而JDK则提供了PKCS5Padding(限制Block Size为8 bytes),但AES等算法,后来都把BlockSize扩充到了16字节或更大,Java中,采用PKCS5实质上就是采用PKCS7(PKCS5Padding与PKCS7Padding填充结果是相等的)。

4.1.4 初始向量

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

以CBC为例:IV是长度为分组大小的一组随机,通常情况下不用保密,不过在大多数情况下,针对同一密钥不应多次使用同一组IV。 CBC要求第一个分组的明文在加密运算前先与IV进行异或;从第二组开始,所有的明文先与前一分组加密后的密文进行异或。

4.2 iOS实现

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
//先定义一个初始向量IV的值。ECB模式不需要
NSString *const kInitVector = @"16-Bytes--String";
//确定密钥长度,这里选择 AES-128。即"密钥是个16位字符串
size_t const kKeySize = kCCKeySizeAES128;

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

NSData *contentData = [content dataUsingEncoding:NSUTF8StringEncoding];
NSUInteger dataLength = contentData.length;

// 为结束符'\0' +1
char keyPtr[kKeySize + 1];
memset(keyPtr, 0, sizeof(keyPtr));
[key getCString:keyPtr maxLength:sizeof(keyPtr) encoding:NSUTF8StringEncoding];

// 密文长度 <= 明文长度 + BlockSize
size_t encryptSize = dataLength + kCCBlockSizeAES128;
void *encryptedBytes = malloc(encryptSize);
size_t actualOutSize = 0;

NSData *initVector = [kInitVector dataUsingEncoding:NSUTF8StringEncoding];

/*
第三个参数:先查看下枚举说明,可以发现里面只有两个枚举变量,并在kCCOptionECBMode的旁边,写着Default is CBC.
kCCOptionPKCS7Padding:表示函数运用CBC加密模式,并且使用PKCS7Padding的填充模式进行加密
kCCOptionPKCS7Padding | kCCOptionECBMode:就表示函数运用ECB加密模式,并且使用PKCS7Padding的填充模式进行加密
如果要设置NoPadding,可以填入0x0000
*/
CCCryptorStatus cryptStatus = CCCrypt(kCCEncrypt, //加密/解密
kCCAlgorithmAES, //选用的加密算法
kCCOptionPKCS7Padding, //设置工作模式+填充
keyPtr, //key
kKeySize, // key length
initVector.bytes, // 初始向量IV的长度,如果不需要IV,设置为nil(不可以为@"")
contentData.bytes,
dataLength,
encryptedBytes,
encryptSize,
&actualOutSize);

if (cryptStatus == kCCSuccess) {
// 对加密后的数据进行 base64 编码
return [[NSData dataWithBytesNoCopy:encryptedBytes length:actualOutSize] base64EncodedStringWithOptions:NSDataBase64EncodingEndLineWithLineFeed];
}
free(encryptedBytes);
return nil;
}

4.3 Java实现

4.3.1 概述

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
//同理先在类中定义一个初始向量,需要与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);
}

关于Java使用大于128 bits的key:使用JCE。注意:JCE 中使用大于128 bits的key时,(测试参数:”AES/ECB/PKCS5Padding”,其他没试) 代码不用做调整,内部自动适用所有秘钥长度

Java Cryptography Extension(JCE)是一组包,它们提供用于加密、密钥生成和协商以及 Message Authentication Code(MAC)算法的框架和实现。它提供对对称、不对称、块和流密码的加密支持,它还支持安全流和密封的对象。它不对外出口,用它开发完成封装后将无法调用。

在早期JDK版本中,由于受美国的密码出口条例约束,Java中涉及加解密功能的API被限制出口,所以Java中安全组件被分成了两部分: 不含加密功能的JCA(Java Cryptography Architecture )和含加密功能的JCE(Java Cryptography Extension)。

在JDK1.1-1.3版本期间,JCE属于扩展包,仅供美国和加拿大的用户下载,到Oracle官网下载对应Java版本的 JCE ,解压后放到 JAVA_HOME/jre/lib/security/ ,然后修改 iOS 端的 kKeySize 和两端对应的 key 即可。

JDK1.4+版本后,随JDK核心包一起分发。

4.3.2 JCE加解密概述

JCE的API都在 javax.crypto 包下,核心功能包括:加解密、密钥生成(对称)、MAC生成、密钥协商。

1. Cipher

加解密功能由Cipher组件提供,其也是JCE中最核心的组件。 Cipher的几个知识点:

  1. Cipher在使用时需以参数方式指定transformation
  2. transformation的格式为algorithm/mode/padding(算法/模式/填充),其中algorithm(算法)为必输项,如: DES/CBC/PKCS5Padding
  3. 缺省的mode为ECB,缺省的padding为PKCS5Padding
  4. 在block算法与流加密模式组合时, 需在mode后面指定每次处理的bit数, 如DES/CFB8/NoPadding, 如未指定则使用缺省值, SunJCE缺省值为64bits
  5. Cipher有4种操作模式: ENCRYPT_MODE(加密), DECRYPT_MODE(解密), WRAP_MODE(导出Key), UNWRAP_MODE(导入Key),初始化时需指定某种操作模式(都是静态参数)。
2. 对称加密的算法与密钥长度选择
算法名称 密钥长 块长 速度 说明
DES 56 64 不安全, 不要使用
3DES 112/168 64 很慢 中等安全, 适合加密较小的数据
AES 128, 192, 256 128 安全
Blowfish (4至56)*8 64 应该安全, 在安全界尚未被充分分析、论证
RC4 40-1024 64 很快 安全性不明确

一般情况下,不要选择DES算法,推荐使用AES算法。一般认为128bits的密钥已足够安全,如果可以请选择256bits的密钥。

<1>密钥长度是在生成密钥时指定的。如:

1
2
3
KeyGenerator generator = KeyGenerator.getInstance("AES/CBC/PKCS5PADDING");
generator.init(256);
SecretKey key = generator.generateKey();

<2>生成长度超128bits的密钥,需单独从Oracle官网下载对应JDK版本的Java Cryptography Extension (JCE) Unlimited Strength Jurisdiction Policy Files文件《详见: java加密——Jasypt开源工具包》,例如JDK7对应的jurisdiction policy files。

3. 简单示例代码
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
/**
* 根据密钥{@link #getKey()}对指定的明文plainText进行加密.
*
* @param plainText 明文
* @return 加密后的密文.
*/
public static final String encrypt(String plainText) {
Key secretKey = getKey();
try {
Cipher cipher = Cipher.getInstance("DES/ECB/PKCS5Padding");
cipher.init(Cipher.ENCRYPT_MODE, secretKey);
byte[] p = plainText.getBytes("UTF-8");
byte[] result = cipher.doFinal(p);
BASE64Encoder encoder = new BASE64Encoder();
String encoded = encoder.encode(result);
return encoded;
} catch (Exception e) {
throw new RuntimeException(e);
}
}

/**
* 根据密钥{@link #getKey()}对指定的密文cipherText进行解密.
*
* @param cipherText 密文
* @return 解密后的明文.
*/
public static final String decrypt(String cipherText) {
Key secretKey = getKey();
try {
Cipher cipher = Cipher.getInstance("DES/ECB/PKCS5Padding");
cipher.init(Cipher.DECRYPT_MODE, secretKey);
BASE64Decoder decoder = new BASE64Decoder();
byte[] c = decoder.decodeBuffer(cipherText);
byte[] result = cipher.doFinal(c);
String plainText = new String(result, "UTF-8");
return plainText;
} catch (Exception e) {
throw new RuntimeException(e);
}
}

Author:Tenloy

原文链接:https://tenloy.github.io/2020/12/22/aes.html

发表日期:2020.12.22 , 7:52 AM

更新日期:2024.04.07 , 8:02 PM

版权声明:本文采用Crative Commons 4.0 许可协议进行许可

CATALOG
  1. 一、数据加密简述
    1. 1.1 密码系统的一般模型
  2. 二、对称密钥加密(单钥加密)
    1. 2.1 对称加密的优缺点
  3. 三、AES(Rijndael)
  4. 四、iOS中的AES
    1. 4.1 四个参数
      1. 4.1.1 密钥长度
      2. 4.1.2 加密模式
      3. 4.1.3 填充方式
      4. 4.1.4 初始向量
    2. 4.2 iOS实现
    3. 4.3 Java实现
      1. 4.3.1 概述
      2. 4.3.2 JCE加解密概述
        1. 1. Cipher
        2. 2. 对称加密的算法与密钥长度选择
        3. 3. 简单示例代码