简体   繁体   中英

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?

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 you 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.

Solved the problem above with the help of Topaco's comment. Instead of picking a new random number each time I run through method:

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

I am using a hard coded byte array now:

public static IvParameterSpec getInitializationVector() {
    byte[] iv = {1, 2, 3, ... 16};
    return new IvParameterSpec(iv);
}

Please note that this solution is not exactly safe, was the IV is hard coded and can be extracted from source code. However, I choose not to pass it to the outside as my application is not very critical. I a proper solution, the random number needs to be returned together with the IvParameterSpec.

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM