December 22, 2017

Choosing Java Cryptographic Algorithms Part 2 - Single key symmetric encryption

Abstract

This is the 2nd of a three-part blog series covering Java cryptographic algorithms. The series covers how to implement the following:

  1. Hashing with SHA-512
  2. Single-key symmetric encryption with AES-256
  3. Public/Private key asymmetric encryption with RSA-4096

This 2nd post details how to implement single key, symmetric, AES-256 encryption. Let’s get started.

Disclaimer

This post is solely informative. Critically think before using any information presented. Learn from it but ultimately make your own decisions at your own risk.

Requirements

I did all of the work for this post using the following major technologies. You may be able to do the same thing with different technologies or versions, but no guarantees.

NOTE As of Java 1.8.0_161, unlimited cryptography is enabled by default. This means if you are using Java 1.8.0_161 or later, you do not need to install Java Cryptography Extension (JCE) Unlimited Strength separately. See the Java 1.8.0_161 release notes

Acknowledgments

Thanks to Peter Jakobsen. In March 2022, he identified an error in my Aes#encrypt() method. The error caused encryption/decryption process to fail when the string being encrypted was greater than 15 characters long. I did not have unit test for this case.

At the time, I did not have availability to look into this problem. Peter posted the question to https://stackoverflow.com/questions/71422498/javax-crypto-aeadbadtagexception-tag-mismatch-when-password-length-16. Additional thanks to Topaco on Stack Overflow for providing comments.

In November 2022, I was able to update my Aes class to address the problem Peter identified. Using the Template Method Design Pattern, I now have two sub-classes of Aes demonstrating both the multiple-part and single-part encryption operations. I have updated this blog to reflect these new classes.

Download

Visit my GitHub Page to see all of my open source projects. The code for this post is located in project: thoth-cryptography

Symmetric Encryption

About

Symmetric encryption algorithms are based on a single key. This one key is used for both encryption and decryption. As such, symmetric algorithms should only be used where strict controls are in place to protect the key.

Symmetric algorithms are commonly used for encryption and decryption of data in secured environments. A good example of this is securing Microservice communication. If an OAuth-2/JWT architecture is out of scope, the API Gateway can use a symmetric algorithm’s single key to encrypt a token. This token is then passed to other Microservices. The other Microservices use the same key to decrypt token. Another good example are hyperlinks embedded in emails. The hyperlinks in emails contain an encoded token which allow automatic login request processing when the hyperlink is clicked. This token is a strongly encrypted value generated by a symmetric algorithm so it can only be decoded on the application server. And of course, anytime passwords or credentials of any kind need to be protected, a symmetric algorithm is used to encrypt them and the bytes can later be decrypted with the same key.

Research done as of today seems to indicate the best and most secure single key, symmetric, encryption algorithm is the following (Sheth, 2017, “Choosing the correct algorithm”, para.2):

  1. Algorithm: AES
  2. Mode: GCM
  3. Padding: PKCS5Padding
  4. Key size: 256 bit
  5. IV size: 96 bit

AES-256 uses a 256-bit key which requires installation of the Java Cryptography Extension (JCE) Unlimited Strength package. Let’s take a look at an example.

NOTE The Java Cryptography Extension (JCE) Unlimited Strength package is required for 256-bit keys. If it’s not installed, 128-bit keys are the max.

Example

If you don’t already have it, download and install the Java Cryptography Extension (JCE) Unlimited Strength package. It is required to use 256-bit keys. Otherwise, the example below must be updated to use a 128-bit key.

Listing 1 and Listing 2 are the unit tests AesUsingSinglePartEncryptionTest.java and AesUsingMultiplePartEncryptionTest.java. Both unit tests are full demonstrations of the following:

  1. Generate and store an AES 256-bit key
  2. AES Encryption
  3. AES Decryption

One demonstrates multiple-part encryption and the other demonstrates single-part encryption. The difference between the two is how the Cipher#update and Cipher#doFinal methods are used to get the encrypted bytes.

Listing 3 shows AesSecretKeyProducer.java. This is a helper class which is responsible for producing a new key or reproducing an existing key from a byte[].

Listing 4 shows ByteArrayWriter.java and Listing 5 shows ByteArrayReader.java. These are helper classes responsible for reading and writing a byte[] to a file. It’s up to you to determine how to store the byte[] of your key, but it needs to be stored securely somewhere (file, database, git repository, etc.).

