April 22, 2020

Encrypt with OpenSSL, Decrypt with Java, Using OpenSSL RSA Public Private Keys

Abstract

In 2017 I wrote a 3 part series on choosing the best hashing and encryption algorithms. While doing the research for the series, I learned a lot about hashing and encryption. The most important thing I learned is that although I must educate myself on how to use the safest algorithms possible, I must also leave the development of these algorithms to the experts. With that said, I started thinking about Java's interoperability with encryption experts, specifically OpenSSL. My 3 part series focused only on encryption from the Java point of view. I wondered how difficult it would be for Java to interoperate with a tool like OpenSSL. The purpose of this blog is to demonstrate Java's interoperability with OpenSSL:

  • Generate private and public keys with OpenSSL
  • Encrypt values with OpenSSL
  • Decrypt values with Java

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.

  • OpenJDK Runtime Environment Zulu11.39+15-CA (build 11.0.7+10-LTS)
  • OpenSSL 1.1.1c 28 May 2019
  • Apache NetBeans IDE 11.3
  • Maven 3.3.9 (Bundled with NetBeans)
<dependencies>
  <dependency>
    <groupId>org.junit.jupiter</groupId>
    <artifactId>junit-jupiter-api</artifactId>
    <version>5.5.2</version>
    <scope>test</scope>
  </dependency>
  <dependency>
    <groupId>org.junit.jupiter</groupId>
    <artifactId>junit-jupiter-params</artifactId>
    <version>5.5.2</version>
    <scope>test</scope>
  </dependency>
  <dependency>
    <groupId>org.junit.jupiter</groupId>
    <artifactId>junit-jupiter-engine</artifactId>
    <version>5.5.2</version>
    <scope>test</scope>
  </dependency>
</dependencies>
<pluginManagement>
  <plugins>
    <plugin>
      <groupId>org.apache.maven.plugins</groupId>
      <artifactId>maven-clean-plugin</artifactId>
      <version>2.5</version>
    </plugin>
    <plugin>
      <groupId>org.apache.maven.plugins</groupId>
      <artifactId>maven-resources-plugin</artifactId>
      <version>2.6</version>
    </plugin>
    <plugin>
      <groupId>org.apache.maven.plugins</groupId>
      <artifactId>maven-compiler-plugin</artifactId>
      <version>3.8.1</version>
      <configuration>
        <debug>true</debug>
      </configuration>
    </plugin>
    <plugin>
      <groupId>org.apache.maven.plugins</groupId>
      <artifactId>maven-surefire-plugin</artifactId>
      <version>3.0.0-M4</version>
      <configuration>
         <argLine>-Dfile.encoding=UTF8</argLine>
      </configuration>
    </plugin>
    <plugin>
      <groupId>org.apache.maven.plugins</groupId>
      <artifactId>maven-jar-plugin</artifactId>
      <version>2.4</version>
    </plugin>
    <plugin>
      <groupId>org.apache.maven.plugins</groupId>
      <artifactId>maven-install-plugin</artifactId>
      <version>2.4</version>
    </plugin>
  </plugins>
</pluginManagement>

Download

Visit my GitHub page https://github.com/mjremijan to see all of my open source projects. The code for this post is located in: https://github.com/mjremijan/thoth-rsa

Background

I started wondering about being able to interoperate OpenSSL and Java as I was modularizing Monolith applications with Microservices. When using Microservices, applications still need to encrypt and decrypt sensitive configuration data - such as database passwords - yet the small runtimes used by Microservices make this a challenge.

With a Monolith architecture, the Java/Jakarta EE application server handles encryption and decryption for an application. Managed resources such as database connection pools are configured within the EE application server and other other encrypted values may be generally stored within JNDI. In both cases, the server provides both encryption and decryption without the application knowing any of the details. The application is either provided a managed resource or a decrypted value by the application server.

However, in a Microservice architecture, runtimes (such as Spring Boot) are kept "small" and do not provide as many features as an EE application server. A database connection is a good example. It's easy to configure a database connection in Spring Boot, however how do you support password encryption and decryption? It now must be supported by DevOps and the Development team.

NOTE Other Microservice technologies like Kubernetes are working to fill the gap and provide encryption features similar to EE application servers.

So this got me thinking. DevOps lives in the Linux/Unix world. Developers live in the Java world. Why not bring the 2 worlds together to support an encryption/decryption strategy? This would allow DevOps and Developers to do what they each do best. In order to do this, I first needed clearly defined goals.

