File Encryption and Decryption using RSA in Java

Use RSA for File Encryption and Decryption in Java

“If you can’t do anything about it, laugh like hell.”
― David Cook

1. Introduction

RSA (Rivest–Shamir–Adleman) is an asymmetric encryption algorithm widely used in public-key cryptography today. The word asymmetric denotes the use of a pair of keys for encryption – a public key and a private key. When data is encrypted by one key, it can only be decrypted using the other key. The public key is publicized and the private key is kept secret.

An earlier article described how to use the RSA algorithm for digital signature. In this article, we examine how to use RSA for file encryption and decryption in java.

2. Java Imports

We need the following java classes for implementing RSA file encryption.

import java.nio.file.Files;
import java.nio.file.Paths;
import java.io.FileWriter;
import java.io.InputStream;
import java.io.OutputStream;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.util.Base64;
import java.util.Arrays;
import java.security.Key;
import java.security.KeyPair;
import java.security.KeyPairGenerator;
import java.security.KeyFactory;
import java.security.PrivateKey;
import java.security.PublicKey;
import java.security.SecureRandom;
import java.security.spec.PKCS8EncodedKeySpec;
import java.security.spec.X509EncodedKeySpec;
import javax.crypto.Cipher;
import javax.crypto.KeyGenerator;
import javax.crypto.SecretKey;
import javax.crypto.spec.SecretKeySpec;
import javax.crypto.spec.IvParameterSpec;

3. Generating Public and Private Keys

First step is to generate the RSA public and private keys. The keys are generated using the class KeyPairGenerator. We generate RSA keys of 2048 bits as these are of good strength today.

KeyPairGenerator kpg = KeyPairGenerator.getInstance("RSA");
kpg.initialize(2048);
KeyPair kp = kpg.generateKeyPair();

Once the KeyPair is obtained, we can get the public and private keys as follows:

PublicKey pub = kp.getPublic();
PrivateKey = kp.getPrivate();

4. Saving and Restoring Keys

Once the keys are generated, we need to save them for future use. The public key can be distributed, but the private key needs to be secured properly.

The public keys is saved into a file with the extension .pub and the private key is stored in a file with the extension .key. Both keys are stored in binary format.

try (FileOutputStream out = new FileOutputStream(fileBase + ".key")) {
    out.write(kp.getPrivate().getEncoded());
}

try (FileOutputStream out = new FileOutputStream(fileBase + ".pub")) {
    out.write(kp.getPublic().getEncoded());
}

For restoring the keys, we need to use specific classes for each. The public key is saved in X.509 format and must be restored using the appropriate class:

byte[] bytes = Files.readAllBytes(Paths.get(pubKeyFile));
X509EncodedKeySpec ks = new X509EncodedKeySpec(bytes);
KeyFactory kf = KeyFactory.getInstance("RSA");
PublicKey pub = kf.generatePublic(ks);

The private key is saved in PKCS #8 Encoding and can be restored as follows:

byte[] bytes = Files.readAllBytes(Paths.get(pvtKeyFile));
PKCS8EncodedKeySpec ks = new PKCS8EncodedKeySpec(bytes);
KeyFactory kf = KeyFactory.getInstance("RSA");
PrivateKey pvt = kf.generatePrivate(ks);

5. Should I Use the Public Key or the Private Key for Encryption?

We now have two keys with us: a public key and a private key. Which of these should we use for encryption?

Well, that depends on your purpose of course.

Remember that if you encrypt your data with the private key, then the encrypted data can only be decrypted with the public key. Since the public key is, by definition public, anyone can get at this key. So using this method of encrypting with the private key serves the purpose of non-repudiation i.e. you are declaring that the data came from you, and you only.

Another way of putting it is that the recipient can be sure that the data came from you. This is the basis of secure data interchange between a client program and a server program. The client needs to be sure that the data came from the server, without any middlemen tampering with the data.

On the other hand, if you use, say Alice‘s public key to encrypt, then only Alice (who has access to the corresponding private key) can decrypt the data. This method then serves as a method of secure file exchange with Alice.

In the following examples, we demonstrate using the private key for encryption. To go the other way, you can replace the private key with the public key.

6. Encrypting a File Using the Private Key

The main class handling the encryption is the Cipher class. We create a suitable cipher, initialize it with the private key (or the public key as required, see above), and perform the encryption.

PrivateKey pvt = ...; # see above
Cipher cipher = Cipher.getInstance("RSA/ECB/PKCS1Padding");
cipher.init(Cipher.ENCRYPT_MODE, pvt);
try (FileInputStream in = new FileInputStream(inFile);
     FileOutputStream out = new FileOutputStream(encFile)) {
    processFile(ci, in, out);
}

The static method processFile() is defined as follows:

static private void processFile(Cipher ci,InputStream in,OutputStream out)
    throws javax.crypto.IllegalBlockSizeException,
           javax.crypto.BadPaddingException,
           java.io.IOException
{
    byte[] ibuf = new byte[1024];
    int len;
    while ((len = in.read(ibuf)) != -1) {
        byte[] obuf = ci.update(ibuf, 0, len);
        if ( obuf != null ) out.write(obuf);
    }
    byte[] obuf = ci.doFinal();
    if ( obuf != null ) out.write(obuf);
}

7. Decrypting an Encrypted File

Once we obtain the file encrypted with the private key, we can decrypt it using the public key. The process is similar and shown below. We write the decrypted data to an output file for verification (called verFile below).

PublicKey pub = ...; # see above
Cipher cipher = Cipher.getInstance("RSA/ECB/PKCS1Padding");
cipher.init(Cipher.DECRYPT_MODE, pub);
try (FileInputStream in = new FileInputStream(encFile);
     FileOutputStream out = new FileOutputStream(verFile)) {
    processFile(ci, in, out);
}

And that’s all there is to it. Or is there more?

8. Verifying the Output

Let us now run a couple of tests to check that the encryption and decryption proceeded correctly. We use the following simple test file:

$ cat hello.txt
hello world

Let us look at the encrypted output (partial):

$ cat hello.txt.enc
cmrTzYCFsdgYZfsjbyyjdUIrIw...

Now the verify-file:

$ cat hello.txt.ver
hello world

So the file has been decrypted properly.

Let us now try a larger file, about 500kb in size. However, on encryption, we encounter the following error:

Exception in thread "main" javax.crypto.IllegalBlockSizeException: Data must not be longer than 245 bytes
        at com.sun.crypto.provider.RSACipher.doFinal(RSACipher.java:344)
        at com.sun.crypto.provider.RSACipher.engineDoFinal(RSACipher.java:389)
...

RSA can only work with data less than 245 bytes? What ….?!?

Well, it so happens that asymmetric encryption has good performance only for small sizes. For larger sizes, we need to combine RSA encryption with a symmetric encryption algorithm such as AES. We cover this in the next part of the article.