Java Encryption: a Practical Use Case

When requesting data using online communication, privacy is very important. Sensitive data could be intercepted by an attacker and then misused.

To decrease the risk of data interchange, the involved parties use cryptography to encode the data in case an attacker captures it, to guarantee its content safety.

For this purpose, two types of cryptography are used in tandem, symmetric and asymmetric, the reason to use both and not only one is that each has its advantages and disadvantages.

Asymmetric encryption tends to be more secure but slower, while symmetric encryption tends to be faster but less secure.

The usual process is to create a shared key to encrypt the data with a symmetric algorithm like “AES”, then encrypt the shared key with an asymmetric algorithm like “RSA”. This way we use the asymmetric algorithm to encrypt a small chunk of data (the shared key) and the symmetric algorithm to encrypt the payload to be sent.

Practical use case:

A client wants to request a one-time token (OTP) from our platform and we need to ensure that the OTP is adequately encrypted.

The client will have an RSA key pair (probably in the form of certificates) and will request the OTP to send their public key.

We will then proceed to encrypt the data following a series of steps:

When the client receives the response, they will proceed to decrypt the data to obtain the token by executing the reverse steps:

Encryption code examples:

RSA key pair generation:

The client and the data requester must provide a public key and keep the private key of the pair to decrypt the data later.

Generating a key pair is easy thanks to the provided Java classes, we just need to get a KeyPairGenerator instance for the RSA algorithm and initialise with the length of the key we want (the longer, the stronger the key will be). Then, when we have the key pair, we can extract the public and the private keys with the corresponding methods:

public static KeyPair generateRSAKeypair() throws NoSuchAlgorithmException {
   KeyPairGenerator kpg = KeyPairGenerator.getInstance("RSA");
   kpg.initialize(1024);
   return kpg.generateKeyPair();
}

Generate AES key:

We, the data providers, will need to create a new AES key that will be used to encrypt the data, this is very similar to the key pair generation but with a different algorithm that produces a SecretKey object:

public static SecretKey generateAESKey() throws NoSuchAlgorithmException {
   KeyGenerator kg = KeyGenerator.getInstance("AES");
   kg.init(128);
   return kg.generateKey();
}

Encrypt data with generated AES key:

To encrypt the data, we are going to use an AES algorithm with GCM mode of operation and NoPadding. We need to provide an initialisation vector that is a fixed size array of bytes. We will generate a random vector using Java security class SecureRandom:

public static byte[] generateInitialisationVector() {
   byte[] iv = new byte[12];
   new SecureRandom().nextBytes(iv);
   return iv;
}

With the data to encrypt, the generated AES key, and the initialisation vector, we can proceed to the encryption.

Java provides implementations for the standard encryption algorithm, so the job once the parameters that we want to use for encryption are established, is easy. We need to generate a cipher instance, then we initialise the cipher with the mode (encrypt/decrypt) and the parameters, and finally we call the doFinal method with the data to be processed.

The code shows how to do so by using all inputs as byte arrays so we know how to recreate the key:

public static byte[] symmetricEncryptionWithAES(byte[] toEncrypt, byte[] key,
                                                byte[] iv)
        throws NoSuchPaddingException, NoSuchAlgorithmException,
        InvalidAlgorithmParameterException, InvalidKeyException,
        IllegalBlockSizeException, BadPaddingException {
   SecretKey secretKey = new SecretKeySpec(key, 0, key.length, "AES");
   Cipher aesCipher = Cipher.getInstance("AES/GCM/NoPadding");
   aesCipher.init(Cipher.ENCRYPT_MODE, secretKey,
           new GCMParameterSpec(iv.length * 8, iv));
   return aesCipher.doFinal(toEncrypt);
}

Encrypt symmetric key:

Now we will need to encrypt the AES key so that we can send it to the data requester to decrypt it. The process is very similar to the previous encryption, we just need to change the algorithm and parameters (AES algorithm with ECB mode of operation and OAEPPadding).

