From bca18640d5c427c674c3470e0d8d2dc41f6124f3 Mon Sep 17 00:00:00 2001
From: Chahat
Date: Tue, 3 Feb 2026 15:41:13 -0600
Subject: [PATCH] feat: add ElGamalCipher with Safe Prime generation and
stateless design
---
.../thealgorithms/ciphers/ElGamalCipher.java | 174 ++++++++++++++++++
.../ciphers/ElGamalCipherTest.java | 145 +++++++++++++++
2 files changed, 319 insertions(+)
create mode 100644 src/main/java/com/thealgorithms/ciphers/ElGamalCipher.java
create mode 100644 src/test/java/com/thealgorithms/ciphers/ElGamalCipherTest.java
diff --git a/src/main/java/com/thealgorithms/ciphers/ElGamalCipher.java b/src/main/java/com/thealgorithms/ciphers/ElGamalCipher.java
new file mode 100644
index 000000000000..6383caa59b1f
--- /dev/null
+++ b/src/main/java/com/thealgorithms/ciphers/ElGamalCipher.java
@@ -0,0 +1,174 @@
+package com.thealgorithms.ciphers;
+
+import java.math.BigInteger;
+import java.security.SecureRandom;
+
+/**
+ * ElGamal Encryption Algorithm Implementation.
+ *
+ *
+ * ElGamal is an asymmetric key encryption algorithm for public-key cryptography
+ * based on the Diffie–Hellman key exchange. It relies on the difficulty
+ * of computing discrete logarithms in a cyclic group.
+ *
+ *
+ *
+ * Key Features:
+ *
+ * - Uses Safe Primes (p = 2q + 1) to ensure group security.
+ * - Verifies the generator is a primitive root modulo p.
+ * - Stateless design using Java Records.
+ * - SecureRandom for all cryptographic operations.
+ *
+ *
+ *
+ * @author Chahat Sandhu, singhc7
+ * @see ElGamal Encryption (Wikipedia)
+ * @see Safe Primes
+ */
+public final class ElGamalCipher {
+
+ private static final SecureRandom RANDOM = new SecureRandom();
+ private static final int PRIME_CERTAINTY = 40;
+ private static final int MIN_BIT_LENGTH = 256;
+
+ private ElGamalCipher() {
+ }
+
+ /**
+ * A container for the Public and Private keys.
+ *
+ * @param p The prime modulus.
+ * @param g The generator (primitive root).
+ * @param y The public key component (g^x mod p).
+ * @param x The private key.
+ */
+ public record KeyPair(BigInteger p, BigInteger g, BigInteger y, BigInteger x) {
+ }
+
+ /**
+ * Container for the encryption result.
+ *
+ * @param a The first component (g^k mod p).
+ * @param b The second component (y^k * m mod p).
+ */
+ public record CipherText(BigInteger a, BigInteger b) {
+ }
+
+ /**
+ * Generates a valid ElGamal KeyPair using a Safe Prime.
+ *
+ * @param bitLength The bit length of the prime modulus p. Must be at least 256.
+ * @return A valid KeyPair (p, g, y, x).
+ * @throws IllegalArgumentException if bitLength is too small.
+ */
+ public static KeyPair generateKeys(int bitLength) {
+ if (bitLength < MIN_BIT_LENGTH) {
+ throw new IllegalArgumentException("Bit length must be at least " + MIN_BIT_LENGTH + " for security.");
+ }
+
+ BigInteger p;
+ BigInteger q;
+ BigInteger g;
+ BigInteger x;
+ BigInteger y;
+
+ // Generate Safe Prime p = 2q + 1
+ do {
+ q = new BigInteger(bitLength - 1, PRIME_CERTAINTY, RANDOM);
+ p = q.multiply(BigInteger.TWO).add(BigInteger.ONE);
+ } while (!p.isProbablePrime(PRIME_CERTAINTY));
+
+ // Find a Generator g (Primitive Root modulo p)
+ do {
+ g = new BigInteger(bitLength, RANDOM).mod(p.subtract(BigInteger.TWO)).add(BigInteger.TWO);
+ } while (!isValidGenerator(g, p, q));
+
+ // Generate Private Key x in range [2, p-2]
+ do {
+ x = new BigInteger(bitLength, RANDOM);
+ } while (x.compareTo(BigInteger.TWO) < 0 || x.compareTo(p.subtract(BigInteger.TWO)) > 0);
+
+ // Compute Public Key y = g^x mod p
+ y = g.modPow(x, p);
+
+ return new KeyPair(p, g, y, x);
+ }
+
+ /**
+ * Encrypts a message using the public key.
+ *
+ * @param message The message converted to BigInteger.
+ * @param p The prime modulus.
+ * @param g The generator.
+ * @param y The public key component.
+ * @return The CipherText pair (a, b).
+ * @throws IllegalArgumentException if inputs are null, negative, or message >= p.
+ */
+ public static CipherText encrypt(BigInteger message, BigInteger p, BigInteger g, BigInteger y) {
+ if (message == null || p == null || g == null || y == null) {
+ throw new IllegalArgumentException("Inputs cannot be null.");
+ }
+ if (message.compareTo(BigInteger.ZERO) < 0) {
+ throw new IllegalArgumentException("Message must be non-negative.");
+ }
+ if (message.compareTo(p) >= 0) {
+ throw new IllegalArgumentException("Message must be smaller than the prime modulus p.");
+ }
+
+ BigInteger k;
+ BigInteger pMinus1 = p.subtract(BigInteger.ONE);
+
+ // Select ephemeral key k such that 1 < k < p-1 and gcd(k, p-1) = 1
+ do {
+ k = new BigInteger(p.bitLength(), RANDOM);
+ } while (k.compareTo(BigInteger.ONE) <= 0 || k.compareTo(pMinus1) >= 0 || !k.gcd(pMinus1).equals(BigInteger.ONE));
+
+ BigInteger a = g.modPow(k, p);
+ BigInteger b = y.modPow(k, p).multiply(message).mod(p);
+
+ return new CipherText(a, b);
+ }
+
+ /**
+ * Decrypts a ciphertext using the private key.
+ *
+ * @param cipher The CipherText (a, b).
+ * @param x The private key.
+ * @param p The prime modulus.
+ * @return The decrypted message as BigInteger.
+ * @throws IllegalArgumentException if inputs are null.
+ */
+ public static BigInteger decrypt(CipherText cipher, BigInteger x, BigInteger p) {
+ if (cipher == null || x == null || p == null) {
+ throw new IllegalArgumentException("Inputs cannot be null.");
+ }
+
+ BigInteger a = cipher.a();
+ BigInteger b = cipher.b();
+
+ BigInteger s = a.modPow(x, p);
+ BigInteger sInverse = s.modInverse(p);
+
+ return b.multiply(sInverse).mod(p);
+ }
+
+ /**
+ * Verifies if g is a valid generator for safe prime p = 2q + 1.
+ *
+ * @param g The candidate generator.
+ * @param p The safe prime.
+ * @param q The Sophie Germain prime (p-1)/2.
+ * @return True if g is a primitive root, False otherwise.
+ */
+ private static boolean isValidGenerator(BigInteger g, BigInteger p, BigInteger q) {
+ // Fix: Must use braces {} for all if statements
+ if (g.equals(BigInteger.ONE)) {
+ return false;
+ }
+ if (g.modPow(BigInteger.TWO, p).equals(BigInteger.ONE)) {
+ return false;
+ }
+ return !g.modPow(q, p).equals(BigInteger.ONE);
+ }
+}
diff --git a/src/test/java/com/thealgorithms/ciphers/ElGamalCipherTest.java b/src/test/java/com/thealgorithms/ciphers/ElGamalCipherTest.java
new file mode 100644
index 000000000000..63dec4846bbc
--- /dev/null
+++ b/src/test/java/com/thealgorithms/ciphers/ElGamalCipherTest.java
@@ -0,0 +1,145 @@
+package com.thealgorithms.ciphers;
+
+import java.math.BigInteger;
+import java.util.stream.Stream;
+import org.junit.jupiter.api.Assertions;
+import org.junit.jupiter.api.BeforeAll;
+import org.junit.jupiter.api.DisplayName;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.params.ParameterizedTest;
+import org.junit.jupiter.params.provider.MethodSource;
+
+/**
+ * Unit tests for ElGamalCipher.
+ * Includes property-based testing (homomorphism), probabilistic checks,
+ * and boundary validation.
+ */
+class ElGamalCipherTest {
+
+ private static ElGamalCipher.KeyPair sharedKeys;
+
+ @BeforeAll
+ static void setup() {
+ // Generate 256-bit keys for efficient unit testing
+ sharedKeys = ElGamalCipher.generateKeys(256);
+ }
+
+ @Test
+ @DisplayName("Test Key Generation Validity")
+ void testKeyGeneration() {
+ Assertions.assertNotNull(sharedKeys.p());
+ Assertions.assertNotNull(sharedKeys.g());
+ Assertions.assertNotNull(sharedKeys.x());
+ Assertions.assertNotNull(sharedKeys.y());
+
+ // Verify generator bounds: 1 < g < p
+ Assertions.assertTrue(sharedKeys.g().compareTo(BigInteger.ONE) > 0);
+ Assertions.assertTrue(sharedKeys.g().compareTo(sharedKeys.p()) < 0);
+
+ // Verify private key bounds: 1 < x < p-1
+ Assertions.assertTrue(sharedKeys.x().compareTo(BigInteger.ONE) > 0);
+ Assertions.assertTrue(sharedKeys.x().compareTo(sharedKeys.p().subtract(BigInteger.ONE)) < 0);
+ }
+
+ @Test
+ @DisplayName("Security Check: Probabilistic Encryption")
+ void testSemanticSecurity() {
+ // Encrypting the same message twice MUST yield different ciphertexts
+ // due to the random ephemeral key 'k'.
+ BigInteger message = new BigInteger("123456789");
+
+ ElGamalCipher.CipherText c1 = ElGamalCipher.encrypt(message, sharedKeys.p(), sharedKeys.g(), sharedKeys.y());
+ ElGamalCipher.CipherText c2 = ElGamalCipher.encrypt(message, sharedKeys.p(), sharedKeys.g(), sharedKeys.y());
+
+ // Check that the ephemeral keys (and thus 'a' components) were different
+ Assertions.assertNotEquals(c1.a(), c2.a(), "Ciphertexts must be randomized (Semantic Security violation)");
+ Assertions.assertNotEquals(c1.b(), c2.b());
+
+ // But both must decrypt to the original message
+ Assertions.assertEquals(ElGamalCipher.decrypt(c1, sharedKeys.x(), sharedKeys.p()), message);
+ Assertions.assertEquals(ElGamalCipher.decrypt(c2, sharedKeys.x(), sharedKeys.p()), message);
+ }
+
+ @ParameterizedTest
+ @MethodSource("provideMessages")
+ @DisplayName("Parameterized Test: Encrypt and Decrypt various messages")
+ void testEncryptDecrypt(String messageStr) {
+ BigInteger message = new BigInteger(messageStr.getBytes());
+
+ // Skip if message exceeds the test key size (256 bits)
+ if (message.compareTo(sharedKeys.p()) >= 0) {
+ return;
+ }
+
+ ElGamalCipher.CipherText ciphertext = ElGamalCipher.encrypt(message, sharedKeys.p(), sharedKeys.g(), sharedKeys.y());
+ BigInteger decrypted = ElGamalCipher.decrypt(ciphertext, sharedKeys.x(), sharedKeys.p());
+
+ Assertions.assertEquals(message, decrypted, "Decrypted BigInteger must match original");
+ Assertions.assertEquals(messageStr, new String(decrypted.toByteArray()), "Decrypted string must match original");
+ }
+
+ static Stream provideMessages() {
+ return Stream.of("Hello World", "TheAlgorithms", "A", "1234567890", "!@#$%^&*()");
+ }
+
+ @Test
+ @DisplayName("Edge Case: Message equals 0")
+ void testMessageZero() {
+ BigInteger zero = BigInteger.ZERO;
+ ElGamalCipher.CipherText ciphertext = ElGamalCipher.encrypt(zero, sharedKeys.p(), sharedKeys.g(), sharedKeys.y());
+ BigInteger decrypted = ElGamalCipher.decrypt(ciphertext, sharedKeys.x(), sharedKeys.p());
+
+ Assertions.assertEquals(zero, decrypted, "Should successfully encrypt/decrypt zero");
+ }
+
+ @Test
+ @DisplayName("Edge Case: Message equals p-1")
+ void testMessageMaxBound() {
+ BigInteger pMinus1 = sharedKeys.p().subtract(BigInteger.ONE);
+ ElGamalCipher.CipherText ciphertext = ElGamalCipher.encrypt(pMinus1, sharedKeys.p(), sharedKeys.g(), sharedKeys.y());
+ BigInteger decrypted = ElGamalCipher.decrypt(ciphertext, sharedKeys.x(), sharedKeys.p());
+
+ Assertions.assertEquals(pMinus1, decrypted, "Should successfully encrypt/decrypt p-1");
+ }
+
+ @Test
+ @DisplayName("Negative Test: Message >= p should fail")
+ void testMessageTooLarge() {
+ BigInteger tooLarge = sharedKeys.p();
+ Assertions.assertThrows(IllegalArgumentException.class, () -> ElGamalCipher.encrypt(tooLarge, sharedKeys.p(), sharedKeys.g(), sharedKeys.y()));
+ }
+
+ @Test
+ @DisplayName("Negative Test: Decrypt with wrong private key")
+ void testWrongKeyDecryption() {
+ BigInteger message = new BigInteger("99999");
+ ElGamalCipher.CipherText ciphertext = ElGamalCipher.encrypt(message, sharedKeys.p(), sharedKeys.g(), sharedKeys.y());
+
+ // Generate a fake private key
+ BigInteger wrongX = sharedKeys.x().add(BigInteger.ONE);
+
+ BigInteger decrypted = ElGamalCipher.decrypt(ciphertext, wrongX, sharedKeys.p());
+
+ Assertions.assertNotEquals(message, decrypted, "Decryption with wrong key must yield incorrect result");
+ }
+
+ @Test
+ @DisplayName("Property Test: Multiplicative Homomorphism")
+ void testHomomorphism() {
+ BigInteger m1 = new BigInteger("50");
+ BigInteger m2 = BigInteger.TEN; // Fix: Replaced new BigInteger("10") with BigInteger.TEN
+
+ ElGamalCipher.CipherText c1 = ElGamalCipher.encrypt(m1, sharedKeys.p(), sharedKeys.g(), sharedKeys.y());
+ ElGamalCipher.CipherText c2 = ElGamalCipher.encrypt(m2, sharedKeys.p(), sharedKeys.g(), sharedKeys.y());
+
+ // Multiply ciphertexts component-wise: (a1*a2, b1*b2)
+ BigInteger aNew = c1.a().multiply(c2.a()).mod(sharedKeys.p());
+ BigInteger bNew = c1.b().multiply(c2.b()).mod(sharedKeys.p());
+ ElGamalCipher.CipherText cCombined = new ElGamalCipher.CipherText(aNew, bNew);
+
+ BigInteger decrypted = ElGamalCipher.decrypt(cCombined, sharedKeys.x(), sharedKeys.p());
+ BigInteger expected = m1.multiply(m2).mod(sharedKeys.p());
+
+ Assertions.assertEquals(expected, decrypted, "Cipher must satisfy multiplicative homomorphism");
+ }
+}