Listing 6 shows Aes.java. This is a helper class which is responsible for both encryption and decryption. It is implemented as an abstract class so it can not be instantiated directly. Instead, one of its sub-classes need to be used. There are 2 sub-classes, one to demonstrate the single-part encryption operation and the other to demonstrate the multiple-part encryption operation.

NOTE In reality, you probably won’t need an abstract class. Instead you’ll most likely create 1 class which uses the encryption operation (single-part or multiple-part) you want to use.

Listing 7 shows AesUsingSinglePartEncryption.java. This extends the Aes class and implements the getEncryptedBytes() method. Its implementation demonstrates the single-part encryption operation, which uses a single call to Cipher#doFinal(byte[]) to get the encrypted bytes.

Listing 8 shows AesUsingMultiplePartEncryption.java. This extends the Aes class and implements the getEncryptedBytes() method. Its implementation demonstrates the multiple-part encryption operation which makes multiple calls to Cipher#update(byte[]) and then a final call to Cipher#doFinal(). All the encrypted bytes are store along the way and returned after the call to Cipher#doFinal().

Listing 1 - AesUsingSinglePartEncryptionTest.java class

package org.thoth.crypto.symmetric;

import java.io.ByteArrayOutputStream;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.Arrays;
import java.util.Optional;
import javax.crypto.SecretKey;
import org.junit.Assert;
import org.junit.BeforeClass;
import org.junit.Test;
import org.thoth.crypto.io.ByteArrayReader;
import org.thoth.crypto.io.ByteArrayWriter;

/**
 *
 * @author Michael Remijan mjremijan@yahoo.com @mjremijan
 */
public class AesUsingSinglePartEncryptionTest {

    static Path secretKeyFile;

    @BeforeClass
    public static void beforeClass() throws Exception {
        // Store the SecretKey bytes in the ./target diretory. Do
        // this so it will be ignore by source control.  We don't
        // want this file committed.
        secretKeyFile
            = Paths.get("./target/Aes256.key").toAbsolutePath();

        // Generate a SecretKey for the test
        SecretKey secretKey
            = new AesSecretKeyProducer().produce();

        // Store the byte[] of the SecretKey.  This is the
        // "private key file" you want to keep safe.
        ByteArrayWriter writer = new ByteArrayWriter(secretKeyFile);
        writer.write(secretKey.getEncoded());
    }


    @Test
    public void encrypt_and_decrypt_using_same_Aes256_instance_long() {
        // setup
        SecretKey secretKey
            = new AesSecretKeyProducer().produce(
                new ByteArrayReader(secretKeyFile).read()
            );

        Aes aes
            = new AesUsingSinglePartEncryption(secretKey);

        String toEncrypt
            = "encrypt me1, encrypt me2, encrypt me3, encrypt me4, encrypt me5, encrypt me6, encrypt me7, encrypt me8, encrypt me9, encrypt me10";

        // run
        byte[] encryptedBytes
            = aes.encrypt(toEncrypt, Optional.empty());

        String decrypted
            = aes.decrypt(encryptedBytes, Optional.empty());

        // assert
        Assert.assertEquals(toEncrypt, decrypted);
    }
    
    
    @Test
    public void encrypt_and_decrypt_using_same_Aes256_instance_short() {
        // setup
        SecretKey secretKey
            = new AesSecretKeyProducer().produce(
                new ByteArrayReader(secretKeyFile).read()
            );

        Aes aes
            = new AesUsingMultiplePartEncryption(secretKey);

        String toEncrypt
            = "encrypt me";

        // run
        byte[] encryptedBytes
            = aes.encrypt(toEncrypt, Optional.empty());

        String decrypted
            = aes.decrypt(encryptedBytes, Optional.empty());

        // assert
        Assert.assertEquals(toEncrypt, decrypted);
    }


