Asymmetric (Public-key) Encryption Using RSA, Java and OpenSSL

Sorry, comments are closed.

Public-key Encryption Basics

Unless otherwise noted, public-key cryptography is a field or science, while public-key encryption is the applied knowledge of that science. Some articles use the terms interchangeably. There are also numerous articles that cover the implementation specifics and mathematics behind public-key cryptography. It is less important for the purposes of this article that you understand those details, and that you instead grasp the holistic benefits of using public-key encryption.

First and foremost, public-key encryption is asymmetric. In asymmetric cryptography two keys are required: a public key that encrypts plain text into cipher text, and a private key that decrypts the cipher text back into plain text. This is in contrast to symmetric cryptography where there is a single private key that encrypts and decrypts.

In a single domain, like a website or application, symmetric encryption is often sufficient. The same party encrypting is typically the same party decrypting. However, asymmetric encryption is preferred when dealing with multiple parties or domains. The public key can be shared with everyone, while the private key remains a secret to all but one party. Theoretically since the private key does not exist in multiple locations it reduces the likelihood it could be intercepted during transmission or stolen outright.

Because of the nature of public-key encryption there are limits on the size of the data that can be encrypted. Some analysts will argue that public-key encryption should only be used in a hybrid cryptosystem because of this inefficiency. There are certainly use cases for a hybrid approach, however, if the size of your data is within the constraints of your private key then public-key encryption can stand alone.

Another common use for public-key cryptography is digital signatures. Although public-key encryption is a necessary component of digital signatures, this article does not describe the process of using a private and public key to generate and verify a digital signature.

If you have questions about Java and cryptography after some research using the Java Cryptography Architecture (JCA) reference guide, then you can visit the Cryptography Stack Exchange or StackOverflow.

Creating Private and Public Key Pair Files Using OpenSSL

You may be building an application where you are required to programmatically create a private and public key pair using Java. However, if you are going to be providing the public key for limited use via email, FTP, or via a download, then using OpenSSL can simplify the effort. After generating the keys you are only required to write the code for encryption and decryption.

Many Linux distributions come with pre-compiled versions of OpenSSL already installed. If you are on Windows then you can visit the OpenSSL project page for any maintained binaries. Another option for Windows is to download and install the Apache HTTP Server binary that includes OpenSSL and mod_ssl. Whatever option you choose the command line arguments will be the same across similar versions.

The first step is generating an RSA private key.

> openssl genrsa -out "c:\temp\private.pem" 2048

As short as that command is, there are two very important pieces of it to understand. The first of which relates to the key format. This generates a PEM, which is an ASCII human-readable standard format that is used by OpenSSL. This format allows for easy copy/paste and verification of keys. Java does not work well with the PEM format, and instead prefers the binary DER format, which will be covered shortly.

The second piece of the command to understand is called the key length, bit key length, or the modulus. This is the number at the end. With regard to RSA public-key encryption the key length is both an indication of the amount of data you can encrypt and the private key’s strength. So if more bits equals greater strength, then why not 4096, or 8192? This primarily has to do with performance, and the speed of encryption and decryption. Also, some applications that make use of public-key encryption will only support certain key lengths.

You will need to know the domain in which you are using a key pair in order to determine the correct key length. In most circumstances a 2048 bit key length is sufficient. However, if you are a financial institution, or you need to encrypt more than 245 bytes then RSA public-key encryption alone may not be the best choice for you.

Now that a private key has been generated for decryption, a public key must be derived from it for use in encryption.

> openssl rsa -in "c:\temp\private.pem" -inform pem -out "c:\temp\public.key" -outform der -pubout

The command is self explanatory, and the only real difference with the previous command is that the public key that was output using the -outform argument is in DER format. More specifically, it also conforms to the X.509 public key infrastructure standards. Although the extension chosen is .key, it could very well have been .cer, .crt, or .der, and it would not affect the format. However, it is important to understand that what is output is not an actual X.509 certificate. It is a public key.

