How to Encrypt/Decrypt files and byte arrays in Java using AES-GCM

โ€”

by

in ,

In this post, we will discuss how to encrypt and decrypt a file using the AES encryption algorithm in GCM mode. We will start by writing a file reader / writer to read and write files into byte arrays. Then we will attempt to encrypt and decrypt these byte arrays. This example has been written in Java 11. However, with little modifications, it can be back-ported to older JDK versions.

Reading / Writing files from and to byte arrays

The first step in our tutorial is to build the ability to read and write files. We will be converting files to and from byte arrays. We need the data to be in byte array format for encryption and decryption purposes.

Reading files in Java is quite straightforward. All that is needed is to initialize a new File object and read the file data into a byte array using a file input stream.

        File file = new File(path);

        byte [] fileData = new byte[(int) file.length()];

        try(FileInputStream fileInputStream = new FileInputStream(file)) {
            fileInputStream.read(fileData);
        }

Once we read the file using the fileInputStream, the data is copied into our byte array. Notice that we are using the try-with-resources style here to initialize our FileInputStream. If you are using a Java version older than 8, then you will need to explicitly call fileInputStream.close() in order to avoid any memory leaks.

Writing a byte array into a file is even simpler than readying from a file. All that is needed is to write the byte array using a FileInputStream. Here, we do not need to initialize a file. We simply need to indicate to the FileOutputStream where our output file is.

    public static void writeFile(String path, byte [] data) throws IOException {

        try(FileOutputStream fileOutputStream = new FileOutputStream(path)) {
            fileOutputStream.write(data);
        }

    }

Again, please make sure to close the stream if you are using a JDK version which is older than 8. To put it all together, we have put the two functions into a class called the NullbeansFileManager.

package com.nullbeans;

import java.io.*;

/**
 * example filereader/writer for nullbeans.com
 */
public class NullbeansFileManager {


    /**
     * Reads the file in the given path into a byte array
     * @param path: Path to the file, including the file name. For example: "C:/myfolder/myfile.txt"
     * @return byte array of the file data
     * @throws IOException
     */
    public static byte[] readFile(String path) throws IOException {

        File file = new File(path);

        byte [] fileData = new byte[(int) file.length()];

        try(FileInputStream fileInputStream = new FileInputStream(file)) {
            fileInputStream.read(fileData);
        }

        return fileData;
    }

    /**
     * Writes a file with the given data into a file with the given path
     * @param path: Path to the file to be created, including the file name. For example: "C:/myfolder/myfile.txt"
     * @param data: byte array of the data to be written
     * @throws IOException
     */
    public static void writeFile(String path, byte [] data) throws IOException {

        try(FileOutputStream fileOutputStream = new FileOutputStream(path)) {
            fileOutputStream.write(data);
        }

    }

}


Encrypting and decrypting byte arrays using AES + GCM mode

First of all, if you are not familiar with GCM, then I would recommend that you take a few minutes to read about it here: https://en.wikipedia.org/wiki/Galois/Counter_Mode

If you are not interested in the theoretical part, then you can skip ahead to the implementation. But if not, then let us discuss our choice of algorithm for this tutorial ๐Ÿ™‚

The advantage of using GCM mode over block cipher modes or block chain ciphers is because data blocks can be encrypted in parallel, while also maintaining confidentiality by using a different counter value for encrypting each block. Other block cipher modes such as a blockchain cipher require that each block to be encrypted depend on a hash value generated from encrypting the previous block. While this my be useful for integrity, it is also slow performing.

When encrypting data using AES, we need three main components:

  1. Key: An AES key can be a 128 bit, 192-bit or a 256 bit. While a larger key theoretically provides more protection as it would be less vulnerable for a brute force attack, an 128-bit key is usually enough for everyday usage. This is because it would take an astronomical amount of computing power to brute force a 128-bit key (unless a quantum computer is used…). In our example, we will use a 128-bit key.
  2. Nonce: A nonce, also called an initialization vector is a random value chosen at encryption time and is meant to be used only once. The nonce is usually sent in plain format during an encrypted transmission as decryption is virtually impossible without it. Usually, the nonce is combined with a user generated password to provide an encryption key and to perform the encryption itself. If a constant encryption key is used everytime to encrypt a transmission, an attacker might eventually deduce the key if given enough encrypted data. However, since a different key is created everytime an encryption occurs by combining the user password and a nonce, it is much harder for the attacker to decrypt a transmission or to deduce the user password.
  3. Data: The third component in this formula is the actual data.

One can visualize the process in Java via the following diagram:

A simplified diagram of the AES encryption process
A simplified diagram of the AES encryption process

Implementation

Generating an AES key

In order to perform any encryption / decryption using AES, we will first need to generate the key. Since our example will use a user chosen password and a nonce/initialization vector (iv), let us start by creating our key generation method:

    public static SecretKey generateSecretKey(String password, byte [] iv) throws NoSuchAlgorithmException, InvalidKeySpecException {
        KeySpec spec = new PBEKeySpec(password.toCharArray(), iv, 65536, 128); // AES-128
        SecretKeyFactory secretKeyFactory = SecretKeyFactory.getInstance("PBKDF2WithHmacSHA1");
        byte[] key = secretKeyFactory.generateSecret(spec).getEncoded();
        return new SecretKeySpec(key, "AES");
    }