Goals

Migration from a Monolith architecture to Microservices is slow. Yes, there are Microservice infrastructure solutions for encryption and decryption. However, those won't help you in the 3-5 year transition period when that infrastructure isn't available. To support the transition, I decided on the following goals.

  1. Encryption tool of choice is OpenSSL. It is on every Linux/Unix system, is an industry standard, and will be familiar to all DevOps teams.
  2. Encryption performed by DevOps, or another team, so there is a separation of responsibilities. No one on the development team may know an unencrypted value.
  3. All environments will use their own keys. No key sharing.
  4. All keys and encrypted values may be regenerated at any time with no change to the application.
  5. Encryption will be either of an entire file or of specific values within a (properties) file.
  6. Encrypted values and keys are made available to the Java runtime using a strategy agreed upon and enforced by both DevOps and Development teams.
  7. Decryption is performed by the Java application for whatever purposes it needs. Don't log the encrypted values!

With these goals in mind, let's take a journey to get there.

Which Algorithm to Use

The first question I needed to answer is which encryption algorithm to use. For encryption I have a choice between single key symmetric encryption or public/private key asymmetry encryption. My choice is:

RSA-4096 public/private key asymmetric encryption

The reason for choosing an asymmetric encryption algorithm is because the public/private keys allow for the highest possible level of separation of responsibilities. There may be separate teams for generating the keys, encrypting the values, and putting everything together for runtime. In reality this may all be done by one team or even a single person, but an asymmetric encryption algorithm gives flexibility separating these concerns.

As for using the RSA-4096 algorithm, according to my research, it's the best and most secure today (Remijan, 2017).

Now we know which algorithm to use. Next, we'll look at generating the private key.

OpenSSL Generate the Private Key

In Java, the PKCS8EncodedKeySpec class expects the RSA private key with a PKCS8 encoding. (Java Code, n.d.). I found 2 ways of doing this with OpenSSL.

Listing 2.1 - Generate Private Key with 2 Commands

# Generate private key with pkcs1 encoding
openssl genrsa -out private_key_rsa_4096_pkcs1.pem 4096

# Convert private key to pkcs8 encoding
openssl pkcs8 -topk8 -in private_key_rsa_4096_pkcs1.pem -inform pem -out private_key_rsa_4096_pkcs8-exported.pem -outform pem -nocrypt

In listing 2.1 (destan, 2017), the private key is generated with 2 commands. The first command generates the key with a PKCS1 encoding. The second command converts the PKCS1 encoded key to a key with PKCS8 encoding.

Listing 2.2 - Generate Private Key with 1 Command

# Generate private key with pkcs8 encoding
openssl genpkey -out private_key_rsa_4096_pkcs8-generated.pem -algorithm RSA -pkeyopt rsa_keygen_bits:4096

In listing 2.2, the private key is generated using a single command. This produces a key with a PKCS8 encoding. No additional conversion is needed.

Whether you use listing 2.1 or 2.2 to generate the private key, when generated it will look something like this.

-----BEGIN PRIVATE KEY-----
MIIJQgIBADANBgkqhkiG9w0BAQEFAASCCSwwggkoAgEAAoICAQDVgLrCSDC5mLRL
JY+okYX5MOMGi+bvtRQ9qIQ90d3BO1gAao6ZsbPEFxnOTR9Q3bGsEE5oRlh/FSYS
.
.
kvCjd0ineNZ6OgPVJ/mhPULsZb11+noSUPmFqvClb8SQ0BipbKIcSTIJlQt1ZRZ2
INdXsP5kNlRK181jtU/xtQYfwSjkKA==
-----END PRIVATE KEY-----

Great! The private key is generated! Now let's move on to generating the public key.

OpenSSL Generate the Public Key

In Java, the X509EncodedKeySpec class expects the RSA public key with an X509 encoding. (Java Code, n.d.). The public key is generated from the private key, so you must have the private key first.

Listing 3.1 - Generate Public Key

# Export public key in pkcs8 format
openssl rsa -pubout -outform pem -in private_key_rsa_4096_pkcs8-generated.pem -out public_key_rsa_4096_pkcs8-exported.pem

Listing 3.1 shows the command using the private key private_key_rsa_4096_pkcs8-generated.pem to generate the public key public_key_rsa_4096_pkcs8-exported.pem.

The public key will look something like this.