    public void encrypt_and_decrypt_with_aad_using_same_Aes256_instance_short() {
        // setup
        SecretKey secretKey
            = new AesSecretKeyProducer().produce(
                new ByteArrayReader(secretKeyFile).read()
            );

        Aes aes
            = new AesUsingMultiplePartEncryption(secretKey);

        String toEncrypt
            = "encrypt me aad";

        // run
        byte[] encryptedBytes
            = aes.encrypt(toEncrypt, Optional.of("JUnit AAD"));

        String decrypted
            = aes.decrypt(encryptedBytes, Optional.of("JUnit AAD"));

        // assert
        Assert.assertEquals(toEncrypt, decrypted);
    }
    
    public void encrypt_and_decrypt_with_aad_using_same_Aes256_instance_long() {
        // setup
        SecretKey secretKey
            = new AesSecretKeyProducer().produce(
                new ByteArrayReader(secretKeyFile).read()
            );

        Aes aes
            = new AesUsingMultiplePartEncryption(secretKey);

        String toEncrypt
            = "encrypt me aad 1, encrypt me aad 2, encrypt me aad 3, encrypt me aad 4, encrypt me aad 5, encrypt me aad 6, encrypt me aad 7, encrypt me aad 8, encrypt me aad 9, encrypt me aad 10";

        // run
        byte[] encryptedBytes
            = aes.encrypt(toEncrypt, Optional.of("JUnit AAD"));

        String decrypted
            = aes.decrypt(encryptedBytes, Optional.of("JUnit AAD"));

        // assert
        Assert.assertEquals(toEncrypt, decrypted);
    }


    @Test
    public void encrypt_and_decrypt_using_different_Aes256_instance_long()
    throws Exception {
        // setup
        SecretKey secretKey
            = new AesSecretKeyProducer().produce(
                new ByteArrayReader(secretKeyFile).read()
            );

        Aes aesForEncrypt
            = new AesUsingMultiplePartEncryption(secretKey);

        Aes aesForDecrypt
            = new AesUsingMultiplePartEncryption(secretKey);

        String toEncrypt
            = "encrypt me1, encrypt me2, encrypt me3, encrypt me4, encrypt me5, encrypt me6, encrypt me7, encrypt me8, encrypt me9, encrypt me10";

        // run
        byte[] encryptedBytes
            = aesForEncrypt.encrypt(toEncrypt, Optional.empty());

        ByteArrayOutputStream baos
            = new ByteArrayOutputStream();
        baos.write(encryptedBytes);

        String decrypted
            = aesForDecrypt.decrypt(baos.toByteArray(), Optional.empty());

        // assert
        Assert.assertEquals(toEncrypt, decrypted);
    }
    
    
    @Test
    public void encrypt_and_decrypt_using_different_Aes256_instance_short()
    throws Exception {
        // setup
        SecretKey secretKey
            = new AesSecretKeyProducer().produce(
                new ByteArrayReader(secretKeyFile).read()
            );

        Aes aesForEncrypt
            = new AesUsingMultiplePartEncryption(secretKey);

        Aes aesForDecrypt
            = new AesUsingMultiplePartEncryption(secretKey);

        String toEncrypt
            = "eNcryPt Me";

        // run
        byte[] encryptedBytes
            = aesForEncrypt.encrypt(toEncrypt, Optional.empty());

        ByteArrayOutputStream baos
            = new ByteArrayOutputStream();
        baos.write(encryptedBytes);

        String decrypted
            = aesForDecrypt.decrypt(baos.toByteArray(), Optional.empty());

        // assert
        Assert.assertEquals(toEncrypt, decrypted);
    }
    
    @Test
    public void foo() {
        String s = "ABC";
        int chunkSize = 5;
        String[] chunks = s.split("(?<=\\G.{" + chunkSize + "})");
        System.out.println(Arrays.toString(chunks));
    }
}

Listing 2 - AesUsingMultiplePartEncryptionTest.java class

package org.thoth.crypto.symmetric;

import java.io.ByteArrayOutputStream;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.Arrays;
import java.util.Optional;
import javax.crypto.SecretKey;
import org.junit.Assert;
import org.junit.BeforeClass;
import org.junit.Test;
import org.thoth.crypto.io.ByteArrayReader;
import org.thoth.crypto.io.ByteArrayWriter;

/**
 *
 * @author Michael Remijan mjremijan@yahoo.com @mjremijan
 */
public class AesUsingMultiplePartEncryptionTest {

    static Path secretKeyFile;

