Over a million developers have joined DZone.
{{announcement.body}}
{{announcement.title}}

In Encryption We Trust! A Tutorial

DZone's Guide to

In Encryption We Trust! A Tutorial

In this article, we cover the basics of encryption and build a simple encryption protocol to test out the theories presented here.

· Security Zone ·
Free Resource

Discover how to provide active runtime protection for your web applications from known and unknown vulnerabilities including Remote Code Execution Attacks.

Many people view encryption as a complicated subject, something difficult to understand. And certain aspects of its implementation can be, but everyone can understand how it works on a higher level.

This is what I want to do with this article. Explain in simple terms how it works and then play around with some code.

Yes, in encryption we trust. What do I mean by trust? We trust that our messages are read only by authorized parties (confidentiality), they are not altered during transmission (integrity), and are indeed sent by those we believe they were sent (authentication).

Wikipedia provides a good definition of encryption: “the process of encoding a message or information in such a way that only authorized parties can access it.”

So encryption is turning our message with the use of a key (cipher) to an incomprehensible one (ciphertext) which can only be turned back to the original from authorized parties.

There are two types of encryption schemes, symmetric and asymmetric key encryption.

In symmetric encryption, the same key is used for encrypting and decrypting the message. Those we wish to have access to the message must have the key but no one else, otherwise our messages are compromised.

Asymmetric key encryption is my interest here. Asymmetric key schemes, use two keys, a private and a public. These pairs of keys are special. They are special because they are generated using a category of algorithms called asymmetric algorithms. The actual algorithms are out of scope for this discussion, but later in the tutorial, we will use RSA.

What you need to know now is that these keys have the following properties. A message encrypted with the:

  1. Public key can be decrypted only using the private key.
  2. Private key can be decrypted only using the public key.

Seems simple enough right? So how is it used in practice? Let’s consider two friends, Alice and Bob. They have their own pairs of public and private keys and they want privacy in their chats. Each of them openly provides their public key but takes good care hiding their private key.

When Alice wants to send a message only to be read from Bob, she uses Bob’s public key to encrypt the message. Then Bob and only him can decrypt the message using his private key. That’s it.

That explains the use of the first property, but what about the second? Seems there is no reason to encrypt using our private key. Well, there is. How do we know that Alice was the one who sent the message? If we can decrypt the message using Alice’s public key, we can be sure that Alice’s private key was used for the encryption, so it was indeed sent from Alice. Simply put, the public key is used so people can send things only to you and the private key is used to prove your identity.

So we can have confidentiality using the public key and authenticity using the private. What about integrity? To achieve this, we use cryptographic hashing. A good cryptographic hash takes an input message and generates a message digest with the following properties:

  1. The message digest is easy to generate.
  2. It is extremely difficult to calculate which input provided the hash.
  3. It is extremely unlikely that two different inputs/messages would generate the same hash value.

If we want to be sure that the message received was not compromised during transition, the hash value is sent along the encrypted message. On the receiving end, we hash the decrypted message with the same algorithm and compare them to make sure the hashes are an exact match. If they are, then we can be confident that the message was not altered.

These hashes or message digests have other uses as well. You see, sometimes Bob makes promises and then denies he ever did. We want to keep him in check. In fancy terms, it is called non-repudiation and prevents parties from being able to deny sending a message. A well-known application of this is digital signatures.

Before we move and have some fun with code, let me mention a couple more things.

  1. Asymmetric key algorithms actually have two algorithms for different functionalities. One is of course for key generation and the other functionality is for function evaluation. Function evaluation means taking an input (i.e. the message) and a key and spitting out an encrypted or decrypted message, depending on the input it got. So function evaluation is how messages are encrypted and decrypted using the public/private keys.
  2. Maybe you already thought, how do we know that a public key is actually related to Bob or Alice? What if it is someone pretending to be them? There is a standard that can help us with that. It is the X.509 which defines the format for public key certificates. These certificates are provided by Certification Authorities and usually contain:
    1. Subject, detailed description of the party (e.g. Alice).
    2. Validity range, for how long the certificate is valid.
    3. A public key, which helps us send encrypted messages to the party.
    4. The certificate authority, the issuer of the certificate.
  3. Hashing and encrypting are different things. An encrypted message is intended to eventually be turned back to the original message. It should not be possible to turn a hashed message back to the original.

