Handling of IV and salt in Java encryption and decryption

10,245

You have at least four problems and you discovered only one of them.

  1. The IV is generated during encryption and used. The exact same IV needs to be used during decryption. The IV is not supposed to be secret. You can simply send/store it along with the ciphertext. Usually, the IV is stored in front of the ciphertext and sliced off before decryption.
    For CBC mode, it just needs to be unpredictable (read: random). For CTR mode, it would need to be unique (when the same key is used).

  2. The random salt needs to be treated in the exact same way as the IV: it is not secret and can be written in front of the ciphertext.

  3. String is not a container for binary data. When you use new String(encryptedText), you probably lose some unprintable bytes which breaks your ciphertext and makes the plaintext unrecoverable. You need to use something like Base64 or Hex encoding to represent binary data as printable text.

  4. If you encrypt something, you need two things: a plaintext and a password (used for key derivation). You also need to two things during decryption: a ciphertext and a password (key). You're using the same string value during encryption. The ciphertext is then mangled in such a way that you would need the original string value during decryption. And that would defeat the purpose of encrypting it in the first place. Your encryption method is basically a hash function.
    Thanks to wittyameta for pointing this out.

So the resulting code would look similar to this:

public static String encrypt(String str, String password) {
    try {
        SecureRandom random = new SecureRandom();
        byte[] salt = new byte[16];
        random.nextBytes(salt);

        SecretKeyFactory factory = SecretKeyFactory.getInstance("PBKDF2WithHmacSHA1");
        KeySpec spec = new PBEKeySpec(password.toCharArray(), salt, 65536, 256);
        SecretKey tmp = factory.generateSecret(spec);
        SecretKey secret = new SecretKeySpec(tmp.getEncoded(), "AES");

        Cipher cipher = Cipher.getInstance("AES/CBC/PKCS5Padding");
        cipher.init(Cipher.ENCRYPT_MODE, secret);
        AlgorithmParameters params = cipher.getParameters();
        byte[] iv = params.getParameterSpec(IvParameterSpec.class).getIV();
        byte[] encryptedText = cipher.doFinal(str.getBytes("UTF-8"));

        // concatenate salt + iv + ciphertext
        ByteArrayOutputStream outputStream = new ByteArrayOutputStream();
        outputStream.write(salt);
        outputStream.write(iv);
        outputStream.write(encryptedText);

        // properly encode the complete ciphertext
        return DatatypeConverter.printBase64Binary(outputStream.toByteArray());
    } catch (Exception e) {
        e.printStackTrace();
    }
    return null;
}

public static String decrypt(String str, String password) {
    try {
        byte[] ciphertext = DatatypeConverter.parseBase64Binary(str);
        if (ciphertext.length < 48) {
            return null;
        }
        byte[] salt = Arrays.copyOfRange(ciphertext, 0, 16);
        byte[] iv = Arrays.copyOfRange(ciphertext, 16, 32);
        byte[] ct = Arrays.copyOfRange(ciphertext, 32, ciphertext.length);

        SecretKeyFactory factory = SecretKeyFactory.getInstance("PBKDF2WithHmacSHA1");
        KeySpec spec = new PBEKeySpec(password.toCharArray(), salt, 65536, 256);
        SecretKey tmp = factory.generateSecret(spec);
        SecretKey secret = new SecretKeySpec(tmp.getEncoded(), "AES");
        Cipher cipher = Cipher.getInstance("AES/CBC/PKCS5Padding");

        cipher.init(Cipher.DECRYPT_MODE, secret, new IvParameterSpec(iv));
        byte[] plaintext = cipher.doFinal(ct);

        return new String(plaintext, "UTF-8");
    } catch (Exception e) {
        e.printStackTrace();
    }
    return null;
}

I've used this for Java 7 compatible Base64 encoding/decoding.

Share:
10,245

Related videos on Youtube

BeastlyMC956
Author by

BeastlyMC956

Love programming :)

Updated on June 04, 2022

