有项目对AES对称加密有需求,使用Java使用Aes过程中有些坑是要注意一下的。
跨平台问题
AES是对称加密,利用秘钥,可以对内容加密和解密,于是我在PC开发电脑上尝试以下代码:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22
| public static byte[] encrypt(String content, String password) { KeyGenerator kgen = KeyGenerator.getInstance("AES"); kgen.init(128, new SecureRandom(password.getBytes())); SecretKey secretKey = kgen.generateKey(); byte[] enCodeFormat = secretKey.getEncoded(); SecretKeySpec key = new SecretKeySpec(enCodeFormat, "AES"); Cipher cipher = Cipher.getInstance("AES"); byte[] byteContent = content.getBytes("utf-8"); cipher.init(Cipher.ENCRYPT_MODE, key); return cipher.doFinal(byteContent); }
public static byte[] decrypt(byte[] content, String password) { KeyGenerator kgen = KeyGenerator.getInstance("AES"); kgen.init(128, new SecureRandom(password.getBytes())); SecretKey secretKey = kgen.generateKey(); byte[] enCodeFormat = secretKey.getEncoded(); SecretKeySpec key = new SecretKeySpec(enCodeFormat, "AES"); Cipher cipher = Cipher.getInstance("AES"); cipher.init(Cipher.DECRYPT_MODE, key); return cipher.doFinal(content); }
|
这段代码在本机加密和解密都没问题,是正常的,但如果跨机器执行很大概率会解密失败,这是由于不同机器很大概率是安装了不同平台厂商的JDK和版本(特别是Android 平台)。
这是由于:上面的代码只是指定了”AES”加密,而”AES”的默认实现,不同的JDK实现不一样。
** 先了解一下AES加密:**
- 秘钥长度:128位/192位/256位
- 加密模式有:ECB/CBC/CTR/OFB/CFB
- 补码填充模式有:pkcs5padding/pkcs7padding/zeropadding/nopadding/iso10126/ansix923
所以上面代码到底用的是哪一种?是由JDK决定的。 推荐的代码写法是:指定所有配置项,如”AES/ECB/PKCS5Padding”
接下来了解一下AES对称加密…..
秘钥长度
AES支持三种长度的密钥: 128位,192位,256位。
AES256安全性最高,AES128性能最高。
加密模式
AES加密模式有两个是最常用的,ECB 和 CBC,本文就说一下这两个。
- ECB 电码本模式(Electronic Codebook Book): 简单快速利于计算的加密模式,但不太安全,加密时只需要密码。
- CBC 密码分组链接模式(Cipher Block Chaining): 安全性很高的加密模式,是SSL、IPSec的标准,加密时需要密码和初始化向量IV。
补码填充
- NoPadding: 不做任何填充,但是要求明文必须是16字节的整数倍,所以要自己补码。
- 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 }
- PKCS7Padding原理与PKCS5Padding相似,区别是PKCS5Padding的blocksize为8字节,而PKCS7Padding的blocksize可以为1到255字节
- 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}
正确示例
ECB示例
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 48
| private static final int KEY_LEN = 16; private static final String CIP_TYPE_PKCS5_PADDING = "AES/ECB/PKCS5Padding";
public static void main(String[] args) throws Exception {
String passwd = "1234567890123456"; String src = "hello world!hello world!hello world!hello world!hello world!hello world!";
byte[] enByte = encrypt(src, passwd,CIP_TYPE_PKCS5_PADDING); String enString = Base64.getEncoder().encodeToString(enByte); System.out.println("加密后的字串是:" + enString); String deString = decrypt(enByte, passwd,CIP_TYPE_PKCS5_PADDING); System.out.println("解密后的字串是:" + deString); } private static byte[] encrypt(String src, String passwd,String cipherType) throws Exception { Cipher cipher = Cipher.getInstance(cipherType); cipher.init(Cipher.ENCRYPT_MODE, generateSecretKeySpec(passwd)); return cipher.doFinal(src.getBytes(StandardCharsets.UTF_8)); }
private static String decrypt(byte[] encrypt, String passwd,String cipherType) { try {
Cipher cipher = Cipher.getInstance(cipherType); cipher.init(Cipher.DECRYPT_MODE, generateSecretKeySpec(passwd)); byte[] original = cipher.doFinal(encrypt); return new String(original,StandardCharsets.UTF_8); } catch (Exception ex) { ex.printStackTrace(); } return null; }
private static SecretKeySpec generateSecretKeySpec(String passwd) throws UnsupportedEncodingException {
if (passwd == null ||passwd.length() != KEY_LEN) { System.out.print("passwd长度不是" + KEY_LEN); return null; } return new SecretKeySpec(passwd.getBytes(StandardCharsets.UTF_8), "AES"); }
|
CBC示例
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
| private static final int KEY_LEN = 16; private static final String CIP_TYPE_CBC_PKCS5_PADDING = "AES/CBC/PKCS5Padding";
public static void main(String[] args) throws Exception {
String passwd = "1234567890123456"; String iv = "abcdabcdabcdabcd"; String src = "hello world!hello world!hello world!hello world!hello world!hello world!";
byte[] enByte = encrypt(src, passwd,iv,CIP_TYPE_CBC_PKCS5_PADDING); String enString = Base64.getEncoder().encodeToString(enByte); System.out.println("加密后的字串是:" + enString); String deString = decrypt(enByte, passwd,iv,CIP_TYPE_CBC_PKCS5_PADDING); System.out.println("解密后的字串是:" + deString); }
private static byte[] encrypt(String src, String passwd, String iv, String cipherType) throws Exception { Cipher c = Cipher.getInstance(cipherType); c.init(Cipher.ENCRYPT_MODE, generateSecretKeySpec(passwd), new IvParameterSpec(iv.getBytes(StandardCharsets.UTF_8))); return c.doFinal(src.getBytes(StandardCharsets.UTF_8)); }
private static String decrypt(byte[] encrypt,String passwd, String iv, String cipherType) { try { Cipher c = Cipher.getInstance(cipherType); c.init(Cipher.DECRYPT_MODE, generateSecretKeySpec(passwd), new IvParameterSpec(iv.getBytes(StandardCharsets.UTF_8))); return new String(c.doFinal(encrypt),StandardCharsets.UTF_8); } catch (Exception e) { e.printStackTrace(); } return null; }
private static SecretKeySpec generateSecretKeySpec(String passwd)throws UnsupportedEncodingException{
if (passwd == null ||passwd.length() != KEY_LEN) { System.out.print("passwd长度不是" + KEY_LEN); return null; } return new SecretKeySpec(passwd.getBytes(StandardCharsets.UTF_8), "AES"); }
|
** NoPadding 示例 **
NoPadding
要手动补码,也就是当明文的长度不是 cipher.getBlockSize()
的倍数,就手动补齐。
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 48 49 50 51 52
| import javax.crypto.Cipher; import javax.crypto.spec.IvParameterSpec; import javax.crypto.spec.SecretKeySpec; import java.nio.charset.StandardCharsets; import java.util.Base64;
public class TestAesCbcNoPadding { public static byte[] key = "1234567890123456".getBytes(StandardCharsets.UTF_8);
public static void main(String[] args) throws Exception{
byte[] textBytes = "Input length not multiple of 16 bytes".getBytes(StandardCharsets.UTF_8);
byte[] enByte = encrypt(textBytes); String enString = Base64.getEncoder().encodeToString(enByte).trim(); System.out.println("加密后的字串是:" + enString); String deString = decrypt(enByte); System.out.println("解密后的字串是:" + deString); } public static String decrypt(byte[] enByte) throws Exception{ SecretKeySpec secretKeySpec = new SecretKeySpec(key, "AES"); Cipher cipher = Cipher.getInstance("AES/CBC/NoPadding");
byte[] iv = new byte[16]; cipher.init(Cipher.DECRYPT_MODE, secretKeySpec,new IvParameterSpec(iv)); byte[] plaintextBytes = cipher.doFinal(enByte);
return new String(plaintextBytes,StandardCharsets.UTF_8).trim(); }
public static byte[] encrypt(byte[] textBytes) throws Exception{ SecretKeySpec secretKeySpec = new SecretKeySpec(key, "AES"); Cipher cipher = Cipher.getInstance("AES/CBC/NoPadding");
int blockSize = cipher.getBlockSize(); int length = textBytes.length; if (length % blockSize != 0) { length += (blockSize - (length % blockSize)); } System.out.println("length = " + length); byte[] plaintextNoPadding = new byte[length]; System.arraycopy(textBytes, 0, plaintextNoPadding, 0, textBytes.length);
byte[] iv = new byte[16]; cipher.init(Cipher.ENCRYPT_MODE, secretKeySpec,new IvParameterSpec(iv)); return cipher.doFinal(plaintextNoPadding); } }
|
注意:手动补码的数据0x00
在 JDK
库会自动去除,但如果用的是C语言库解密后可能会有补码的 0x00
数据是无用数据。