Search code examples
javaencryptionbase64aescbc-mode

Almost getting the original text with different IVs, is it normal?


Is it normal that when decrypting AES-encoded encrypted text in CBC mode with an initialization vector different from the original, you get almost the original text?

I am attaching a complete example code, I did not create it but I took it from an online tutorial, I just modified the main to explain better and with an example what I mean:

import javax.crypto.Cipher;
import javax.crypto.spec.IvParameterSpec;
import javax.crypto.spec.SecretKeySpec;
import java.util.Base64;

public class Main {
    private static final String key = "aesEncryptionKey";
    private static String initVector = "encryptionIntVec";

    public static String encrypt(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());
            return Base64.getEncoder().encodeToString(encrypted);
        } catch (Exception ex) {
            ex.printStackTrace();
        }
        return null;
    }

    public static String decrypt(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 originalString = "password";
        System.out.println("Original String to encrypt - " + originalString);
        String encryptedString = encrypt(originalString);
        System.out.println("Encrypted String with initVector: 'encryptionIntVec' - " + encryptedString);

        String decryptedString = decrypt(encryptedString);
        System.out.println("Decrypted String with initVector: 'encryptionIntVec' - " + decryptedString);
        //output: "password"

        initVector = "dncryftionIntVec";
        String decryptedString2 = decrypt(encryptedString);
        System.out.println("Decrypted String with initVector: 'dncryftionIntVec' - " + decryptedString2);
        //output: "qasswyrd"

    }
}

Output:

Original String to encrypt - password
Encrypted String with initVector: 'encryptionIntVec' - AIDTAIiCazaQavILI07rtA==
Decrypted String with initVector: 'encryptionIntVec' - password
Decrypted String with initVector: 'dncryftionIntVec' - qasswyrd

Solution

  • Yes. The ciphertext block is first block decrypted and then XOR'ed with the last ciphertext block or the IV if it is the first ciphertext block.

    So if you look at the first character (characters in ASCII):

    Difference in the init vector:

    'e' ^ 'd' = 65h ^ 64h = 0110_0101b ^ 0110_0100b = 0000_0001b
    

    Difference XOR'ed with the plaintext character:

    'p' ^ 0000_0001b = 70h ^ 0000_0001b = 0111_0000b ^ 0000_0001b = 0111_0001b = 71h = 'q'
    

    CBC has limited so called error propagation. Authenticated encryption such as AES-GCM should be preferred in most situations.

    Note that CBC mode requires a unpredictable IV, which means basically that it should consist of (pseudo)random bytes.