Skip to content

Commit 6f414a0

Browse files
committed
feat: add ElGamalCipher with Safe Prime generation and stateless design
1 parent 8e30bcb commit 6f414a0

File tree

2 files changed

+324
-0
lines changed

2 files changed

+324
-0
lines changed
Lines changed: 174 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,174 @@
1+
package com.thealgorithms.ciphers;
2+
3+
import java.math.BigInteger;
4+
import java.security.SecureRandom;
5+
6+
/**
7+
* ElGamal Encryption Algorithm Implementation.
8+
*
9+
* <p>
10+
* ElGamal is an asymmetric key encryption algorithm for public-key cryptography
11+
* based on the Diffie–Hellman key exchange. It relies on the difficulty
12+
* of computing discrete logarithms in a cyclic group.
13+
* </p>
14+
*
15+
* <p>
16+
* <strong>Key Features:</strong>
17+
* <ul>
18+
* <li>Uses Safe Primes (p = 2q + 1) to ensure group security.</li>
19+
* <li>Verifies the generator is a primitive root modulo p.</li>
20+
* <li>Stateless design using Java Records.</li>
21+
* <li>SecureRandom for all cryptographic operations.</li>
22+
* </ul>
23+
* </p>
24+
*
25+
* @author Chahat Sandhu, <a href="https://github.com/singhc7">singhc7</a>
26+
* @see <a href="https://en.wikipedia.org/wiki/ElGamal_encryption">ElGamal Encryption (Wikipedia)</a>
27+
* @see <a href="https://en.wikipedia.org/wiki/Safe_and_Sophie_Germain_primes">Safe Primes</a>
28+
*/
29+
public final class ElGamalCipher {
30+
31+
private static final SecureRandom RANDOM = new SecureRandom();
32+
private static final int PRIME_CERTAINTY = 40;
33+
private static final int MIN_BIT_LENGTH = 256;
34+
35+
private ElGamalCipher() {
36+
}
37+
38+
/**
39+
* A container for the Public and Private keys.
40+
*
41+
* @param p The prime modulus.
42+
* @param g The generator (primitive root).
43+
* @param y The public key component (g^x mod p).
44+
* @param x The private key.
45+
*/
46+
public record KeyPair(BigInteger p, BigInteger g, BigInteger y, BigInteger x) {
47+
}
48+
49+
/**
50+
* Container for the encryption result.
51+
*
52+
* @param a The first component (g^k mod p).
53+
* @param b The second component (y^k * m mod p).
54+
*/
55+
public record CipherText(BigInteger a, BigInteger b) {
56+
}
57+
58+
/**
59+
* Generates a valid ElGamal KeyPair using a Safe Prime.
60+
*
61+
* @param bitLength The bit length of the prime modulus p. Must be at least 256.
62+
* @return A valid KeyPair (p, g, y, x).
63+
* @throws IllegalArgumentException if bitLength is too small.
64+
*/
65+
public static KeyPair generateKeys(int bitLength) {
66+
if (bitLength < MIN_BIT_LENGTH) {
67+
throw new IllegalArgumentException("Bit length must be at least " + MIN_BIT_LENGTH + " for security.");
68+
}
69+
70+
BigInteger p;
71+
BigInteger q;
72+
BigInteger g;
73+
BigInteger x;
74+
BigInteger y;
75+
76+
// Generate Safe Prime p = 2q + 1
77+
do {
78+
q = new BigInteger(bitLength - 1, PRIME_CERTAINTY, RANDOM);
79+
p = q.multiply(BigInteger.TWO).add(BigInteger.ONE);
80+
} while (!p.isProbablePrime(PRIME_CERTAINTY));
81+
82+
// Find a Generator g (Primitive Root modulo p)
83+
do {
84+
g = new BigInteger(bitLength, RANDOM).mod(p.subtract(BigInteger.TWO)).add(BigInteger.TWO);
85+
} while (!isValidGenerator(g, p, q));
86+
87+
// Generate Private Key x in range [2, p-2]
88+
do {
89+
x = new BigInteger(bitLength, RANDOM);
90+
} while (x.compareTo(BigInteger.TWO) < 0 || x.compareTo(p.subtract(BigInteger.TWO)) > 0);
91+
92+
// Compute Public Key y = g^x mod p
93+
y = g.modPow(x, p);
94+
95+
return new KeyPair(p, g, y, x);
96+
}
97+
98+
/**
99+
* Encrypts a message using the public key.
100+
*
101+
* @param message The message converted to BigInteger.
102+
* @param p The prime modulus.
103+
* @param g The generator.
104+
* @param y The public key component.
105+
* @return The CipherText pair (a, b).
106+
* @throws IllegalArgumentException if inputs are null, negative, or message >= p.
107+
*/
108+
public static CipherText encrypt(BigInteger message, BigInteger p, BigInteger g, BigInteger y) {
109+
if (message == null || p == null || g == null || y == null) {
110+
throw new IllegalArgumentException("Inputs cannot be null.");
111+
}
112+
if (message.compareTo(BigInteger.ZERO) < 0) {
113+
throw new IllegalArgumentException("Message must be non-negative.");
114+
}
115+
if (message.compareTo(p) >= 0) {
116+
throw new IllegalArgumentException("Message must be smaller than the prime modulus p.");
117+
}
118+
119+
BigInteger k;
120+
BigInteger pMinus1 = p.subtract(BigInteger.ONE);
121+
122+
// Select ephemeral key k such that 1 < k < p-1 and gcd(k, p-1) = 1
123+
do {
124+
k = new BigInteger(p.bitLength(), RANDOM);
125+
} while (k.compareTo(BigInteger.ONE) <= 0 || k.compareTo(pMinus1) >= 0 || !k.gcd(pMinus1).equals(BigInteger.ONE));
126+
127+
BigInteger a = g.modPow(k, p);
128+
BigInteger b = y.modPow(k, p).multiply(message).mod(p);
129+
130+
return new CipherText(a, b);
131+
}
132+
133+
/**
134+
* Decrypts a ciphertext using the private key.
135+
*
136+
* @param cipher The CipherText (a, b).
137+
* @param x The private key.
138+
* @param p The prime modulus.
139+
* @return The decrypted message as BigInteger.
140+
* @throws IllegalArgumentException if inputs are null.
141+
*/
142+
public static BigInteger decrypt(CipherText cipher, BigInteger x, BigInteger p) {
143+
if (cipher == null || x == null || p == null) {
144+
throw new IllegalArgumentException("Inputs cannot be null.");
145+
}
146+
147+
BigInteger a = cipher.a();
148+
BigInteger b = cipher.b();
149+
150+
BigInteger s = a.modPow(x, p);
151+
BigInteger sInverse = s.modInverse(p);
152+
153+
return b.multiply(sInverse).mod(p);
154+
}
155+
156+
/**
157+
* Verifies if g is a valid generator for safe prime p = 2q + 1.
158+
*
159+
* @param g The candidate generator.
160+
* @param p The safe prime.
161+
* @param q The Sophie Germain prime (p-1)/2.
162+
* @return True if g is a primitive root, False otherwise.
163+
*/
164+
private static boolean isValidGenerator(BigInteger g, BigInteger p, BigInteger q) {
165+
// Fix: Must use braces {} for all if statements
166+
if (g.equals(BigInteger.ONE)) {
167+
return false;
168+
}
169+
if (g.modPow(BigInteger.TWO, p).equals(BigInteger.ONE)) {
170+
return false;
171+
}
172+
return !g.modPow(q, p).equals(BigInteger.ONE);
173+
}
174+
}
Lines changed: 150 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,150 @@
1+
package com.thealgorithms.ciphers;
2+
3+
import static org.junit.jupiter.api.Assertions.assertEquals;
4+
import static org.junit.jupiter.api.Assertions.assertNotEquals;
5+
import static org.junit.jupiter.api.Assertions.assertNotNull;
6+
import static org.junit.jupiter.api.Assertions.assertThrows;
7+
import static org.junit.jupiter.api.Assertions.assertTrue;
8+
9+
import java.math.BigInteger;
10+
import java.util.stream.Stream;
11+
import org.junit.jupiter.api.BeforeAll;
12+
import org.junit.jupiter.api.DisplayName;
13+
import org.junit.jupiter.api.Test;
14+
import org.junit.jupiter.params.ParameterizedTest;
15+
import org.junit.jupiter.params.provider.MethodSource;
16+
17+
/**
18+
* Unit tests for ElGamalCipher.
19+
* Includes property-based testing (homomorphism), probabilistic checks,
20+
* and boundary validation.
21+
*/
22+
class ElGamalCipherTest {
23+
24+
private static ElGamalCipher.KeyPair sharedKeys;
25+
26+
@BeforeAll
27+
static void setup() {
28+
// Generate 256-bit keys for efficient unit testing
29+
sharedKeys = ElGamalCipher.generateKeys(256);
30+
}
31+
32+
@Test
33+
@DisplayName("Test Key Generation Validity")
34+
void testKeyGeneration() {
35+
assertNotNull(sharedKeys.p());
36+
assertNotNull(sharedKeys.g());
37+
assertNotNull(sharedKeys.x());
38+
assertNotNull(sharedKeys.y());
39+
40+
// Verify generator bounds: 1 < g < p
41+
assertTrue(sharedKeys.g().compareTo(BigInteger.ONE) > 0);
42+
assertTrue(sharedKeys.g().compareTo(sharedKeys.p()) < 0);
43+
44+
// Verify private key bounds: 1 < x < p-1
45+
assertTrue(sharedKeys.x().compareTo(BigInteger.ONE) > 0);
46+
assertTrue(sharedKeys.x().compareTo(sharedKeys.p().subtract(BigInteger.ONE)) < 0);
47+
}
48+
49+
@Test
50+
@DisplayName("Security Check: Probabilistic Encryption")
51+
void testSemanticSecurity() {
52+
// Encrypting the same message twice MUST yield different ciphertexts
53+
// due to the random ephemeral key 'k'.
54+
BigInteger message = new BigInteger("123456789");
55+
56+
ElGamalCipher.CipherText c1 = ElGamalCipher.encrypt(message, sharedKeys.p(), sharedKeys.g(), sharedKeys.y());
57+
ElGamalCipher.CipherText c2 = ElGamalCipher.encrypt(message, sharedKeys.p(), sharedKeys.g(), sharedKeys.y());
58+
59+
// Check that the ephemeral keys (and thus 'a' components) were different
60+
assertNotEquals(c1.a(), c2.a(), "Ciphertexts must be randomized (Semantic Security violation)");
61+
assertNotEquals(c1.b(), c2.b());
62+
63+
// But both must decrypt to the original message
64+
assertEquals(ElGamalCipher.decrypt(c1, sharedKeys.x(), sharedKeys.p()), message);
65+
assertEquals(ElGamalCipher.decrypt(c2, sharedKeys.x(), sharedKeys.p()), message);
66+
}
67+
68+
@ParameterizedTest
69+
@MethodSource("provideMessages")
70+
@DisplayName("Parameterized Test: Encrypt and Decrypt various messages")
71+
void testEncryptDecrypt(String messageStr) {
72+
BigInteger message = new BigInteger(messageStr.getBytes());
73+
74+
// Skip if message exceeds the test key size (256 bits)
75+
if (message.compareTo(sharedKeys.p()) >= 0) {
76+
return;
77+
}
78+
79+
ElGamalCipher.CipherText ciphertext = ElGamalCipher.encrypt(message, sharedKeys.p(), sharedKeys.g(), sharedKeys.y());
80+
BigInteger decrypted = ElGamalCipher.decrypt(ciphertext, sharedKeys.x(), sharedKeys.p());
81+
82+
assertEquals(message, decrypted, "Decrypted BigInteger must match original");
83+
assertEquals(messageStr, new String(decrypted.toByteArray()), "Decrypted string must match original");
84+
}
85+
86+
static Stream<String> provideMessages() {
87+
return Stream.of("Hello World", "TheAlgorithms", "A", "1234567890", "!@#$%^&*()");
88+
}
89+
90+
@Test
91+
@DisplayName("Edge Case: Message equals 0")
92+
void testMessageZero() {
93+
BigInteger zero = BigInteger.ZERO;
94+
ElGamalCipher.CipherText ciphertext = ElGamalCipher.encrypt(zero, sharedKeys.p(), sharedKeys.g(), sharedKeys.y());
95+
BigInteger decrypted = ElGamalCipher.decrypt(ciphertext, sharedKeys.x(), sharedKeys.p());
96+
97+
assertEquals(zero, decrypted, "Should successfully encrypt/decrypt zero");
98+
}
99+
100+
@Test
101+
@DisplayName("Edge Case: Message equals p-1")
102+
void testMessageMaxBound() {
103+
BigInteger pMinus1 = sharedKeys.p().subtract(BigInteger.ONE);
104+
ElGamalCipher.CipherText ciphertext = ElGamalCipher.encrypt(pMinus1, sharedKeys.p(), sharedKeys.g(), sharedKeys.y());
105+
BigInteger decrypted = ElGamalCipher.decrypt(ciphertext, sharedKeys.x(), sharedKeys.p());
106+
107+
assertEquals(pMinus1, decrypted, "Should successfully encrypt/decrypt p-1");
108+
}
109+
110+
@Test
111+
@DisplayName("Negative Test: Message >= p should fail")
112+
void testMessageTooLarge() {
113+
BigInteger tooLarge = sharedKeys.p();
114+
assertThrows(IllegalArgumentException.class, () -> ElGamalCipher.encrypt(tooLarge, sharedKeys.p(), sharedKeys.g(), sharedKeys.y()));
115+
}
116+
117+
@Test
118+
@DisplayName("Negative Test: Decrypt with wrong private key")
119+
void testWrongKeyDecryption() {
120+
BigInteger message = new BigInteger("99999");
121+
ElGamalCipher.CipherText ciphertext = ElGamalCipher.encrypt(message, sharedKeys.p(), sharedKeys.g(), sharedKeys.y());
122+
123+
// Generate a fake private key
124+
BigInteger wrongX = sharedKeys.x().add(BigInteger.ONE);
125+
126+
BigInteger decrypted = ElGamalCipher.decrypt(ciphertext, wrongX, sharedKeys.p());
127+
128+
assertNotEquals(message, decrypted, "Decryption with wrong key must yield incorrect result");
129+
}
130+
131+
@Test
132+
@DisplayName("Property Test: Multiplicative Homomorphism")
133+
void testHomomorphism() {
134+
BigInteger m1 = new BigInteger("50");
135+
BigInteger m2 = new BigInteger("10");
136+
137+
ElGamalCipher.CipherText c1 = ElGamalCipher.encrypt(m1, sharedKeys.p(), sharedKeys.g(), sharedKeys.y());
138+
ElGamalCipher.CipherText c2 = ElGamalCipher.encrypt(m2, sharedKeys.p(), sharedKeys.g(), sharedKeys.y());
139+
140+
// Multiply ciphertexts component-wise: (a1*a2, b1*b2)
141+
BigInteger aNew = c1.a().multiply(c2.a()).mod(sharedKeys.p());
142+
BigInteger bNew = c1.b().multiply(c2.b()).mod(sharedKeys.p());
143+
ElGamalCipher.CipherText cCombined = new ElGamalCipher.CipherText(aNew, bNew);
144+
145+
BigInteger decrypted = ElGamalCipher.decrypt(cCombined, sharedKeys.x(), sharedKeys.p());
146+
BigInteger expected = m1.multiply(m2).mod(sharedKeys.p());
147+
148+
assertEquals(expected, decrypted, "Cipher must satisfy multiplicative homomorphism");
149+
}
150+
}

0 commit comments

Comments
 (0)