Cryptography encryption and signing

Today we are going to take a look at how to use cryptography encryption and signing in the Java programming language. Let’s start with a bit of theory first.

Encryption is a process of transforming data in a form that only authorized parties should be able to interpret. Encryption is a purely mathematical process – there should be raw data that needs to be encrypted, an encryption algorithm, and a key (or cipher as it is usually called). By applying the key to raw data in a way that is described in the selected algorithm we get an encrypted message. Then this message is usually passed somewhere to another party that wants to get access to it. Obtaining data from its encrypted form is called decryption. The approach is strictly opposite to encryption – the key is applied to the encrypted message in a way that is specified in the algorithm and initial raw data is formed. Any encryption algorithm has an assumption that encrypted data is theoretically impossible to decrypt without knowing the key. This is an “assumption” because it should be hard enough to perform decryption without the key in a reasonable amount of time. However, with recent hardware improvements, even usage of only brute-force algorithms appeared to be quite fast and successful against encryption algorithms of past decades. Thus, it is suggested to use keys of bigger sizes, rotate them in a reasonable time and apply most break-tolerant algorithms known at the moment.

There is also a big discussion over using elliptic curves because processing keys of bigger size is a very CPU-intensive operation and quite often is a bottleneck for most of the services. Elliptic curves may be quite secure with much smaller key sizes.

Encryption is transforming data in a way that only authorized parties would be able to access it (by decrypting).

There are 2 encryption algorithm types that require a key – symmetric and asymmetric.

Symmetric encryption

In symmetric encryption, only one encryption key is used both to encrypt and decrypt data. This key should be stored in a safe place and not shared with anyone who should not have access to encrypted data. Symmetric encryption is faster than asymmetric and is usually used on high volumes of data. As well, safely passing over key may be quite problematic, thus it is usually used locally (for example, to encrypt/decrypt local file system). The most commonly used symmetric encryption algorithm is AES.

Asymmetric encryption

Asymmetric encryption uses a keypair – a combination of 2 keys (1 private key and 1 public key). The private key should be securely stored and never disclosed to anyone. Public key on the other hand can be shared with anyone (and even published somewhere on a social network). The public key is used for encryption but only the holder of a private key from this keypair (public + private) can decrypt data. Asymmetric encryption is also used for digitally signing messages. The digital signature is a signature of a message that, if valid, proves message authenticity and that it was not tampered with. In asymmetric encryption private key is used to sign the message. Then any party that holds a public key from the same keypair can verify if a signature is valid. Note that the initial message, upon which signature was constructed, should be provided as well. So, signing data is not the same as encrypting it – when signing, data would be always accessible to anyone who would receive it. A widely used asymmetric algorithm for both encryption and signing is RSA.

Code examples (encryption)

Now let’s take a look at some code. In this article, we would study only asymmetric algorithm examples (both encryption and signing). In Java, there is a KeyPairGenerator that automatically generates a pair of keys for asymmetric encryption. We only need to provide the algorithm (RSA in this case) and key size (2048 bits is a recommended minimum for RSA).

public class CryptographyService {

    private final String cryptoAlgorithm;
    private final KeyPair keyPair;

    public CryptographyService(int keySize, String cryptoAlgorithm) throws NoSuchAlgorithmException {
        this.cryptoAlgorithm = cryptoAlgorithm;

        KeyPairGenerator keyPairGenerator = KeyPairGenerator.getInstance(cryptoAlgorithm);
        keyPairGenerator.initialize(keySize);
        this.keyPair = keyPairGenerator.generateKeyPair();
    }
}

Now we need to implement encryption and decryption functions. We will rely on Cipher class to do the math for us. Remember that we use the public key to encrypt data.

public byte[] encrypt(String message) throws Exception {
    Cipher cipher = Cipher.getInstance(cryptoAlgorithm);
    cipher.init(Cipher.ENCRYPT_MODE, keyPair.getPublic());
    return cipher.doFinal(message.getBytes());
}