Now that the public key is in DER format, it is necessary to convert the private key into binary DER format for use with Java, which will conform to the PKCS#8 public-key cryptography standard.

> openssl rsa -in "c:\temp\private.pem" -inform pem -out "c:\temp\private.key" -outform der

For security reasons you should now delete the PEM file. If you wish to convert the DER back into PEM you can do so with another command.

> openssl pkcs8 -inform der -nocrypt < "c:\temp\private.key" > "c:\temp\private.pem"

From this PEM you can then derive the public key again.

Creating Private and Public Key Pair Files Using Java

Java has a feature rich cryptography API to support the generation of a key pair if a more systematic approach is required. The following is an immutable class that will generate a key pair using this API, and will allow you to save it to the file system as well.

package com.reindel.keys;

import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import java.security.GeneralSecurityException;
import java.security.KeyPair;
import java.security.KeyPairGenerator;
import java.security.PrivateKey;
import java.security.PublicKey;

public final class RSAKeyPair {
    
    private int keyLength;
    
    private PrivateKey privateKey;
    
    private PublicKey publicKey;
    
    public RSAKeyPair(int keyLength)
            throws GeneralSecurityException {
        
        this.keyLength = keyLength;
        KeyPairGenerator keyPairGenerator = KeyPairGenerator.getInstance("RSA");
        keyPairGenerator.initialize(this.keyLength);
        KeyPair keyPair = keyPairGenerator.generateKeyPair();
        privateKey = keyPair.getPrivate();
        publicKey = keyPair.getPublic();
    }
    
    public final PrivateKey getPrivateKey() {
        return privateKey;
    }
    
    public final PublicKey getPublicKey() {
        return publicKey;
    }
    
    public final void toFileSystem(String privateKeyPathName, String publicKeyPathName)
            throws IOException {
        
        FileOutputStream privateKeyOutputStream = null;
        FileOutputStream publicKeyOutputStream = null;
        
        try {
        
            File privateKeyFile = new File(privateKeyPathName);
            File publicKeyFile = new File(publicKeyPathName);

            privateKeyOutputStream = new FileOutputStream(privateKeyFile);
            privateKeyOutputStream.write(privateKey.getEncoded());

            publicKeyOutputStream = new FileOutputStream(publicKeyFile);
            publicKeyOutputStream.write(publicKey.getEncoded());
            
        } catch(IOException ioException) {
            throw ioException;
        } finally {
        
            try {
                
                if (privateKeyOutputStream != null) {
                    privateKeyOutputStream.close();
                }
                if (publicKeyOutputStream != null) {
                    publicKeyOutputStream.close();
                }   
                
            } catch(IOException ioException) {
                throw ioException;
            }
        }
    }
}

This is very similar to the OpenSSL command counterpart. There is an RSA private and public key pair, and bit key length argument in the constructor. When saving to the file system be sure to specify the full path, file name and extension. The following test demonstrates usage.

package com.reindel.keys;

import java.io.FileInputStream;
import java.security.KeyFactory;
import java.security.spec.PKCS8EncodedKeySpec;
import java.security.spec.X509EncodedKeySpec;
import org.apache.commons.io.IOUtils;
import org.junit.Assert;
import org.junit.Test;

public class RSAKeyPairTest {

    private final String privateKeyPathName = "C://temp//private.key";
    private final String publicKeyPathName = "C://temp//public.key";
    
    @Test
    public void testToFileSystem()
            throws Exception {

        try {
            
            RSAKeyPair rsaKeyPair = new RSAKeyPair(2048);
            rsaKeyPair.toFileSystem(privateKeyPathName, publicKeyPathName);

            KeyFactory rsaKeyFactory = KeyFactory.getInstance("RSA");

            Assert.assertNotNull(rsaKeyPair.getPrivateKey());
            Assert.assertNotNull(rsaKeyPair.getPublicKey());
            Assert.assertEquals(rsaKeyPair.getPrivateKey(), rsaKeyFactory.generatePrivate(new PKCS8EncodedKeySpec(IOUtils.toByteArray(new FileInputStream(privateKeyPathName)))));
            Assert.assertEquals(rsaKeyPair.getPublicKey(), rsaKeyFactory.generatePublic(new X509EncodedKeySpec(IOUtils.toByteArray(new FileInputStream(publicKeyPathName)))));
            
        } catch(Exception exception) {
            Assert.fail("The testToFileSystem() test failed because: " + exception.getMessage());
        }
    }
}