Here we use a PBE key spec to generate a 128-bit key. This is required in order to generate a fixed length key. Since the password provided by the user can vary in length, we will need to make sure that we always have a 128-bit key. Otherwise, the key will be rejected by the encryption algorithm.

Encrypting a byte array

Now, let us start with the process of encrypting the byte array. To start the encryption, we need the three components mentioned previously. Let us start by generating the nonce.

        //Prepare the nonce
        SecureRandom secureRandom = new SecureRandom();

        //Noonce should be 12 bytes
        byte[] iv = new byte[12];
        secureRandom.nextBytes(iv);

The SecureRandom class provides us with random values to be used for generating the initialization vector. The GCM specification recommends a 12 byte nonce. Therefore, we choose to create a 12 byte array for the job. Our next step is to generate our encryption key and perform the encryption.

        //Prepare your key/password
        SecretKey secretKey = generateSecretKey(key, iv);


        Cipher cipher = Cipher.getInstance("AES/GCM/NoPadding");
        GCMParameterSpec parameterSpec = new GCMParameterSpec(128, iv);

        //Encryption mode on!
        cipher.init(Cipher.ENCRYPT_MODE, secretKey, parameterSpec);

        //Encrypt the data
        byte [] encryptedData = cipher.doFinal(data);

The Cipher class can perform different types of encryption/decryption procedures. Here, we configured our instance for AES + GCM encryption. The cipher.doFinal(data) call takes-in the plain text data byte array and returns the encrypted array.

Note that the encrypted array does not include the nonce or the nonce size. Therefore we will need to generate another byte array with the nonce and the nonce size prepended to it. This can be achieved using a ByteBuffer. Also notice the Cipher.ENCRYPT_MODE which we selected during initialization. You can find the complete encryption function in the next subsection.

Decrypting a byte array

To decrypt a byte array, we basically need to perform the same steps as in the encrypt function, but this time, we chose the Cipher.DECRYPT_MODE. The only extra steps that we need to perform is to extract the nonce and the nonce length from the beginning of the byte array.

        //Wrap the data into a byte buffer to ease the reading process
        ByteBuffer byteBuffer = ByteBuffer.wrap(encryptedData);

        int noonceSize = byteBuffer.getInt();

        //Make sure that the file was encrypted properly
        if(noonceSize < 12 || noonceSize >= 16) {
            throw new IllegalArgumentException("Nonce size is incorrect. Make sure that the incoming data is an AES encrypted file.");
        }
        byte[] iv = new byte[noonceSize];
        byteBuffer.get(iv);

Once we have the initialization vector, we can simply combine it with the user’s password to generate the secret key and begin our decryption process. Below is our complete AESEncryptionManager example class:

package com.nullbeans;

import javax.crypto.*;
import javax.crypto.spec.GCMParameterSpec;
import javax.crypto.spec.PBEKeySpec;
import javax.crypto.spec.SecretKeySpec;
import java.nio.ByteBuffer;
import java.security.InvalidAlgorithmParameterException;
import java.security.InvalidKeyException;
import java.security.NoSuchAlgorithmException;
import java.security.SecureRandom;
import java.security.spec.InvalidKeySpecException;
import java.security.spec.KeySpec;

/**
 * Encryption / Decryption service using the AES algorithm
 * example for nullbeans.com
 */
public class AESEncryptionManager {

    /**
     * This method will encrypt the given data
     * @param key : the password that will be used to encrypt the data
     * @param data : the data that will be encrypted
     * @return Encrypted data in a byte array
     */
    public static byte [] encryptData(String key, byte [] data) throws NoSuchPaddingException,
            NoSuchAlgorithmException,
            InvalidAlgorithmParameterException,
            InvalidKeyException,
            BadPaddingException,
            IllegalBlockSizeException, InvalidKeySpecException {

        //Prepare the nonce
        SecureRandom secureRandom = new SecureRandom();

        //Noonce should be 12 bytes
        byte[] iv = new byte[12];
        secureRandom.nextBytes(iv);

        //Prepare your key/password
        SecretKey secretKey = generateSecretKey(key, iv);


        Cipher cipher = Cipher.getInstance("AES/GCM/NoPadding");
        GCMParameterSpec parameterSpec = new GCMParameterSpec(128, iv);

        //Encryption mode on!
        cipher.init(Cipher.ENCRYPT_MODE, secretKey, parameterSpec);

        //Encrypt the data
        byte [] encryptedData = cipher.doFinal(data);

        //Concatenate everything and return the final data
        ByteBuffer byteBuffer = ByteBuffer.allocate(4 + iv.length + encryptedData.length);
        byteBuffer.putInt(iv.length);
        byteBuffer.put(iv);
        byteBuffer.put(encryptedData);
        return byteBuffer.array();
    }


