April 24, 2020

Apache Derby Database JVM Security Policy

Abstract

I have already posted a number of blogs about Derby:

This wasn't intended to be a series. But over the years I've been using Derby more and more. I started using Derby as my database of choice for my Microservice architecture. These are personal-use applications, so Derby is more than sufficient. Even though these are personal-use applications, I require multiple servers with limited user permissions, and of course database backup and restoration. The final requirement is security. I run my Derby databases on an Ubuntu Linux VM with the derby usr account. Although the derby usr account has limited permissions on the VM, any extra layer of security is good. So the purpose of this blog is to demonstrate how to run Derby with a Java security policy to limit the JVM's permissions and enhance runtime security.

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.

  • Apache Derby 10.14.2.0
  • Java zulu11.39.15-ca-jdk11.0.7-linux_x64

I am not going to go through the process of downloading and installing these technologies. I'll leave that as an exercise for you.

NOTE Starting with version 10.15, the Derby project has been updated to use the Java 9 module system. As a result, the JAR files have changed quite a bit. It's unlikely the security.policy below will work with version 10.15+. As of this blog's publication date, I've yet to try it.

Linux bash scripts

In order to manage Derby to run with a Java security policy, you need 3 scripts. The 1st script will setup the setup environment variables to configure Derby. The 2nd script will start the Derby network server, passing the correct command line parameters. The 3rd will stop the Derby network server.

Listing 1.1 shows you the first of these scripts. It exports a number of system environment variables with configuration values specific to run Derby in your environment.

Listing 1.1 - setenv.sh

#!/bin/bash

export DERBY_HOME=/home/derby/opt/derby
export PATH="$DERBY_HOME/bin:$PATH"
echo "DERBY_HOME=$DERBY_HOME"

export JAVA_HOME=/home/derby/opt/java
echo "JAVA_HOME=$JAVA_HOME"

export NS_HOME=/var/local/derby/1527
mkdir -p $NS_HOME
echo "NS_HOME=$NS_HOME"

export NS_PORT=1527
echo "NS_PORT=$NS_PORT"

export NS_HOST=0.0.0.0
echo "NS_HOST=$NS_HOST"

export DERBY_OPTS=""
export DERBY_OPTS="$DERBY_OPTS -Dderby.drda.host=$NS_HOST"
export DERBY_OPTS="$DERBY_OPTS -Dderby.drda.portNumber=$NS_PORT"
export DERBY_OPTS="$DERBY_OPTS -Dderby.system.home=$NS_HOME"
# Security Policy
export DERBY_OPTS="$DERBY_OPTS -Dderby.stream.error.logSeverityLevel=0"
export DERBY_OPTS="$DERBY_OPTS -Dderby.security.port=$NS_PORT"
export DERBY_OPTS="$DERBY_OPTS -Dderby.install.url=file:$DERBY_HOME/lib/"
export DERBY_OPTS="$DERBY_OPTS -Djava.security.manager"
export DERBY_OPTS="$DERBY_OPTS -Djava.security.policy=$NS_HOME/security.policy"

DERBY_HOME is self explanatory. It's where Derby is unzipped (installed). Add Derby's bin directory to the PATH.

JAVA_HOME is self explanatory. It's where Java is unzipped (installed). Add Java's bin directory to the PATH.

NS_HOME is "Network Server Home". This is the directory the Derby network server will use to store its configuration and databases. Whenever a new database is created on this Derby network server, a new sub-directory will be created under NS_HOME for the new database. This allows multiple Derby network servers running on the same host to keep their data separate.

NS_PORT is "Network Server Port". It's the port the Derby network server uses to listen for connections. This allows multiple Derby network servers to run on the same host.

NS_HOST is "Network Server Host". It sets the network interface used by the Derby network server when listening for connections. By default, the Derby network server only listens for connections on the loopback address of 127.0.0.1. This default means clients must run on the same host as the network server - not very useful. By setting the host to 0.0.0.0, the Derby network server will listen for connections on any network interface on the host. If your VM has multiple network interfaces, NS_HOST should be set to the IP of one of those interfaces. Setting this value allows clients to be remote.

DERBY_OPTS is the system property used to get all of the configuration options to Derby. Its value is created by concatenating together the appropriate Derby system properties with their associated values. The first 3 properties are needed to start Derby with or without a security policy.

  1. derby.drda.host
  2. derby.drda.portNumber
  3. derby.system.home

The final 5 properties are needed for configuring Derby to run with a security policy.

  1. derby.stream.error.logSeverityLevel
  2. derby.security.port
  3. derby.install.url
  4. java.security.manager
  5. java.security.policy

