RSA on the JVM

By Moritz Kammerer's Personal Blog

This post shows how to work with RSA on the JVM with the Java language. We’re gonna:

  • Generate two keypairs (one for alice, one for bob)
  • Save these keypairs to a file and load them again
  • Send a message from alice to bob using a hybrid encryption scheme with confidentiality and authenticity

First, let’s generate the key pairs:

private KeyPair generateKeyPair() { KeyPairGenerator keyPairGenerator = KeyPairGenerator.getInstance("RSA"); keyPairGenerator.initialize(2048); // bits
 return keyPairGenerator.generateKeyPair();
}

Note that we use RSA keys with 2048 bit size. If you’re paranoid, you can set this to 4096 (see this Wikipedia site for recommendations).

KeyPair alice = generateKeyPair();
KeyPair bob = generateKeyPair();

Let’s add some methods to save these keys for later use:

private void savePrivateKey(Path file, PrivateKey privateKey) { // Saves the private key encoded in PKCS #8
 PKCS8EncodedKeySpec privateSpec = new PKCS8EncodedKeySpec(privateKey.getEncoded()); Files.write(file, privateSpec.getEncoded());
}

This method will save the RSA private key in a format called PKCS #8. Note that we don’t use the PEM encoding here (stuff starting with -----BEGIN ...), we just save the binary representation.

And here’s the method to save the public key:

private void savePublicKey(Path file, PublicKey publicKey) { // Saves the public key encoded in X.509
 X509EncodedKeySpec publicSpec = new X509EncodedKeySpec(publicKey.getEncoded()); Files.write(file, publicSpec.getEncoded());
}

The public key is stored in a format called X.509.

Now we can use these to store the private and public keys from alice and bob:

savePrivateKey(Paths.get("alice.private"), alice.getPrivate());
savePublicKey(Paths.get("alice.public"), alice.getPublic()); savePrivateKey(Paths.get("bob.private"), bob.getPrivate());
savePublicKey(Paths.get("bob.public"), bob.getPublic());

As we can now save private and public keys in files, methods to load them back from the files would be great:

private PrivateKey loadPrivateKey(Path file) { byte[] privateKey = Files.readAllBytes(file); KeyFactory keyFactory = KeyFactory.getInstance("RSA"); // Expects the private key encoded in PKCS #8
 return keyFactory.generatePrivate(new PKCS8EncodedKeySpec(privateKey));
}

This will load the private key from the given file. The file needs to be encoded in PKCS #8 format.

private PublicKey loadPublicKey(Path file) { byte[] publicKey = Files.readAllBytes(file); KeyFactory keyFactory = KeyFactory.getInstance("RSA"); // Expects the public key encoded in X.509
 return keyFactory.generatePublic(new X509EncodedKeySpec(publicKey));
}

This method will load the public key from the given file. The file needs to be encoded in X.509 format.

Now we can use these methods to load the keys from the file again:

alice = new KeyPair( loadPublicKey(Paths.get("alice.public")), loadPrivateKey(Paths.get("alice.private"))
); bob = new KeyPair( loadPublicKey(Paths.get("bob.public")), loadPrivateKey(Paths.get("bob.private"))
); 

Great. Now we can start to encrypt data. We will use a hybrid encryption scheme: first, we generate an AES 256 bit session key. With this session key, we will AES/GCM encrypt the secret message. Then we will encrypt this session key with the RSA public key of the receiver. We do this as RSA has some limitations:

First, it’s really slow, AES is much faster. Second, RSA can only encrypt data which is smaller than the key size (2048 bits in our example). As our AES session key is only 256 bit, the slowness and data limitation of RSA doesn’t matter.

First, let’s generate a random session key:

private byte[] generateSessionKey() { byte[] sessionKey = new byte[256 / 8]; // 256 bits
 new SecureRandom().nextBytes(sessionKey); return sessionKey;
}

Second, we need a method to encrypt data with AES/GCM:

private byte[] encryptWithAES(byte[] data, byte[] sessionKey) { Cipher cipher = Cipher.getInstance("AES/GCM/NoPadding"); Key keySpec = new SecretKeySpec(sessionKey, "AES"); // 128 is the size of the authentication tag in bits. 128 is the maximum.
 // Nonce is always 96 bit long. We use 0 as nonce here, as we generate a new session key each time. Reusing a nonce
 // is only fatal when the key is the same.
 GCMParameterSpec gcmSpec = new GCMParameterSpec(128, new byte[96 / 8]); cipher.init(Cipher.ENCRYPT_MODE, keySpec, gcmSpec); return cipher.doFinal(data);
}

This method will encrypt the given data with the given session key.

Now we can use these two methods to encrypt some data with a generated session key:

byte[] sessionKey = generateSessionKey(); String data = "Dear Bob, this is my very secret message, only for your eyes.";
byte[] aesEncrypted = encryptWithAES(data.getBytes(StandardCharsets.UTF_8), sessionKey);