Encrypting and Decrypting Data With a Public and Private Key File

Data encryption and decryption can take place now that a public and private key file can be generated and saved to the file system. The RSACipher class will be built upon for the next two examples. With each example two new methods will be added to the class.

package com.reindel.ciphers;

import org.apache.commons.codec.binary.Base64;
import org.apache.commons.io.IOUtils;

import java.io.FileInputStream;
import java.io.IOException;
import java.security.GeneralSecurityException;
import java.security.KeyFactory;
import java.security.spec.PKCS8EncodedKeySpec;
import java.security.spec.X509EncodedKeySpec;
import javax.crypto.Cipher;

public class RSACipher {

    public String encrypt(String rawText, String publicKeyPath, String transformation, String encoding)
            throws IOException, GeneralSecurityException {

        X509EncodedKeySpec x509EncodedKeySpec = new X509EncodedKeySpec(IOUtils.toByteArray(new FileInputStream(publicKeyPath)));

        Cipher cipher = Cipher.getInstance(transformation);
        cipher.init(Cipher.ENCRYPT_MODE, KeyFactory.getInstance("RSA").generatePublic(x509EncodedKeySpec));

        return Base64.encodeBase64String(cipher.doFinal(rawText.getBytes(encoding)));
    }

    public String decrypt(String cipherText, String privateKeyPath, String transformation, String encoding)
            throws IOException, GeneralSecurityException {

        PKCS8EncodedKeySpec pkcs8EncodedKeySpec = new PKCS8EncodedKeySpec(IOUtils.toByteArray(new FileInputStream(privateKeyPath)));

        Cipher cipher = Cipher.getInstance(transformation);
        cipher.init(Cipher.DECRYPT_MODE, KeyFactory.getInstance("RSA").generatePrivate(pkcs8EncodedKeySpec));

        return new String(cipher.doFinal(Base64.decodeBase64(cipherText)), encoding);
    }
}

Most of the class is self documenting, but there are a few key aspects of ciphers that need to be discussed that are primarily related to the cipher’s transformation. The transformation is a set of characteristics for a cipher that makes it unique. Not only can it be symmetric or asymmetric (the algorithm), but it can also be constructed with a particular “mode” and “padding”. Out of the box the JDK supports several transformations, each with an algorithm, mode and padding definition. There is a lot that can be said about the mode and padding in a cipher, but there are two points that are especially important.

The first point is that even though the mode specified for the RSA algorithm will be ECB, it is ignored by Java. That’s because ECB (Electronic codebook), like other block modes of operation, are only used with symmetric encryption. Since RSA and public-key cryptography in general is asymmetric it doesn’t encrypt in blocks. For this reason some other encryption providers like Bouncy Castle use “NONE” as the mode when specifying RSA as the algorithm.

The second point is regarding the padding. Using RSA without padding is susceptible to known attacks, and should never be used. Realistically this is not even possible with Java’s cryptography API when specifying RSA as the algorithm, but it is worth noting. The following test demonstrates usage.

package com.reindel.ciphers;

import com.reindel.keys.RSAKeyPair;
import org.junit.Assert;
import org.junit.Test;

public class RSACipherTest {

    private final String privateKeyPathName = "C://temp//private.key";
    private final String publicKeyPathName = "C://temp//public.key";
    private final String transformation = "RSA/ECB/PKCS1Padding";
    private final String encoding = "UTF-8";
    
