Friday, April 4, 2014

Encrypting JDBC Connection Strings

Authentication between an public-facing web application and a data source is always difficult because of the possibility that the information may be compromised at the application server. There are two primary approaches: simple user names and passwords or RSA public-private keys to identify the application. We are interested in the first case because it is commonly used.

Our assumption is that the application server has already been breached, so what can we do to protect the data servers behind the application? We need to use a defense-in-depth approach: multiple layers of security using different technologies frustrates attackers: that forces them to have exploits to circumvent every device used.

Defense-in-depth of the data includes protecting the database from SQL injection attacks and limiting access to the database from a small set of computers. These defenses are discussed in other posts, our immediate focus is on protecting the credentials for authentication.

Protecting the Credentials

The user name and password for basic authentication are often kept along with a connection string used to access the database. Storing this as clear text makes the information readily available on a compromised server. Protecting the password is not enough; the user name and path to the database server and the network port used for communication must also be protected. Even without the password that information provides a starting point for a malicious attack against the database.

So all three values should be encrypted. The encrypted values should be externalized from the application (kept in a properties file) because they are likely to change. Keeping them embedded in the application will require a recompilation if the string changes, which is a violation of the open for extension, closed for modification design principle (Martin).

The key used to encrypt a these strings is not very likely to change; infrequently if ever. So compiling the key into the application does not violate our principles. The compilation is one step on a path to making the key difficult to retrieve, even if the server is compromised and the application is available.

Our focus right now will be on using an encrypted connection string, so, we need:
  1. A pair of public and private asymmetric encryption keys
  2. A class to use those keys to encrypt and decrypt the string values

Generate a public and private key pair

The first step is to create an asymmetric key pair to encrypt the connection string. We can do this easily with the openssl program from http://www.openssl.org. The keys are placed in the file keys.pem. A key size of 2048 bits is selected for better security:

openssl genrsa -out keys.pem 2048


If we use the public key to encrypt the connection string, then the private key may be used to decrypt it. Openssl created a pem file with a private key, and the public key is embedded in that data. We can use the following command to isolate the public key in its own publickey.txt file:

openssl rsa -in keys.pem -pubout > publickey.txt


We also need the private key in  PKCS8 format, not PEM format. We can place it in a privatekey.txt file with this command:

openssl pkcs8 -topk8 -nocrypt -in keys.pem -out privatekey.txt

Create a class to manage encryption

This StringCipher class will be used to encrypt and decrypt a string using the public and private keys generated from openssl. The steps to build this class are to:
  1. Embed the public and private keys in the class
  2. Build methods to create a public or private key class instance from the embedded data
  3. Provide a public method to encrypt a connection string using the public key
  4. Provide a public method to decrypt a connection string using the private key
So from the top, the first part is to embed the base64 data from the privatekey.txt and publickey.txt files into a class. The data is on multiple lines in our file, but can be joined into a single string in the application. The ellipses represent the key data we are not showing:

package com.wonderfulwidgets;

public class StringCipher {

@tab;private String privateKeyData = "-----BEGIN PRIVATE KEY----- ...";
@tab;private String publicKeyData = "-----BEGIN PUBLIC KEY----- ...";
}


The second step is to build the methods to translate the data into keys. Both methods actually need to do the same thing but with different output, so a helper method to avoid duplication is warranted. The helper method returns a different class of object for the public and private key. Both methods use a KeyFactory, so a constructor is used to create a shared instance. We will handle the exceptions that may be thrown whereever these methods are used:

import java.security.Key;
import java.security.KeyFactory;
import java.security.spec.InvalidKeySpecException;
import java.security.spec.KeySpec;
import java.security.spec.PKCS8EncodedKeySpec;
import java.security.spec.X509EncodedKeySpec;

public class StringCipher {
@tab;...

@tab;public StringCipher() throws StringCipherException {

@tab;@tab;try {

@tab;@tab;@tab;keyFactory = KeyFactory.getInstance("RSA");
@tab;@tab;}

@tab;@tab;catch (Exception e) {

@tab;@tab;@tab;throw new StringCipherException(e);
@tab;@tab;}
@tab;}

@tab;private Key getPrivateKey() throws InvalidKeySpecException {

@tab;@tab;KeySpec keySpec = getKeySpec(privateKeyData);

@tab;@tab;return keyFactory.generatePrivate(keySpec);
@tab;}

@tab;private Key getPublicKey() throws InvalidKeySpecException {

@tab;@tab;KeySpec keySpec = getKeySpec(publicKeyData);

@tab;@tab;return keyFactory.generatePublic(keySpec);
@tab;}

@tab;private KeySpec getKeySpec(String data) {

@tab;@tab;KeySpec result = null;
@tab;@tab;boolean privateKey = data.contains("PRIVATE");

@tab;@tab;// Remove the marker lines surrounding the data

@tab;@tab;String modifiedData = data.replaceAll("-----[A-Z ]*-----", "");

@tab;@tab;// Base64 decode the data

@tab;@tab;byte [] decoded = DatatypeConverter.parseBase64Binary(modifiedData);

@tab;@tab;// Return the appropriate KeySpec for a private or public key

@tab;@tab;if (privateKey) {

@tab;@tab;@tab;result = new PKCS8EncodedKeySpec(decoded);

@tab;@tab;} else {

@tab;@tab;@tab;result = new X509EncodedKeySpec(decoded);
@tab;@tab;}

@tab;@tab;return result;
@tab;}
}


