Search code examples
javaencryptionaesencryption-symmetricpbkdf2

Encryption/decryption fails


I have a helper class AES encrypting/decrypting a String which is mostly a clone of the code provided in Baeldung AES encryption example

The code looks as follows:

import java.security.InvalidAlgorithmParameterException;
import java.security.InvalidKeyException;
import java.security.NoSuchAlgorithmException;
import java.security.SecureRandom;
import java.security.Security;
import java.security.spec.InvalidKeySpecException;
import java.security.spec.KeySpec;
import java.util.Arrays;
import java.util.Base64;

import javax.crypto.BadPaddingException;
import javax.crypto.Cipher;
import javax.crypto.IllegalBlockSizeException;
import javax.crypto.NoSuchPaddingException;
import javax.crypto.SecretKey;
import javax.crypto.SecretKeyFactory;
import javax.crypto.spec.IvParameterSpec;
import javax.crypto.spec.PBEKeySpec;
import javax.crypto.spec.SecretKeySpec;

public class CryptoHelper {

    private static final String CIPHER_ALGORITM_NAME = "AES/CBC/PKCS5Padding";
    private static final String HASHING_ALGO_NAME = "PBKDF2WithHmacSHA1";
    private static final int KEY_TARGET_LENGTH = 256;
    private static final int HASHING_ITERATIONS = 65536;

    public static SecretKey getSecretKey(String string, byte[] salt)
            throws NoSuchAlgorithmException, InvalidKeySpecException {

        KeySpec spec = new PBEKeySpec(string.toCharArray(), salt, HASHING_ITERATIONS, KEY_TARGET_LENGTH);

        try {
            SecretKeyFactory keyFactory = SecretKeyFactory.getInstance(HASHING_ALGO_NAME);
            SecretKey encryptedPassword = new SecretKeySpec(keyFactory.generateSecret(spec).getEncoded(), "AES");
            return encryptedPassword;
        } catch (NoSuchAlgorithmException | InvalidKeySpecException e) {
            throw e;
        }

    }

    public static IvParameterSpec getInitializationVector() {
        byte[] iv = new byte[16];
        new SecureRandom().nextBytes(iv);
        return new IvParameterSpec(iv);
    }

    public static String encrypt(String input, String password, byte[] salt)
            throws NoSuchPaddingException, NoSuchAlgorithmException, InvalidAlgorithmParameterException,
            InvalidKeyException, BadPaddingException, IllegalBlockSizeException {

        Cipher cipher = Cipher.getInstance(CIPHER_ALGORITM_NAME);

        SecretKey secretKey = null;
        try {
            secretKey = getSecretKey(password, salt);
        } catch (NoSuchAlgorithmException | InvalidKeySpecException e) {
            // code
        }
        cipher.init(Cipher.ENCRYPT_MODE, secretKey, getInitializationVector());
        byte[] cipherText = cipher.doFinal(input.getBytes());
        return Base64.getEncoder().encodeToString(cipherText);
    }

    public static String decrypt(String encryptedText, String password, byte[] salt)
            throws NoSuchPaddingException, NoSuchAlgorithmException, InvalidAlgorithmParameterException,
            InvalidKeyException, BadPaddingException, IllegalBlockSizeException {

        Cipher cipher = Cipher.getInstance(CIPHER_ALGORITM_NAME);
        SecretKey secretKey = null;
        try {
            secretKey = getSecretKey(password, salt);
        } catch (NoSuchAlgorithmException | InvalidKeySpecException e) {
            // code
        }
        cipher.init(Cipher.DECRYPT_MODE, secretKey, getInitializationVector());
        byte[] plainText = cipher.doFinal(Base64.getDecoder().decode(encryptedText));
        return new String(plainText);
    }
}

Now I test this with a unit test, but this test fails with

javax.crypto.BadPaddingException: Given final block not properly padded. Such issues can arise if a bad key is used during decryption.

@Test
public void testDecrypt() {
    String encryptedString = "";
    String password = "password";

    try {
        encryptedString = CryptoHelper.encrypt("some string", password, password.getBytes());
    } catch (InvalidKeyException | NoSuchPaddingException | NoSuchAlgorithmException
            | InvalidAlgorithmParameterException | BadPaddingException | IllegalBlockSizeException e) {
        e.printStackTrace();
        assertNull(e);
    }

    try {
        CryptoHelper.decrypt(encryptedString, password, password.getBytes());
    } catch (InvalidKeyException | NoSuchPaddingException | NoSuchAlgorithmException
            | InvalidAlgorithmParameterException | BadPaddingException | IllegalBlockSizeException e) {
        // TODO Auto-generated catch block
        e.printStackTrace();
    }
}

What is wrong here?