Now let’s use a tutorial to help all of this sink in. We will allow three individuals, Alice, Bob and Paul, to communicate with Confidentiality, Integrity, and Authentication (from here on, we will refer to this as CIA). The complete code is available on GitHub.

The project has a couple of dependencies, as shown below:

<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
  xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>

    <groupId>com.tasosmartidis.tutorial.encryption</groupId>
    <artifactId>encryption-tutorial</artifactId>
    <version>1.0-SNAPSHOT</version>
    <packaging>jar</packaging>

    <name>encryption-tutorial</name>

    <properties>
        <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
        <lombok.version>1.16.18</lombok.version>
        <commons-codec.version>1.11</commons-codec.version>
        <junit.version>4.12</junit.version>
        <bouncycastle.version>1.58</bouncycastle.version>
    </properties>

    <dependencies>
        <dependency>
            <groupId>org.projectlombok</groupId>
            <artifactId>lombok</artifactId>
            <version>${lombok.version}</version>
        </dependency>
        <dependency>
            <groupId>commons-codec</groupId>
            <artifactId>commons-codec</artifactId>
            <version>${commons-codec.version}</version>
        </dependency>
        <dependency>
            <groupId>org.bouncycastle</groupId>
            <artifactId>bcprov-jdk15on</artifactId>
            <version>${bouncycastle.version}</version>
        </dependency>
        <dependency>
            <groupId>junit</groupId>
            <artifactId>junit</artifactId>
            <version>${junit.version}</version>
            <scope>test</scope>
        </dependency>
    </dependencies>

    <build>
        <finalName>encryption-tutorial</finalName>
        <pluginManagement>
            <plugins>
                <plugin>
                    <artifactId>maven-compiler-plugin</artifactId>
                    <version>3.0</version>
                    <configuration>
                        <source>1.8</source>
                        <target>1.8</target>
                    </configuration>
                </plugin>
            </plugins>
        </pluginManagement>
    </build>
</project>

We will start with the EncryptedMessage class, which will provide all the information we need to ensure CIA. The message will contain the actual encrypted message for confidentiality, a hash of the message to be used to ensure integrity and identification of the sender, raw and encrypted for authentication. We also provide a method to compromise the message payload, so we can test the validation against the digest (more on that later).

package com.tasosmartidis.tutorial.encryption.domain;

import lombok.AllArgsConstructor;
import lombok.EqualsAndHashCode;
import lombok.Getter;

@AllArgsConstructor
@Getter
@EqualsAndHashCode
public class EncryptedMessage {
    private String encryptedMessagePayload;
    private String senderId;
    private String encryptedSenderId;
    private String messageDigest;

    // FOR DEMO PURPOSES ONLY!
    public void compromiseEncryptedMessagePayload(String message) {
        this.encryptedMessagePayload = message;
    }

    @Override
    public String toString() {
        return encryptedMessagePayload;
    }
}

Now let’s get to the encryption part. We will create a base encryptor class independent of the actual asymmetric algorithm and key length. It will create keys and ciphers, and have methods for encrypting and decrypting text as well as providing access to the keys. It looks something like this:

package com.tasosmartidis.tutorial.encryption.encryptor;

import com.tasosmartidis.tutorial.encryption.domain.EncryptorProperties;
import com.tasosmartidis.tutorial.encryption.exception.DecryptionException;
import com.tasosmartidis.tutorial.encryption.exception.EncryptionException;
import com.tasosmartidis.tutorial.encryption.exception.EncryptorInitializationException;
import com.tasosmartidis.tutorial.encryption.exception.UnauthorizedForDecryptionException;
import org.apache.commons.codec.binary.Base64;

import javax.crypto.BadPaddingException;
import javax.crypto.Cipher;
import javax.crypto.IllegalBlockSizeException;
import javax.crypto.NoSuchPaddingException;
import java.nio.charset.StandardCharsets;
import java.security.*;

public class BaseAsymmetricEncryptor {
    private final KeyPairGenerator keyPairGenerator;
    private final KeyPair keyPair;
    private final Cipher cipher;
    private final EncryptorProperties encryptorProperties;