And very similar code for decryption using a private key.

public String decrypt(byte[] encryptedMessage) throws Exception {
    Cipher cipher = Cipher.getInstance(cryptoAlgorithm);
    cipher.init(Cipher.DECRYPT_MODE, keyPair.getPrivate());
    return new String(cipher.doFinal(encryptedMessage));
}

Let’s also write some tests and check whether our simple service works.

@Test
public void testCryptographyService_positiveFlow() throws Exception {
    CryptographyService service = new CryptographyService(2048, "RSA");
    String message = "Hello cryptography world!!!";

    byte[] encryptedMessage = service.encrypt(message);
    String decryptedMessage = service.decrypt(encryptedMessage);

    assertThat(decryptedMessage).isEqualTo(message);
}

@Test(expected = GeneralSecurityException.class)
public void whenDecryptingNotEncryptedMessage_shouldThrowException() throws Exception {
    CryptographyService service = new CryptographyService(2048, "RSA");
    String message = "Hello cryptography world!!!";

    service.decrypt(message.getBytes());
}

Code examples (signatures)

Ok, that part is done and seemed to be quite simple. Now let’s move on to signing and verifying signatures. We will use another algorithm – SHA256withRSA, which is a combination of asymmetric encryption and hashing. Hashing aims to transform data into a non-readable fixed-length string. The most commonly used hashing algorithm is SHA-2 which we will use in our example.

Let’s take a look at the code. Very similar to the previous example we would need to pass over the selected algorithm as well as the size of the key. Note that we need to provide two algorithms in this case – one for key pair generation and another one for signing and verifying a signature.

public class SignatureService {

    private static final String SIGNATURE_ALGORITHM = "SHA256withRSA";

    private final KeyPair keyPair;

    public SignatureService(int keySize, String cryptoAlgorithm) throws NoSuchAlgorithmException {
        KeyPairGenerator keyPairGenerator = KeyPairGenerator.getInstance(cryptoAlgorithm);
        keyPairGenerator.initialize(keySize);
        this.keyPair = keyPairGenerator.generateKeyPair();
    }
}

To implement signing, we will use Signature class that is available in Java out of the box. Message signing is done with a generated previously private key.

public byte[] sign(String message) throws Exception {
    Signature sign = Signature.getInstance(SIGNATURE_ALGORITHM);
    sign.initSign(keyPair.getPrivate());
    sign.update(message.getBytes());
    return sign.sign();
}

For signature verification, we need to pass over the initial message as well as the signature itself. Now we can verify the signature with the public key generated previously.

public boolean verify(String message, byte[] signedMessage) throws Exception {
    Signature verify = Signature.getInstance(SIGNATURE_ALGORITHM);
    verify.initVerify(keyPair.getPublic());
    verify.update(message.getBytes());
    return verify.verify(signedMessage);
}

And let’s check if our code works properly. For the negative test, we would need to provide a string of the same size as expected signature to reach the verification step.

@Test
public void testSignatureService_positiveFlow() throws Exception {
    SignatureService service = new SignatureService(2048, "RSA");
    String message = "Hello signed world!!!";

    byte[] signedMessage = service.sign(message);
    boolean verificationOutcome = service.verify(message, signedMessage);

    assertThat(verificationOutcome).isTrue();
}

@Test
public void whenVerifyingIncorrectMessage_shouldFailVerification() throws Exception {
    SignatureService service = new SignatureService(2048, "RSA");
    String message = "Hello signed world!!!";

    boolean verificationOutcome = service.verify(message, aString(256).getBytes());

    assertThat(verificationOutcome).isFalse();
}

private static String aString(int length) {
    return Stream.generate(() -> "a").limit(length).collect(Collectors.joining());
}

That’s it for this tutorial, hope you enjoyed it and learned something new today. All the code together with tests you can find on Github. New articles soon to be released, so stay tuned!

0