In this post, I demonstrate how to securely exchange cryptographic keys over an untrusted public channel to create a simple end-to-end encrypted communication.

## What Is End-to-end Encryption?

End-to-end encryption (E2EE) is a secure way of communication that ensures the privacy and confidentiality of data exchanged between two or more parties. It is a cryptographic technique where the message or data is encrypted on the sender's side, and can only be decrypted by the recipient. The data remains encrypted during transit, ensuring intermediaries or service providers cannot read the content.

Cryptographic algorithms are the mathematical equations used to scramble the plain text and make it unreadable. In encryption, a secret key (also known as a symmetric key) is a piece of information used to control the encryption and decryption processes. It is a key that is known only to the entities involved in the communication, such as the sender and the intended recipient. The secret key is used in symmetric encryption algorithms, where the same key is used for both encryption and decryption. This means that if you have the secret key, you can encrypt data and then decrypt it to obtain the original message.

Symmetric encryption algorithms are commonly used to encrypt and decrypt data, but how to securely share them in an untrusted public network?!

One approach to accomplish this is to use key exchange algorithms. In this post, I use the **Diffie–Hellman key exchange** algorithm, because of it’s ease of use and easy implementation in different programming languages. I’ll use Java for the example code.

**What Is the Diffie–Hellman key Exchange?**

The Diffie–Hellman key exchange is a mathematical method of securely exchanging cryptographic keys over an untrusted public channel in a way that only the participants in the conversation learn the secret, while observers learn nothing. Imagine a crowded coffee shop across which you can do "verbal Diffie-Hellman" with someone, shouting numbers at each other, and end up with the same secret key in both your heads — which no one listening to your conversation could determine, as they don't know either of your secrets.

Here is how it works:

Alice and Bob publicly agree to use a P = 23 (which is a prime number) and base G = 5 (which is a primitive root modulo 23).

Alice chooses a secret random integer a = 9, then sends Bob A = G

^{a}mod P

A = 5^{9}mod 23 = 11Bob chooses a secret random integer b = 3, then sends Alice B = G

^{b}mod P

B = 5^{3}mod 23 = 10Alice computes s = B

^{a}mod P

s = 10^{9}mod 23 = 20Bob computes s = A

^{b}mod P

s = 11^{3}mod 23 = 20Alice and Bob now share a secret (the number 20).

Both Alice and Bob have arrived at the same values.

*There's an important note to mention: This algorithm gives you confidentiality (no one outside of participants knows the secret) and authenticity (you know the data is shared with your conversation partner) — but not integrity (Diffie–Hellman can't convince Alice that her conversation partner "is Bob").*