-----BEGIN PUBLIC KEY-----
MIICIjANBgkqhkiG9w0BAQEFAAOCAg8AMIICCgKCAgEA1YC6wkgwuZi0SyWPqJGF
+TDjBovm77UUPaiEPdHdwTtYAGqOmbGzxBcZzk0fUN2xrBBOaEZYfxUmEkOFzPbF
.
.
oNta8CSsVrqgFW/tI6+MQwrQFEOcBPCbh6Pr7NbiuR2LrfoJhUJlD5ofz5eM0419
JSS0RvKh0dF3ddlOKV/TQUsCAwEAAQ==
-----END PUBLIC KEY-----

Great! We have both the private key and the public key and both were generated by OpenSSL. Next, we need Java to use these key files. Do do that, we'll need to create instances of the KeyFactory, PrivateKey, and PublicKey objects. Let's dive into some Java code!

Java KeyFactory, PrivateKey, PublicKey

After using OpenSSL to generate private and public key files, it is time for some Java code. Listing 4.1 is my complete Rsa4096 class. I discuss each individual method in detail below.

Listing 4.1 - Rsa4096 class

package org.thoth.rsa;

import java.io.InputStream;
import java.security.KeyFactory;
import java.security.PrivateKey;
import java.security.PublicKey;
import java.security.spec.KeySpec;
import java.security.spec.PKCS8EncodedKeySpec;
import java.security.spec.X509EncodedKeySpec;
import java.util.Base64;
import javax.crypto.Cipher;

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

  private KeyFactory keyFactory;
  private PrivateKey privateKey;
  private PublicKey publicKey;

  public Rsa4096(
      String privateKeyClassPathResource
    , String publicKeyClassPathResource
  ) throws Exception {
    setKeyFactory();
    setPrivateKey(privateKeyClassPathResource);
    setPublicKey(publicKeyClassPathResource);
  }

  protected void setKeyFactory() throws Exception {
    this.keyFactory = KeyFactory.getInstance("RSA");
  }

  protected void setPrivateKey(String classpathResource)
  throws Exception {
    InputStream is = this
      .getClass()
      .getClassLoader()
      .getResourceAsStream(classpathResource);

    String stringBefore
      = new String(is.readAllBytes());
    is.close();

    String stringAfter = stringBefore
      .replaceAll("\\n", "")
      .replaceAll("-----BEGIN PRIVATE KEY-----", "")
      .replaceAll("-----END PRIVATE KEY-----", "")
      .trim();

    byte[] decoded = Base64
      .getDecoder()
      .decode(stringAfter);

    KeySpec keySpec
      = new PKCS8EncodedKeySpec(decoded);

    privateKey = keyFactory.generatePrivate(keySpec);
  }


  protected void setPublicKey(String classpathResource)
  throws Exception {

    InputStream is = this
      .getClass()
      .getClassLoader()
      .getResourceAsStream(classpathResource);

    String stringBefore
      = new String(is.readAllBytes());
    is.close();

    String stringAfter = stringBefore
      .replaceAll("\\n", "")
      .replaceAll("-----BEGIN PUBLIC KEY-----", "")
      .replaceAll("-----END PUBLIC KEY-----", "")
      .trim()
    ;

    byte[] decoded = Base64
      .getDecoder()
      .decode(stringAfter);

    KeySpec keySpec
      = new X509EncodedKeySpec(decoded);

    publicKey = keyFactory.generatePublic(keySpec);
  }


  public String encryptToBase64(String plainText) {
    String encoded = null;
    try {
      Cipher cipher = Cipher.getInstance("RSA");
      cipher.init(Cipher.ENCRYPT_MODE, publicKey);
      byte[] encrypted = cipher.doFinal(plainText.getBytes());
      encoded = Base64.getEncoder().encodeToString(encrypted);
    } catch (Exception e) {
      e.printStackTrace();
    }
    return encoded;
  }

  public String decryptFromBase64(String base64EncodedEncryptedBytes) {
    String plainText = null;
    try {
      final Cipher cipher = Cipher.getInstance("RSA");
      cipher.init(Cipher.DECRYPT_MODE, privateKey);
      byte[] decoded = Base64
        .getDecoder()
        .decode(base64EncodedEncryptedBytes);
      byte[] decrypted = cipher.doFinal(decoded);
      plainText = new String(decrypted);
    } catch (Exception ex) {
      ex.printStackTrace();
    }
    return plainText;
  }
}