    @BeforeClass
    public static void beforeClass() throws Exception {
        // Store the SecretKey bytes in the ./target diretory. Do
        // this so it will be ignore by source control.  We don't
        // want this file committed.
        secretKeyFile
            = Paths.get("./target/Aes256.key").toAbsolutePath();

        // Generate a SecretKey for the test
        SecretKey secretKey
            = new AesSecretKeyProducer().produce();

        // Store the byte[] of the SecretKey.  This is the
        // "private key file" you want to keep safe.
        ByteArrayWriter writer = new ByteArrayWriter(secretKeyFile);
        writer.write(secretKey.getEncoded());
    }


    @Test
    public void encrypt_and_decrypt_using_same_Aes256_instance_long() {
        // setup
        SecretKey secretKey
            = new AesSecretKeyProducer().produce(
                new ByteArrayReader(secretKeyFile).read()
            );

        Aes aes
            = new AesUsingMultiplePartEncryption(secretKey);

        String toEncrypt
            = "encrypt me1, encrypt me2, encrypt me3, encrypt me4, encrypt me5, encrypt me6, encrypt me7, encrypt me8, encrypt me9, encrypt me10";

        // run
        byte[] encryptedBytes
            = aes.encrypt(toEncrypt, Optional.empty());

        String decrypted
            = aes.decrypt(encryptedBytes, Optional.empty());

        // assert
        Assert.assertEquals(toEncrypt, decrypted);
    }
    
    
    @Test
    public void encrypt_and_decrypt_using_same_Aes256_instance_short() {
        // setup
        SecretKey secretKey
            = new AesSecretKeyProducer().produce(
                new ByteArrayReader(secretKeyFile).read()
            );

        Aes aes
            = new AesUsingMultiplePartEncryption(secretKey);

        String toEncrypt
            = "encrypt me";

        // run
        byte[] encryptedBytes
            = aes.encrypt(toEncrypt, Optional.empty());

        String decrypted
            = aes.decrypt(encryptedBytes, Optional.empty());

        // assert
        Assert.assertEquals(toEncrypt, decrypted);
    }


    public void encrypt_and_decrypt_with_aad_using_same_Aes256_instance_short() {
        // setup
        SecretKey secretKey
            = new AesSecretKeyProducer().produce(
                new ByteArrayReader(secretKeyFile).read()
            );

        Aes aes
            = new AesUsingMultiplePartEncryption(secretKey);

        String toEncrypt
            = "encrypt me aad";

        // run
        byte[] encryptedBytes
            = aes.encrypt(toEncrypt, Optional.of("JUnit AAD"));

        String decrypted
            = aes.decrypt(encryptedBytes, Optional.of("JUnit AAD"));

        // assert
        Assert.assertEquals(toEncrypt, decrypted);
    }
    
    public void encrypt_and_decrypt_with_aad_using_same_Aes256_instance_long() {
        // setup
        SecretKey secretKey
            = new AesSecretKeyProducer().produce(
                new ByteArrayReader(secretKeyFile).read()
            );

        Aes aes
            = new AesUsingMultiplePartEncryption(secretKey);

        String toEncrypt
            = "encrypt me aad 1, encrypt me aad 2, encrypt me aad 3, encrypt me aad 4, encrypt me aad 5, encrypt me aad 6, encrypt me aad 7, encrypt me aad 8, encrypt me aad 9, encrypt me aad 10";

        // run
        byte[] encryptedBytes
            = aes.encrypt(toEncrypt, Optional.of("JUnit AAD"));

        String decrypted
            = aes.decrypt(encryptedBytes, Optional.of("JUnit AAD"));

        // assert
        Assert.assertEquals(toEncrypt, decrypted);
    }


