In encryption we trust! A tutorial
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 with 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 for encryption: “is 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 access the message must have the key but none 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:
- public key can be decrypted only using the private key
- private key can be decrypted only using the public key
Seems simple enough right? So how is it used in practise? 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 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:
- The message digest is easy to generate
- It is extremely difficult to calculate which input provided the hash
- 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. In the receiving end we hash the decrypted message with the same algorithm and compare 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 digest 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. Well known application of this, are digital signatures.
Before we move and have some fun with code, let me mention a couple more things.
- Asymmetric key algorithms have actually two algorithms for different functionalities. One is of course for keys generation and the other functionality is for function evaluation. Function evaluation means taking an input (i.e. the message) and a key and result an encrypted or decrypted message, depending the input it got. So function evaluation is how messages are encrypted and decrypted using the public/private keys.
- 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:
- Subject, detailed description of the party (e.g. Alice)
- Validity range, for how long the certificate is valid
- Public key, which help us send encrypted messages to the party
- Certificate authority, the issuer of the certificate
- Hashing and encrypting are different things. An encrypted message is intended to eventually be turned back to the original message. A hashed message should not be possible to be turned back to the original.
Now let’s use a tutorial to help all these sink in. We will allow three individuals Alice, Bob and Paul to communicate with Confidentiality, Integrity and Authentication (further will refer to them 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 cipher, 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 have simply a constructor. But you can check them out in the project in github under the com.tasosmartidis.tutorial.encryption.exception package.
Their actual use you will see 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 communicates with.
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, we will try to verify that it is send 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:
When Alice (a trusted contact of Bob) sends an encrypted message to him, Bob can decrypt it and know it is from Bob. Also to ensure that the payload was not altered.
The same message from Alice to Bob, is not available for Paul to decrypt and an UnauthorizedForDecryptionException
will be thrown.
When Paul (not known to Bob) sends an encrypted message, Bob will be able to read it but not be able to know who send it.
Finally, when we compromise the payload of the encrypted message, the validation with its message digest will recognise 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.
Published on Java Code Geeks with permission by Tasos Martidis, partner at our JCG program. See the original article here: In encryption we trust! A tutorial Opinions expressed by Java Code Geeks contributors are their own. |