When it comes to encryption and signing RSA is the de facto standard for public key cryptography. Invented in 1977 RSA (named after it’s inventors, Ron Rivest, Adi Shamir, and Leonard Adleman) and it’s successors are still used in many if not most of the systems you use today. It is in fact one of the core components of what keeps your passwords safe when logging in; HTTPS. In this post I will show you how to use RSA in Java.
Introduction
As said RSA is a public key cryptography 'asymmetric' algorithm. This differs from the 'shared secret' 'symmetric' algorithms like DES or AES in that there are two keys. A public key that you share with anyone and a private key you keep secret. The public key can be used to encrypt data which can then only be decrypted using the private key. The private key can also be used to sign data; this signature can then be sent together with the data and used with the public key to verify that the data is not tampered with.
When you connect to a server over HTTPS the security layer (SSL) uses this mechanism to secure your connection. It however does not use RSA to encrypt your data directly; RSA is a rather slow algorithm. Instead RSA is used to exchange a symmetric key (for example AES) which is then used to encrypt data.
Note
|
This is a very high level overview; SSL (and it’s successor) actually support a whole range of different combinations of key exchanges and ciphers. This wikipedia page provides a nice list. |
The complete code example can be found in this Gist.
Generating key pairs in Java
So before we can actually do any form of encryption we need to have a public/private key pair; fortunately Java makes it quite easy to generate these for us! Let’s look at some code:
public static KeyPair generateKeyPair() throws Exception {
KeyPairGenerator generator = KeyPairGenerator.getInstance("RSA");
generator.initialize(2048, new SecureRandom());
KeyPair pair = generator.generateKeyPair();
return pair;
}
We get a RSA KeyPairGenerator instance and initialize it with a bit size of 2048 bits and pass in a new SecureRandom(). The latter is used as the entropy or random data source for the generator. On the third line we request it to generate a key pair. That’s all there is to generating a key pair.
If you want to use a key pair stored in a key store instead: I will explain this a bit further down. Let’s first try to encrypt and decrypt some information.
Encryption / Decryption
So now that we have a public/private pair, let’s use it to first encrypt a message and then decrypt the cipher text. Let’s start with the encrypt method:
public static String encrypt(String plainText, PublicKey publicKey) throws Exception {
Cipher encryptCipher = Cipher.getInstance("RSA");
encryptCipher.init(Cipher.ENCRYPT_MODE, publicKey);
byte[] cipherText = encryptCipher.doFinal(plainText.getBytes(UTF_8));
return Base64.getEncoder().encodeToString(cipherText);
}
Note
|
Instead of passing byte arrays around I base 64 encode/decode them because this is a rather common use case in the REST API’s I typically work on. You can of course leave out the base64 encoding/decoding and work with the byte arrays directly. |
Note
|
Make sure you explicitly and consistently specify the byte encoding for the strings you use. A single flipped bit will result in cipherTexts and signatures that won’t decrypt / verify. |
In the example we get an instance of the RSA cipher and set it up in encrypt mode where we also pass in the public key used to encrypt the message. We pass in the bytes of the plainText string in one go and end up with a byte array of encrypted bytes. These bytes then get base 64 encoded and returned.
Now we’ll also need a decrypt method:
public static String decrypt(String cipherText, PrivateKey privateKey) throws Exception {
byte[] bytes = Base64.getDecoder().decode(cipherText);
Cipher decriptCipher = Cipher.getInstance("RSA");
decriptCipher.init(Cipher.DECRYPT_MODE, privateKey);
return new String(decriptCipher.doFinal(bytes), UTF_8);
}
So the code above looks very similar. We get a RSA cipher, initialize it with the private key this time, and decrypt the bytes and turn them into a String again. So let’s put it all together:
//First generate a public/private key pair
KeyPair pair = generateKeyPair();
//Our secret message
String message = "the answer to life the universe and everything";
//Encrypt the message
String cipherText = encrypt(message, pair.getPublic());
//Now decrypt it
String decipheredMessage = decrypt(cipherText, pair.getPrivate());
System.out.println(decipheredMessage);
This will, after a short wait, print "the answer to life the universe and everything" in your console. You should also notice that it takes quite a bit of time. As explained RSA is rather slow; much slower than symmetric algorithms like AES.
Sign / Verify
So with a public key we can encrypt messages which then can be decrypted with the private key. While theoretically possible to do the reverse (encrypt with private key, decrypt with public) this is not secure at all and most libraries (including java.security) won’t let you.
What we can do however, and this is very useful when building API’s, is sign a message with our private key and verify the signature with the public key. This allows us to make sure that a message indeed comes from the creator of our public key (the private key holder) and that it wasn’t tampered with in transit. So let’s start with the sign function:
public static String sign(String plainText, PrivateKey privateKey) throws Exception {
Signature privateSignature = Signature.getInstance("SHA256withRSA");
privateSignature.initSign(privateKey);
privateSignature.update(plainText.getBytes(UTF_8));
byte[] signature = privateSignature.sign();
return Base64.getEncoder().encodeToString(signature);
}
It looks quite similar to our encrypt/decrypt functions. We get a Signature of type SHA256withRSA, initialize it with the private key, updated it with all the bytes in our message (you can do this in chunks with for example large files) and then generate a signature with the .sign() method. This signature is then returned as a Base64 encoded string.
You might wonder what that SHA256 bit is doing there. As shown before RSA is a rather slow algorithm. So SHA256withRSA doesn’t actually calculate a signature over all the input (which can be gigabytes worth of data), it actually calculates a SHA 256 over the entire input, pads it, and then calculates a signature. If you’re interested the entire process is described in this RFC.
So now that we have a signature we can use it to verify the message:
public static boolean verify(String plainText, String signature, PublicKey publicKey) throws Exception {
Signature publicSignature = Signature.getInstance("SHA256withRSA");
publicSignature.initVerify(publicKey);
publicSignature.update(plainText.getBytes(UTF_8));
byte[] signatureBytes = Base64.getDecoder().decode(signature);
return publicSignature.verify(signatureBytes);
}
Again quite similar to the previous bit. We get a Signature instance, set it up to verify with the public key, feed it all the plain text bytes and then use the signature bytes to see if the signature matches. This verify method returns a boolean indicating whether the signature is valid or not.
So to put this all together:
KeyPair pair = generateKeyPair();
String signature = sign("foobar", pair.getPrivate());
//Let's check the signature
boolean isCorrect = verify("foobar", signature, pair.getPublic());
System.out.println("Signature correct: " + isCorrect);
This should output "Signature correct: true". Go ahead and try to change a single byte of the input message or message to verify; it will fail to verify (return false instead).
Java KeyStore
If you ever are going to use this in a production scenario you are probably going to not generate keypairs on the fly (while that certainly has it’s uses!) but instead use Java KeyStores. KeyStores are a storage mechanism where you can have a number of keys stored in a single file. This file is encrypted itself (with PBEWithMD5AndTripleDES, quite a mouthful!) and has both per-store and per-key passwords.
So first let’s generate a keystore using the keytool JDK utility:
keytool -genkeypair -alias mykey -storepass s3cr3t -keypass s3cr3t -keyalg RSA -keystore keystore.jks
Note
|
If your OS can’t find keytool make sure you have the JDK installed and it’s bin directory is on your path |
This will create a keystore.jks file containing a single public / private key pair stored under the alias 'mykey'. Both the store and the key are password protected with the password 's3cr3t'. Keytool will ask you a bunch of questions (like firstname, lastname, etc.) which you can leave empty if you want. The last question, if the information is correct, will default to 'no' so don’t just blindly enter through all of them ;)
We should now have a keystore.jks file, I suggest putting it in your src/java/resources folder or in any other folder where it ends up on your classpath. So let’s see how we can read our keys from the store:
public static KeyPair getKeyPairFromKeyStore() throws Exception {
InputStream ins = RsaExample.class.getResourceAsStream("/keystore.jks");
KeyStore keyStore = KeyStore.getInstance("JCEKS");
keyStore.load(ins, "s3cr3t".toCharArray()); //Keystore password
KeyStore.PasswordProtection keyPassword = //Key password
new KeyStore.PasswordProtection("s3cr3t".toCharArray());
KeyStore.PrivateKeyEntry privateKeyEntry = (KeyStore.PrivateKeyEntry) keyStore.getEntry("mykey", keyPassword);
java.security.cert.Certificate cert = keyStore.getCertificate("mykey");
PublicKey publicKey = cert.getPublicKey();
PrivateKey privateKey = privateKeyEntry.getPrivateKey();
return new KeyPair(publicKey, privateKey);
}
In the example we open the key store on the classpath via getResourceAsStream. If you want you can adapt this to reading from a FileInputStream if your key store is not on the classpath.
We need to specify the same password twice; once for the store and once for the key itself. In production environments these are often different keys and not hard-coded so keep that in mind! We get the "mykey" private key and certificate (public key) entries which we can then use to create a KeyPair. We can use this key pair in exactly the same way in the code we created before:
KeyPair pair = getKeyPairFromKeyStore();
String signature = sign("foobar", pair.getPrivate());
//Let's check the signature
boolean isCorrect = verify("foobar", signature, pair.getPublic());
System.out.println("Signature correct: " + isCorrect);
Conclusion
I hope this is a nice starting point to help you integrate RSA or similar asymmetric algorithms in your code. I myself had to piece most of this information together from different sources recently when I had to sign REST requests to a partner API with a private key of which they have the public counterpart. This allows them to verify that my side is actually the party creating the requests and that the content of the requests weren’t tampered with.