    @Test
    public void encrypt_and_decrypt_using_different_Aes256_instance_long()
    throws Exception {
        // setup
        SecretKey secretKey
            = new AesSecretKeyProducer().produce(
                new ByteArrayReader(secretKeyFile).read()
            );

        Aes aesForEncrypt
            = new AesUsingMultiplePartEncryption(secretKey);

        Aes aesForDecrypt
            = new AesUsingMultiplePartEncryption(secretKey);

        String toEncrypt
            = "encrypt me1, encrypt me2, encrypt me3, encrypt me4, encrypt me5, encrypt me6, encrypt me7, encrypt me8, encrypt me9, encrypt me10";

        // run
        byte[] encryptedBytes
            = aesForEncrypt.encrypt(toEncrypt, Optional.empty());

        ByteArrayOutputStream baos
            = new ByteArrayOutputStream();
        baos.write(encryptedBytes);

        String decrypted
            = aesForDecrypt.decrypt(baos.toByteArray(), Optional.empty());

        // assert
        Assert.assertEquals(toEncrypt, decrypted);
    }
    
    
    @Test
    public void encrypt_and_decrypt_using_different_Aes256_instance_short()
    throws Exception {
        // setup
        SecretKey secretKey
            = new AesSecretKeyProducer().produce(
                new ByteArrayReader(secretKeyFile).read()
            );

        Aes aesForEncrypt
            = new AesUsingMultiplePartEncryption(secretKey);

        Aes aesForDecrypt
            = new AesUsingMultiplePartEncryption(secretKey);

        String toEncrypt
            = "eNcryPt Me";

        // run
        byte[] encryptedBytes
            = aesForEncrypt.encrypt(toEncrypt, Optional.empty());

        ByteArrayOutputStream baos
            = new ByteArrayOutputStream();
        baos.write(encryptedBytes);

        String decrypted
            = aesForDecrypt.decrypt(baos.toByteArray(), Optional.empty());

        // assert
        Assert.assertEquals(toEncrypt, decrypted);
    }
    
    @Test
    public void foo() {
        String s = "ABC";
        int chunkSize = 5;
        String[] chunks = s.split("(?<=\\G.{" + chunkSize + "})");
        System.out.println(Arrays.toString(chunks));
    }
}

Listing 3 - AesSecretKeyProducer.java class

package org.thoth.crypto.symmetric;

import javax.crypto.KeyGenerator;
import javax.crypto.SecretKey;
import javax.crypto.spec.SecretKeySpec;

/**
 *
 * @author Michael Remijan mjremijan@yahoo.com @mjremijan
 */
public class AesSecretKeyProducer {

    /**
     * Generates a new AES-256 bit {@code SecretKey}.
     *
     * @return {@code SecretKey}, never null
     * @throws RuntimeException All exceptions are caught and re-thrown as {@code RuntimeException}
     */
    public SecretKey produce() {
        KeyGenerator keyGen;
        try {
            keyGen = KeyGenerator.getInstance("AES");
            keyGen.init(256);
            SecretKey secretKey = keyGen.generateKey();
            return secretKey;
        } catch (Exception ex) {
            throw new RuntimeException(ex);
        }
    }


    /**
     * Generates an AES-256 bit {@code SecretKey}.
     *
     * @param encodedByteArray The bytes this method will use to regenerate a previously created {@code SecretKey}
     *
     * @return {@code SecretKey}, never null
     * @throws RuntimeException All exceptions are caught and re-thrown as {@code RuntimeException}
     */
    public SecretKey produce(byte [] encodedByteArray) {
        try {
            return new SecretKeySpec(encodedByteArray, "AES");
        } catch (Exception ex) {
            throw new RuntimeException(ex);
        }
    }
}

Listing 4 - ByteArrayWriter.java class

package org.thoth.crypto.io;

import java.io.IOException;
import java.io.OutputStream;
import java.io.PrintWriter;
import java.nio.file.Files;
import java.nio.file.Path;

/**
 *
 * @author Michael Remijan mjremijan@yahoo.com @mjremijan
 */
public class ByteArrayWriter {

    protected Path outputFile;

    private void initOutputFile(Path outputFile) {
        this.outputFile = outputFile;
    }

    private void initOutputDirectory() {
        Path outputDirectory = outputFile.getParent();
        if (!Files.exists(outputDirectory)) {
            try {
                Files.createDirectories(outputDirectory);
            } catch (IOException e) {
                throw new RuntimeException(e);
            }
        }
    }

    public ByteArrayWriter(Path outputFile) {
        initOutputFile(outputFile);
        initOutputDirectory();
    }

    public void write(byte[] bytesArrayToWrite) {
        try (
            OutputStream os
                = Files.newOutputStream(outputFile);

            PrintWriter writer
                =  new PrintWriter(os);
        ){
            for (int i=0; i<bytesArrayToWrite.length; i++) {
                if (i>0) {
                    writer.println();
                }
                writer.print(bytesArrayToWrite[i]);
            }
        } catch (IOException ex) {
            throw new RuntimeException(ex);
        }
    }
}

