Exercise 6: Symmetric Ciphers in Java

This exercise explores a good way of using symmetric ciphers in Java. Specifically, it considers AEAD: Authenticated Encryption With Associated Data. AEAD combines a symmetric cipher with a MAC, thereby providing protection against alteration of the ciphertext. The MAC is computed over the ciphertext plus, optionally, some arbitrary associated data. This can be useful in scenarios where it is not necessary or desirable to encrypt some of the data, yet guarantees that it hasn’t been altered are still needed.

We use Google’s Tink library, as it provides explicit support for AEAD, via a high-level API that is substantially cleaner and more user-friendly than what is provided by the Java standard library.

Setting Up

If working on your own PC, you will need to have a recent release of the Java Development Kit installed.

Download tink-symcipher.zip and unzip it. This should create a directory named tink-symcipher. You can work from the command line inside this directory or, if you prefer IDEs, open the directory as a new IntelliJ Java project. The rest of these instructions assume the use of the command line.

Either way, Gradle is used to manage downloading of dependencies and the compilation & execution of the code - see the README file for further information on this. Note that you do not need to install Gradle yourself first; the only prerequisite for this exercise is a properly installed JDK. If you are working from the command line, you will need to make sure that your PATH variable is set up so that the javac and java commands are accessible.

Key Creation

  1. We have provided a complete program to generate a cipher key, in the file CreateKey.java. You’ll find this file and the other source code files in the src/main/java subdirectory. Spend a few minutes studying the code of CreateKey in a text editor or your IDE. Note, in particular, how the key is created:

    KeysetHandle key = KeysetHandle.generateNew(
      KeyTemplates.get("AES128_GCM"));
    

    This creates the key material from a template that specifies the desired cipher - in this case, AES-GCM (AES in Galois/Counter Mode), with a 128-bit key. This is the only place anywhere in the code where we need to be specific about our choice of cipher! For further information about this, see the Tink API docs.

  2. Compile and run the program like so1:

    ./gradlew createkey
    

    This will output the key to the file build/key.json. Take a moment to examine the contents of this file. The file consists of minified JSON data, which is easier to parse if you ‘pretty print’ it. You can do this using Python 3, with:

    python -m json.tool build/key.json
    

    Alternatively, if you don’t have convenient access to Python, you can use one of the many online JSON pretty printers.

Encryption

  1. Open the program Encrypt.java in a text editor or your IDE. This program is supposed to use a key generated by CreateKey to encrypt the contents of a file, writing the resulting ciphertext out to a new file. The key, the input file and the output file are all specified using command line arguments. This program is incomplete but contains comments to indicate what needs to be added.

    Under the ‘Load key material’ comment, add code to read the cipher key from a JSON file, assuming that the name of this file is provided as the first command line argument:

    KeysetHandle key = CleartextKeysetHandle.read(
      JsonKeysetReader.withPath(args[0])
    );
    

    After adding these lines, and after each subsequent addition of code in the steps below, check that the code still compiles, like so:

    ./gradlew classes
    
  2. Under the relevant comment, add this line to read the bytes of the file to be encrypted, assuming that its name has been specified as the second command line argument:

    byte[] plaintext = Files.readAllBytes(Paths.get(args[1]));
    
  3. Now add code to perform the encryption. This involves retrieving an AEAD primitive from the object representing the key, then calling this primitive’s encrypt() method:

    Aead primitive = key.getPrimitive(Aead.class);
    byte[] ciphertext = primitive.encrypt(plaintext, null);
    

    The first argument to encrypt() is a byte array containing the plaintext to be encrypted and the second argument is a byte array containing the associated data (which will be authenticated, not encrypted). In this case, we have no associated data, so we pass null for the second argument.

  4. Finally, add code to write the ciphertext out to a file, assuming that its name has been specified as a third command line argument:

    Files.write(Paths.get(args[2]), ciphertext);
    
  5. To test the finished program, do

    ./gradlew encrypt
    

    This task will use the previously generated key in build/key.json and the sample plaintext in data/message.txt. The ciphertext will be put in the file build/encrypted.bin.

Decryption

  1. Now open Decrypt.java. This program is supposed to decrypt a file previously encrypted by Encrypt, but it is incomplete. Add the required code under the various comments.

    You should be able to figure out what is needed, based on what you added to the Encrypt program. Refer to the Tink API docs if you need to. Make sure that you treat the program’s first command line argument as the key, its second command line argument as the file of encrypted input and its third command line argument as file to be used for the decrypted output.

  2. Test the finished program with

    ./gradlew decrypt
    

    This should work, provided that you’ve previously run the createkey and encrypt tasks. It will put the decrypted data in the file build/decrypted.txt. The contents of this file should be identical to the original plaintext, data/message.txt.

    You can also run all three programs in sequence, with

    ./gradlew run
    

    You can even omit run from the above command, as it has been set as the default Gradle task.

  3. Optional: If you have access to a binary file editor (e.g., VSCode with the Hex Editor extension installed), try modifying a single byte of build/encrypted.bin, then save the file and attempt to run the decrypt task again. This time, you should see an exception. Verification of the authentication tag fails if the ciphertext has been altered in any way.

  4. Optional: Experiment with using associated data. Add a line like this to Encrypt.java:

    byte[] data = { 0, 1, 2, 3 };
    

    Then change the second argument of the encrypt() method call from null to data. Make the equivalent changes to Decrypt.java and then rerun everything with ./gradlew run. Encryption and decryption should still succeed.

    Now go back to Decrypt.java and change one of the values in the data array, to simulate a change in the associated data. If you do ./gradlew run again, the decryption step should now trigger an exception. Verification of the authentication tag fails if the associated data has been altered in any way.

Other Languages & Libraries

You can use Tink for symmetric encryption in other languages besides Java - e.g., C++ and Python. An alternative to Tink in Python is PyCryptodome, which has a similarly clean and simple API for doing AES-GCM and other flavours of AEAD.


  1. Omit the ./ if running this on Windows. If you are using Linux or macOS and this script won’t run, you might need to set execute permissions using chmod u+x gradlew.

    NOTE: This command may be very slow the first time that it runs, as it may need to download Gradle and the Tink library to your PC. ↩︎