Now for the method that encrypts a string and then encodes it as a base64 readable string value. There are many potential exceptions that could be thrown by the classes that this method uses. To simplify that we have our own exception class and wrap anything thrown at us with a new StringCipherException:

import javax.crypto.Cipher;
import javax.xml.bind.DatatypeConverter;

public class StringCipher {

@tab;private KeyFactory keyFactory;
...

@tab;public String encrypt(String data) throws StringCipherException {

@tab;@tab;byte[] cipherText = null;

@tab;@tab;try {

@tab;@tab;@tab;Cipher cipher = Cipher.getInstance("RSA");

@tab;@tab;@tab;cipher.init(Cipher.ENCRYPT_MODE, getPublicKey());
@tab;@tab;@tab;cipherText = cipher.doFinal(data.getBytes());
@tab;@tab;}

@tab;@tab;catch (Exception e) {

@tab;@tab;@tab;throw new StringCipherException(e);
@tab;@tab;}

@tab;@tab;// Encode the encrypted connection string in base64 and print it.

@tab;@tab;return DatatypeConverter.printBase64Binary(cipherText);
@tab;}
}


The last method we need is to decrypt an encrypted, base64 encoded string:

public class StringCipher {
@tab;...

@tab;public String decrypt(String data) throws StringCipherException {

@tab;@tab;byte[] decryptedText = null;

@tab;@tab;try {

@tab;@tab;@tab;// Decode and decrypt the encrypted string

@tab;@tab;@tab;Cipher cipher = Cipher.getInstance("RSA");

@tab;@tab;@tab;cipher.init(Cipher.DECRYPT_MODE, getPrivateKey());
@tab;@tab;@tab;decryptedText = cipher.doFinal(DatatypeConverter.parseBase64Binary(data));
@tab;@tab;}

@tab;@tab;catch (Exception e) {

@tab;@tab;@tab;throw new StringCipherException(e);
@tab;@tab;}

@tab;@tab;// Convert the decrypted text to a string and return it.

@tab;@tab;return new String(decryptedText);
@tab;}
}


An Eclipse project with this class is provided along with a unit test that demonstrates how the methods work and a class with a main method, EncryptString, that builds an encoded, encrypted string at the command line. EncryptString is useful for encrypting the connection string, username, and password to insert them into a properties file.

A step farther

A problem that remains is the potential decompilation of the application and the discovery of the key used to decrypt the connection string. A good decompiler, like JD, will reveal the source to a class file almost exactly as it was compiled.

The additional step that could be taken would be to try and make using a decompiled version difficult. One way is to use a Java obfuscator, but they tend to degrade the performance of the application. Another method is to use a string encryption technology internally in the application, but ultimately it suffers from the same problem we have: the keys to encrypt the strings can be found. And the third method is to obfuscate the keys manually. A possible way to do this is break the keys into multiple strings scattered throughout the application, perhaps even altered using a simple bit-shift-cipher, and then brought together to perform the encryption and decryption. The code that assembles the pieces may help, because the purpose for having it in the application is not really clear unless it is documented.

Unfortunately though, if I were trying to attack the application through decompilation I would not work forwards trying to find the key buried in the program data. I would locate where the Cipher class is used and work backwards, finding out where the key it uses came from, and then decipher the rules to put the key back together. Nothing is perfect, and ultimately obfuscating the keys may slow down an attacker only a little.

To be fair, everyone is in the same fix. The keys have to be on the application sever and that makes them vulnerable. Microsoft Windows and OS X both sport secure repositories for the keys that applications can use. But the repositories can be defeated just as easily as the application if the computer is compromised.

So I will leave it as an exercise for you to develop the code to read the encoded strings from the properties file, decrypt them, and then use them to establish a database connection :) Enjoy!

References

See the references page.

No comments:

Post a Comment