Constructor

  public Rsa4096(
      String privateKeyClassPathResource
    , String publicKeyClassPathResource
  ) throws Exception {
    setKeyFactory();
    setPrivateKey(privateKeyClassPathResource);
    setPublicKey(publicKeyClassPathResource);
  }

The constructor is simple and takes 2 parameters. By the names of the parameters you can guess what they are. The 1st parameter is the fully-qualified Class Path location of the private key file generated by OpenSSL. The 2nd parameter is the same for the public key file.

Why put the key files on the Class Path? I'm using Maven to run unit tests to research this code. Maven makes is easy to make resources available on the Class Path, so that's what I'm using here. Again, this is research (See Disclaimer)!

Remember, one of the goals is to make the keys available to the Java runtime using a strategy agreed upon and enforced by both DevOps and Development teams. So your strategy may be different, but the end goal is the same: point to some location where you can read the bytes of the files.

setKeyFactory()

  protected void setKeyFactory() throws Exception {
    this.keyFactory = KeyFactory.getInstance("RSA");
  }

The setKeyFactory() method instantiates a KeyFactory class for the RSA algorithm. Really simple; one line of code. You'll use this object later to build the PrivateKey and the PublicKey...it is a factory class after all :)

setPrivateKey()

  protected void setPrivateKey(String classpathResource)
  throws Exception {
    InputStream is = this
      .getClass()
      .getClassLoader()
      .getResourceAsStream(classpathResource);

    String stringBefore
      = new String(is.readAllBytes());

    String stringAfter = stringBefore
      .replaceAll("\\n", "")
      .replaceAll("-----BEGIN PRIVATE KEY-----", "")
      .replaceAll("-----END PRIVATE KEY-----", "")
      .trim();

    byte[] decoded = Base64
      .getDecoder()
      .decode(stringAfter);

    KeySpec keySpec
      = new PKCS8EncodedKeySpec(decoded);

    privateKey = keyFactory.generatePrivate(keySpec);
  }

The setPrivateKey() method instantiates a PrivateKey. In this method, the ClassLoader is used to get an InputStream to the private key file on the Class Path. The bytes of the file are read into a new String. Next, the String is processed as follows:

    String stringAfter = stringBefore
      .replaceAll("\\n", "")
      .replaceAll("-----BEGIN PRIVATE KEY-----", "")
      .replaceAll("-----END PRIVATE KEY-----", "")
      .trim();

This processing is necessary because even though we used OpenSSL to generate a private key file with PKCS8 encoding, the file is not directly usable by Java. If you try without the above processing, you'll get the following exception:

java.security.spec.InvalidKeySpecException: java.security.InvalidKeyException: invalid key format

The PKCS8EncodedKeySpec class expects the private key to be a single line of text with all comments removed (Java Code Example..., n.d.). This is the reason for the processing.

After processing removes the newlines and comments, the PKCS8EncodedKeySpec and KeyFactory are used to create the PrivateKey.

    KeySpec keySpec
      = new PKCS8EncodedKeySpec(decoded);

    privateKey = keyFactory.generatePrivate(keySpec);

setPublicKey()

  protected void setPublicKey(String classpathResource)
  throws Exception {

    InputStream is = this
      .getClass()
      .getClassLoader()
      .getResourceAsStream(classpathResource);

    String stringBefore
      = new String(is.readAllBytes());

    String stringAfter = stringBefore
      .replaceAll("\\n", "")
      .replaceAll("-----BEGIN PUBLIC KEY-----", "")
      .replaceAll("-----END PUBLIC KEY-----", "")
      .trim();

    byte[] decoded = Base64
      .getDecoder()
      .decode(stringAfter);

    KeySpec keySpec
      = new X509EncodedKeySpec(decoded);

    publicKey = keyFactory.generatePublic(keySpec);
  }

The setPublicKey() method instantiates a PublicKey. This method is nearly identical to the setPrivateKey() method, but let's take a look at the details.

The ClassLoader is used to get an InputStream to the public key file on the Class Path. The bytes of the file are read into a new String. Next, the String is processed as follows:

    String stringAfter = stringBefore
      .replaceAll("\\n", "")
      .replaceAll("-----BEGIN PUBLIC KEY-----", "")
      .replaceAll("-----END PUBLIC KEY-----", "")
      .trim();

This processing is necessary because even though we used OpenSSL to generate a private key file with an X509 encoding, this file is not directly usable by Java. If you try without the above processing, you'll get the following exception:

java.security.spec.InvalidKeySpecException: java.security.InvalidKeyException: invalid key format