    @Test
    public void testEncryptDecryptWithKeyPairFiles()
            throws Exception {
    
        try {
     
            RSAKeyPair rsaKeyPair = new RSAKeyPair(2048);
            rsaKeyPair.toFileSystem(privateKeyPathName, publicKeyPathName);

            RSACipher rsaCipher = new RSACipher();
            String encrypted = rsaCipher.encrypt("John has a long mustache.", publicKeyPathName, transformation, encoding);
            String decrypted = rsaCipher.decrypt(encrypted, privateKeyPathName, transformation, encoding);
            Assert.assertEquals(decrypted, "John has a long mustache.");   
            
        } catch(Exception exception) {
            Assert.fail("The testEncryptDecryptWithKeyPairFiles() test failed because: " + exception.getMessage());
        }
    }
}

Encrypting and Decrypting Data With an In-memory Public and Private Key

Encrypting and decrypting data with an in-memory public and private key doesn’t require much additional work. Two overloaded methods have been added to the RSACipher to handle passing in a PublicKey and PrivateKey instead of the publicKeyPath and privateKeyPath.

package com.reindel.ciphers;

import org.apache.commons.codec.binary.Base64;
import org.apache.commons.io.IOUtils;

import java.io.FileInputStream;
import java.io.IOException;
import java.security.GeneralSecurityException;
import java.security.KeyFactory;
import java.security.PrivateKey;
import java.security.PublicKey;
import java.security.spec.PKCS8EncodedKeySpec;
import java.security.spec.X509EncodedKeySpec;
import javax.crypto.Cipher;

public class RSACipher {
    
    public String encrypt(String rawText, String publicKeyPath, String transformation, String encoding)
            throws IOException, GeneralSecurityException {
        
        X509EncodedKeySpec x509EncodedKeySpec = new X509EncodedKeySpec(IOUtils.toByteArray(new FileInputStream(publicKeyPath)));

        Cipher cipher = Cipher.getInstance(transformation);
        cipher.init(Cipher.ENCRYPT_MODE, KeyFactory.getInstance("RSA").generatePublic(x509EncodedKeySpec));

        return Base64.encodeBase64String(cipher.doFinal(rawText.getBytes(encoding)));
    }
    
    public String encrypt(String rawText, PublicKey publicKey, String transformation, String encoding)
            throws IOException, GeneralSecurityException {

        Cipher cipher = Cipher.getInstance(transformation);
        cipher.init(Cipher.ENCRYPT_MODE, publicKey);

        return Base64.encodeBase64String(cipher.doFinal(rawText.getBytes(encoding)));
    }

    public String decrypt(String cipherText, String privateKeyPath, String transformation, String encoding)
            throws IOException, GeneralSecurityException {

        PKCS8EncodedKeySpec pkcs8EncodedKeySpec = new PKCS8EncodedKeySpec(IOUtils.toByteArray(new FileInputStream(privateKeyPath)));

        Cipher cipher = Cipher.getInstance(transformation);
        cipher.init(Cipher.DECRYPT_MODE, KeyFactory.getInstance("RSA").generatePrivate(pkcs8EncodedKeySpec));

        return new String(cipher.doFinal(Base64.decodeBase64(cipherText)), encoding);
    }
    
    public String decrypt(String cipherText, PrivateKey privateKey, String transformation, String encoding)
            throws IOException, GeneralSecurityException {

        Cipher cipher = Cipher.getInstance(transformation);
        cipher.init(Cipher.DECRYPT_MODE, privateKey);

        return new String(cipher.doFinal(Base64.decodeBase64(cipherText)), encoding);
    }
}

The following test demonstrates usage. The test method testEncryptDecryptWithKeyPair() has been added to the RSACipherTest.

package com.reindel.ciphers;

import com.reindel.keys.RSAKeyPair;
import org.junit.Assert;
import org.junit.Test;

public class RSACipherTest {

    private final String privateKeyPathName = "C://temp//private.key";
    private final String publicKeyPathName = "C://temp//public.key";
    private final String transformation = "RSA/ECB/PKCS1Padding";
    private final String encoding = "UTF-8";
    
