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.
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.
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.
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.
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
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]));
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.
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);
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
.
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.
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.
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.
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.
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.
□
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. ↩︎