Solution

  • Here is my attempt at solving this problem and also removing other security problems I've noticed in your code. It is obvious you just copied the code without understanding the underlying concepts behind it.

    First things first, you need to differentiate between encryption and hashing.

    In cryptography domain, one of the usage of hashing is to store user passwords in a form which isn't a plaintext. This is possible because the hash function are a one way functions, meaning that you cannot derive a password from a hash. Usually when hashing, a password's salt is used. Salt should be random bits of data that are unique for every hash you create. Also, there are security guidelines when it comes to salt sizes. I think the current recommended length of a salt should be at least 32 bits according to NIST.

    hashing_function(input, salt) -> hashed_input

    When using symmetric encryption, you will need to choose an algorithm, mode and a padding schema alongside a unique IV value. In your code you have chosen AES algorithm with CBC mode and PKCS5 padding. If you reuse an IV, you open yourself up to attacks which are easier to execute than those when you use a unique IV. Every time you encrypt a new sequence, you should use a new IV value. For example, you will use one, unique, IV value when encrypting one file or a message, but as soon as you want to encrypt another one you should use a new IV value. The main difference between hashing and encrypting is that encryption is not a one way function, meaning that with a proper key and iv you can decrypt your encrypted data.

    k - encryption key
    iv - initialization vector
    p - plaintext
    c - ciphertext
    E - encryption function
    D - decryption function
    
    => E(p, k, iv) = c
    => D(c, k, iv) = p
    

    The reason why you are hashing your password is because you want to transform your text password into an encryption key. This is known as key stretching. You are using PBKDF2 algorithm with HMAC-SHA1 with 65,536 iterations. I think the current NIST recommendation is to use over 70,000 iterations with HMAC-SHA256. Keep in mind this is a recommendation, the combination of algorithms, iterations and other parameters depend on your desired level of security.

    In this example, the reason why your unit test fails is that you are using different IV values for encryption and decryption. Once you fix this, your test will pass. However, in your code, your salt value, which you use for password hashing which is then used as a key for your symmetric encryption isn't correct. The salt should be unique and in the code snipped you provided you are hashing your password by using your own password as a salt. Essentially, you have this: hashing_function(input, input) -> hashed_input, where input in this case is your password.

    I would solve this problem by adding another method called getSalt() and I would update the encryption and decryption methods, by adding an IV to methods parameters.

        public static byte[] getSalt() {
            byte[] salt = new byte[32];
            new SecureRandom().nextBytes(salt);
            return salt;
        }
    
        public static String encrypt(String input, String password, byte[] salt, IvParameterSpec iv)
                throws NoSuchPaddingException, NoSuchAlgorithmException, InvalidAlgorithmParameterException,
                InvalidKeyException, BadPaddingException, IllegalBlockSizeException {
    
            Cipher cipher = Cipher.getInstance(CIPHER_ALGORITM_NAME);
    
            SecretKey secretKey = null;
            try {
                secretKey = getSecretKey(password, salt);
            } catch (NoSuchAlgorithmException | InvalidKeySpecException e) {
                // code
            }
            cipher.init(Cipher.ENCRYPT_MODE, secretKey, iv);
            byte[] cipherText = cipher.doFinal(input.getBytes());
            return Base64.getEncoder().encodeToString(cipherText);
        }
    
        public static String decrypt(String encryptedText, String password, byte[] salt, IvParameterSpec iv)
                throws NoSuchPaddingException, NoSuchAlgorithmException, InvalidAlgorithmParameterException,
                InvalidKeyException, BadPaddingException, IllegalBlockSizeException {
    
            Cipher cipher = Cipher.getInstance(CIPHER_ALGORITM_NAME);
            SecretKey secretKey = null;
            try {
                secretKey = getSecretKey(password, salt);
            } catch (NoSuchAlgorithmException | InvalidKeySpecException e) {
                // code
            }
            cipher.init(Cipher.DECRYPT_MODE, secretKey, iv);
            byte[] plainText = cipher.doFinal(Base64.getDecoder().decode(encryptedText));
            return new String(plainText);
        }
    

    Then I would use/test the code like this:

    @Test
    public void testDecrypt() {
        String encryptedString;
        String decryptedString;
        String plaintext = "some string";
        String password = "password";
        byte[] salt = CryptoHelper.getSalt();
        IvParameterSpec iv = CryptoHelper.getInitializationVector();
    
        encryptedString = CryptoHelper.encrypt(plaintext, password, salt, iv);
        decryptedString = CryptoHelper.decrypt(encryptedString, password, salt, iv);
    
        assertEquals(plaintext,decryptedString)
    }
    

    Finally, I would consider removing the try-catch from your test code as it doesn't, in its current form, provide any value. If an exception is thrown from the code being tested then the test will fail and that is normally what you want to happen. One exception would be when your test wants to provoke an exception.