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.
- 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.
- 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.
- All environments will use their own keys. No key sharing.
- All keys and encrypted values may be regenerated at any time with no change to the application.
- Encryption will be either of an entire file or of specific values within a (properties) file.
- Encrypted values and keys are made available to the Java runtime using a strategy agreed upon and enforced by both DevOps and Development teams.
- 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