Listing 5 - ByteArrayReader.java class

package org.thoth.crypto.io;

import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.nio.file.Path;
import java.util.Scanner;

/**
 *
 * @author Michael Remijan mjremijan@yahoo.com @mjremijan
 */
public class ByteArrayReader {

    protected Path inputFile;

    public ByteArrayReader(Path inputFile) {
        this.inputFile = inputFile;
    }

    public byte[] read() {
        try (
            Scanner scanner
                =  new Scanner(inputFile);

            ByteArrayOutputStream baos
                = new ByteArrayOutputStream();
        ){
            while (scanner.hasNext()) {
                baos.write(Byte.parseByte(scanner.nextLine()));
            }
            
            baos.flush();
            return baos.toByteArray();

        } catch (IOException ex) {
            throw new RuntimeException(ex);
        }
    }
}

Listing 6 - Aes.java class

package org.thoth.crypto.symmetric;

import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.security.SecureRandom;
import java.util.Optional;
import javax.crypto.BadPaddingException;
import javax.crypto.Cipher;
import javax.crypto.IllegalBlockSizeException;
import javax.crypto.SecretKey;
import javax.crypto.spec.GCMParameterSpec;

/**
 *
 * @author Michael Remijan mjremijan@yahoo.com @mjremijan
 */
public abstract class Aes {

    // If you don't have the Java Cryptography Extension
    // (JCE) Unlimited Strength packaged installed, use
    // a 128 bit KEY_SIZE.
    public static int KEY_SIZE = 256;
    
    public static int IV_SIZE = 12; // 12bytes * 8 = 96bits
    public static int TAG_BIT_SIZE = 128;
    public static String ALGORITHM_NAME = "AES";
    public static String MODE_OF_OPERATION = "GCM";
    public static String PADDING_SCHEME = "PKCS5Padding";

    protected SecretKey secretKey;
    protected SecureRandom secureRandom;

    protected Aes(SecretKey secretKey) {
        this.secretKey = secretKey;
        this.secureRandom = new SecureRandom();
    }


    public byte[] encrypt(String message, Optional<String> aad) {
        try {
            // Transformation specifies algortihm, mode of operation and padding
            Cipher c = Cipher.getInstance(
                String.format("%s/%s/%s",ALGORITHM_NAME,MODE_OF_OPERATION,PADDING_SCHEME)
            );

            // Generate IV
            byte iv[] = new byte[IV_SIZE];
            secureRandom.nextBytes(iv); // SecureRandom initialized using self-seeding

            // Initialize GCM Parameters
            GCMParameterSpec spec = new GCMParameterSpec(TAG_BIT_SIZE, iv);

            // Init for encryption
            c.init(Cipher.ENCRYPT_MODE, secretKey, spec, secureRandom);

            // Add AAD tag data if present
            aad.ifPresent(t -> {
                try {
                    c.updateAAD(t.getBytes("UTF-8"));
                } catch (Exception e) {
                    throw new RuntimeException(e);
                }
            });

            // I demonstrate 2 different ways of getting the
            // encrypted bytes. See the 2 sub-classes which
            // implement the method of this abstract class.
            ByteArrayOutputStream baos = getEncryptedBytes(message, c);
            
            // Concatinate IV and encrypted bytes.  The IV is needed later
            // in order to to decrypt.  The IV value does not need to be
            // kept secret, so it's OK to encode it in the return value
            //
            // Create a new byte[] the combined length of IV and encryptedBytes
            byte[] ivPlusEncryptedBytes = new byte[iv.length + baos.size()];
            // Copy IV bytes into the new array
            System.arraycopy(iv, 0, ivPlusEncryptedBytes, 0, iv.length);
            // Copy encryptedBytes into the new array
            System.arraycopy(baos.toByteArray(), 0, ivPlusEncryptedBytes, iv.length, baos.size());

            // Return
            return ivPlusEncryptedBytes;

        } catch (Exception e) {
            throw new RuntimeException(e);
        }
    }