The X509EncodedKeySpec class expects the public key to be a single line of text with all comments removed (Java Code Example..., n.d.). This is the reason for the processing.

After processing removes the newlines and comments, the X509EncodedKeySpec and KeyFactory are used to create the PublicKey.

    KeySpec keySpec
      = new X509EncodedKeySpec(decoded);

    publicKey = keyFactory.generatePublic(keySpec);

We now have instances of PrivateKey and PublicKey which we created from the private and public key files generated by OpenSSL. So what do you think, want to start encrypting and decrypting? Let's do it!

Java In-Memory Test

It's time to put things together and see if we can encrypt and decrypt a value. But we can't do this without the encryption and decryption methods. We need them first.

The following listings are snips from my Rsa4096 class. Look at the class on GitHub or read through the "Java KeyFactory, PrivateKey, PublicKey" section above for the full source of the class. The Rsa4096 class contains the encryption and decryption methods. Let's take a look at the encryption method first.

Encryption

Listing 5.1 - encryptToBase64() Method

  public String encryptToBase64(String plainText) {
    String encoded = null;
    try {
      Cipher cipher = Cipher.getInstance("RSA");
      cipher.init(Cipher.ENCRYPT_MODE, publicKey);
      byte[] encrypted = cipher.doFinal(plainText.getBytes());
      encoded = Base64.getEncoder().encodeToString(encrypted);
    } catch (Exception e) {
      e.printStackTrace();
    }
    return encoded;
  }

Listing 5.1 shows the encryptToBase64() method. The method has one String parameter which is the value to be encrypted. Passing in a byte[] array may be more robust, but in my experience, the need is usually to encrypt String values. Of course, update for whatever meets your needs.

The name and return type of the method implies a Base64 encoded String will be returned. Passing back a byte[] array may be more robust, but in my experience, a String return value is usually what's needed. Of course, update for whatever meets your needs.

Only the PublicKey is needed for encryption.

Decryption

Listing 5.2 - decryptFromBase64() Method

  public String decryptFromBase64(String base64EncodedEncryptedBytes) {
    String plainText = null;
    try {
      final Cipher cipher = Cipher.getInstance("RSA");
      cipher.init(Cipher.DECRYPT_MODE, privateKey);
      byte[] decoded = Base64
        .getDecoder()
        .decode(base64EncodedEncryptedBytes);
      byte[] decrypted = cipher.doFinal(decoded);
      plainText = new String(decrypted);
    } catch (Exception ex) {
      ex.printStackTrace();
    }
    return plainText;
  }

Listing 5.2 shows the decryptFromBase64() method. The method has one String parameter which by its name is a Base64 encoded String of the encrypted byte[] array. Passing in a byte[] array may be more robust, but in my experience, the need is usually to decrypt a String back to it's original value. Of course, update for whatever meets your needs.

The name and return type of the method implies the original, String value will be returned. Passing back a byte[] array may be more robust, but in my experience, the original value is always a String. Of course, update for whatever meets your needs.

Only the PrivateKey is needed for decryption.

Unit Test

Now let's take a look at the InMemoryTest unit test to see if everything works together.

NOTE In-memory encryption & decryption is NOT one of my goals. The goal is to encrypt with OpenSSL outside the application and decrypt with Java inside the application. However, trying in-memory first is a good test to make sure everything is working OK.

Listing 5.3 - InMemoryTest Unit Test

package org.thoth.rsa;

import org.junit.jupiter.api.Assertions;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;

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

  @Test
  public void test_in_memory_encryption_decryption()
  throws Exception
  {
    // Setup
    Rsa4096 rsa = new Rsa4096(
        "./private_key_rsa_4096_pkcs8-generated.pem"
      , "./public_key_rsa_4096_pkcs8-exported.pem"
    );
    String expected
      = "Text to be encrypted";

    // Test
    String encryptedAndEncoded
      = rsa.encryptToBase64(expected);
    String actual
      = rsa.decryptFromBase64(encryptedAndEncoded);

    // Assert
    Assertions.assertEquals(expected, actual);
  }
}

Listing 5.3 shows the InMemoryTest unit test. This test finally runs all the code and verifies a String can be encrypted and decrypted back to the same value.

First, the // Setup of the unit test specifies where to find the private and public key files. Remember, these files were generated by OpenSSL. I put them in the project's src/test/resources/ directory so they would appear in the Class Path when the unit test runs. They are used to create an instance of my Rsa4096 class.