    protected BaseAsymmetricEncryptor(EncryptorProperties encryptorProperties) {
        this.encryptorProperties = encryptorProperties;
        this.keyPairGenerator = generateKeyPair();
        this.keyPairGenerator.initialize(this.encryptorProperties.getKeyLength());
        this.keyPair = this.keyPairGenerator.generateKeyPair();
        this.cipher = createCipher(encryptorProperties);
    }

    protected PrivateKey getPrivateKey() {
        return this.keyPair.getPrivate();
    }

    public PublicKey getPublicKey() {
        return this.keyPair.getPublic();
    }

    protected String encryptText(String textToEncrypt, Key key) {
        try {
            this.cipher.init(Cipher.ENCRYPT_MODE, key);
            return Base64.encodeBase64String(cipher.doFinal(textToEncrypt.getBytes(StandardCharsets.UTF_8)));
        } catch (InvalidKeyException | BadPaddingException | IllegalBlockSizeException ex) {
            throw new EncryptionException("Encryption of message failed", ex);
        }
    }

    protected String decryptText(String textToDecrypt, Key key) {
        try {
            this.cipher.init(Cipher.DECRYPT_MODE, key);
            return new String(cipher.doFinal(Base64.decodeBase64(textToDecrypt)), StandardCharsets.UTF_8);
        }catch (InvalidKeyException | BadPaddingException ex){
            throw new UnauthorizedForDecryptionException("Not authorized to decrypt message", ex);
        } catch (IllegalBlockSizeException ex) {
            throw new DecryptionException("Decryption of message failed", ex);
        }
    }

    private Cipher createCipher(EncryptorProperties encryptorProperties) {
        try {
            return Cipher.getInstance(encryptorProperties.getAsymmetricAlgorithm());
        } catch (NoSuchAlgorithmException | NoSuchPaddingException ex) {
            throw new EncryptorInitializationException("Creation of cipher failed", ex);
        }
    }

    private KeyPairGenerator generateKeyPair() {

        try {
            return KeyPairGenerator.getInstance(this.encryptorProperties.getAsymmetricAlgorithm());
        } catch (NoSuchAlgorithmException ex) {
            throw new EncryptorInitializationException("Creation of encryption keypair failed", ex);
        }
    }

}

There are a lot of exceptions we need to handle for implementing our functionality, but since we are not going to do anything with them in case they happen, we will wrap them with semantically meaningful runtime exceptions. I am not going to show here the exception classes since they simply have a constructor. But you can check them out in the project on GitHub under the com.tasosmartidis.tutorial.encryption.exception package.

You will see their actual use in different parts of the code. The constructor of the BaseAsymmetricEncryptor takes an EncryptorProperites instance as an argument.

package com.tasosmartidis.tutorial.encryption.domain;

import lombok.AllArgsConstructor;


@AllArgsConstructor
public class EncryptorProperties {
    private final AsymmetricAlgorithm asymmetricAlgorithm;
    private final int keyLength;

    public String getAsymmetricAlgorithm() {
        return asymmetricAlgorithm.toString();
    }

    public int getKeyLength() {
        return keyLength;
    }
}

We will create an RSA based encryptor implementation. The code should speak for itself:

package com.tasosmartidis.tutorial.encryption.encryptor;

import com.tasosmartidis.tutorial.encryption.domain.AsymmetricAlgorithm;
import com.tasosmartidis.tutorial.encryption.domain.EncryptedMessage;
import com.tasosmartidis.tutorial.encryption.domain.EncryptorProperties;
import org.bouncycastle.jcajce.provider.digest.SHA3;
import org.bouncycastle.util.encoders.Hex;

import java.security.PublicKey;

public class RsaEncryptor extends BaseAsymmetricEncryptor {
    private static final int KEY_LENGTH = 2048;

    public RsaEncryptor() {
        super(new EncryptorProperties(AsymmetricAlgorithm.RSA, KEY_LENGTH));
    }

    public String encryptMessageForPublicKeyOwner(String message, PublicKey key) {
         return super.encryptText(message, key);
    }

    public String encryptMessageWithPrivateKey(String message) {
        return super.encryptText(message, super.getPrivateKey());
    }

    public String decryptReceivedMessage(EncryptedMessage message) {
        return super.decryptText(message.getEncryptedMessagePayload(), super.getPrivateKey());
    }

    public String decryptMessageFromOwnerOfPublicKey(String message, PublicKey publicKey) {
        return super.decryptText(message, publicKey);
    }

