ECDH Example in Java

Overview

Sequence Diagram

ECDH Sequence Diagram

Server

  1. Generate server key pair(\(S_{pri}\) and \(S_{pub}\))
  2. Send \(S_{pub}\) to client
  3. Receive \(C_{pub}\) from client
  4. Key Agreement using \(S_{pri}\) and \(C_{pub}\)
  5. Generate shared secret
  6. Derive final key using shared secret

Client

  1. Generate client key pair(\(C_{pri}\) and \(C_{pub}\))
  2. Receive \(S_{pub}\) from server
  3. Send \(C_{pub}\) to server
  4. Key Agreement using \(C_{pri}\) and \(S_{pub}\)
  5. Generate shared secret
  6. Derive final key using shared secret

Implementation

Server

package io.github.bindon;

import java.io.BufferedReader;
import java.io.InputStreamReader;
import java.io.OutputStreamWriter;
import java.io.PrintWriter;
import java.net.ServerSocket;
import java.net.Socket;
import java.nio.ByteBuffer;
import java.nio.charset.StandardCharsets;
import java.security.KeyFactory;
import java.security.KeyPair;
import java.security.KeyPairGenerator;
import java.security.MessageDigest;
import java.security.PublicKey;
import java.security.spec.X509EncodedKeySpec;
import java.util.Arrays;
import java.util.Base64;
import java.util.Collections;
import java.util.List;

import javax.crypto.Cipher;
import javax.crypto.KeyAgreement;
import javax.crypto.spec.GCMParameterSpec;
import javax.crypto.spec.SecretKeySpec;

import org.apache.commons.codec.binary.Hex;

public class ECDHServer {
	public static void testECDHServer() {
		ServerSocket serverSocket = null;
		Socket clientSocket = null;
		BufferedReader reader = null;
		PrintWriter writer = null;
		
		try {
			serverSocket = new ServerSocket(32768);
			clientSocket = serverSocket.accept();
			reader = new BufferedReader(new InputStreamReader(clientSocket.getInputStream()));
			writer = new PrintWriter(new OutputStreamWriter(clientSocket.getOutputStream()));
	
			// Generate ephemeral ECDH keypair
			KeyPairGenerator keyPairGenerator = KeyPairGenerator.getInstance("EC");
			keyPairGenerator.initialize(256);
			KeyPair serverKeyPair = keyPairGenerator.generateKeyPair();
			byte[] serverPublicKeyBytes = serverKeyPair.getPublic().getEncoded();
			
			// Send server public key
			writer.println(Base64.getUrlEncoder().encodeToString(serverPublicKeyBytes));
			writer.flush();
			
			// Receive client public key
			byte[] clientPublicKeyBytes = Base64.getUrlDecoder().decode(reader.readLine());
			System.out.println("[Server] " + Hex.encodeHexString(clientPublicKeyBytes));
			KeyFactory clientKeyFactory = KeyFactory.getInstance("EC");
			X509EncodedKeySpec clientKeySpec = new X509EncodedKeySpec(clientPublicKeyBytes);
			PublicKey clientPublicKey = clientKeyFactory.generatePublic(clientKeySpec);
			
			// Perform key agreement
			KeyAgreement keyAgreement = KeyAgreement.getInstance("ECDH");
			keyAgreement.init(serverKeyPair.getPrivate());
			keyAgreement.doPhase(clientPublicKey, true);
			
			// Generate shared secret
			byte[] sharedSecret = keyAgreement.generateSecret();
			System.out.println("[Server] Shared secret: " + Hex.encodeHexString(sharedSecret));
			
			// Derive a key from the shared secret and both public keys
			MessageDigest hash = MessageDigest.getInstance("SHA-256");
			hash.update(sharedSecret); // Simple deterministic ordering
			List<ByteBuffer> keys = Arrays.asList(
					ByteBuffer.wrap(serverPublicKeyBytes), ByteBuffer.wrap(clientPublicKeyBytes));
			Collections.sort(keys);
			hash.update(keys.get(0));
			hash.update(keys.get(1));
			
			byte[] derivedKey = hash.digest();
			System.out.println("[Server] Final key: " + Hex.encodeHexString(derivedKey));
			
			// AES-GCM Decrypt and Send data using derivedKey
			byte[] iv = new byte[12];
			Cipher cipher = Cipher.getInstance("AES/GCM/NoPadding");
			SecretKeySpec keySpec = new SecretKeySpec(derivedKey, "AES");
			GCMParameterSpec gcmParameterSpec = new GCMParameterSpec(128, iv);
			cipher.init(Cipher.DECRYPT_MODE, keySpec, gcmParameterSpec);
			byte[] plaintext = cipher.doFinal(Base64.getUrlDecoder().decode(reader.readLine()));
			System.out.println("[Server] Received data : " + new String(plaintext, StandardCharsets.UTF_8));
		} catch(Exception e) {
			e.printStackTrace();
		} finally {
			if(reader != null) { try { reader.close(); } catch(Exception ignore) {} }
			if(writer != null) { try { writer.close(); } catch(Exception ignore) {} }
			if(clientSocket != null) { try { clientSocket.close(); } catch(Exception ignore) {} }
			if(serverSocket != null) { try { serverSocket .close(); } catch(Exception ignore) {} }
		}
	}
}

Client

package io.github.bindon;

