Handling of IV and salt in Java encryption and dec

2019-02-07 15:08发布

问题:

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)

回答1:

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.



回答2:

You are not initializing the cipher correctly first you need the instance (you are doing it correctly):

Cipher cipher = Cipher.getInstance("AES/CBC/PKCS5Padding");

Then you need to initialize it there are multiple init method:

public void init(int opmode, Key key);

public void init(int opmode, Certificate certificate);

public void init(int opmode, Key key, SecureRandom random);

public void init(int opmode, Certificate certificate,
                 SecureRandom random);

public void init(int opmode, Key key,
                 AlgorithmParameterSpec params);

public void init(int opmode, Key key,
                 AlgorithmParameterSpec params, SecureRandom random);

public void init(int opmode, Key key,
                 AlgorithmParameters params);

public void init(int opmode, Key key,
                 AlgorithmParameters params, SecureRandom random);

In you case you want to use this one:

public void init(int opmode, Key key,
             AlgorithmParameterSpec params);

However calling cipher.getParameters(); before init always return null. There is your NullPointerException.

Furthermore there are more mistakes in the code for instance using the data to encrypt as a key.