Q:

Concatenating MAC and salt with ciphertext

I've been having trouble adding a MAC to my password-based AES encryption/decryption program. I am trying to add the MAC'd plaintext and salt (to be used with password) (both byte arrays) to a final array along with the ciphertext, and then decrypt by reading in the ciphertext file and splitting it back up into salt, MAC, and cipher text byte arrays.
The encryption class seems to be running smoothly but the decryption class does not. I debugged the the class and found that it fails because it never enters the if statement that checks whether the computed and recovered MACs are the same:
if(Arrays.equals(macBytes, hmac))

I couldn't figure out why until I printed out the byte arrays for the salt, message, and MAC, and found that they don't match when printed from the encryption and decryption classes. All the array sizes match up across the two classes, but the byte values change somewhere.
Both classes worked perfectly without the MAC before, but I didn't add the salt directly to the encrypted data then and instead wrote it to a separate file. Including it with the encrypted data makes this slightly more portable for me, but was it a bad choice to do so? Is it better to write it to a separate file? Or am I just missing something blatantly obvious in my code?
Here is the full code.
Encryption class
public class AESEncryption
{
private final String ALGORITHM = "AES";
private final String MAC_ALGORITHM = "HmacSHA256";
private final String TRANSFORMATION = "AES/CBC/PKCS5Padding";
private final String KEY_DERIVATION_FUNCTION = "PBKDF2WithHmacSHA1";
private final String PLAINTEXT = "/Volumes/CONNOR P/Unencrypted.txt";
private final String ENCRYPTED = "/Volumes/CONNOR P/Encrypted.txt";
private final String PASSWORD = "javapapers";
private final String LOC = Paths.get(".").toAbsolutePath().normalize().toString();
private static final int SALT_SIZE = 64;
private final int KEY_LENGTH = 128;
private final int ITERATIONS = 100000;

public AESEncryption()
{
    try
    {
        encrypt();
    }
    catch(Exception ex)
    {
        JOptionPane.showMessageDialog(null, "Error: " + ex.getClass().getName(), "Error", JOptionPane.ERROR_MESSAGE);
    }
}
private void encrypt() throws Exception
{
    File encrypted = new File(ENCRYPTED);
    File plaintext = new File(PLAINTEXT);
    int encryptedSize = (int)encrypted.length();
    int plaintextSize = (int)plaintext.length();

    BufferedInputStream bufferedInputStream = new BufferedInputStream(new FileInputStream(PLAINTEXT));
    BufferedOutputStream bufferedOutputStream = new BufferedOutputStream(new FileOutputStream(ENCRYPTED));

    //Create salt
    byte[] salt = new byte[SALT_SIZE];
    SecureRandom secureRandom = new SecureRandom();
    secureRandom.nextBytes(salt);

    //Create cipher key    
    SecretKeyFactory factory = SecretKeyFactory.getInstance(KEY_DERIVATION_FUNCTION);
    KeySpec keySpec = new PBEKeySpec(PASSWORD.toCharArray(), salt, ITERATIONS, KEY_LENGTH);
    SecretKey secret = new SecretKeySpec(factory.generateSecret(keySpec).getEncoded(), ALGORITHM);

    //Create cipher
    Cipher cipher = Cipher.getInstance(TRANSFORMATION);
    cipher.init(Cipher.ENCRYPT_MODE, secret, new IvParameterSpec(new byte[16]));

    //Read plaintext file into byte array
    byte[] input = new byte[encryptedSize];
    Path path = Paths.get(PLAINTEXT);
    input = Files.readAllBytes(path);
    byte[] crypt = cipher.doFinal(input);

    //Create MAC object and apply to the byte array crypt[] containing the plaintext
    KeyGenerator keyGenerator = KeyGenerator.getInstance(MAC_ALGORITHM);
    SecretKey macKey = keyGenerator.generateKey();
    Mac mac = Mac.getInstance(MAC_ALGORITHM);
    mac.init(macKey);
    byte[] macBytes = mac.doFinal(crypt);

    //Add salt, MAC'd plaintext, and encrypted plaintext to final array
    byte[] output = new byte[SALT_SIZE + crypt.length + macBytes.length];
    System.arraycopy(salt, 0, output, 0, SALT_SIZE);
    System.arraycopy(macBytes, 0, output, SALT_SIZE, macBytes.length);
    System.arraycopy(crypt, 0, output, SALT_SIZE + macBytes.length, crypt.length);

    //Write array with encrypted data to a new file
    bufferedOutputStream.write(output);
    bufferedInputStream.close();   
    bufferedOutputStream.flush();
    bufferedOutputStream.close();
}

Decryption class
public class AESDecryption
{
private final String ALGORITHM = "AES";
private final String MAC_ALGORITHM = "HmacSHA256";
private final String TRANSFORMATION = "AES/CBC/PKCS5Padding";
private final String KEY_DERIVATION_FUNCTION = "PBKDF2WithHmacSHA1";
private final String PLAINTEXT = "/Volumes/CONNOR P/De-Encrypted.txt";
private final String ENCRYPTED = "/Volumes/CONNOR P/Encrypted.txt";
private final String PASSWORD = "javapapers";
private final String LOC = Paths.get(".").toAbsolutePath().normalize().toString();
private final int SALT_SIZE = 64;
private final int IV_SIZE = 16;
private final int KEY_LENGTH = 128;
private final int ITERATIONS = 100000;

public AESDecryption()
{
    try
    {
        decrypt();
    }
    catch(Exception ex)
    {
        JOptionPane.showMessageDialog(null, "Error: " + ex.getClass().getName(), "Error", JOptionPane.ERROR_MESSAGE);
    }
}
private void decrypt() throws Exception
{
    File encrypted = new File(ENCRYPTED);
    File plaintext = new File(PLAINTEXT);
    int encryptedSize = (int)encrypted.length();
    int plaintextSize = (int)plaintext.length();
    BufferedInputStream bufferedInputStream = new BufferedInputStream(new FileInputStream(encrypted));
    BufferedOutputStream bufferedOutputStream = new BufferedOutputStream(new FileOutputStream(plaintext));

    //Read in the encrypted data
    byte[] input = new byte[encryptedSize];
    Path path = Paths.get(ENCRYPTED);
    input = Files.readAllBytes(path);
    int increment = (input.length-SALT_SIZE)/2;

    if(input.length >= (SALT_SIZE + increment))
    {
        //Recover salt, MAC, and encrypted data and store in arrays
        byte[] salt = Arrays.copyOfRange(input, 0, SALT_SIZE);
        byte[] macBytes = Arrays.copyOfRange(input, SALT_SIZE, SALT_SIZE + increment);
        byte[] crypt = Arrays.copyOfRange(input, SALT_SIZE + increment, input.length);

        //Regenerate original MAC
        KeyGenerator keyGenerator = KeyGenerator.getInstance(MAC_ALGORITHM);
        SecretKey macKey = keyGenerator.generateKey();
        Mac mac = Mac.getInstance(MAC_ALGORITHM);
        mac.init(macKey);
        byte[] hmac = mac.doFinal(crypt);        

        if(Arrays.equals(macBytes, hmac)) //This is where it fails, never enters
        {
            //Regenerate cipher and decrypt data
            SecretKeyFactory factory = SecretKeyFactory.getInstance(KEY_DERIVATION_FUNCTION);
            KeySpec keySpec = new PBEKeySpec(PASSWORD.toCharArray(), salt, ITERATIONS, KEY_LENGTH);
            SecretKey secret = new SecretKeySpec(factory.generateSecret(keySpec).getEncoded(), ALGORITHM);

            Cipher cipher = Cipher.getInstance(TRANSFORMATION);
            cipher.init(Cipher.DECRYPT_MODE, secret, new IvParameterSpec(new byte[16]));

            //Write decrypted data to new text file
            byte[] output = cipher.doFinal(crypt);
            bufferedOutputStream.write(output);   
            bufferedInputStream.close();
            bufferedOutputStream.flush();
            bufferedOutputStream.close();
        }
    }
}

Thanks for any help. It is much appreciated.

A:

public class AESEncryption
{
    private final String ALGORITHM = "AES";
    private final String MAC_ALGORITHM = "HmacSHA256";
    private final String PRNG_ALGORITHM = "SHA1PRNG";
    private final String TRANSFORMATION = "AES/CBC/PKCS5Padding";
    private final String PLAINTEXT = "/Volumes/CONNOR P/Unencrypted.txt";
    private final String ENCRYPTED = "/Volumes/CONNOR P/Encrypted.txt";
    private final String PASSWORD = "javapapers";
    private final String IV_FILE_NAME = "iv.enc";
    private final String LOC = Paths.get(".").toAbsolutePath().normalize().toString();
    private final int SALT_SIZE = 16;
    private final int IV_SIZE = 16;
    private final int KEY_LENGTH = 128;
    private final int ITERATIONS = 100000;
    private final int START = 0;
    public AESEncryption()
    {
        try
        {
            encrypt();
        }
        catch(Exception ex)
        {
            JOptionPane.showMessageDialog(null, "Error: " + ex.getClass().getName(), "Error", JOptionPane.ERROR_MESSAGE);
        }
    }
    private void encrypt() throws Exception
    {
        File encrypted = new File(ENCRYPTED);
        File plaintext = new File(PLAINTEXT);
        int plaintextSize = (int)plaintext.length();

        BufferedInputStream bufferedInputStream = new BufferedInputStream(new FileInputStream(plaintext));
        BufferedOutputStream bufferedOutputStream = new BufferedOutputStream(new FileOutputStream(encrypted));

        //Create salt for cipher key
        byte[] salt = new byte[SALT_SIZE];
        SecureRandom saltSecureRandom = SecureRandom.getInstance(PRNG_ALGORITHM);
        saltSecureRandom.nextBytes(salt);

        //Create cipher key & use to initialize cipher
        byte[] keyBytes = PBEKeyFactory.getKey(PASSWORD, salt, ITERATIONS, KEY_LENGTH);
        SecretKeySpec secret = new SecretKeySpec(keyBytes, ALGORITHM);

        Cipher cipher = Cipher.getInstance(TRANSFORMATION);
        cipher.init(Cipher.ENCRYPT_MODE, secret, new IvParameterSpec(new byte[16]));

        //Create byte array of encrypted data
        byte[] input = new byte[plaintextSize];
        Path path = Paths.get(PLAINTEXT);
        input = Files.readAllBytes(path);
        byte[] crypt = cipher.doFinal(input);

        //Create salt for the MAC key for added security
        byte[] macsalt = new byte[SALT_SIZE];
        SecureRandom macsaltSecureRandom = SecureRandom.getInstance(PRNG_ALGORITHM);
        macsaltSecureRandom.nextBytes(macsalt);

        //PBEKeyFactory.getKey(password, salt, iterations, keylength)    
        //returns a byte array representation of a SecretKey.
        //Used a SecretKeyFactory instead of a KeyGenerator to make key.
        //SecretKeyFactory gives back the same key given the same specifications
        //whereas KeyGenerator gives back a new random key each time.
        byte[] macPBE = PBEKeyFactory.getKey(PASSWORD, macsalt, ITERATIONS, KEY_LENGTH);

        SecretKeySpec macKey = new SecretKeySpec(macPBE, MAC_ALGORITHM);
        Mac mac = Mac.getInstance(MAC_ALGORITHM);
        mac.init(macKey);
        byte[] macBytes = mac.doFinal(crypt);

        byte[] output = new byte[SALT_SIZE + SALT_SIZE + crypt.length + macBytes.length];
        System.arraycopy(salt, START, output, START, SALT_SIZE);
        System.arraycopy(macsalt, START, output, SALT_SIZE, SALT_SIZE);
        System.arraycopy(macBytes, START, output, SALT_SIZE + SALT_SIZE, macBytes.length);
        System.arraycopy(crypt, START, output, SALT_SIZE + SALT_SIZE + macBytes.length, crypt.length);

        bufferedInputStream.close();   
        bufferedOutputStream.write(output);
        bufferedOutputStream.flush();
        bufferedOutputStream.close();
    }
}

public class AESDecryption
{
    private final String ALGORITHM = "AES";
    private final String MAC_ALGORITHM = "HmacSHA256";
    private final String TRANSFORMATION = "AES/CBC/PKCS5Padding";
    private final String PLAINTEXT = "/Volumes/CONNOR P/De-Encrypted.txt";
    private final String ENCRYPTED = "/Volumes/CONNOR P/Encrypted.txt";
    private final String PASSWORD = "javapapers";
    private final String LOC = Paths.get(".").toAbsolutePath().normalize().toString();
    private final int SALT_SIZE = 16;
    //MAC key size is 256 bits (32 bytes) since it is created with
    //the HmacSHA256 algorithm
    private final int MAC_SIZE = 32;
    private final int IV_SIZE = 16;
    private final int START = 0;
    private final int KEY_LENGTH = 128;
    private final int ITERATIONS = 100000;
    public AESDecryption()
    {
        try
        {
            decrypt();
        }
        catch(Exception ex)
        {
            JOptionPane.showMessageDialog(null, "Error: " + ex.getClass().getName(), "Error", JOptionPane.ERROR_MESSAGE);
        }
    }
    private void decrypt() throws Exception
    {
        File encrypted = new File(ENCRYPTED);
        File plaintext = new File(PLAINTEXT);
        int encryptedSize = (int)encrypted.length();
        BufferedInputStream bufferedInputStream = new BufferedInputStream(new FileInputStream(encrypted));
        BufferedOutputStream bufferedOutputStream = new BufferedOutputStream(new FileOutputStream(plaintext));

        //Read in encrypted data
        byte[] input = new byte[encryptedSize];
        Path path = Paths.get(ENCRYPTED);
        input = Files.readAllBytes(path);

        if(input.length >= (SALT_SIZE*2 + MAC_SIZE))
        {
            byte[] cryptSalt = Arrays.copyOfRange(input, START, SALT_SIZE);
            byte[] macSalt = Arrays.copyOfRange(input, SALT_SIZE, SALT_SIZE*2);
            byte[] macBytes = Arrays.copyOfRange(input, SALT_SIZE*2, (SALT_SIZE*2 + MAC_SIZE));
            byte[] cryptBytes = Arrays.copyOfRange(input, (SALT_SIZE*2 + MAC_SIZE), input.length);

            //This generates the same MAC key from encryption.
            //Before, the KeyGenerator created a new random key
            //meaning the derived and computed MAC keys were never the same
            byte[] macKeyBytes = PBEKeyFactory.getKey(PASSWORD, macSalt, ITERATIONS, KEY_LENGTH);
            SecretKeySpec macKey = new SecretKeySpec(macKeyBytes, MAC_ALGORITHM);
            Mac mac = Mac.getInstance(MAC_ALGORITHM);
            mac.init(macKey);
            byte[] compMacBytes = mac.doFinal(cryptBytes);
            //Check if computed and derived MAC's are the same
            if(Arrays.equals(macBytes, compMacBytes))
            {
                //Creates same key from encryption 
                byte[] cryptKeyBytes = PBEKeyFactory.getKey(PASSWORD, cryptSalt, ITERATIONS, KEY_LENGTH);
                SecretKeySpec cryptKey = new SecretKeySpec(cryptKeyBytes, ALGORITHM);

                //Creates cipher and reads decrypted data to array
                Cipher cipher = Cipher.getInstance(TRANSFORMATION);
                cipher.init(Cipher.DECRYPT_MODE, cryptKey, new IvParameterSpec(new byte[16]));
                byte[] output = cipher.doFinal(cryptBytes);

                bufferedInputStream.close();
                bufferedOutputStream.write(output);   
                bufferedOutputStream.flush();
                bufferedOutputStream.close();
            }
        }
    }
}
//This class has only one method, getKey(), which returns a byte array
//of a SecretKey of the corresponding parameters
public class PBEKeyFactory
{
    private static final String KEY_DERIVATION_FUNCTION = "PBKDF2WithHmacSHA1";
    public static byte[] getKey(String password, byte[] salt, int iterations, int length) throws Exception
    {
        KeySpec keySpec = new PBEKeySpec(password.toCharArray(), salt, iterations, length);
        SecretKeyFactory factory = SecretKeyFactory.getInstance(KEY_DERIVATION_FUNCTION);
        return factory.generateSecret(keySpec).getEncoded();
    }
}