import java.io.BufferedReader;
import java.io.InputStreamReader;
import java.io.OutputStreamWriter;
import java.io.PrintWriter;
import java.net.Socket;
import java.nio.ByteBuffer;
import java.nio.charset.StandardCharsets;
import java.security.KeyFactory;
import java.security.KeyPair;
import java.security.KeyPairGenerator;
import java.security.MessageDigest;
import java.security.PublicKey;
import java.security.spec.X509EncodedKeySpec;
import java.util.Arrays;
import java.util.Base64;
import java.util.Collections;
import java.util.List;

import javax.crypto.Cipher;
import javax.crypto.KeyAgreement;
import javax.crypto.spec.GCMParameterSpec;
import javax.crypto.spec.SecretKeySpec;

import org.apache.commons.codec.binary.Hex;

public class ECDHClient {
	public static void testECDHClient() {
		Socket socket = null;
		PrintWriter writer = null;
		BufferedReader reader = null;
		
		try {
			socket = new Socket("127.0.0.1", 32768);
			reader = new BufferedReader(new InputStreamReader(socket.getInputStream()));
			writer = new PrintWriter(new OutputStreamWriter(socket.getOutputStream()));
			
			// Generate ephemeral ECDH keypair
			KeyPairGenerator keyPairGenerator = KeyPairGenerator.getInstance("EC");
			keyPairGenerator.initialize(256);
			KeyPair clientKeyPair = keyPairGenerator.generateKeyPair();
			byte[] clientPublicKeyBytes = clientKeyPair.getPublic().getEncoded();
			
			// Receive server public key
			byte[] serverPublicKeyBytes = Base64.getUrlDecoder().decode(reader.readLine());
			System.out.println("[Client] " + Hex.encodeHexString(serverPublicKeyBytes));
			KeyFactory serverKeyFactory = KeyFactory.getInstance("EC");
			X509EncodedKeySpec serverKeySpec = new X509EncodedKeySpec(serverPublicKeyBytes);
			PublicKey serverPublicKey = serverKeyFactory.generatePublic(serverKeySpec);

			// Send client public key
			writer.println(Base64.getUrlEncoder().encodeToString(clientPublicKeyBytes));
			writer.flush();
	
			// Perform key agreement
			KeyAgreement keyAgreement = KeyAgreement.getInstance("ECDH");
			keyAgreement.init(clientKeyPair.getPrivate());
			keyAgreement.doPhase(serverPublicKey, true);
			
			// Read shared secret
			byte[] sharedSecret = keyAgreement.generateSecret();
			System.out.println("[Client] Shared secret: " + Hex.encodeHexString(sharedSecret));
			
			// Derive a key from the shared secret and both public keys
			MessageDigest hash = MessageDigest.getInstance("SHA-256");
			hash.update(sharedSecret); // Simple deterministic ordering
			List<ByteBuffer> keys = Arrays.asList(
					ByteBuffer.wrap(clientPublicKeyBytes), ByteBuffer.wrap(serverPublicKeyBytes));
			Collections.sort(keys);
			hash.update(keys.get(0));
			hash.update(keys.get(1));
			
			byte[] derivedKey = hash.digest();
			System.out.println("[Client] Final key: " + Hex.encodeHexString(derivedKey));
			
			// AES-GCM Encrypt and Send data using derivedKey
			byte[] iv = new byte[12];
			Cipher cipher = Cipher.getInstance("AES/GCM/NoPadding");
			SecretKeySpec keySpec = new SecretKeySpec(derivedKey, "AES");
			GCMParameterSpec gcmParameterSpec = new GCMParameterSpec(128, iv);
			cipher.init(Cipher.ENCRYPT_MODE, keySpec, gcmParameterSpec);
			byte[] ciphertext = cipher.doFinal("bindon".getBytes(StandardCharsets.UTF_8));
			writer.println(Base64.getUrlEncoder().encodeToString(ciphertext));
			writer.flush();
		} catch(Exception e) {
			e.printStackTrace();
		} finally {
			if(reader != null) { try { reader.close(); } catch(Exception ignore) {} }
			if(writer != null) { try { writer.close(); } catch(Exception ignore) {} }
			if(socket != null) { try { socket.close(); } catch(Exception ignore) {} }
		}
	}
}

Result

[Client] 3059301306072a8648ce3d020106082a8648ce3d0301070342000418fb88eda20f1dce6dc17c06185991a82d4f4839549c27166ee6c07d85da2ac8b6c185beadfef65c1dfa3469ba527084559fd34e837c8981b514a39c4643a5f6
[Server] 3059301306072a8648ce3d020106082a8648ce3d03010703420004a57ac41814377830609000c1058f3b4ed5ca95ddbfed3875141edb2e224a2d08b79167e0676306c35cd0bca343f2e0a9033acd6da8059ed4717825c403b22d41
[Server] Shared secret: f588354b9a3b308178c977e3170934e3d44bd4741883f796c6ad292df5445f8f
[Client] Shared secret: f588354b9a3b308178c977e3170934e3d44bd4741883f796c6ad292df5445f8f
[Server] Final key: 742c85973fab5af22b83b0e69e0ba7ed7503a8a022d595fe6ddafc71d9e3b91a
[Client] Final key: 742c85973fab5af22b83b0e69e0ba7ed7503a8a022d595fe6ddafc71d9e3b91a
[Server] Received data : bindon

Security Guide Post List

TITLE DATE
File Upload 2020-02-26
API Security 2020-02-26
HTTP Security Headers 2020-02-21
CSRF(Cross Site Request Forgery) Prevention 2020-02-20
XSS(Cross-Site Scripting) Prevention 2020-02-17
AES-GCM Example in Java 2019-11-28
ECDH Example in Java 2019-11-25