    public static byte [] decryptData(String key, byte [] encryptedData) 
            throws NoSuchPaddingException, 
            NoSuchAlgorithmException, 
            InvalidAlgorithmParameterException, 
            InvalidKeyException, 
            BadPaddingException, 
            IllegalBlockSizeException, 
            InvalidKeySpecException {


        //Wrap the data into a byte buffer to ease the reading process
        ByteBuffer byteBuffer = ByteBuffer.wrap(encryptedData);

        int noonceSize = byteBuffer.getInt();

        //Make sure that the file was encrypted properly
        if(noonceSize < 12 || noonceSize >= 16) {
            throw new IllegalArgumentException("Nonce size is incorrect. Make sure that the incoming data is an AES encrypted file.");
        }
        byte[] iv = new byte[noonceSize];
        byteBuffer.get(iv);

        //Prepare your key/password
        SecretKey secretKey = generateSecretKey(key, iv);

        //get the rest of encrypted data
        byte[] cipherBytes = new byte[byteBuffer.remaining()];
        byteBuffer.get(cipherBytes);

        Cipher cipher = Cipher.getInstance("AES/GCM/NoPadding");
        GCMParameterSpec parameterSpec = new GCMParameterSpec(128, iv);

        //Encryption mode on!
        cipher.init(Cipher.DECRYPT_MODE, secretKey, parameterSpec);

        //Encrypt the data
        return cipher.doFinal(cipherBytes);

    }

    /**
     * Function to generate a 128 bit key from the given password and iv
     * @param password
     * @param iv
     * @return Secret key
     * @throws NoSuchAlgorithmException
     * @throws InvalidKeySpecException
     */
    public static SecretKey generateSecretKey(String password, byte [] iv) throws NoSuchAlgorithmException, InvalidKeySpecException {
        KeySpec spec = new PBEKeySpec(password.toCharArray(), iv, 65536, 128); // AES-128
        SecretKeyFactory secretKeyFactory = SecretKeyFactory.getInstance("PBKDF2WithHmacSHA1");
        byte[] key = secretKeyFactory.generateSecret(spec).getEncoded();
        return new SecretKeySpec(key, "AES");
    }

}

Encrypting and decrypting files

At the beginning of our example, we mentioned that our goal is to be able to encrypt and decrypt files using our own written Java program. So let us start by defining our main class and run an example:

public class Main {

    public static void main(String[] args) throws NoSuchPaddingException, InvalidKeyException, NoSuchAlgorithmException, IllegalBlockSizeException, BadPaddingException, InvalidAlgorithmParameterException, InvalidKeySpecException, IOException {
	// write your code here

        String file = args[0];
        String destFile = args[1];
        String mode = args[2];
        String password = args[3];

        byte[] fileBytes = NullbeansFileManager.readFile(file);
        byte[] resultBytes = null;

        if(mode.equalsIgnoreCase("encrypt")){
            resultBytes = AESEncryptionManager.encryptData(password, fileBytes);
        }else {
            resultBytes = AESEncryptionManager.decryptData(password, fileBytes);
        }

        NullbeansFileManager.writeFile(destFile, resultBytes);

    }

}

Let us run our example with the following plain text file as our plain text data.

plain text file
plain text file


Our arguments will include the file “PlainText.txt” as the input file, “EncryptedText.txt” as the output file, the “encrypt” argument and the password “GreatePass”. If we open the encrypted file it will look like this:

An example of AES encrypted text
An example of AES encrypted text


As you can see, the data has been encrypted successfully. Now let us try to decrypt the file. As AES is a symmetric encryption algorithm, we will need to provide the same password in order to decrypt the file. The output file looks as follows:

How the text looks like after decryption
How the text looks like after decryption

As our decryption result is the same as the plainText file, we can verify that our encryption/decryption process has been performed successfully.ย  Please feel free to try our example with different key lengths and different passwords.

Decrypting using the wrong password

So what if we tried to insert the wrong password. What would happen? Well, we tried it out, and this was the output:

Exception in thread "main" javax.crypto.AEADBadTagException: Tag mismatch!
	at java.base/com.sun.crypto.provider.GaloisCounterMode.decryptFinal(GaloisCounterMode.java:580)
	at java.base/com.sun.crypto.provider.CipherCore.finalNoPadding(CipherCore.java:1116)
	at java.base/com.sun.crypto.provider.CipherCore.fillOutputBuffer(CipherCore.java:1053)
	at java.base/com.sun.crypto.provider.CipherCore.doFinal(CipherCore.java:853)
	at java.base/com.sun.crypto.provider.AESCipher.engineDoFinal(AESCipher.java:446)
	at java.base/javax.crypto.Cipher.doFinal(Cipher.java:2202)
	at com.nullbeans.AESEncryptionManager.decryptData(AESEncryptionManager.java:92)
	at com.nullbeans.Main.main(Main.java:28)

This exception indicates that the Cipher could not verify the given Authentication tag. In other words, the combination of password and nonce did not match the one used to encrypt the file, and therefore the decryption process fails. So if your user tried to insert the wrong passwords, you will know ๐Ÿ˜‰

Summary

In this tutorial, we discussed how to read and write a file from the filesystem into a byte array. We then discussed how to encrypt and decrypt data using AES in GCM mode. Thank you for reading ๐Ÿ™‚