encrypted now contains the AES encrypted data we want to send to Bob. To decrypt it, Bob needs the session key, and in the next step we will encrypt the session key with the RSA public key from Bob.

First, create a new method to encrypt data with a RSA public key:

private byte[] encryptWithRSA(byte[] data, PublicKey recipientPublicKey) { Cipher rsa = Cipher.getInstance("RSA/ECB/OAEPWITHSHA-256ANDMGF1PADDING"); rsa.init(Cipher.ENCRYPT_MODE, recipientPublicKey); return rsa.doFinal(data);
}

We use RSA with OAEP padding (Optimal asymmetric encryption padding - great name!). Don’t let the ECB in the code above scare you, in this case it’s safe. OAEP will make sure that the same plaintext will not encrypt to the same ciphertext (a property ECB has and which makes it insecure).

Now we can use this method to encrypt the session key with the recipients public key (in this case Bobs public key):

byte[] rsaEncrypted = encryptWithRSA(sessionKey, bob.getPublic());

We can now send aesEncrypted and rsaEncrypted over an insecure wire to Bob. But how does Bob knows that the data is really from Alice? With the code above we have created confidentiality, but not authenticity (and no, use of AES/GCM hasn’t fixed that). To do that, we can use another great property of RSA: it can sign data so that the receiver can be sure that the sender really is the sender and not some attacker.

Let’s create a method for that:

private byte[] signWithRSA(byte[] data, PrivateKey senderPrivateKey) { Signature rsa = Signature.getInstance("SHA256withRSA"); rsa.initSign(senderPrivateKey); rsa.update(data); return rsa.sign();
}

This method will hash the given data with SHA-256 and then sign this hash with the private key of the sender (in our case Alice). To verify the signature the recipient (Bob) needs the public key of the sender (Alice). That’s the only open problem with this scheme: you’ll need to somehow get Alice’s public key to Bob without letting an attacker swap Alice’s public key with his own. Unfortunately I can’t give you a great solution for that :(

But back to signing the message with RSA:

byte[] signature = signWithRSA(rsaEncrypted, alice.getPrivate());

Now we can send aesEncrypted, rsaEncrypted and signature to Bob over an insecure wire. Bob needs the public key of Alice to verify the signature and then his own private key to decrypt the session key. Then he can use the session key to AES decrypt the data.

Let’s start with signature verification:

private boolean verifyWithRSA(byte[] data, byte[] signature, PublicKey senderPublicKey) { Signature rsa = Signature.getInstance("SHA256withRSA"); rsa.initVerify(senderPublicKey); rsa.update(data); return rsa.verify(signature);
}

This method will create the SHA-256 hash of the given data and uses the sender’s public key to verify if it matches the given signature. If everything is good, it returns true, otherwise it returns false. We can now use this to verify that the message is really from Alice:

if (!verifyWithRSA(rsaEncrypted, signature, alice.getPublic())) { throw new IllegalStateException("Unable to verify the authenticity of the message!");
}

Next, we add a method to decrypt data with RSA:

private byte[] decryptWithRSA(byte[] ciphertext, PrivateKey recipientSecretKey) { Cipher rsa = Cipher.getInstance("RSA/ECB/OAEPWITHSHA-256ANDMGF1PADDING"); rsa.init(Cipher.DECRYPT_MODE, recipientSecretKey); return rsa.doFinal(ciphertext);
}

Now we can use this method to decrypt the ciphertext of rsaEncrypted to Bobs copy of the session key using his private key:

byte[] bobsSessionKey = decryptWithRSA(rsaEncrypted, bob.getPrivate());

This session key can now be used to AES decrypt the ciphertext in aesEncrypted. First, let’s add a method to decrypt AES:

private byte[] decryptWithAES(byte[] ciphertext, byte[] sessionKey) { Cipher cipher = Cipher.getInstance("AES/GCM/NoPadding"); Key keySpec = new SecretKeySpec(sessionKey, "AES"); // See encryptWithAES for the details on the numbers
 GCMParameterSpec gcmSpec = new GCMParameterSpec(128, new byte[96 / 8]); cipher.init(Cipher.DECRYPT_MODE, keySpec, gcmSpec); return cipher.doFinal(ciphertext);
}

and now let’s use it to get Bob’s copy of the data:

byte[] bobsDataAsBytes = decryptWithAES(aesEncrypted, bobsSessionKey);
String bobsData = new String(bobsDataAsBytes, StandardCharsets.UTF_8);

Let’s print it to check if everything has worked:

System.out.println(bobsData);

And it prints:

Dear Bob, this is my very secret message, only for your eyes.

Yeeeeees, great success! You have now learned to build a hybrid encryption scheme using the Java Cryptography Extension framework, leveraging RSA for the public key encryption and AES for the symmetric encryption.

Armed with that knowledge it should be no problem for you to build the reverse way, sending a message from Bob to Alice.

You can see the whole code in action in this GitHub repository.



Back to posts