*In particular, an attacker in the middle (such as Alice or Bob's ISP) could pretend to be Alice when talking to Bob, and Bob when talking to Alice. The attacker can then seamlessly decrypt, re-encrypt, and forward messages while reading the cleartext (**Man-in-the-middle attack**).*

*So unless you're in a coffee shop and can visually verify that you're talking to whom you intend, Diffie–Hellman requires some additional elements to authenticate both sides of the connection.*

Learn more about Diffie–Hellman key exchange algorithm.

## Implementing the Diffie–Hellman Algorithm in Java

Alice starts the key exchange.

Alice generates a random number and keeps it in memory (or somewhere) for later use. Using this random number, a shared key is created that can be transmitted over the public channel to Bob. This is not a secret!

```
BigInteger aliceRandomKey = createRandomKey();
BigInteger aliceSharedKey = G.modPow(aliceRandomKey, P);
```

- Bob receives a request from Alice to exchange a key.

When the request is received, Bob does the same process: He generates a random number, creates a shared key, and uses Alice's shared key to create a secret key. This secret key should not be transmitted over the network and will be used to encrypt/decrypt further communications.

```
BigInteger bobRandomKey = createRandomKey();
BigInteger bobSharedKey = G.modPow(bobRandomKey, P);
BigInteger bobSecretKey = aliceSharedKey.modPow(bobRandomKey, P);
```

In response to Alice’s key exchange request, Bob sends their own shared key. With that, Alice could also generate the secret key based on Bob's shared key. Once this step is done, Alice and Bob have agreed to use a secret key, which is not transmitted over the public network and is the same for both.

```
BigInteger aliceSecretKey = bobSharedKey.modPow(aliceRandomKey, P);
```

From now on, one of them can encrypt data with their own secret key, transfer data over the public and untrusted network, and the recipient can decrypt it with their own secret key. Now the communication is end-to-end encrypted.

This serves as a basic demonstration of how end-to-end encryption works. Numerous production-ready implementations are available, allowing you to select the one that best aligns with your specific software requirements.

The following code is a basic implementation in Java using Java Cryptography Architecture (JCA) for demonstration purposes only. You can find the standard implementation libraries in different programming languages. I like https://github.com/google/tink and https://www.bouncycastle.org.

```
/*
* IMPORTANT: DEMONSTRATION PURPOSES ONLY
* This code is intended for illustrative and educational purposes only.
* It is NOT SUITABLE for production use and may contain security vulnerabilities, inefficiencies, or incomplete functionality.
*/
package org.example;
import javax.crypto.Cipher;
import javax.crypto.spec.SecretKeySpec;
import java.math.BigInteger;
import java.nio.charset.StandardCharsets;
import java.util.Random;
public class Main {
private static final BigInteger G = new BigInteger("5");
private static final BigInteger P = new BigInteger("23");
public static void main(String[] args) throws Exception {
// 1. Alice starts the key exchange
BigInteger aliceRandomKey = createRandomKey();
System.out.println("aliceRandomKey: " + aliceRandomKey);
BigInteger aliceSharedKey = G.modPow(aliceRandomKey, P);
System.out.println("aliceSharedKey: " + aliceSharedKey);
// 2. aliceSharedKey is sent to Bob, Bob processes the request and creates a secret key
BigInteger bobRandomKey = createRandomKey();
System.out.println("bobRandomKey: " + bobRandomKey);
BigInteger bobSharedKey = G.modPow(bobRandomKey, P);
System.out.println("bobSharedKey: " + bobSharedKey);
BigInteger bobSecretKey = aliceSharedKey.modPow(bobRandomKey, P);
System.out.println("Bob agreed to use secretKey: " + bobSecretKey);
// 3. bobSharedKey is sent to Alice, Alice processes the request and creates a secret key
BigInteger aliceSecretKey = bobSharedKey.modPow(aliceRandomKey, P);
System.out.println("Alice agreed to use secretKey: " + aliceSecretKey);
// 4. Alice encrypts data using aliceSecretKey and sends it to Bob
String plainText = "Hello World!";
byte[] encryptedData = encrypt(plainText, aliceSecretKey);
// 5. Bob receives encrypted data and decrypt is with its own secret
String decryptedData = decrypt(encryptedData, bobSecretKey);
System.out.println("Decrypted data is: " + decryptedData);
}
private static BigInteger createRandomKey() {
Random random = new Random();
return new BigInteger(128, random);
}
private static byte[] encrypt(String plainText, BigInteger secretKey) throws Exception {
SecretKeySpec secretKeySpec = createSecretKeySpec(secretKey);
Cipher cipher = Cipher.getInstance("AES/ECB/PKCS5Padding");
cipher.init(Cipher.ENCRYPT_MODE, secretKeySpec);
return cipher.doFinal(plainText.getBytes(StandardCharsets.UTF_8));
}
private static String decrypt(byte[] encryptedData, BigInteger secretKey) throws Exception {
SecretKeySpec secretKeySpec = createSecretKeySpec(secretKey);
Cipher cipher = Cipher.getInstance("AES/ECB/PKCS5Padding");
cipher.init(Cipher.DECRYPT_MODE, secretKeySpec);
byte[] decryptedBytes = cipher.doFinal(encryptedData);
return new String(decryptedBytes, StandardCharsets.UTF_8);
}
private static SecretKeySpec createSecretKeySpec(BigInteger secretKey) {
byte[] keyBytes = secretKey.toByteArray();
byte[] validKeyBytes = new byte[16];
System.arraycopy(keyBytes, 0, validKeyBytes, 0, Math.min(keyBytes.length, validKeyBytes.length));
return new SecretKeySpec(validKeyBytes, "AES");
}
}
```