Exercise 7: Hybrid Encryption in Java

This exercise explores hybrid encryption, which combines symmetric and asymmetric (public key) cryptographic techniques. A symmetric cipher and MAC are used to do AEAD, and the problem of how the communicating parties agree on the keys used by the cipher and MAC is solved using public key cryptography.

Make sure you do the exercise on Symmetric Ciphers in Java before attempting this one. As in that exercise, we use Google’s Tink library, because it provides direct support for hybrid encryption, via a clean, convenient high-level API.

Setting Up

Download tink-hybrid.zip and unzip it. This should create a directory named tink-hybrid. 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.

Once again, 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.

Creating a Key Pair

Open CreateKeys.java in your editor or IDE. You’ll find this file and the other source code files in the src/main/java subdirectory. The program is incomplete but contains comments to guide implementation.

  1. Under the ‘Configure Tink’ comment, add the following:

    HybridConfig.register();
    

    This initializes the Tink library to use all the primitives needed for hybrid encryption and decryption operations.

  2. Under the ‘Generate key material’ comment, add this code:

    KeysetHandle privateKey = KeysetHandle.generateNew(
      KeyTemplates.get("ECIES_P256_HKDF_HMAC_SHA256_AES128_GCM")
    );
    

    Then check that the code compiles, with

    ./gradlew classes
    

    In the code you’ve just added, focus on the cipher specification string ECIES_P256_HKDF_HMAC_SHA256_AES128_GCM. The first part of this string specifies the use of ECIES: the Elliptic Curve Integrated Encryption Scheme. This uses elliptic curves for the public key cryptography side of things. The last part of the string indicates that AES-GCM with a 128-bit key will be used for the symmetric encryption part of the scheme. Finally, the middle part of the string, beginning HKDF, indicates the use of a hash-based key derivation function to derive the cipher and MAC keys needed for AEAD. Specifically, an HMAC based on SHA-256 will be used.

  3. The next step is to write the private key to a file. We assume that the name of this file is specified as the first command line argument. This uses code that you have already seen in earlier exercises:

    CleartextKeysetHandle.write(
      privateKey, JsonKeysetWriter.withPath(args[0])
    );
    

    The key written by this code will be used by the decryption program.

  4. The final step is to obtain the public key and then write it to another JSON file, this time assuming that the file name has been supplied as the second command line argument:

    KeysetHandle publicKey = privateKey.getPublicKeysetHandle();
    
    CleartextKeysetHandle.write(
      publicKey, JsonKeysetWriter.withPath(args[1])
    );
    

    The key written by this code will be used by the encryption program.

  5. Test the finished program with

    ./gradlew createkeys
    

    This should output the two keys to files named private_key.json and public_key.json, in the build subdirectory. Examine the contents of these files.

Encryption & Decryption

Encryption and decryption are handled by the programs Encrypt.java and Decrypt.java, which can both be found in src/main/java. Both programs are incomplete skeletons that compile and run but currently do nothing. You’ll need to add the required code to each. We’ll leave it to you to figure this code out for yourself!

Use the comments in the files and the Tink API docs to help you. Keep in mind that Tink has a remarkably consistent API, so the code you need to write is very similar to what you’ve seen in the previous exercise. The main difference is that you’ll need to create separate encryption and decryption primitives here, using the classes HybridEncrypt and HybridDecrypt.

You can test the encryption program with

./gradlew encrypt

If you’ve implemented it correctly, the encrypted output should appear in the file build/encrypted.bin.

You can test decryption with

./gradlew decrypt

If you’ve implemented the program correctly, the decrypted output should appear in the file build/decrypted.txt. You can compare this with the original input like so1:

diff -s build/decrypted.txt data/message.txt

If you wish to run all three programs in the required sequence, do

./gradlew run

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

Other Languages & Libraries

You can use Tink for hybrid encryption in several other languages besides Java - e.g., C++ and Python. An alternative to Tink in Python is PyCryptodome. This doesn’t support hybrid encryption explicitly, but the documentation shows a simple example of using RSA to protect an AES session key.


  1. This is on Linux or macOS. On Windows you can use the comp command instead. ↩︎