One of the most important properties is java.security.policy=$NS_HOME/security.policy". The value of this property points to a security.policy file which will configure the Java SecurityManager. You will read about creating the security.policy file in just a little bit. Next, you will look at the script for starting the server.

Listing 1.2 shows you the second of these scripts. It starts the Derby networks server, passing the correct command line parameters so Derby runs with a security policy.

Listing 1.2 - start.sh

#!/bin/bash

# Directory of the script
SD=$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )

# Source in common variables
source $SD/setenv.sh

# Symlink the network server configurations
ln -sf $SD/../conf/security.policy $NS_HOME/security.policy
ln -sf $SD/../conf/derby.properties $NS_HOME/derby.properties

startNetworkServer

SD is Script Directory. The evaluation determines the fully-qualified file system location of the start.sh script and assigns it to SD. This is useful when referencing other scripts.

source is self explanatory. It sources in the system environment variables to configure the Derby network server. See listing 1.1 for details.

Symlink configuration is for the security.policy file and the derby.properties file. The purpose of the symlinks is to get these 2 files into the $NS_HOME directory. Derby looks for the derby.properties file in the $NS_HOME directory, so it needs to be there. For consistency (not a necessity), you want to put the security.policy file there as well. In listing 1.1 the java.security.policy=$NS_HOME/security.policy" property configures this location. For my environment, I have separated the $NS_HOME directory from the directory where I keep the management scripts and other Derby configuration files. The reason I do this is because of disaster recovery. I consider the $NS_HOME directory to be volitile, meaning if for some reason it goes missing (deleted, disk drive error, corrupted, new VM built, etc) I must be able to restore the database data, management scripts (setenv.sh, start.sh, stop.sh) and configuration files (security.policy, derby.properties) from my cloud backups. The real configuration files are kept outside of the $NS_HOME directory and start.sh symlinks them in the proper location.

startNetworkServer is a script provided by Derby ($DERBY_HOME/bin) to start the network server. The DERBY_OPTS variable - set in setenv.sh - is used to configure the network server. By default, Derby runs with a limited security policy. However, since you configured the security policy, Derby will use your configuration instead of the default.

You now have the Derby server environment configuration and start script. What you don't have yet is the ability to stop the Derby network server. Stopping the server is easy. You will look at the script for stopping the server next.

NOTE The security.policy file is also needed still. You will read about it in just a few moments, I promise!

Listing 1.3 shows you the third of these scripts. It stops the Derby networks server. Not too exciting, but it's important to have a managed shutdown of the server to prevent data corruption.

Listing 1.3 - stop.sh

#!/bin/bash

# Directory of the script
SD=$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )

# Source in common variables
source $SD/setenv.sh

stopNetworkServer

All of this is self explanatory. No further comments are needed for this script.

The security.policy file

Derby comes with a demo security policy file. It is located in DERBY_HOME/demo/templates/security.policy. Using this file as the starting point, I was able to produce a final version that met my requirements for:

  • Network (remote) access
  • Localhost access
  • Startup
  • Shutdown
  • Backup

Listing 2.1 - security.policy

//
//   Licensed to the Apache Software Foundation (ASF) under one or more
//   contributor license agreements.  See the NOTICE file distributed with
//   this work for additional information regarding copyright ownership.
//   The ASF licenses this file to You under the Apache License, Version 2.0
//   (the "License"); you may not use this file except in compliance with
//   the License.  You may obtain a copy of the License at
//
//      http://www.apache.org/licenses/LICENSE-2.0
//
//   Unless required by applicable law or agreed to in writing, software
//   distributed under the License is distributed on an "AS IS" BASIS,
//   WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
//   See the License for the specific language governing permissions and
//   limitations under the License.
//