    @Test
    public void testEncryptDecryptWithKeyPairFiles()
            throws Exception {
    
        try {
     
            RSAKeyPair rsaKeyPair = new RSAKeyPair(2048);
            rsaKeyPair.toFileSystem(privateKeyPathName, publicKeyPathName);

            RSACipher rsaCipher = new RSACipher();
            String encrypted = rsaCipher.encrypt("John has a long mustache.", publicKeyPathName, transformation, encoding);
            String decrypted = rsaCipher.decrypt(encrypted, privateKeyPathName, transformation, encoding);
            Assert.assertEquals(decrypted, "John has a long mustache.");   
            
        } catch(Exception exception) {
            Assert.fail("The testEncryptWithPublicKeyFile() test failed because: " + exception.getMessage());
        }
    }

    @Test
    public void testEncryptDecryptWithKeyPair()
            throws Exception {
    
        try {
     
            RSAKeyPair rsaKeyPair = new RSAKeyPair(2048);
            
            RSACipher rsaCipher = new RSACipher();
            String encrypted = rsaCipher.encrypt("John has a long mustache.", rsaKeyPair.getPublicKey(), transformation, encoding);
            String decrypted = rsaCipher.decrypt(encrypted, rsaKeyPair.getPrivateKey(), transformation, encoding);
            Assert.assertEquals(decrypted, "John has a long mustache.");   
            
        } catch(Exception exception) {
            Assert.fail("The testEncryptDecryptWithKeyPair() test failed because: " + exception.getMessage());
        }
    }
}

Working With Keys in a Java Keystore

There is a third option for working with keys, and that is to store them and retrieve them from a Java keystore. Creating a private and public key pair with keytool actually requires specifying a keystore. Many developers find that working with the Java keystore is cumbersome and often times the terminology used is confusing. This is because of the vast array of information available describing techniques for manipulating keys and certificates.

The first thing to remember is that in theory there is a difference between a Java keystore and a truststore. A keystore contains private keys, and the certificates with their corresponding public keys. A truststore should contain just the certificates from other parties that you want to use for encryption (and digital signature verification), and also contains Certificate Authorities (CAs) that you trust to identify other parties. The /jre/lib/security/cacerts file is a truststore with well known CAs trusted by Java.

The following keytool command will generate a key pair and store it in a keystore.

> keytool -genkey -keyalg RSA -keysize 2048 -keystore "c:\temp\keystore.jks" -alias yourkeyalias -storepass yourstorepassword -keypass yourkeypassword -dname "CN=Your Full Name, OU=Your Department Name, O=Your Company Name, L=Your City, S=Your State, C=Your Country Code"

By default the key size for the RSA algorithm is limited to 1024 bits. In order to get a 2048 bit key you may need to download the Java Cryptography Extension (JCE) Unlimited Strength Jurisdiction Policy Files 6.

The following keytool command will export the certificate containing the public key.

> keytool -exportcert -alias yourkeyalias -keypass yourkeypassword -keystore "c:\temp\keystore.jks" -storepass yourstorepassword -file "c:\temp\public.cer"

Remember that unlike the public key derived from the private key using OpenSSL, this is an actual X.509 certificate. You have the option to send the certificate to another party as is, and it can be imported into their truststore, or you can export the public key from the certificate using the following OpenSSL command.

> openssl x509 -inform der -in "c:\temp\public.cer" -pubkey -noout | openssl pkey -pubin -outform der > "c:\temp\public.key"

Keep in mind the other party is trusting that the certificate is yours based upon your word alone. There has been no chain of trust by signing your certificate with an established third party CA. However, depending upon your application needs and implementation details this may not be a requirement.

Reading keys and certificates from a truststore and keystore using Java is not covered in this revision.

Revision History

Revision 1.0, 11/28/2013 – Publication.

Code examples assume JDK 1.6+, and tests assume JUnit 4+.