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: + *

+ *

+ * + * @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"); + } +}