    public String hashMessage(String message) {
        SHA3.DigestSHA3 digestSHA3 = new SHA3.Digest512();
        byte[] messageDigest = digestSHA3.digest(message.getBytes());
        return Hex.toHexString(messageDigest);
    }
}

For our demo, we will need actors, people that will exchange messages with each other. Each person will have a unique identity, a name, and a list of trusted contacts that communicate with them.

package com.tasosmartidis.tutorial.encryption.demo;

import com.tasosmartidis.tutorial.encryption.domain.EncryptedMessage;
import com.tasosmartidis.tutorial.encryption.message.RsaMessenger;
import lombok.EqualsAndHashCode;

import java.security.PublicKey;
import java.util.HashSet;
import java.util.Set;
import java.util.UUID;

@EqualsAndHashCode
public class Person {
    private final String id;
    private final String name;
    private final Set<Person> trustedContacts;
    private final RsaMessenger rsaMessenger;

    public Person(String name) {
        this.id = UUID.randomUUID().toString();
        this.name = name;
        this.trustedContacts = new HashSet<>();
        this.rsaMessenger = new RsaMessenger(this.trustedContacts, this.id);
    }

    public PublicKey getPublicKey() {
        return this.rsaMessenger.getPublicKey();
    }

    public String getName() {
        return name;
    }

    public String getId() {
        return id;
    }

    public void addTrustedContact(Person newContact) {
        if(trustedContacts.contains(newContact)) {
            return;
        }

        trustedContacts.add(newContact);
    }

    public EncryptedMessage sendEncryptedMessageToPerson(String message, Person person) {
        return this.rsaMessenger.encryptMessageForPerson(message, person);
    }

    public void readEncryptedMessage(EncryptedMessage encryptedMessage) {
        this.rsaMessenger.readEncryptedMessage(encryptedMessage);
    }

}

Next, let’s create an RsaMessanger class which will allow people to send encrypted messages using the RsaEncryptor. When sending an encrypted message, we will provide all the necessary information to guarantee confidentiality, integrity, and authentication. When reading, we will decrypt the message, try to verify that it was sent by a trusted contact, and ensure that the message has not been compromised, or altered.

package com.tasosmartidis.tutorial.encryption.message;

import com.tasosmartidis.tutorial.encryption.demo.Person;
import com.tasosmartidis.tutorial.encryption.domain.EncryptedMessage;
import com.tasosmartidis.tutorial.encryption.encryptor.RsaEncryptor;
import com.tasosmartidis.tutorial.encryption.exception.PayloadAndDigestMismatchException;

import java.security.PublicKey;
import java.util.Optional;
import java.util.Set;

public class RsaMessenger {

    private final RsaEncryptor encryptionHandler;
    private final Set<Person> trustedContacts;
    private final String personId;

    public RsaMessenger(Set<Person> trustedContacts, String personId) {
        this.encryptionHandler = new RsaEncryptor();
        this.trustedContacts = trustedContacts;
        this.personId = personId;
    }

    public PublicKey getPublicKey() {
        return this.encryptionHandler.getPublicKey();
    }

    public EncryptedMessage encryptMessageForPerson(String message, Person person) {
        String encryptedMessage = this.encryptionHandler.encryptMessageForPublicKeyOwner(message, person.getPublicKey());
        String myEncryptedId = this.encryptionHandler.encryptMessageWithPrivateKey(this.personId);
        String hashedMessage = this.encryptionHandler.hashMessage(message);
        return new EncryptedMessage(encryptedMessage, this.personId, myEncryptedId, hashedMessage);
    }

    public void readEncryptedMessage(EncryptedMessage message) {
        String decryptedMessage = this.encryptionHandler.decryptReceivedMessage(message);
        Optional<Person> sender = tryIdentifyMessageSender(message.getSenderId());

        if(!decryptedMessageHashIsValid(decryptedMessage, message.getMessageDigest())) {
            throw new PayloadAndDigestMismatchException(
                    "Message digest sent does not match the one generated from the received message");
        }

        if(sender.isPresent() && senderSignatureIsValid(sender.get(), message.getEncryptedSenderId())) {
            System.out.println(sender.get().getName() +" send message: " + decryptedMessage);
        }else {
            System.out.println("Unknown source send message: " + decryptedMessage);
        }
    }

