Initial bytes incorrect after Java AES/CBC decryption
下面的例子有什么问题?
问题是解密字符串的第一部分是无意义的。但是,其他的都很好,我知道…
1 Result: `£eB6O?geS??i are you? Have a nice day.
号
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 | @Test public void testEncrypt() { try { String s ="Hello there. How are you? Have a nice day."; // Generate key KeyGenerator kgen = KeyGenerator.getInstance("AES"); kgen.init(128); SecretKey aesKey = kgen.generateKey(); // Encrypt cipher Cipher encryptCipher = Cipher.getInstance("AES/CBC/PKCS5Padding"); encryptCipher.init(Cipher.ENCRYPT_MODE, aesKey); // Encrypt ByteArrayOutputStream outputStream = new ByteArrayOutputStream(); CipherOutputStream cipherOutputStream = new CipherOutputStream(outputStream, encryptCipher); cipherOutputStream.write(s.getBytes()); cipherOutputStream.flush(); cipherOutputStream.close(); byte[] encryptedBytes = outputStream.toByteArray(); // Decrypt cipher Cipher decryptCipher = Cipher.getInstance("AES/CBC/PKCS5Padding"); IvParameterSpec ivParameterSpec = new IvParameterSpec(aesKey.getEncoded()); decryptCipher.init(Cipher.DECRYPT_MODE, aesKey, ivParameterSpec); // Decrypt outputStream = new ByteArrayOutputStream(); ByteArrayInputStream inStream = new ByteArrayInputStream(encryptedBytes); CipherInputStream cipherInputStream = new CipherInputStream(inStream, decryptCipher); byte[] buf = new byte[1024]; int bytesRead; while ((bytesRead = cipherInputStream.read(buf)) >= 0) { outputStream.write(buf, 0, bytesRead); } System.out.println("Result:" + new String(outputStream.toByteArray())); } catch (Exception ex) { ex.printStackTrace(); } } |
很多人,包括我自己,在使这项工作面临许多问题,因为丢失了一些信息,例如,忘记转换成base64,初始化向量,字符集等,所以我想做一个完整的功能代码。
希望这对你们大家都有帮助:要编译,您需要额外的ApacheCommons编解码器jar,可从以下位置获得:http://commons.apache.org/proper/commons-codec/download_codec.cgi
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 53 | import javax.crypto.Cipher; import javax.crypto.spec.IvParameterSpec; import javax.crypto.spec.SecretKeySpec; import org.apache.commons.codec.binary.Base64; public class Encryptor { public static String encrypt(String key, String initVector, String value) { try { IvParameterSpec iv = new IvParameterSpec(initVector.getBytes("UTF-8")); SecretKeySpec skeySpec = new SecretKeySpec(key.getBytes("UTF-8"),"AES"); Cipher cipher = Cipher.getInstance("AES/CBC/PKCS5PADDING"); cipher.init(Cipher.ENCRYPT_MODE, skeySpec, iv); byte[] encrypted = cipher.doFinal(value.getBytes()); System.out.println("encrypted string:" + Base64.encodeBase64String(encrypted)); return Base64.encodeBase64String(encrypted); } catch (Exception ex) { ex.printStackTrace(); } return null; } public static String decrypt(String key, String initVector, String encrypted) { try { IvParameterSpec iv = new IvParameterSpec(initVector.getBytes("UTF-8")); SecretKeySpec skeySpec = new SecretKeySpec(key.getBytes("UTF-8"),"AES"); Cipher cipher = Cipher.getInstance("AES/CBC/PKCS5PADDING"); cipher.init(Cipher.DECRYPT_MODE, skeySpec, iv); byte[] original = cipher.doFinal(Base64.decodeBase64(encrypted)); return new String(original); } catch (Exception ex) { ex.printStackTrace(); } return null; } public static void main(String[] args) { String key ="Bar12345Bar12345"; // 128 bit key String initVector ="RandomInitVector"; // 16 bytes IV System.out.println(decrypt(key, initVector, encrypt(key, initVector,"Hello World"))); } } |
。
这里有一个没有
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 | import javax.crypto.Cipher; import javax.crypto.spec.SecretKeySpec; public class AdvancedEncryptionStandard { private byte[] key; private static final String ALGORITHM ="AES"; public AdvancedEncryptionStandard(byte[] key) { this.key = key; } /** * Encrypts the given plain text * * @param plainText The plain text to encrypt */ public byte[] encrypt(byte[] plainText) throws Exception { SecretKeySpec secretKey = new SecretKeySpec(key, ALGORITHM); Cipher cipher = Cipher.getInstance(ALGORITHM); cipher.init(Cipher.ENCRYPT_MODE, secretKey); return cipher.doFinal(plainText); } /** * Decrypts the given byte array * * @param cipherText The data to decrypt */ public byte[] decrypt(byte[] cipherText) throws Exception { SecretKeySpec secretKey = new SecretKeySpec(key, ALGORITHM); Cipher cipher = Cipher.getInstance(ALGORITHM); cipher.init(Cipher.DECRYPT_MODE, secretKey); return cipher.doFinal(cipherText); } } |
用法示例:
1 2 3 4 5 6 7 8 9 10 | byte[] encryptionKey ="MZygpewJsCpRrfOr".getBytes(StandardCharsets.UTF_8); byte[] plainText ="Hello world!".getBytes(StandardCharsets.UTF_8); AdvancedEncryptionStandard advancedEncryptionStandard = new AdvancedEncryptionStandard( encryptionKey); byte[] cipherText = advancedEncryptionStandard.encrypt(plainText); byte[] decryptedCipherText = advancedEncryptionStandard.decrypt(cipherText); System.out.println(new String(plainText)); System.out.println(new String(cipherText)); System.out.println(new String(decryptedCipherText)); |
。
印刷品:
1 2 3 | Hello world! ?;??LA+??b* Hello world! |
。
在我看来,您没有正确处理初始化向量(IV)。我很久没读到关于AES、IVS和区块链的文章了,但是你的路线
1 | IvParameterSpec ivParameterSpec = new IvParameterSpec(aesKey.getEncoded()); |
好像不太好。在AES的情况下,您可以将初始化向量视为密码实例的"初始状态",而这个状态是一点信息,您不能从密钥中获得,而是从加密密码的实际计算中获得。(有人可能会说,如果可以从密钥中提取IV,那么它将没有用处,因为密钥已经在其初始化阶段提供给了密码实例)。
因此,您应该在加密结束时从密码实例中以字节的形式获取IV
1 2 | cipherOutputStream.close(); byte[] iv = encryptCipher.getIV(); |
号
你应该用这个字节初始化你在
1 | IvParameterSpec ivParameterSpec = new IvParameterSpec(iv); |
那么,你的解密就可以了。希望这有帮助。
四,你使用的解密是不正确的。替换此代码
1 2 3 4 | //Decrypt cipher Cipher decryptCipher = Cipher.getInstance("AES/CBC/PKCS5Padding"); IvParameterSpec ivParameterSpec = new IvParameterSpec(aesKey.getEncoded()); decryptCipher.init(Cipher.DECRYPT_MODE, aesKey, ivParameterSpec); |
使用此代码
1 2 3 4 | //Decrypt cipher Cipher decryptCipher = Cipher.getInstance("AES/CBC/PKCS5Padding"); IvParameterSpec ivParameterSpec = new IvParameterSpec(encryptCipher.getIV()); decryptCipher.init(Cipher.DECRYPT_MODE, aesKey, ivParameterSpec); |
。
这样就能解决你的问题。
下面包含一个简单的AES类在Java中的例子。我不建议在生产环境中使用这个类,因为它可能不能满足应用程序的所有特定需求。
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 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 | import java.nio.charset.StandardCharsets; import java.security.InvalidAlgorithmParameterException; import java.security.InvalidKeyException; import java.security.NoSuchAlgorithmException; import java.security.SecureRandom; import javax.crypto.BadPaddingException; import javax.crypto.Cipher; import javax.crypto.IllegalBlockSizeException; import javax.crypto.NoSuchPaddingException; import javax.crypto.spec.IvParameterSpec; import javax.crypto.spec.SecretKeySpec; import java.util.Base64; public class AES { public static byte[] encrypt(final byte[] keyBytes, final byte[] ivBytes, final byte[] messageBytes) throws InvalidKeyException, InvalidAlgorithmParameterException { return AES.transform(Cipher.ENCRYPT_MODE, keyBytes, ivBytes, messageBytes); } public static byte[] decrypt(final byte[] keyBytes, final byte[] ivBytes, final byte[] messageBytes) throws InvalidKeyException, InvalidAlgorithmParameterException { return AES.transform(Cipher.DECRYPT_MODE, keyBytes, ivBytes, messageBytes); } private static byte[] transform(final int mode, final byte[] keyBytes, final byte[] ivBytes, final byte[] messageBytes) throws InvalidKeyException, InvalidAlgorithmParameterException { final SecretKeySpec keySpec = new SecretKeySpec(keyBytes,"AES"); final IvParameterSpec ivSpec = new IvParameterSpec(ivBytes); byte[] transformedBytes = null; try { final Cipher cipher = Cipher.getInstance("AES/CTR/NoPadding"); cipher.init(mode, keySpec, ivSpec); transformedBytes = cipher.doFinal(messageBytes); } catch (NoSuchAlgorithmException | NoSuchPaddingException | IllegalBlockSizeException | BadPaddingException e) { e.printStackTrace(); } return transformedBytes; } public static void main(final String[] args) throws InvalidKeyException, InvalidAlgorithmParameterException { //Retrieved from a protected local file. //Do not hard-code and do not version control. final String base64Key ="ABEiM0RVZneImaq7zN3u/w=="; //Retrieved from a protected database. //Do not hard-code and do not version control. final String shadowEntry ="AAECAwQFBgcICQoLDA0ODw==:ZtrkahwcMzTu7e/WuJ3AZmF09DE="; //Extract the iv and the ciphertext from the shadow entry. final String[] shadowData = shadowEntry.split(":"); final String base64Iv = shadowData[0]; final String base64Ciphertext = shadowData[1]; //Convert to raw bytes. final byte[] keyBytes = Base64.getDecoder().decode(base64Key); final byte[] ivBytes = Base64.getDecoder().decode(base64Iv); final byte[] encryptedBytes = Base64.getDecoder().decode(base64Ciphertext); //Decrypt data and do something with it. final byte[] decryptedBytes = AES.decrypt(keyBytes, ivBytes, encryptedBytes); //Use non-blocking SecureRandom implementation for the new IV. final SecureRandom secureRandom = new SecureRandom(); //Generate a new IV. secureRandom.nextBytes(ivBytes); //At this point instead of printing to the screen, //one should replace the old shadow entry with the new one. System.out.println("Old Shadow Entry =" + shadowEntry); System.out.println("Decrytped Shadow Data =" + new String(decryptedBytes, StandardCharsets.UTF_8)); System.out.println("New Shadow Entry =" + Base64.getEncoder().encodeToString(ivBytes) +":" + Base64.getEncoder().encodeToString(AES.encrypt(keyBytes, ivBytes, decryptedBytes))); } } |
请注意,AES与编码无关,这就是为什么我选择单独处理它,而不需要任何第三方库。
在这个答案中,我选择了"简单Java AES加密/解密示例"的主题,而不是具体的调试问题,因为我认为这将使大多数读者受益。
这是我的一篇关于Java中的AES加密的博客文章的简单摘要,所以我建议在实现任何东西之前阅读它。不过,我仍然会提供一个简单的示例来使用,并给出一些注意事项。
在这个例子中,我将选择在galois/counter模式或gcm模式下使用认证加密。原因是,在大多数情况下,您希望完整性和真实性与保密性相结合(在博客中阅读更多内容)。
AES-GCM加密/解密教程下面是用Java加密体系结构(JCA)对AES-GCM进行加密/解密所需的步骤。不要与其他示例混合,因为细微的差异可能会使代码完全不安全。
1。创建密钥因为这取决于您的用例,所以我假设最简单的情况:随机密钥。
1 2 3 4 | SecureRandom secureRandom = new SecureRandom(); byte[] key = new byte[16]; secureRandom.nextBytes(key); SecretKey secretKey = SecretKeySpec(key,"AES"); |
重要提示:
- 总是使用像
SecureRandom 这样的强伪随机数生成器 - 使用16字节/128位长的密钥(或更多-但很少需要更多)
- 如果需要从用户密码派生密钥,请查看具有拉伸属性(如pbkdf2或bcrypt)的密码哈希函数(或kdf)。
- 如果您想要从其他源派生的密钥,使用适当的密钥派生函数(KDF),如HKDF(Java实现这里)。不要使用简单的加密散列(如sha-256)。
2。创建初始化向量
使用初始化向量(iv),以便相同的密钥创建不同的密码文本。
1 2 | byte[] IV = new byte[12]; //NEVER REUSE THIS IV WITH SAME KEY secureRandom.nextBytes(IV); |
号
重要提示:
- 切勿用同一把钥匙重复使用同一个IV(在GCM/CTR模式中非常重要)
- IV必须是唯一的(即使用随机IV或计数器)
- 静脉注射不需要保密。
- 总是使用像
SecureRandom 这样的强伪随机数生成器 - 12字节IV是AES-GCM模式的正确选择
三。用IV和密钥加密
1 2 3 4 | final Cipher cipher = Cipher.getInstance("AES/GCM/NoPadding"); GCMParameterSpec parameterSpec = new GCMParameterSpec(128, IV); //128 bit auth tag length cipher.init(Cipher.ENCRYPT_MODE, secretKey, parameterSpec); byte[] cipherText = cipher.doFinal(plainText); |
重要提示:
- 使用16字节/128位身份验证标签(用于验证完整性/真实性)
- 身份验证标记将自动附加到密码文本(在JCA实现中)
- 由于gcm的行为类似于流密码,因此不需要填充。
- 加密大数据块时使用
CipherInputStream 。 - 如果更改了其他(非机密)数据,是否要进行检查?您可能需要在这里更多地使用与
cipher.updateAAD(associatedData); 关联的数据。
三。序列化为单个消息
只需附加IV和密文。如上所述,静脉注射不需要保密。
1 2 3 4 5 | ByteBuffer byteBuffer = ByteBuffer.allocate(4 + IV.length + cipherText.length); byteBuffer.putInt(IV.length); byteBuffer.put(IV); byteBuffer.put(cipherText); byte[] cipherMessage = byteBuffer.array(); |
。
如果需要字符串表示形式,可以选择使用base64进行编码。要么使用Android或Java 8的内置实现(不要使用Apache Con CODEC——这是一个可怕的实现)。编码用于将字节数组"转换"为字符串表示形式,以确保它是ASCII安全的,例如:
1 |
4。准备解密:反序列化
如果已对消息进行编码,请首先将其解码为字节数组:
1 | byte[] cipherMessage = Base64.getDecoder().decode(base64CipherMessage) |
。
然后解构消息
1 2 3 4 5 6 7 8 9 | ByteBuffer byteBuffer = ByteBuffer.wrap(cipherMessage); int ivLength = byteBuffer.getInt(); if(ivLength < 12 || ivLength >= 16) { // check input parameter throw new IllegalArgumentException("invalid IV length"); } byte[] IV = new byte[ivLength]; byteBuffer.get(IV); byte[] cipherText = new byte[byteBuffer.remaining()]; byteBuffer.get(cipherText); |
。
重要提示:
- 请注意验证输入参数,以避免因分配过多内存而导致拒绝服务攻击(例如,攻击者可能会将长度值更改为231,分配2GB堆)
5。解密
初始化密码并设置与加密相同的参数:
1 2 3 | final Cipher cipher = Cipher.getInstance("AES/GCM/NoPadding"); cipher.init(Cipher.DECRYPT_MODE, new SecretKeySpec(key,"AES"), new GCMParameterSpec(128, IV)); byte[] plainText= cipher.doFinal(cipherText); |
重要提示:
- 如果您在加密过程中添加了与
cipher.updateAAD(associatedData); 关联的数据,请不要忘记添加它。
注意,最近的Android(SDK 21 +)和Java(7 +)实现应该具有AES-GCM。旧版本可能缺少它。我仍然选择这种模式,因为与类似的加密模式相比,它更容易实现,而且效率更高(例如,使用aes-cbc+hmac)。请参阅本文,了解如何使用HMAC实现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 48 49 50 51 52 53 54 55 | import javax.crypto.Cipher; import javax.crypto.spec.IvParameterSpec; import javax.crypto.spec.SecretKeySpec; //import org.apache.commons.codec.binary.Base64; import java.util.Base64; public class Encryptor { public static String encrypt(String key, String initVector, String value) { try { Cipher cipher = Cipher.getInstance("AES/CBC/PKCS5PADDING"); IvParameterSpec iv = new IvParameterSpec(initVector.getBytes("UTF-8")); SecretKeySpec skeySpec = new SecretKeySpec(key.getBytes("UTF-8"),"AES"); cipher.init(Cipher.ENCRYPT_MODE, skeySpec, iv); byte[] encrypted = cipher.doFinal(value.getBytes()); //System.out.println("encrypted string:" // + Base64.encodeBase64String(encrypted)); //return Base64.encodeBase64String(encrypted); String s = new String(Base64.getEncoder().encode(encrypted)); return s; } catch (Exception ex) { ex.printStackTrace(); } return null; } public static String decrypt(String key, String initVector, String encrypted) { try { IvParameterSpec iv = new IvParameterSpec(initVector.getBytes("UTF-8")); SecretKeySpec skeySpec = new SecretKeySpec(key.getBytes("UTF-8"),"AES"); Cipher cipher = Cipher.getInstance("AES/CBC/PKCS5PADDING"); cipher.init(Cipher.DECRYPT_MODE, skeySpec, iv); byte[] original = cipher.doFinal(Base64.getDecoder().decode(encrypted)); return new String(original); } catch (Exception ex) { ex.printStackTrace(); } return null; } public static void main(String[] args) { String key ="Bar12345Bar12345"; // 128 bit key String initVector ="RandomInitVector"; // 16 bytes IV System.out.println(encrypt(key, initVector,"Hello World")); System.out.println(decrypt(key, initVector, encrypt(key, initVector,"Hello World"))); } } |
号
这是对公认答案的改进。
变化:
(1)使用随机IV并在加密文本前进行预处理。
(2)使用sha-256从密码短语生成密钥
(3)不依赖Apache Commons
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 | public static void main(String[] args) throws GeneralSecurityException { String plaintext ="Hello world"; String passphrase ="My passphrase"; String encrypted = encrypt(passphrase, plaintext); String decrypted = decrypt(passphrase, encrypted); System.out.println(encrypted); System.out.println(decrypted); } private static SecretKeySpec getKeySpec(String passphrase) throws NoSuchAlgorithmException { MessageDigest digest = MessageDigest.getInstance("SHA-256"); return new SecretKeySpec(digest.digest(passphrase.getBytes(UTF_8)),"AES"); } private static Cipher getCipher() throws NoSuchPaddingException, NoSuchAlgorithmException { return Cipher.getInstance("AES/CBC/PKCS5PADDING"); } public static String encrypt(String passphrase, String value) throws GeneralSecurityException { byte[] initVector = new byte[16]; SecureRandom.getInstanceStrong().nextBytes(initVector); Cipher cipher = getCipher(); cipher.init(Cipher.ENCRYPT_MODE, getKeySpec(passphrase), new IvParameterSpec(initVector)); byte[] encrypted = cipher.doFinal(value.getBytes()); return DatatypeConverter.printBase64Binary(initVector) + DatatypeConverter.printBase64Binary(encrypted); } public static String decrypt(String passphrase, String encrypted) throws GeneralSecurityException { byte[] initVector = DatatypeConverter.parseBase64Binary(encrypted.substring(0, 24)); Cipher cipher = getCipher(); cipher.init(Cipher.DECRYPT_MODE, getKeySpec(passphrase), new IvParameterSpec(initVector)); byte[] original = cipher.doFinal(DatatypeConverter.parseBase64Binary(encrypted.substring(24))); return new String(original); } |
依赖标准库提供的解决方案通常是个好主意:
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 | private static void stackOverflow15554296() throws NoSuchAlgorithmException, NoSuchPaddingException, InvalidKeyException, IllegalBlockSizeException, BadPaddingException { // prepare key KeyGenerator keygen = KeyGenerator.getInstance("AES"); SecretKey aesKey = keygen.generateKey(); String aesKeyForFutureUse = Base64.getEncoder().encodeToString( aesKey.getEncoded() ); // cipher engine Cipher aesCipher = Cipher.getInstance("AES/ECB/PKCS5Padding"); // cipher input aesCipher.init(Cipher.ENCRYPT_MODE, aesKey); byte[] clearTextBuff ="Text to encode".getBytes(); byte[] cipherTextBuff = aesCipher.doFinal(clearTextBuff); // recreate key byte[] aesKeyBuff = Base64.getDecoder().decode(aesKeyForFutureUse); SecretKey aesDecryptKey = new SecretKeySpec(aesKeyBuff,"AES"); // decipher input aesCipher.init(Cipher.DECRYPT_MODE, aesDecryptKey); byte[] decipheredBuff = aesCipher.doFinal(cipherTextBuff); System.out.println(new String(decipheredBuff)); } |
。
这将打印"要编码的文本"。
解决方案是基于Java密码体系结构参考指南和HTTPS://StaskOfFult.COM/A/2059153/146755的答案。