mirror of
https://github.com/TheAlgorithms/Java.git
synced 2026-03-13 08:40:43 +08:00
feat: add ElGamalCipher with Safe Prime generation and stateless design (#7257)
This commit is contained in:
174
src/main/java/com/thealgorithms/ciphers/ElGamalCipher.java
Normal file
174
src/main/java/com/thealgorithms/ciphers/ElGamalCipher.java
Normal file
@@ -0,0 +1,174 @@
|
||||
package com.thealgorithms.ciphers;
|
||||
|
||||
import java.math.BigInteger;
|
||||
import java.security.SecureRandom;
|
||||
|
||||
/**
|
||||
* ElGamal Encryption Algorithm Implementation.
|
||||
*
|
||||
* <p>
|
||||
* 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.
|
||||
* </p>
|
||||
*
|
||||
* <p>
|
||||
* <strong>Key Features:</strong>
|
||||
* <ul>
|
||||
* <li>Uses Safe Primes (p = 2q + 1) to ensure group security.</li>
|
||||
* <li>Verifies the generator is a primitive root modulo p.</li>
|
||||
* <li>Stateless design using Java Records.</li>
|
||||
* <li>SecureRandom for all cryptographic operations.</li>
|
||||
* </ul>
|
||||
* </p>
|
||||
*
|
||||
* @author Chahat Sandhu, <a href="https://github.com/singhc7">singhc7</a>
|
||||
* @see <a href="https://en.wikipedia.org/wiki/ElGamal_encryption">ElGamal Encryption (Wikipedia)</a>
|
||||
* @see <a href="https://en.wikipedia.org/wiki/Safe_and_Sophie_Germain_primes">Safe Primes</a>
|
||||
*/
|
||||
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);
|
||||
}
|
||||
}
|
||||
145
src/test/java/com/thealgorithms/ciphers/ElGamalCipherTest.java
Normal file
145
src/test/java/com/thealgorithms/ciphers/ElGamalCipherTest.java
Normal file
@@ -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<String> 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");
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user