Next, the test does the encryption and decryption. Seems a bit anti-climatic, but all the work is in the Rsa4096 class.

Finally, the JUnit assertion checks the expected value equals the actual value. If all goes well, the test should pass meaning encrypting then decrypting returned the original value. Clone my thoth-rsa repository and run the unit test for yourself to see that it works!

So the private and public keys generated by OpenSSL can be used within Java to encrypt and decrypt in-memory values. However, can a value be encrypted with OpenSSL outside of Java and yet be decrypted inside the application? Let's try it!

Encrypted File

One of the stated goals of this research is for OpenSSL to encrypt an entire file and the Java application would decrypt it. It is very common for Java applications to externalize values into properties files. Although it may be better to encrypt only specific properties (which we will get to in the next section), encrypting the entire file is a quick and easy way to make sure no sensitive properties are missed.

To start, we need to encrypt an entire file. We already have the public key for encryption. So all that's left is the correct OpenSSL command. Let's take a look at the command.

File Encryption

Listing 6.1 - OpenSSL Encrypt a File

openssl rsautl -encrypt -inkey public_key_rsa_4096_pkcs8-exported.pem -pubin -in file_unencrypted.txt | openssl enc -A -base64 > file_encrypted_and_encoded.txt

Listing 6.1 (admin. 2018) shows the OpenSSL command to both encrypt and Base64 encode the contents of a plain text file into a new file. Remember, when encrypting, only the public key file is needed. Thus separation of responsibilities can be maintained while handling sensitive data. The file_encrypted_and_encoded.txt file created by this command contain a Base64 encoded string that looks something like this:

UwXBjowtfDQix2lOiBbaX6J8GayYmo5EsZuHxPUtS+MW9kncnVNpeWw+jpOc1yEiSanFEeRE4QQz/DKWr16LHAt4B8OMOSvXikEpnv0uvr+UtKTE1KalHZDKBHvk5op44gMhhQVpyjKQrVMY/76R83o0/kj60fNsuqpx5DIH/RHhnwBCNvjpjlsvLPPlL1YqUIn0i+t+5XCaZcTiJhpsOh2LmEhfARLgMqVGZxb0zIPvn0zPerhVSZK1wUcI4Va+nOj2rDOflL1Sr5eiimAaIC5/zZniIZP4RDdF3VvlMur5MzUkgxM8CkIJPxKUj8QsEPEcVt3p3/cIvR9YeBmP6Gsw78NutJH3vXAvduPIB2/z/w8iRn/NYcCRX8xZUEGcM44Ks1n7eT+pUWJE1T+3KfH08HOhXuMJUocaxSiZiX2ROQt/gKPJsz27b3u967y9s1DozaaJY+1nKOqEbHDg/uVcgmwYXD5CDy+/qAqKXRJ3dCmJWw46OwPSTMAhkBGOihDhrcQbid3O9rsTU/Od19Fa+OGnS55HHv/4cnIwJnKXBtziG5EaJlouu/H+poabQEoiwgcuh2OOj41Rm6nG3Ef3uxppdoXCn9x3wMDHlqc8K+0Nenc2IbAM//Vd98PVwBf5/nvNyQKwfpQOFJrT4Ygyt3qWQ00cLG7u3fsngg0=

Great! Encrypted file; check! Now here's the big question: Can Java decrypt it? Let's find out!

Unit Test

Let's have a look at the EncryptedFileTest unit test.

Listing 6.2 - EncryptedFileTest Unit Test

package org.thoth.rsa;

import java.io.InputStream;
import org.junit.jupiter.api.Assertions;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;

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

  protected Rsa4096 rsa;

  @BeforeEach
  public void setUp() throws Exception {
    rsa = new Rsa4096(
        "./private_key_rsa_4096_pkcs8-generated.pem"
      , "./public_key_rsa_4096_pkcs8-exported.pem"
    );
  }

  @Test
  public void test_encrypted_file()
    throws Exception {
    // Setup
    String expected
      = getFileAsString("./file_unencrypted.txt");

    String encryptedAndEncoded
      = getFileAsString("./file_encrypted_and_encoded.txt");

    // Test
    String actual
      = rsa.decryptFromBase64(encryptedAndEncoded);
    System.out.printf("%s%n", actual);

    // Assert
    Assertions.assertEquals(expected, actual);
  }

  public String getFileAsString(String classPathResourceLocation)
  throws Exception {
    InputStream is = this
      .getClass()
      .getClassLoader()
      .getResourceAsStream(
        classPathResourceLocation
      );

    byte[] bytes = is.readAllBytes();
    is.close();

    return new String(bytes);
  }
}