Once again, input parameters are byte arrays to show how to recreate the keys:

public static byte[] asymmetricEncryptionWithRSA(byte[] toEncrypt,
                                                 byte[] publicKeyBytes)
       throws NoSuchAlgorithmException, InvalidKeySpecException, 
       NoSuchPaddingException, InvalidAlgorithmParameterException,
       InvalidKeyException, IllegalBlockSizeException, BadPaddingException {
   X509EncodedKeySpec X509publicKey = new X509EncodedKeySpec(publicKeyBytes);
   KeyFactory kf = KeyFactory.getInstance("RSA");
   PublicKey publicKey = kf.generatePublic(X509publicKey);

   Cipher rsaCipher = Cipher.getInstance("RSA/ECB/OAEPPadding");
   OAEPParameterSpec oaepParameterSpec = new OAEPParameterSpec("SHA-256", "MGF1",
           new MGF1ParameterSpec("SHA-256"), PSource.PSpecified.DEFAULT);
   rsaCipher.init(Cipher.ENCRYPT_MODE, publicKey, oaepParameterSpec);
   return rsaCipher.doFinal(toEncrypt);
}

So all the data is ready to be sent. The requester will receive:

  • Encrypted data, in this example the OTP requested.
  • Encrypted symmetric key, to be able to decrypt the data.
  • Initialisation vector, to be able to decrypt the data.

Decryption code examples:

Decrypt symmetric key:

The code to decrypt the symmetric key is pretty similar to the encryption one, the only significative change is that now the cipher mode should be changed to decrypt and the key should be the private key:

Public static byte[] asymmetricDecryptionWithRSA(byte[] toDecrypt,
                                                 byte[] privateKeyBytes) 
        throws NoSuchAlgorithmException, InvalidKeySpecException,
        NoSuchPaddingException, InvalidAlgorithmParameterException,
        InvalidKeyException, IllegalBlockSizeException, BadPaddingException {
   PKCS8EncodedKeySpec pkcs8PrivateKey = new PKCS8EncodedKeySpec(privateKeyBytes);
   KeyFactory kf = KeyFactory.getInstance("RSA");
   PrivateKey privateKey = kf.generatePrivate(pkcs8PrivateKey);

   Cipher rsaCipher = Cipher.getInstance("RSA/ECB/OAEPPadding");
   OAEPParameterSpec oaepParameterSpec = new OAEPParameterSpec("SHA-256", "MGF1",
           new MGF1ParameterSpec("SHA-256"), PSource.PSpecified.DEFAULT);
   rsaCipher.init(Cipher.DECRYPT_MODE, privateKey, oaepParameterSpec);
   return rsaCipher.doFinal(toDecrypt);
}

Decrypt data:

Now the data requester is ready to get the final data, we only need to decrypt it by using the initialisation vector provided and the symmetric key already decrypted:

private byte[] symmetricDecryptionWithAES(byte[] toDecrypt, byte[] key, byte[] iv)
        throws NoSuchPaddingException, NoSuchAlgorithmException,
        InvalidAlgorithmParameterException, InvalidKeyException,
        IllegalBlockSizeException, BadPaddingException {
   SecretKey secretKey = new SecretKeySpec(key, 0, key.length, "AES");
   Cipher aesCipher = Cipher.getInstance("AES/GCM/NoPadding");
   aesCipher.init(Cipher.DECRYPT_MODE, secretKey,
           new GCMParameterSpec(iv.length * 8, iv));
   return aesCipher.doFinal(toDecrypt);
}

Conclusion:

Java provides libraries that help to work with cryptography and they tend to be very intuitive, so knowing the processes usually followed to exchange secure data will be useful.

This knowledge has been very useful for us when working on bank applications to provide secure data for payment systems like Apple Pay.

More information on Java cryptography can be found on the java official documentation: Java Cryptography Architecture (JCA) Reference Guide