关于加密:Java AES / CBC解密后初始字节不正确

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


这里有一个没有Apache Commons CodecBase64的解决方案:

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

你应该用这个字节初始化你在DECRYPT_MODE中的Cipher

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
String base64CipherMessage = Base64.getEncoder().encodeToString(cipherMessage);

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的答案。