背景
上一期我们谈到网络安全常用的加密算法分类,
本次重点分享我们开发中常见的对称加密-AES算法
算法AES简介–秘钥/填充/模式
AES(Advanced Encryption Standard)是一种对称加密算法(也叫共享密钥),对称加密算法的意思是加密和解密都是用同一个密钥(密钥和秘钥是同义词)同时也是一种分组加密算法,通常来说,对称加密算法效率要优于非对称加密算法,它用来代替DES(Data Encryption Standard,56位密钥)。那么为什么原来的DES会被取代呢,原因就在于其使用56位密钥,比较容易被破解。而AES可以使用128、192、和256位密钥,并且用128位分组加密和解密数据,相对来说安全很多。即使使用目前世界上运算速度最快的计算机,穷尽128位密钥也要花上几十亿年的时间
AES有三个关键点:密钥、填充、模式。
-
秘钥
密钥分为128位(16字节)、192位(24字节)、256位(32字节),位数越多,解密和加密的运算量越大,相应的越安全,可以折中使用192位兼顾效率和安全。
-
填充
AES算法在对明文加密的时候,并不是把整个明文一股脑的加密成一整段密文,而是把明文拆分成一个个独立的明文块,每一个明文块长度128bit(16字节)。然后对明文块分别加密;这些明文块经过AES加密器复杂处理,生成一个个独立的密文块,这些密文块拼接在一起,就是最终的AES加密的结果,当然加密时与解密时的填充必须一致,否则无法解密。
1、NoPadding—不填充,但要求明文必须是128位的整数倍;如果使用NoPadding,程序要对明文预处理进行填充,以达到16字节整数倍。
2、PKCS5Padding(默认)—如果明文块少于128位,则补足相应数量的字符,且字符的值等于缺少的字节数。比如明文:{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}。
3、ISO10126Padding—如果明文块少于16字节,则补足相应数量的字符,且最后一个字符的值等于缺少的字节数,其他字符填充随机数;比如明文:{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加解密只管Block内的事情,而加密模式Mode管的是Block之间的关系。加密模式解决的是Block之间的混淆扩散问题,严格来讲它不属于AES的一部分,任何基于块加密的算法都可以套用这些模式。AES的工作提供了五种不同的工作模式:(ECB、CBC、CFB、OFB)
1、ECB模式
这个模式是默认的,就只是根据密钥的位数,将数据分成不同的块进行加密,加密完成后,再将加密后的数据拼接起来。如下图所示
优点::简单、有利于并行计算、误差不会被传送;
缺点:不能隐藏明文的模式、如果明文块相同,则生成的密文块也相同,这样会导致安全性降低。
2、CBC模式
CBC的模式引入了一个初始向量概念,该向量必须是一个与密钥长度相等的数据,
在第一次加密前,会使用初始化向量与第一块数据做异或运算,生成的新数据再进行加密,
加密第二块之前,会拿第一块的密文数据与第二块明文进行异或运算后再进行加密,
以此类推,解密时也是在解密后,进行异或运算,生成最终的明文
优点:不容易主动攻击、安全性好于ECB、适合传输长度长的报文,是SSL、IPSec的标准、每次加密盐值不同,加强了安全性
缺点:不利于并行计算、误差传递、需要初始化向量IV
3、CFB模式:
优点:隐藏了明文模式、分组密码转化为流模式、可以及时加密传送小于分组的数据;
缺点:不利于并行计算;、误差传送:一个明文单元损坏影响多个单元、唯一的IV;
算法AES加密过程
上面我们也提到过AES是分组加密的,加密过程是在一个4×4的字节矩阵上运作的,矩阵初值是一个明文块,加密时根据密钥的长度,加密的轮数也会有变化(每一个明文块都要经过n轮加密),比如当密钥长度为128位时,加密轮数为10轮。
每一轮(最后一轮除外)加密均包含4个步骤:字节替代、行移位、列混淆、轮密钥加。最后一轮加密循环省略列混淆这一步骤.
AES加解密实现
- 使用原生秘钥key,统一生成加密Key对象
/**
* 生成密钥对象
* 1、根据指定的 RNG 算法, 创建安全随机数生成器,提供秘钥随机数来源
* 2、设置 密钥key的字节数组 作为安全随机数生成器的种子
* 加密没关系,SecureRandom是生成安全随机数序列, 只要种子相同,序列就一样
* 3、创建 AES算法生成器, 初始化算法生成器
* 4、生成 AES密钥对象,
* 5、也可以直接创建密钥对象: new SecretKeySpec(sourceKey.getBytes(StandardCharsets.UTF_8),"AES")
* 6、使用5操作方式sourceKey必须为16*2个的byte
* 7、加密算法的秘钥长度一般都是有限的。jca 附录A; 对应算法允许的密钥长度
* Algorithm Maximum Keysize
* DES 56
* DESede 112或168
* RC2 128
* RC4 128
* RC5 128
* RSA 128
* others 128
*/
private static SecretKeySpec generateKey(String sourceKeyStr, String encryptType) throws Exception {
SecureRandom random = new SecureRandom(sourceKeyStr.getBytes(StandardCharsets.UTF_8));
KeyGenerator keyGen = KeyGenerator.getInstance(encryptType);
keyGen.init(256, random);
SecretKey secretKey = keyGen.generateKey();
return new SecretKeySpec(secretKey.getEncoded(), encryptType);
}
- 使用动态盐值对原生内容加密
- 加密时必须要使用安全随机数SecureRadom生成强随机数,保证每次生成的盐值salt不一样
- 在输出时需要将盐值与结果做合并以便做解密
public static String encryptCBCModeDynamicSalt(String content) throws Exception {
SecureRandom random = SecureRandom.getInstanceStrong();
byte[] seedByte = new byte[16];
random.nextBytes(seedByte);
SecretKey secretKey = generateKey(SOURCE_KEY, "AES");
Cipher encryptCipher = Cipher.getInstance(CBC_PKCS5_PAD);
encryptCipher.init(Cipher.ENCRYPT_MODE, secretKey, new IvParameterSpec(seedByte));
byte[] encryptBytes = encryptCipher.doFinal(content.getBytes(StandardCharsets.UTF_8));
String encryptResult = String.format("%s*%s", byteArrayToHex(seedByte), byteArrayToHex(encryptBytes));
System.out.println("encryptWithCBCPkcVi current content is =" + content);
System.out.println("encryptWithCBCPkcVi encrypt result is =" + encryptResult);
return encryptResult;
}
- 使用动态盐值做解密
- 原生的加密串中一定要带着当前加密所需要的盐值,不然无法解密
/**
* encryptWithCBCPkcVi
* 使用安全随机数生成种子
*/
public static String decryptCBCModeDynamicSalt(String hexContent) throws Exception {
String[] hexArray = hexContent.split("\\*");
byte[] saltBytes = hex2ByteArray(hexArray[0]);
byte[] encryptedBytes = hex2ByteArray(hexArray[1]);
Cipher decryptCipher = Cipher.getInstance(CBC_PKCS5_PAD);
SecretKey secretKey = generateKey(SOURCE_KEY, "AES");
decryptCipher.init(Cipher.DECRYPT_MODE, secretKey, new IvParameterSpec(saltBytes));
byte[] decryptedBytes = decryptCipher.doFinal(encryptedBytes);
String decryptResult = new String(decryptedBytes, StandardCharsets.UTF_8);
System.out.println("decryptWithCBCPkcVi current encryptedData is =" + hexContent);
System.out.println("decryptWithCBCPkcVi decryptResult =" + decryptResult);
return decryptResult;
}
- 字节数组转换成Hex串,可以使用String.format
private static String byteArrayToHex(byte[] bytes) {
StringBuilder sb = new StringBuilder();
Stream.iterate(0, i -> i+1).limit(bytes.length)
.forEach(i -> sb.append(String.format("%02x", bytes[i])));
return sb.toString();
}
- 一个字符表示两个字节、一个字节使用两个Hex位置
- 因为一个十六进制字符是用半个字节长度实现,所以两位Hex会缩短成一个字节
- 十六进制Hex字符需要先转成十进制,然后才能左右移动
private static byte[] hex2ByteArray(String hexString) {
hexString = hexString.replaceAll(" ", "");
if (StringUtils.isEmpty(hexString) || hexString.length() % 2 != 0) {
return new byte[0];
}
int hexLen = hexString.length();
byte[] outBytes = new byte[hexLen / 2];
for (int i = 0; i < hexLen; i += 2) {
int high = Character.digit(hexString.charAt(i), 16) << 4;
int low = Character.digit(hexString.charAt(i + 1), 16);
outBytes[i/2] = (byte) ((high | low) & 0xff);
}
return outBytes;
}
- 测试结果
安全随机数SecureRadom特性
-
1、什么是安全的随机数
在安全应用场景,随机数应该使用安全的随机数。密码学意义上的安全随机数,要求必须保证其不可预测性。 -
2、怎么得到安全的随机数
可以直接使用真随机数产生器产生的随机数。或者使用真随机数产生器产生的随机数做种子,输入密码学安全的伪随机数产生器产生密码学安全随机数。
非物理真随机数产生器有:
Linux操作系统的/dev/random设备接口
Windows操作系统的CryptGenRandom接口
密码学安全的伪随机数产生器,包括JDK的java.security.SecureRandom -
3、SecureRandom.getInstanceStrong()
对随机性很强的场景才是用,依赖操作系统的随机操作,
比如键盘输入, 鼠标点击, 等等, 当系统扰动很小时, 产生的随机数不够, 导致读取/dev/random的进程会阻塞等待.
对性能有很大影响。推荐使用new SecureRandom()获取SecureRandom, linux下从/dev/urandom读取. 虽然是伪随机, 但大部分场景下都满足。如果使用SecureRandom.getInstanceStrong()这种方法初始化SecureRandom对象的话,会使用NativePRNGBlocking这个算法,完全依赖/dev/random,所以当这个文件随机数不够的时候,自然会导致卡顿了 -
4、解决getInstanceStrong性能问题
SecureRandom本身并不是伪随机算法的实现,而是使用了其他类提供的算法来获取伪随机数。
Linux下的NativePRNG,如果调用generateSeed()方法,这个方法会读取Linux系统的/dev/random文件,这个文件在JAVA_HOME/jre/lib/securiy/java.security里面有默认定义。而/dev/random文件是动态生成的,如果没有数据,就会阻塞。
如果简单的new一个SecureRandom对象的话,在不同的操作平台会获取到不同的算法,windows默认是SHA1PRNG,Linux的话是NativePRNG。
避免getInstanceStrong阻塞,可以使用-Djava.security.egd=file:/dev/./urandom (这个文件名多个u)强制使用/dev/urandom这个文件。
结束
本期探讨了AES重要算法和实现,下期再探讨RSA算法,晚安,再见!!!!