Java AES/CBC 解密后的初始字节不正确

2022-08-31 09:04:35

以下示例有什么问题?

问题是解密字符串的第一部分是无稽之谈。但是,其余的都很好,我得到...

Result: `£eB6O�geS��i are you? Have a nice day.
@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();
  }
}

答案 1

包括我在内的很多人在制作这项工作时都会遇到很多问题,因为缺少一些信息,例如忘记转换为Base64,初始化向量,字符集等。所以我想到了制作一个功能齐全的代码。

希望这对你们所有人都有用:要编译,你需要额外的Apache Commons编解码器jar,可在此处获得:http://commons.apache.org/proper/commons-codec/download_codec.cgi

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")));
    }
}

答案 2

在这个答案中,我选择接近“简单的Java AES加密/解密示例”主题,而不是具体的调试问题,因为我认为这将使大多数读者受益。

这是我关于Java中AES加密的博客文章的简单摘要,因此我建议在实现任何内容之前通读它。但是,我仍然会提供一个简单的示例来使用,并给出一些需要注意的指示。

在此示例中,我将选择将经过身份验证的加密伽罗瓦/计数器模式或 GCM 模式结合使用。原因是在大多数情况下,您希望完整性和真实性与机密性相结合(在博客中阅读更多内容)。

AES-GCM 加密/解密教程

以下是使用带有Java加密体系结构(JCA)AES-GCM进行加密/解密所需的步骤。不要与其他示例混合使用,因为细微的差异可能会使您的代码完全不安全。

1. 创建密钥

由于这取决于您的用例,我将假设最简单的情况:随机密钥。

SecureRandom secureRandom = new SecureRandom();
byte[] key = new byte[16];
secureRandom.nextBytes(key);
SecretKey secretKey = SecretKeySpec(key, "AES");

重要:

2. 创建初始化向量

使用初始化向量(IV),以便相同的密钥将创建不同的密码文本

byte[] iv = new byte[12]; //NEVER REUSE THIS IV WITH SAME KEY
secureRandom.nextBytes(iv);

重要:

3. 使用 IV 和密钥加密

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);

3. 序列化为单条消息

只需附加 IV 和密文即可。如上所述,IV不需要是秘密的。

ByteBuffer byteBuffer = ByteBuffer.allocate(iv.length + cipherText.length);
byteBuffer.put(iv);
byteBuffer.put(cipherText);
byte[] cipherMessage = byteBuffer.array();

如果需要字符串表示形式,可以选择使用 Base64 进行编码。要么使用Android的,要么使用Java 8的内置实现(不要使用Apache Commons编解码器 - 这是一个糟糕的实现)。编码用于将字节数组“转换”为字符串表示,以使其ASCII安全,例如:

String base64CipherMessage = Base64.getEncoder().encodeToString(cipherMessage);

4. 准备解密:反序列化

如果已对消息进行编码,请首先将其解码为字节数组:

byte[] cipherMessage = Base64.getDecoder().decode(base64CipherMessage)

重要:

5. 解密

初始化密码并设置与加密相同的参数:

final Cipher cipher = Cipher.getInstance("AES/GCM/NoPadding");
//use first 12 bytes for iv
AlgorithmParameterSpec gcmIv = new GCMParameterSpec(128, cipherMessage, 0, 12);
cipher.init(Cipher.DECRYPT_MODE, secretKey, gcmIv);
//use everything from 12 bytes on as ciphertext
byte[] plainText = cipher.doFinal(cipherMessage, 12, cipherMessage.length - 12);

重要:

  • 不要忘记添加关联的数据,如果您在加密期间添加了它。cipher.updateAAD(associatedData);

可以在此要点中找到工作代码片段。


请注意,最新的 Android (SDK 21+) 和 Java (7+) 实现都应具有 AES-GCM。旧版本可能缺少它。我仍然选择这种模式,因为与类似的加密Mac模式(例如AES-CBC + HMAC)相比,它除了更有效之外,还更容易实现。请参阅本文,了解如何使用 HMAC 实现 AES-CBC


推荐