AES加密解密介绍
加密算法
在密码学中,加密算法分为单向加密和双向加密。
- 单向加密包括MD5、SHA等摘要算法,它们是不需要密钥、不可逆的。
- 双向加密包括对称加密和非对称加密。双向加密是需要密钥、可逆的,存在密文的密钥。
- 对称加密是指加密和解密使用相同的密钥,包括AES加密、DES加密等。
- 非对称加密是指加密和解密使用不同的密钥,包括RSA加密等。
一、AES加密模式
- ECB(Electronic Codebook,电子密码本)模式
将每个分组独立加密,适用于对称性较弱的数据。
ECB是最简单的AES加密模式之一。它使用相同的密钥对数据进行多次加密,每次加密都是对整个数据块进行的。由于每次加密都使用相同的密钥,因此对于单个数据块而言,ECB模式是一种非常安全的加密方式。然而,当面对大量数据时,ECB模式的加密速度会非常慢。此外,如果攻击者能够解密先前的数据块,那么他们就能够破解后续的数据块。
- CBC(Cipher Block Chaining,串行密钥传输)模式
前一个分组的密文与当前分组的明文进行异或操作后再加密,增加了分组之间的关联性。
CBC是一种比ECB更加安全的加密模式。在CBC模式中,每个数据块都被分成两个部分:明文和密钥。第一个数据块被称为“前向块”,第二个数据块被称为“后向块”。前向块的密文是由密钥和前向块一起计算得到的,而后向块的密文则是由密钥和后向块一起计算得到的。这种模式可以确保即使攻击者能够获得密钥的一部分,也无法解密整个数据块。但是,CBC模式的速度较慢,因为需要处理大量的中间结果。
- CTR(Counter,计数器)模式
通过使用计数器生成密钥流,将密钥流与明文进行异或操作得到密文。
CTR模式与CBC模式非常相似,但是它使用了不同的密钥分配方法。在CTR模式中,每个数据块都由一个随机数生成器产生,该随机数生成器用于确定数据的索引位置。这使得CTR模式非常适合于流式数据传输,例如视频和音频流媒体。CTR模式也非常快,因为它只需要处理每个数据块的中间结果,而不需要像CBC模式那样处理整个数据块。
- CFB(可逆计数器)模式
CFB模式类似于CTR模式,但是它使用的是可变长度的密钥。在CFB模式中,每个数据块都有一个偏移量,这个偏移量是根据密钥计算出来的。这使得CFB模式非常适合于多通道应用程序,例如网络路由表。然而,由于密钥的长度可变,所以CFB模式不如CTR模式快速。
二、填充算法
填充算法用于填充输入数据以匹配AES模式的要求。下面是一些常用的填充算法及其优缺点:
-
NoPadding
不做任何填充,但是要求明文必须是16字节的整数倍。
NoPadding是一种不使用填充算法的加密模式。不做任何填充,但是要求明文必须是16字节的整数倍。这意味着输入数据将被直接编码为字节序列,而不会受到填充的影响。虽然NoPadding模式非常安全,但它会导致填充算法无法正确地处理数据。
-
ZerosPadding
ZerosPadding是一种简单的填充算法,它将输入数据的最后一个字节替换为0。这种填充方式适用于所有AES加密模式,包括ECB、CBC和CTR模式。缺点是填充后的字节数量可能超过原始输入数据的大小。 -
PKCS5padding(默认)
如果明文块少于16个字节(128bit),在明文块末尾补足相应数量的字符,且每个字节的值等于缺少的字符数。
比如明文:{1,2,3,4,5,a,b,c,d,e},缺少6个字节,则补全为{1,2,3,4,5,a,b,c,d,e,6,6,6,6,6,6}。
PKCS5padding是一种常见的填充算法,用于填充数据以符合AES模式的要求。这种填充方式使用一个固定长度的填充字节序列,以确保输入数据被完全填充。PKCS#5 padding的优点是它可以适应各种输入数据的大小,但缺点是填充过程可能很慢。
-
PKCS7padding
PKCS7padding也是一种填充算法,它使用一个固定长度的填充字节序列来填充数据。这种填充方式的优点是可以适应各种输入数据的大小,并且填充过程相对较快。但是,它与PKCS#5 padding一样,可能需要较长的填充时间。 -
ISO10126Padding
如果明文块少于16个字节(128bit),在明文块末尾补足相应数量的字节,最后一个字符值等于缺少的字符数,其他字符填充随机数。
比如明文:{1,2,3,4,5,a,b,c,d,e},缺少6个字节,则可能补全为{1,2,3,4,5,a,b,c,d,e,5,c,3,G,$,6}。
三、AES的加密过程(AES处理单位:字节)
AES的加解密过程和DES一样,都是通过分组加密、分组解密。所谓分组加密,就是将待加解密的内容按照128位进行分组,可以将密钥按照128位、192位、256位进行分组,分别将分组后的明文与相应分组后的密钥进行加解密。
加密: 明文与密钥分组后,对每组:明文组与密钥组处理 -> 轮密钥加 -> 10轮加密 -> 密文组
解密: 对每组:密文组 -> 轮密钥加 -> 10轮解密 -> 明文组
明文分组: 每组长度相等,都是128位(16字节);
密钥分组: 有128位、192位、256位,推荐加密轮数分别为 10、12、14
密钥组处理: 以密钥分组每组128位为例(则推荐加密轮数为10,前9次执行操作一样,第十次有所不同) 类似地,128位密钥也是用字节为单位的矩阵表示,通过密钥编排函数,形成具有44个元素的序列W[0],W[1], … ,W[43](每个元素4个字节);其中,W[0],W[1],W[2],W[3]为原始密钥,其余40个元素分为10组,每组4个元素(4*4=16字节),分别用于10轮加密。
AES加密算法涉及4种操作: 字节替代
(SubBytes)、行移位
(ShiftRows)、列混淆
(MixColumns)和轮密钥加
(AddRoundKey)。
下图给出了AES加解密的流程:
- AddRoundKey (轮密钥加)— 矩阵中的每一个字节都与该次轮密钥(round key)做XOR运算;每个子密钥由密钥生成方案产生。
- SubBytes(字节替代) — 通过非线性的替换函数,用查找表的方式把每个字节替换成对应的字节。
- ShiftRows(行移位) — 将矩阵中的每个横列进行循环式移位。
- MixColumns (列混淆)— 为了充分混合矩阵中各个直行的操作。这个步骤使用线性转换来混合每列的四个字节。
四、注意
现加密结果不一致的原因可能是多种多样的。以下是一些可能导致结果不一致的原因:
- 加密工具使用的是不同的加密算法或加密模式,例如CBC、CFB、OFB等等。
- 加密工具使用的是不同的填充方案,例如PKCS#5、PKCS#7、ISO 10126等等。
- 加密工具对原始数据进行了预处理或后处理,例如添加了Salt或IV。
- 加密工具在进行加密时使用了不同的编码方式,例如Base64、Hex、UTF-8等等。
五、JAVA示例代码
注意:AES加密包含Base64加密
加密: AES加密 -> Base64加密 -> 密文
解密: Base64解密 -> AES解密 -> 明文
package com.study.util;
import org.apache.commons.lang3.RandomStringUtils;
import org.apache.commons.lang3.StringUtils;
import org.springframework.util.Base64Utils;
import javax.crypto.Cipher;
import javax.crypto.KeyGenerator;
import javax.crypto.spec.IvParameterSpec;
import javax.crypto.spec.SecretKeySpec;
import java.math.BigInteger;
import java.security.NoSuchAlgorithmException;
import java.security.SecureRandom;
import java.util.Base64;
import java.util.Random;
public class AESUtil {
private static final String AES_KEY = "AnjiPLUSAjReport";
/**
* 编码
*/
private static final String ENCODING = "UTF-8";
/**
* 算法定义
*/
private static final String AES_ALGORITHM = "AES";
/**
* ECB填充模式
*/
private static final String CIPHER_ECB_PADDING = "AES/ECB/PKCS5Padding";
/**
* 使用CBC模式,需要加入额外的Iv参数。
*/
private static final String CIPHER_CBC_PADDING = "AES/CBC/PKCS5Padding";
/**
* 偏移量(CBC中使用,增强加密算法强度)
*/
private static final String IV_SEED = "1191168741434234";
/**
* 密钥生成工具,生成key
*
* @return
*/
public static String generateKey() {
KeyGenerator kgen = null;
try {
kgen = KeyGenerator.getInstance(AES_ALGORITHM);
} catch (NoSuchAlgorithmException e) {
e.printStackTrace();
}
kgen.init(128);
return Base64.getEncoder().encodeToString(kgen.generateKey().getEncoded());
}
/**
* 获取长度为 6 的随机字母+数字
*
* @return 随机数字
*/
public static String getRandomNumber(int length) {
String symbols = "0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ"; // 数字和26个字母组成
Random random = new SecureRandom();
char[] nonceChars = new char[length]; //指定长度为6位/自己可以要求设置
for (int index = 0; index < nonceChars.length; ++index) {
nonceChars[index] = symbols.charAt(random.nextInt(symbols.length()));
}
return new String(nonceChars);
}
/**
* 获取随机key
*
* @return
*/
public static String getKey() {
return AES_KEY;
}
/**
* 将byte[]转为各种进制的字符串
*
* @param bytes byte[]
* @param radix 可以转换进制的范围,从Character.MIN_RADIX到Character.MAX_RADIX,超出范围后变为10进制
* @return 转换后的字符串
*/
public static String binary(byte[] bytes, int radix) {
return new BigInteger(1, bytes).toString(radix);// 这里的1代表正数
}
/**
* 填充key
* key的字节长度只能是16位。
*
* @return 填充后的key
*/
public static byte[] fillKey(String message, int length) {
byte[] arrBTmp = message.getBytes();
byte[] arrB = new byte[length];
for (int i = 0; i < arrBTmp.length && i < arrB.length; ++i) {
arrB[i] = arrBTmp[i];
}
return arrB;
}
/**
* AES加密(AES加密 -> Base64加密 -> 密文)
*
* @param content 待加密的内容
* @param encryptKey 加密密钥
* @return 加密后的base 64 code
* @throws Exception
*/
public static String aesEncrypt(String content, String encryptKey) throws Exception {
if (StringUtils.isBlank(encryptKey) || StringUtils.isBlank(content)) {
return content;
}
Cipher cipher = Cipher.getInstance(CIPHER_ECB_PADDING);
cipher.init(Cipher.ENCRYPT_MODE, new SecretKeySpec(fillKey(encryptKey, 16), AES_ALGORITHM));
byte[] encryptBytes = cipher.doFinal(content.getBytes(ENCODING));
//return Base64.encodeBase64String(encryptBytes);
return Base64.getEncoder().encodeToString(encryptBytes);
}
/**
* AES解密(Base64解密 -> AES解密 -> 明文)
*
* @param encryptStr 待解密的base 64 code
* @param decryptKey 解密密钥
* @return 解密后的string
* @throws Exception
*/
public static String aesDecrypt(String encryptStr, String decryptKey) throws Exception {
if (StringUtils.isBlank(decryptKey) || StringUtils.isEmpty(encryptStr)) {
return encryptStr;
}
Cipher cipher = Cipher.getInstance(CIPHER_ECB_PADDING);
cipher.init(Cipher.DECRYPT_MODE, new SecretKeySpec(fillKey(decryptKey, 16), AES_ALGORITHM));
byte[] decryptBytes = cipher.doFinal(Base64.getDecoder().decode(encryptStr));
return new String(decryptBytes);
}
/**
* AES_CBC加密
*
* @param content 待加密内容
* @param aesKey 密码
* @return
*/
public static String cbcEncrypt(String content, String aesKey) {
if (StringUtils.isBlank(content) || StringUtils.isBlank(aesKey)) {
return null;
}
try {
//对密码进行编码
byte[] bytes = fillKey(aesKey, 16);
//设置加密算法,生成秘钥
SecretKeySpec skeySpec = new SecretKeySpec(bytes, AES_ALGORITHM);
// "算法/模式/补码方式"
Cipher cipher = Cipher.getInstance(CIPHER_CBC_PADDING);
//偏移
IvParameterSpec iv = new IvParameterSpec(IV_SEED.getBytes(ENCODING));
//选择加密
cipher.init(Cipher.ENCRYPT_MODE, skeySpec, iv);
//根据待加密内容生成字节数组
byte[] encrypted = cipher.doFinal(content.getBytes(ENCODING));
//返回base64字符串
return Base64Utils.encodeToString(encrypted);
} catch (Exception e) {
throw new RuntimeException(e);
}
}
/**
* AES_CBC解密
*
* @param content 待解密内容
* @param aesKey 密码
* @return
*/
public static String cbcDecrypt(String content, String aesKey) {
if (StringUtils.isBlank(content) || StringUtils.isBlank(aesKey)) {
return null;
}
try {
//对密码进行编码
byte[] bytes = fillKey(aesKey, 16);
//设置解密算法,生成秘钥
SecretKeySpec skeySpec = new SecretKeySpec(bytes, AES_ALGORITHM);
//偏移
IvParameterSpec iv = new IvParameterSpec(IV_SEED.getBytes(ENCODING));
// "算法/模式/补码方式"
Cipher cipher = Cipher.getInstance(CIPHER_CBC_PADDING);
//选择解密
cipher.init(Cipher.DECRYPT_MODE, skeySpec, iv);
//先进行Base64解码
byte[] decodeBase64 = Base64Utils.decodeFromString(content);
//根据待解密内容进行解密
byte[] decrypted = cipher.doFinal(decodeBase64);
//将字节数组转成字符串
return new String(decrypted, ENCODING);
} catch (Exception e) {
throw new RuntimeException(e);
}
}
/**
* 测试
*/
public static void main(String[] args) throws Exception {
// String randomString = getKey();
System.out.println("密钥生成key:" + generateKey());
System.out.println("随机key:" + getRandomNumber(16));
System.out.println("--------AES_ECB加密解密---------");
String randomString = "y6KQFlk7OD/H4bms1fOn8g==";
String content = "[{AAC002=350xxx199812110111, AccountStatus=A1, AverageAmountPaidInTheLast3Years=B2}]";
System.out.println("加密前:" + content);
System.out.println("加密密钥和解密密钥:" + randomString);
String encrypt = aesEncrypt(content, randomString);
System.out.println("加密后:" + encrypt);
// System.out.println("加密后:" + aesEncrypt(null, randomString));
// System.out.println("加密后:" + aesEncrypt("", randomString));
String decrypt = aesDecrypt(encrypt, randomString);
System.out.println("解密后:" + decrypt);
System.out.println();
// AES支持三种长度的密钥:128位、192位、256位。
// 代码中这种就是128位的加密密钥,16字节 * 8位/字节 = 128位。
String random = RandomStringUtils.random(16, "abcdefghijklmnopqrstuvwxyz1234567890");
System.out.println("--------AES_CBC加密解密---------");
String cbcContent= "测试AES加密12456";
System.out.println("加密前:" + cbcContent);
System.out.println("加密密钥和解密密钥:" + random);
String cbcEncrypt = cbcEncrypt(cbcContent, random);
System.out.println("加密后:" + cbcEncrypt);
String cbcDecrypt = cbcDecrypt(cbcEncrypt, random);
System.out.println("解密后:" + cbcDecrypt);
System.out.println();
}
}
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· Manus重磅发布:全球首款通用AI代理技术深度解析与实战指南
· 被坑几百块钱后,我竟然真的恢复了删除的微信聊天记录!
· 没有Manus邀请码?试试免邀请码的MGX或者开源的OpenManus吧
· 园子的第一款AI主题卫衣上架——"HELLO! HOW CAN I ASSIST YOU TODAY
· 【自荐】一款简洁、开源的在线白板工具 Drawnix