First, the @BeforeEach method creates an instance of my Rsa4096 class. This uses the private and public key files generated by OpenSSL. These key files are on the Java Class Path when the unit test runs. Rsa4096 is used to decode and decrypt the contents of the encrypted file.

Second, the getFileAsString() helper method is called. The name of the method tells exactly what it does. It finds a file on the Java Class Path and reads its contents into a String. Remember, the OpenSSL file encryption command both encrypted and Base64 encoded the contents of the output file, so it's safe the store those contents as a String.

Third, Rsa4096 is used to decode and decrypt by calling decryptFromBase64().

Finally, the JUnit assertions make sure decoding and decryption were successful and that the test got back the original value.

That's it. We did it! But that's not all. Sure, encrypting an entire file is fun, but what's even more fun is encrypting only specific values within the file. There is no way this can be done...or can it? Let's see.

Encrypted Values in a File

Another goal of this research is to use OpenSSL to encrypt only specific values within a file. For this to work, there must be a starting template file containing placeholders for variable replacement. They will be replaced with encrypted and encoded values. OpenSSL will be used for the encryption and encoding, but we'll also need to pipe in sed for the search and replace. Let's take a look.

Value Encryption

Listing 7.1 - OpenSSL Encrypts Values in a File

sed "s|XXXX|`printf "SECRET" | openssl rsautl -encrypt -inkey public_key_rsa_4096_pkcs8-exported.pem -pubin | openssl enc -A -base64`|g" some_template.properties > some_tmp1.properties

sed "s|YYYY|`printf "123-45-7890" | openssl rsautl -encrypt -inkey public_key_rsa_4096_pkcs8-exported.pem -pubin | openssl enc -A -base64`|g" some_tmp1.properties > some_app.properties

Listing 7.1 gets a little out there with piping Unix commands so let's take a look at this in small pieces.

First, start with the some_template.properties file. This is a standard Java properties file but some of the properties in the file do not have values, they have placeholders for variable replacement:

name=mike
color=blue
password=XXXX
size=L
ssn=YYYY
price=4.99

As you can see, password and ssn have placeholders for encrypted sensitive information. XXXX and YYYY should be replaced.

Second, the sed "s|XXXX|`printf "SECRET" part of the command will obviously do a search and replace of XXXX with the plain text SECRET. What's important to note is that since these commands are all pipped to each other, the sensitive text is never written to a file.

Third, the output file is some_tmp1.properties. This file is appropriately named because it is only temporary. The template has two values needing replacement. The first command only does the search and replace on XXXX. The temporary file will look like this:

name=mike
color=blue
Password=sh3kiZTGtvcPlY3eqnUSkIC+HplryBs....=
size=L
ssn=YYYY
price=4.99

Fourth, the second command has sed "s|YYYY|`printf "123-45-7890" and the input file is some_tmp1.properties. The output is written to some_app.properties. The some_app.properties file is now ready to be used by the application because all sensitive data has been encrypted, encoded, and placed within the file. The some_app.properties now looks like:

name=mike
color=blue
Password=sh3kiZTGtvcPlY3eqnUSk....=
size=L
ssn=trpmRDvKnnjuT6hZvObthguN3A....=
price=4.99

Unit Test

EncryptedValuesInPropertiesFileTest is the last unit test we'll look at.

Listing 7.2 - EncryptedValuesInPropertiesFileTest Unit Test

package org.thoth.rsa;

import java.util.Properties;
import org.junit.jupiter.api.Assertions;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;

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

  protected Rsa4096 rsa;

  @BeforeEach
  public void setUp() throws Exception {
    rsa = new Rsa4096(
        "./private_key_rsa_4096_pkcs8-generated.pem"
      , "./public_key_rsa_4096_pkcs8-exported.pem"
    );
  }

  @Test
  public void test_encrypted_values_in_properties_file()
    throws Exception {
    // Setup
    Properties encryptedAndEncoded
      = new Properties();
    encryptedAndEncoded.load(
      this
      .getClass()
      .getClassLoader()
      .getResourceAsStream(
        "./some_app.properties"
      )
    );

    // Test
    String passwordActual
      = rsa.decryptFromBase64(
        encryptedAndEncoded.getProperty("password")
      );
    String ssnActual
      = rsa.decryptFromBase64(
        encryptedAndEncoded.getProperty("ssn")
      );

    // Assert
    Assertions.assertEquals("SECRET", passwordActual);
    Assertions.assertEquals("123-45-7890", ssnActual);
  }
}

