Encrypt and Sign a File Using RSA in Java

Securely exchange a file with another person using RSA for encryption and digital signature to ensure authentication.

β€œTo acquire knowledge, one must study;
but to acquire wisdom, one must observe.”
― Marilyn Vos Savant

1. Introduction

We have previously covered using RSA for file encryption in java. We have also covered in a separate article the process of generating a digital signature for a file and verification using RSA. Let us now combine the two and develop a procedure for encrypting a file and generating a digital signature for exchange between two parties.

When two parties want to securely exchange messages, we use digital signature in addition to encryption to assure the receiver that the message came from the sender, and has not been tampered in transit. Thus, the encryption provides privacy, and the digital signature provides authentication.

Let us assume that party A wants to send party B a secure message. The process works as follows: A generates an AES secret key and encrypts the key using B’s public key. A now uses the AES secret key to encrypt the actual message to be sent. Finally, A generates a digital signature (using A’s private key) for the message. All these components are packed into a single file in the following order: the encrypted AES key, followed by the initialization vector, the encrypted message and finally by the digital signature.

When this message is sent to B, B can decrypt the message and be assured it came from A only as follows: B decrypts the AES secret key using his own private key, reads the IV and decrypts the message using the AES secret key, and finally verifies the digital signature of the decrypted message by using A’s public key.

Let us see how this is implemented in code.

2. Java Imports

The following java imports are required.

import java.nio.file.Files;
import java.nio.file.Paths;
import java.io.File;
import java.io.InputStream;
import java.io.OutputStream;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.util.Arrays;
import java.util.Base64;
import java.security.KeyPair;
import java.security.KeyPairGenerator;
import java.security.Signature;
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.KeyGenerator;
import javax.crypto.Cipher;
import javax.crypto.SecretKey;
import javax.crypto.spec.SecretKeySpec;
import javax.crypto.spec.IvParameterSpec;

3. Load or Generate the RSA Public and Private Keys

First and foremost, both A and B need to generate RSA public and private key pairs and save them as follows.

KeyPairGenerator kpg = KeyPairGenerator.getInstance("RSA");
kpg.initialize(2048);
KeyPair kp = kpg.generateKeyPair();
try (FileOutputStream out = new FileOutputStream(fileBase + ".key")) {
    out.write(kp.getPrivate().getEncoded());
}

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

A and B exchange the public keys so A has B’s public key, and vice versa.

Load the public and private keys from file as follows:

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

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

4. Generate the AES Encryption Key

The AES secret key and the initialization vector is generated as follows:

KeyGenerator kgen = KeyGenerator.getInstance("AES");
kgen.init(128);
SecretKey skey = kgen.generateKey();

byte[] iv = new byte[128/8];
new SecureRandom().nextBytes(iv);
IvParameterSpec ivspec = new IvParameterSpec(iv);

5. Encrypt and Save the AES Encryption Key

The first part of the output file contains the AES encryption key encrypted using B’s public key, followed by the initialization vector. It is saved as follows:

FileOutputStream out = new FileOutputStream(inputFile + ".enc");
{
Cipher cipher = Cipher.getInstance("RSA/ECB/PKCS1Padding");
cipher.init(Cipher.ENCRYPT_MODE, pub); // Encrypt using B's public key
byte[] b = cipher.doFinal(skey.getEncoded());
out.write(b);
}

out.write(iv);

6. Encrypt the Message and Sign it

Next step is to encrypt the actual message using the AES secret key, and sign the message using A’s public key.

Signature sign = Signature.getInstance("SHA256withRSA");
sign.initSign(pvt); // Sign using A's private key

Cipher ci = Cipher.getInstance("AES/CBC/PKCS5Padding");
ci.init(Cipher.ENCRYPT_MODE, skey, ivspec);
try (FileInputStream in = new FileInputStream(inputFile)) {
    signFile(ci, sign, in, out);
}
byte[] s = sign.sign();
out.write(s);
out.close();

The static method signFile() is defined as follows. It accepts a Cipher object to encrypt the file, a Signature object to sign the message, and an InputStream and OutputStream to read the message from and write the output.

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

The output file is now ready. Send it to B via any means – the communication channel need not be secure.

7. Decrypting the AES Key

Now that the file has been received by B, decrypting it is next.

First step is to compute the length of the encrypted message. Since the output file consists of the encrypted AES key, the initialization vector and the 256 byte signature at the end, the length of the encrypted message is:

long dataLen = new File(inputFile).length()
    - 256       // AES Key
    - 16        // IV
    - 256;      // Signature

Note that this length may be different from the length of the unencrypted message since encryption proceeds by blocks.

Now B can use his private key to decrypt the AES secret key included in the file.

FileInputStream in = new FileInputStream(inputFile);
SecretKeySpec skey = null;
{
Cipher cipher = Cipher.getInstance("RSA/ECB/PKCS1Padding");
cipher.init(Cipher.DECRYPT_MODE, pvt); // B's private key here
byte[] b = new byte[256];
in.read(b);
byte[] keyb = cipher.doFinal(b);
skey = new SecretKeySpec(keyb, "AES");
}

8. Loading the Initialization Vector

Loading the initialization vector (IV) is next.

byte[] iv = new byte[128/8];
in.read(iv);
IvParameterSpec ivspec = new IvParameterSpec(iv);

9. Decrypting the Message and Verifying the Signature

Verifying the signature requires A’s public key. This assures B that A has indeed sent the message.

Decryption of the message is done using the AES secret key.

Signature ver = Signature.getInstance("SHA256withRSA");
ver.initVerify(pub); // Using B's public key
Cipher ci = Cipher.getInstance("AES/CBC/PKCS5Padding");
ci.init(Cipher.DECRYPT_MODE, skey, ivspec);
try (FileOutputStream out = new FileOutputStream(inputFile+".ver")){
    authFile(ci, ver, in, out, dataLen);
}
byte[] s = new byte[256];
int len = in.read(s);
if ( ! ver.verify(s) ) {
    throw new Exception("Signature not valid: " + encoder.encodeToString(s));
}

The static method authFile() is shown below. It decrypts the message update dataLen and verifies the signature of the decrypted message.

static private void authFile(Cipher ci,Signature ver,InputStream in,OutputStream out,long dataLen)
    throws javax.crypto.IllegalBlockSizeException,
           javax.crypto.BadPaddingException,
           java.security.SignatureException,
           java.io.IOException
{
    byte[] ibuf = new byte[1024];
    while (dataLen > 0) {
        int max = (int)(dataLen > ibuf.length ? ibuf.length : dataLen);
        int len = in.read(ibuf, 0, max);
        if ( len < 0 ) throw new java.io.IOException("Insufficient data");
        dataLen -= len;
        byte[] obuf = ci.update(ibuf, 0, len);
        if ( obuf != null ) {
            out.write(obuf);
            ver.update(obuf);
        }
    }
    byte[] obuf = ci.doFinal();
    if ( obuf != null ) {
        out.write(obuf);
        ver.update(obuf);
    }
}

Conclusion

In this article, we saw how to encrypt a file for a receiver and also sign it so the receiver is sure it came from us. It involves generating an AES key, using that AES key for encryption and encrypting the AES key using receiver’s public key. The receiver can then unlock the AES key using his public key and decrypt the file using the AES key.