Comments

  • BeastlyMC956
    BeastlyMC956 almost 2 years

    So I'm trying to decrypt a message in a method but it doesn't work because I need to do cipher.init(Cipher.ENCRYPT_MODE, secret) before I try to add new IvParameterSpec(iv) to cipher.init(Cipher.DECRYPT_MODE, secret, new IvParameterSpec(iv));. Otherwise, it just returns a NullPointerException I'm wondering if it's possible to do this in a method rather than writing it all the time. I can't really think of a solution so that's why I'm here. Encrypt works fine but not Decrypt.

    Project Running: JRE 7

    Encrypt Code:

    public static String encrypt(String str) {
        try {
            SecureRandom random = new SecureRandom();
            byte[] salt = new byte[16];
            random.nextBytes(salt);
    
            SecretKeyFactory factory = SecretKeyFactory.getInstance("PBKDF2WithHmacSHA1");
            KeySpec spec = new PBEKeySpec(str.toCharArray(), salt, 65536, 256);
            SecretKey tmp = factory.generateSecret(spec);
            SecretKey secret = new SecretKeySpec(tmp.getEncoded(), "AES");
    
            Cipher cipher = Cipher.getInstance("AES/CBC/PKCS5Padding");
            cipher.init(Cipher.ENCRYPT_MODE, secret); //<--- Need to do this before writing IvPerameterSpec,
            // But I think that it's not possible if I have it in another method.
            byte[] encryptedText = cipher.doFinal(str.getBytes("UTF-8"));
    
            return new String(encryptedText);
        } catch (Exception e) {
            e.printStackTrace();
        }
        return null;
    }
    

    Decrypt Code:

    public static String decrypt(String str) {
        try {
            SecureRandom random = new SecureRandom();
            byte[] salt = new byte[16];
            random.nextBytes(salt);
    
            SecretKeyFactory factory = SecretKeyFactory.getInstance("PBKDF2WithHmacSHA1");
            KeySpec spec = new PBEKeySpec(str.toCharArray(), salt, 65536, 256);
            SecretKey tmp = factory.generateSecret(spec);
            SecretKey secret = new SecretKeySpec(tmp.getEncoded(), "AES");
            Cipher cipher = Cipher.getInstance("AES/CBC/PKCS5Padding");
    
            AlgorithmParameters params = cipher.getParameters();
            byte[] iv = params.getParameterSpec(IvParameterSpec.class).getIV();
            cipher.init(Cipher.DECRYPT_MODE, secret, new IvParameterSpec(iv));
            //                                                ^^^ Returns NullPointerException
    
            byte[] ciphertext = cipher.doFinal(str.getBytes("UTF-8"));
            String decryptedText = new String(cipher.doFinal(ciphertext), "UTF-8");
    
            return new String(decryptedText);
        } catch (Exception e) {
            e.printStackTrace();
        }
        return null;
    }
    

    Exception:

    java.lang.NullPointerException
            at me.Sansanvi.Encryption.api.ComputerAPI.decrypt(ComputerAPI.java:149)
            at me.Sansanvi.Encryption.EncryptionMain.initializeFiles(EncryptionMain.java:46)
            at me.Sansanvi.Encryption.EncryptionMain.<init>(EncryptionMain.java:36)
            at me.Sansanvi.Encryption.EncryptionMain$1.run(EncryptionMain.java:23)
            at java.awt.event.InvocationEvent.dispatch(Unknown Source)
            at java.awt.EventQueue.dispatchEventImpl(Unknown Source)
            at java.awt.EventQueue.access$200(Unknown Source)
            at java.awt.EventQueue$3.run(Unknown Source)
            at java.awt.EventQueue$3.run(Unknown Source)
            at java.security.AccessController.doPrivileged(Native Method)
            at java.security.ProtectionDomain$1.doIntersectionPrivilege(Unknown Source)
            at java.awt.EventQueue.dispatchEvent(Unknown Source)
            at java.awt.EventDispatchThread.pumpOneEventForFilters(Unknown Source)
            at java.awt.EventDispatchThread.pumpEventsForFilter(Unknown Source)
            at java.awt.EventDispatchThread.pumpEventsForHierarchy(Unknown Source)
            at java.awt.EventDispatchThread.pumpEvents(Unknown Source)
            at java.awt.EventDispatchThread.pumpEvents(Unknown Source)
            at java.awt.EventDispatchThread.run(Unknown Source)
    

    I changed the methods to the following and they work:

    private static final String ALGORITHM = "AES";
        public static byte[] encrypt(byte[] str) {
        try {
            SecretKeySpec secretKey = new SecretKeySpec("MZygpewJsCpRrfOr".getBytes(StandardCharsets.UTF_8), ALGORITHM);
            Cipher cipher = Cipher.getInstance(ALGORITHM);
            cipher.init(Cipher.ENCRYPT_MODE, secretKey);
    
            return cipher.doFinal(str);
        } catch (Exception e) {
            e.printStackTrace();
        }
        return null;
    }
    public static byte[] decrypt(byte[] str) {
        try {
            SecretKeySpec secretKey = new SecretKeySpec("MZygpewJsCpRrfOr".getBytes(StandardCharsets.UTF_8), ALGORITHM);
            Cipher cipher = Cipher.getInstance(ALGORITHM);
            cipher.init(Cipher.DECRYPT_MODE, secretKey);
    
            return cipher.doFinal(str);
        } catch (Exception e) {
            e.printStackTrace();
        }
        return null;
    }
    

    I don't get any errors/exceptions anymore but I get this in the console when I close my application:

    [0x7FFC837C7430] ANOMALY: use of REX.w is meaningless (default operand size is 64)
    [0x7FFC837C7430] ANOMALY: use of REX.w is meaningless (default operand size is 64)
    [0x7FFC837C7430] ANOMALY: use of REX.w is meaningless (default operand size is 64)
    [0x7FFC837C7430] ANOMALY: use of REX.w is meaningless (default operand size is 64)
    [0x7FFC837C7430] ANOMALY: use of REX.w is meaningless (default operand size is 64)
    [0x7FFC837C7430] ANOMALY: use of REX.w is meaningless (default operand size is 64)
    [0x7FFC837C7430] ANOMALY: use of REX.w is meaningless (default operand size is 64)
    [0x7FFC837C7430] ANOMALY: use of REX.w is meaningless (default operand size is 64)
    
  • BeastlyMC956
    BeastlyMC956 almost 7 years
    I get java.security.InvalidKeyException: Illegal key size or default parameters in encrypt.
  • Artjom B.
    Artjom B. almost 7 years
    Either change the 256 to 128 in PBEKeySpec to use AES-128 instead of AES-256 or install the unlimited strength policy files.
  • BeastlyMC956
    BeastlyMC956 almost 7 years
    Changing it to 128 works, Thanks for that! :D, but is there a way to not make the plaintext random? Because I want to write it to a file and then read it later on. It will make a random new String each time I restart my program.
  • Artjom B.
    Artjom B. almost 7 years
    Yes, but that decreases the security of your solution. You could set the salt and IV to a static value.
  • Nikhil
    Nikhil over 5 years
    How do you know that the iv will always be between 16 and 32? What if its greater?
  • Artjom B.
    Artjom B. over 5 years
    @Nikhil For CBC, the IV MUST be as big as the block size of the used block cipher. That is 16 bytes for AES. Note that key size is a different metric and has no dependency to the IV. CFB and OFB modes also need an IV exactly as big as the block size. In CTR mode we usually have an IV as big as the block size but a smaller nonce that is combined with an initial counter to make up the IV.
  • wittyameta
    wittyameta over 5 years
    @ArtjomB. Shouldn't the password used to create PBEKeySpec be same in encrypt and decrypt methods? You have used str.toCharArray() in both the methods. However, str argument passed would be different in both the cases!
  • yolob 21
    yolob 21 over 3 years
    @wittyameta the str argument is not passed for key creation, please check the code once again thoroughly, Its the password which is passed