Listing 7.2 shows the EncryptedValuesInPropertiesFileTest unit test. The test reads in the some_app.properties file and hopefully it is able to decode and decrypt the values within it.

First, the @BeforeEach method creates an instance of my Rsa4096 class. This uses the private and public key files generated by OpenSSL. These key files are on the Java Class Path when the unit test runs. Rsa4096 is used to decode and decrypt the contents of the encrypted file.

Second, a Properties object is created and load() is called to load it with the contents of the properties file. Remember, the some_app.properties file is found on the the Class Path.

Third, the encrypted and encoded values are retrieved from the Properties object and then Rsa4096 is used to decode and decrypt those values them by calling decryptFromBase64().

Finally, the JUnit assertions make sure decoding and decryption were successful and that the test got back the original value.

That's it. We did it! All of the goals we set out to achieve have been accomplished. Just to make sure, let's review.

Summary

The purpose of this blog is to demonstrate Java's interoperability with OpenSSL:

  • Generate private and public keys with OpenSSL
  • Encrypt values with OpenSSL
  • Decrypt values with Java

I was able to demonstrate this by defining and accomplishing the following goals:

Encryption tool of choice is OpenSSL. It is on every Linux/Unix system, is an industry standard, and will be familiar to all DevOps teams. I demonstrated OpenSSL commands to perform all needed operations. For cases openssl could not do everything on its own, the command was piped to other standard Linux/Unix tools like sed.

Encryption performed by DevOps, or another team, so there is a separation of responsibilities. No one on the development team may know an unencrypted value. I demonstrated this showing separate commands for generating private and public key files and for encrypting files or values. Being separate commands, there can be separation of responsibilities if required.

All environments will use their own keys. No key sharing. I demonstrated this by showing how easy it is to execute the commands for generating keys. These commands may even be automated by an infrastructure as code process for each environment.

All keys and encrypted values may be regenerated at any time with no change to the application. Maven can easily add files to the Class Path when running unit tests and I took advantage of this developing my tests. I hope it's clear that even if you use the Class Path strategy as I did, it is trivial to regenerate all keys and encrypted values. A restart the application will read everything anew. No changes to the application are needed. Keep in mind it is possible for you to create your own strategy and write code to support that strategy that also makes the "no changes" goal impossible...try not to do that :)

Encryption will be either of an entire file or of specific values within a (properties) file. I demonstrated this with the OpenSSL commands to do both. I also provide the EncryptedFileTest and the EncryptedValuesInPropertiesFileTest unit tests to prove it works.

Encrypted values and keys are made available to the Java runtime using a strategy agreed upon and enforced by both DevOps and Development teams. I demonstrated this by deciding my code would take advantage of Maven's ability to put files on the Class Path. Therefore, my strategy is reading the files from the Class Path. Of course you can decide on your own strategy and update the code to support it.

Decryption is performed by the Java application for whatever purposes it needs. Don't log the encrypted values! I demonstrated this with the Rsa4096 class which performs the decoding and decryption. Also - and this is very important - I never log any of the decoded and decrypted values in either the Rsa4096 class or in the unit tests.

That's it! Thanks for taking this journey with me. This was a fun topic of research and I hope you have found some value in reading through this. Email me or leave a comment and let me know.

References

Remijan, M. (2017, December 22). Choosing Java Cryptographic Algorithms Part 3 - Public/Private key asymmetric encryption. Retrieved from http://mjremijan.blogspot.com/2017/12/choosing-java-cryptographic-algorithms_5.html.

Java Code Examples for java.security.PrivateKey. (n.d.) Retrieved from http://www.javased.com/index.php?api=java.security.PrivateKey

destan. (2017, October 1). ParseRSAKeys.java. Retrieved from https://gist.github.com/destan/b708d11bd4f403506d6d5bb5fe6a82c5

admin. (2018, August 21). Using OpenSSL to encrypt messages and files on Linux. Retrieved from https://linuxconfig.org/using-openssl-to-encrypt-messages-and-files-on-linux

Java Code Examples for java.security.spec.PKCS8EncodedKeySpec. (n.d.) Retrieved from https://www.programcreek.com/java-api-examples/java.security.spec.PKCS8EncodedKeySpec

No comments:

Post a Comment