grant codeBase "${derby.install.url}derby.jar"
{
  // These permissions are needed for everyday, embedded Derby usage.
  //
  permission java.lang.RuntimePermission "createClassLoader";
  permission java.util.PropertyPermission "derby.*", "read";
  permission java.util.PropertyPermission "user.dir", "read";
  permission org.apache.derby.security.SystemPermission "engine", "usederbyinternals";

  // The next two properties are used to determine if the VM is 32 or 64 bit.
  //
  permission java.util.PropertyPermission "sun.arch.data.model", "read";
  permission java.util.PropertyPermission "os.arch", "read";
  permission java.io.FilePermission "${derby.system.home}","read";
  permission java.io.FilePermission "${derby.system.home}${/}-",
      "read,write,delete";

  // Needed by sysinfo. A file permission is needed to check the existence of
  // jars on the classpath. You can limit this permission to just the locations
  // which hold your jar files. This block is reproduced for all codebases
  // which include the sysinfo classes--the policy file syntax does not let you
  // grant permissions to several codebases all at once.
  //
  permission java.util.PropertyPermission "user.*", "read";
  permission java.util.PropertyPermission "java.home", "read";
  permission java.util.PropertyPermission "java.class.path", "read";
  permission java.util.PropertyPermission "java.runtime.version", "read";
  permission java.util.PropertyPermission "java.fullversion", "read";
  permission java.lang.RuntimePermission "getProtectionDomain";
  permission java.io.FilePermission "java.runtime.version", "read";
  permission java.io.FilePermission "java.fullversion", "read";
  permission java.io.FilePermission "${derby.install.path}${/}-", "read";
  permission java.io.FilePermission "/tmp${/}-", "read,write,delete";

  // Permissions needed for JMX based management and monitoring.
  //
  // Allows this code to create an MBeanServer:
  //
  permission javax.management.MBeanServerPermission "createMBeanServer";

  // Allows access to Derby's built-in MBeans, within the domain
  // org.apache.derby.  Derby must be allowed to register and unregister these
  // MBeans.  To fine tune this permission, see the javadoc of
  // javax.management.MBeanPermission or the JMX Instrumentation and Agent
  // Specification.
  //
  permission javax.management.MBeanPermission
       "org.apache.derby.*#[org.apache.derby:*]",
       "registerMBean,unregisterMBean";

  // Trusts Derby code to be a source of MBeans and to register these in the
  // MBean server.
  //
  permission javax.management.MBeanTrustPermission "register";

  // Gives permission for jmx to be used against Derby but only if JMX
  // authentication is not being used.  In that case the application would need
  // to create a whole set of fine-grained permissions to allow specific users
  // access to MBeans and actions they perform.
  //
  permission org.apache.derby.security.SystemPermission "jmx", "control";
  permission org.apache.derby.security.SystemPermission "engine", "monitor";
  permission org.apache.derby.security.SystemPermission "server", "monitor";

  // getProtectionDomain is an optional permission needed for printing
  // classpath information to derby.log
  //
  permission java.lang.RuntimePermission "getProtectionDomain";

  // The following permission must be granted for Connection.abort(Executor) to
  // work. Note that this permission must also be granted to outer
  // (application) code domains.
  //
  permission java.sql.SQLPermission "callAbort";
  permission java.sql.SQLPermission "deregisterDriver";

  // Needed by FileUtil#limitAccessToOwner
  //
  permission java.lang.RuntimePermission "accessUserInformation";
  permission java.lang.RuntimePermission "getFileStoreAttributes";
};


grant codeBase "${derby.install.url}derbynet.jar"
{
  // These permissions lets the Network Server manage connections from clients.

  // Accept connections from any host. Derby is listening to the host interface
  // specified via the -h option to "NetworkServerControl start" on the command
  // line, via the address parameter to the
  // org.apache.derby.drda.NetworkServerControl constructor in the API or via
  // the property derby.drda.host; the default is localhost.  You may want to
  // restrict allowed hosts, e.g. to hosts in a specific subdomain,
  // e.g. "*.example.com".
  //
  permission java.net.SocketPermission "*", "accept";

  // Allow the server to listen to the socket on the port specified with the
  // -p option to "NetworkServerControl start" on the command line, or with
  // the portNumber parameter to the NetworkServerControl constructor in the
  // API, or with the property derby.drda.portNumber. The default is 1527.  
  permission java.net.SocketPermission "localhost:${derby.security.port}",
      "listen";
  permission java.net.SocketPermission "${derby.drda.host}:${derby.security.port}",
      "listen";      


  // Needed for server tracing.
  //
  permission java.io.FilePermission "${derby.drda.traceDirectory}${/}-",
      "read,write,delete";

  // Needed by FileUtil#limitAccessToOwner
  //
  permission java.lang.RuntimePermission "accessUserInformation";
  permission java.lang.RuntimePermission "getFileStoreAttributes";

  // Needed for NetworkServerMBean access (see JMX section above)
  //
  permission org.apache.derby.security.SystemPermission "server",
      "control,monitor";
  permission org.apache.derby.security.SystemPermission "engine", "usederbyinternals";

  // Needed by sysinfo. A file permission is needed to check the existence of
  // jars on the classpath. You can limit this permission to just the locations
  // which hold your jar files. This block is reproduced for all codebases
  // which include the sysinfo classes--the policy file syntax does not let you
  // grant permissions to several codebases all at once.
  //
  permission java.util.PropertyPermission "user.*", "read";
  permission java.util.PropertyPermission "java.home", "read";
  permission java.util.PropertyPermission "java.class.path", "read";
  permission java.util.PropertyPermission "java.runtime.version", "read";
  permission java.util.PropertyPermission "java.fullversion", "read";
  permission java.lang.RuntimePermission "getProtectionDomain";
  permission java.io.FilePermission "java.runtime.version", "read";
  permission java.io.FilePermission "java.fullversion", "read";
  permission java.io.FilePermission "${derby.install.path}${/}-", "read";

  permission java.util.PropertyPermission "derby.*", "read,write";

  permission java.net.SocketPermission "localhost:${derby.security.port}", "connect,resolve";
  permission java.net.SocketPermission "${derby.drda.host}:${derby.security.port}", "connect,resolve";  
};