    private boolean senderSignatureIsValid(Person sender, String encryptedSenderId) {
        if(rawSenderIdMatchesDecryptedSenderId(sender, encryptedSenderId)) {
            return true;
        }

        return false;
    }

    private boolean rawSenderIdMatchesDecryptedSenderId(Person sender, String encryptedSenderId) {
        return sender.getId().equals(
                this.encryptionHandler.decryptMessageFromOwnerOfPublicKey(encryptedSenderId, sender.getPublicKey()));
    }

    private Optional<Person> tryIdentifyMessageSender(String id) {
        return this.trustedContacts.stream()
                .filter(contact -> contact.getId().equals(id))
                .findFirst();
    }

    private boolean decryptedMessageHashIsValid(String decryptedMessage, String hashedMessage) {
        String decryptedMessageHashed = this.encryptionHandler.hashMessage(decryptedMessage);
        if(decryptedMessageHashed.equals(hashedMessage)) {
            return true;
        }

        return false;
    }
}

Alright! It’s demo time!

We will create some tests to make sure everything works as expected. The scenarios we want to test are:

  1. When Alice (a trusted contact of Bob) sends an encrypted message to him, Bob can decrypt it and know it is from Alice. We also want to be able to ensure that the payload was not altered.
  2. The same message from Alice to Bob is not available for Paul to decrypt and an UnauthorizedForDecryptionException will be thrown.
  3. When Paul (not known to Bob) sends an encrypted message, Bob will be able to read it but not be able to know who sent it.
  4. Finally, when we compromise the payload of the encrypted message, the validation with its message digest will recognize it and throw an exception.
package com.tasosmartidis.tutorial.encryption;

import com.tasosmartidis.tutorial.encryption.demo.Person;
import com.tasosmartidis.tutorial.encryption.domain.EncryptedMessage;
import com.tasosmartidis.tutorial.encryption.exception.PayloadAndDigestMismatchException;
import com.tasosmartidis.tutorial.encryption.exception.UnauthorizedForDecryptionException;
import org.junit.Before;
import org.junit.Test;

public class DemoTest {

    private static final String ALICE_MESSAGE_TO_BOB = "Hello Bob";
    private static final String PAULS_MESSAGE_TO_BOB = "Hey there Bob";
    private final Person bob = new Person("Bob");
    private final Person alice = new Person("Alice");
    private final Person paul = new Person("Paul");
    private EncryptedMessage alicesEncryptedMessageToBob;
    private EncryptedMessage paulsEncryptedMessageToBob;

    @Before
    public void setup() {
        bob.addTrustedContact(alice);
        alicesEncryptedMessageToBob = alice.sendEncryptedMessageToPerson(ALICE_MESSAGE_TO_BOB, bob);
        paulsEncryptedMessageToBob = paul.sendEncryptedMessageToPerson(PAULS_MESSAGE_TO_BOB, bob);
    }

    @Test
    public void testBobCanReadAlicesMessage() {
        bob.readEncryptedMessage(alicesEncryptedMessageToBob);
    }

    @Test(expected = UnauthorizedForDecryptionException.class)
    public void testPaulCannotReadAlicesMessageToBob() {
        paul.readEncryptedMessage(alicesEncryptedMessageToBob);
    }

    @Test
    public void testBobCanReadPaulsMessage() {
        bob.readEncryptedMessage(paulsEncryptedMessageToBob);
    }

    @Test(expected = PayloadAndDigestMismatchException.class)
    public void testChangedMessageIdentifiedAndRejected() {
        EncryptedMessage slightlyDifferentMessage = alice.sendEncryptedMessageToPerson(ALICE_MESSAGE_TO_BOB + " ", bob);
        alicesEncryptedMessageToBob.compromiseEncryptedMessagePayload(slightlyDifferentMessage.getEncryptedMessagePayload());

        bob.readEncryptedMessage(alicesEncryptedMessageToBob);
    }
}

Running the test would produce the following result:

That was it! Thanks for reading, and again, you can find the code on GitHub.

Find out how Waratek’s award-winning application security platform can improve the security of your new and legacy applications and platforms with no false positives, code changes or slowing your application.

Topics:
encryption ,security ,java ,rsa ,hashing

Published at DZone with permission of

Opinions expressed by DZone contributors are their own.

{{ parent.title || parent.header.title}}

{{ parent.tldr }}

{{ parent.urlSource.name }}