    public String decrypt(byte[] ivPlusEncryptedBytes, Optional<String> aad) {

        try {
            // Get IV
            byte iv[] = new byte[IV_SIZE];
            System.arraycopy(ivPlusEncryptedBytes, 0, iv, 0, IV_SIZE);

            // Initialize GCM Parameters
            GCMParameterSpec spec = new GCMParameterSpec(TAG_BIT_SIZE, iv);

            // Transformation specifies algortihm, mode of operation and padding
            Cipher c = Cipher.getInstance(
                String.format("%s/%s/%s",ALGORITHM_NAME,MODE_OF_OPERATION,PADDING_SCHEME)
            );

            // Get encrypted bytes
            byte [] encryptedBytes = new byte[ivPlusEncryptedBytes.length - IV_SIZE];
            System.arraycopy(ivPlusEncryptedBytes, IV_SIZE, encryptedBytes, 0, encryptedBytes.length);

            // Init for decryption
            c.init(Cipher.DECRYPT_MODE, secretKey, spec, secureRandom);

            // Add AAD tag data if present
            aad.ifPresent(t -> {
                try {
                    c.updateAAD(t.getBytes("UTF-8"));
                } catch (Exception e) {
                    throw new RuntimeException(e);
                }
            });

            // Add message to decrypt
            c.update(encryptedBytes);

            // Decrypt
            byte[] decryptedBytes
                = c.doFinal();

            // Return
            return new String(decryptedBytes, "UTF-8");

        } catch (Exception e) {
            throw new RuntimeException(e);
        }
    }
    
    
    /**
     * An abstract method to be implemented by a subclass following the 
     * Gang of four <a href="https://www.digitalocean.com/community/tutorials/template-method-design-pattern-in-java">Template Method Design Pattern</a>.
     * This method is to use the {@link Cipher} provided to return the 
     * encrypted bytes of the {@link message} parameter.
     * 
     * @param message The String to be encrypted.
     * @param cipher  The Cipher object used to encrypt the message.
     * @return
     * @throws BadPaddingException
     * @throws IllegalBlockSizeException
     * @throws IOException 
     */
    abstract ByteArrayOutputStream getEncryptedBytes(String message, Cipher cipher) throws BadPaddingException, IllegalBlockSizeException, IOException;
}

Listing 7 - AesUsingSinglePartEncryption.java class

package org.thoth.crypto.symmetric;

import java.io.ByteArrayOutputStream;
import java.io.IOException;
import javax.crypto.BadPaddingException;
import javax.crypto.Cipher;
import javax.crypto.IllegalBlockSizeException;
import javax.crypto.SecretKey;

/**
 *
 * @author Michael Remijan mjremijan@yahoo.com @mjremijan
 */
public class AesUsingSinglePartEncryption extends Aes {


    public AesUsingSinglePartEncryption(SecretKey secretKey) {
        super(secretKey);
    }
    

    /**
     * This method demonstrates how to perform a  single-part encryption
     * operation by using the {@link Cipher#doFinal(byte[]) } method to 
     * get all of the encrypted bytes.
     * 
     * @param message The message to encrypt.
     * @param c The {@link Cipher} used to get the encrypted bytes.
     * @return
     * @throws BadPaddingException
     * @throws IllegalBlockSizeException
     * @throws IOException 
     */
    @Override
    ByteArrayOutputStream getEncryptedBytes(String message, Cipher c) throws BadPaddingException, IllegalBlockSizeException, IOException {
        // Create output array to hold all the encrypted bytes
        ByteArrayOutputStream baos = new ByteArrayOutputStream();
         
        // Perform single-part encryption.
        baos.write(
            c.doFinal(message.getBytes("UTF-8"))
        );
        
        return baos;
    }
}

Listing 8 - AesUsingMultiplePartEncryption.java class

package org.thoth.crypto.symmetric;

import java.io.ByteArrayOutputStream;
import java.io.IOException;
import javax.crypto.BadPaddingException;
import javax.crypto.Cipher;
import javax.crypto.IllegalBlockSizeException;
import javax.crypto.SecretKey;

/**
 *
 * @author Michael Remijan mjremijan@yahoo.com @mjremijan
 */
public class AesUsingMultiplePartEncryption extends Aes {


    public AesUsingMultiplePartEncryption(SecretKey secretKey) {
        super(secretKey);
    }
    