grant codeBase "${derby.install.url}derbytools.jar"
{
  // Needed by sysinfo. A file permission is needed to check the existence of
  // jars on the classpath. You can limit this permission to just the locations
  // which hold your jar files. This block is for all codebases which include
  // the sysinfo classes--the policy file syntax does not let you grant
  // permissions to several codebases all at once.
  //
  permission java.util.PropertyPermission "user.*", "read";
  permission java.util.PropertyPermission "java.home", "read";
  permission java.util.PropertyPermission "java.class.path", "read";
  permission java.util.PropertyPermission "java.runtime.version", "read";
  permission java.util.PropertyPermission "java.fullversion", "read";
  permission java.lang.RuntimePermission "getProtectionDomain";
  permission java.io.FilePermission "<<ALL FILES>>", "read";
  permission java.io.FilePermission "java.runtime.version", "read";
  permission java.io.FilePermission "java.fullversion", "read";

  permission java.util.PropertyPermission "*", "read,write";
};

grant codeBase "${derby.install.url}derbyclient.jar"
{
  // Needed by sysinfo. A file permission is needed to check the existence of
  // jars on the classpath. You can limit this permission to just the locations
  // which hold your jar files. This block is reproduced for all codebases
  // which include the sysinfo classes--the policy file syntax does not let you
  // grant permissions to several codebases all at once.
  //
  permission java.util.PropertyPermission "user.*", "read";
  permission java.util.PropertyPermission "java.home", "read";
  permission java.util.PropertyPermission "java.class.path", "read";
  permission java.util.PropertyPermission "java.runtime.version", "read";
  permission java.util.PropertyPermission "java.fullversion", "read";
  permission java.lang.RuntimePermission "getProtectionDomain";
  permission java.io.FilePermission "${derby.install.path}${/}-", "read";

  // The following permission must be granted for Connection.abort(Executor) to
  // work.  Note that this permission must also be granted to outer
  // (application) code domains.
  //
  permission java.sql.SQLPermission "callAbort";

  permission java.net.SocketPermission "localhost:${derby.security.port}", "connect,resolve";
  permission java.net.SocketPermission "${derby.drda.host}:${derby.security.port}", "connect,resolve";
};

Policy files are a lot to take in. After 20 years using Java, I've only come across them just a handful of times. I don't pretend to know everything that goes into a policy file. All I know is this file is working for all my requirements. Each Derby update requires testing and maybe some tweeking. The derby-users@db.apache.org mailing list is your best source of information.

A big shout out to Rick Hillegas from the derby-users@db.apache.org mailing list for helping me get to this version of the policy file. He provided most of it and I added the following to meet my requirements.

Line 50 permission java.io.FilePermission "/tmp${/}-", "read,write,delete";. My database backup process uses CALL SYSCS_UTIL.SYSCS_BACKUP_DATABASE (‘/tmp/resiste-backup/1527’). So the derby.jar file needs read,write,delete permissions to the /tmp directory on the file system so it can write the backup into that directory.

Line 92 permission java.sql.SQLPermission "deregisterDriver";. When administering my Derby database with the the ij tool, found an exception in the derby.log file about deregisterDriver. So I added this permission to the derby.jar file as well.

Line 160 permission java.net.SocketPermission "${derby.drda.host}:${derby.security.port}", "connect,resolve";. Properties derby.drda.host and derby.security.port are set in the setenv.sh script (listing 1.1). I had to add this permission because my Derby network server is accessed by remote (non-localhost) clients. In setenv.sh, I use -Dderby.drda.host=0.0.0.0 to override the default localhost-only interface listening. I also found I needed this in the policy file while testing the stop.sh script (listing 1.3).

Summary

That's it. I hope you enjoyed learning how to run a Derby network server with a security policy.

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