    /**
     * This method demonstrates how to perform a  multiple-part encryption
     * operation by using the {@link Cipher#update(byte[]) } and the 
     * {@link Cipher#doFinal() } methods to get all of the encrypted
     * bytes. This method <b>simulates</b> the operation by dividing the
     * {@link message} parameter into many strings and operating on those
     * strings individually. In reality you would probably never do this, but
     * it's an easy way to demonstrate to perform a  multiple-part encryption.
     * 
     * @param message The message to encrypt. This method will <b>simulate</b>
     * multiple-part encryption by <b>unnecessarily</b> dividing this string into many strings.
     * @param c The {@link Cipher} used to get the encrypted bytes.
     * @return
     * @throws BadPaddingException
     * @throws IllegalBlockSizeException
     * @throws IOException 
     */
    @Override
    ByteArrayOutputStream getEncryptedBytes(String message, Cipher c) throws BadPaddingException, IllegalBlockSizeException, IOException {
        // Create output array to hold all the encrypted bytes
        ByteArrayOutputStream baos = new ByteArrayOutputStream();
        
        // Chunk the message into many strings to simulate multiple-part encryption
        int chunkSize = 5;
        String[] chunks = message.split("(?<=\\G.{" + chunkSize + "})");
        
        // Add each chunk to a multiple-part encryption.
        // Don't forget to collect the encypted bytes
        // along the way!
        for (String chunk : chunks) {
            baos.write(
                c.update(chunk.getBytes("UTF-8"))
            );
        }
        
        // Finish multi-part encryption. Again,
        // Don't forget to collect the encypted bytes
        // along the way!
        baos.write(
            c.doFinal()
        );
        
        return baos;
    }
}

Summary

Encryption isn’t easy. And easy examples will result in implementations with security vulnerabilities for your application. If you need a single key, symmetric, encryption algorithm, use cipher AES/GCM/PKCS5Padding with a 256 bit key and a 96 bit IV.

References

Java Cryptography Extension (JCE) Unlimited Strength. (n.d.). Retrieved from http://www.oracle.com/technetwork/java/javase/downloads/jce8-download-2133166.html.

Sheth, M. (2017, April 18). Encryption and Decryption in Java Cryptography. Retrieved from https://www.veracode.com/blog/research/encryption-and-decryption-java-cryptography.

cpast[ Says GCM IV is 96bit which is 96/8 = 12 bytes]. (2015, June 4). Encrypting using AES-256, can I use 256 bits IV [Web log comment]. Retrieved from https://security.stackexchange.com/questions/90848/encrypting-using-aes-256-can-i-use-256-bits-iv.

Bodewes[ Says GCM IV is strongly recommended to be 12 bytes (12*8 = 96) but can be of any size. Other sizes will require additional calculations], M. (2015, July 7). Ciphertext and tag size and IV transmission with AES in GCM mode [Web log comment]. Retrieved from https://crypto.stackexchange.com/questions/26783/ciphertext-and-tag-size-and-iv-transmission-with-aes-in-gcm-mode.

Figlesquidge. (2013, October 18). What’s the difference between a ‘cipher’ and a ‘mode of operation’? [Web log comment]. Retrieved from https://crypto.stackexchange.com/questions/11132/what-is-the-difference-between-a-cipher-and-a-mode-of-operation.

Toust, S. (2013, February 4). Why does the recommended key size between symmetric and asymmetric encryption differ greatly?. Retrieved from https://crypto.stackexchange.com/questions/6236/why-does-the-recommended-key-size-between-symmetric-and-asymmetric-encryption-di.

Karonen, I. (2012, October 5). What is the main difference between a key, an IV and a nonce?. Retrieved from https://crypto.stackexchange.com/questions/3965/what-is-the-main-difference-between-a-key-an-iv-and-a-nonce.

Block cipher mode of operation. (2017, November 6). Wikipedia. Retrieved from https://en.wikipedia.org/wiki/Block_cipher_mode_of_operation#Initialization_vector_.28IV.29

Jakobsen, Peter. (2022, March 10). javax.crypto.AEADBadTagException: Tag mismatch! when password.length >= 16 - Stack Overflow [Web log post]. Retrieved from https://stackoverflow.com/questions/71422498/javax-crypto-aeadbadtagexception-tag-mismatch-when-password-length-16.