From a14b2345f0a40a043ce78ebaf1ff056bbb362891 Mon Sep 17 00:00:00 2001
From: Chahat Sandhu
Date: Wed, 4 Feb 2026 09:49:15 -0600
Subject: [PATCH 1/4] feat: add ElGamalCipher with Safe Prime generation and
stateless design (#7257)
---
.../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");
+ }
+}
From 249b88fea2d8e7d425af8db053196e9b2cf1ed2c Mon Sep 17 00:00:00 2001
From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com>
Date: Thu, 5 Feb 2026 23:03:26 +0100
Subject: [PATCH 2/4] chore(deps): bump com.puppycrawl.tools:checkstyle from
13.1.0 to 13.2.0 (#7259)
Bumps [com.puppycrawl.tools:checkstyle](https://github.com/checkstyle/checkstyle) from 13.1.0 to 13.2.0.
- [Release notes](https://github.com/checkstyle/checkstyle/releases)
- [Commits](https://github.com/checkstyle/checkstyle/compare/checkstyle-13.1.0...checkstyle-13.2.0)
---
updated-dependencies:
- dependency-name: com.puppycrawl.tools:checkstyle
dependency-version: 13.2.0
dependency-type: direct:production
update-type: version-update:semver-minor
...
Signed-off-by: dependabot[bot]
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
---
pom.xml | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/pom.xml b/pom.xml
index f13169cece97..3716323bd115 100644
--- a/pom.xml
+++ b/pom.xml
@@ -112,7 +112,7 @@
com.puppycrawl.tools
checkstyle
- 13.1.0
+ 13.2.0
From 3835c4822a651f90be03830f778bbc41c898c64c Mon Sep 17 00:00:00 2001
From: swativ15 <122958079+swativ15@users.noreply.github.com>
Date: Fri, 6 Feb 2026 03:37:11 +0530
Subject: [PATCH 3/4] Refactor: simplify validation and improve backtracking
cleanup (#7258)
### Summary
This PR makes small readability and maintainability improvements to the algorithm implementation.
### Changes
- Removed a redundant `n < 0` validation check since the method contract already ensures valid `n`
- Replaced `current.remove(current.size() - 1)` with `current.removeLast()` to better express backtracking intent
### Rationale
- Simplifies input validation without changing behavior
- Uses the `Deque` API to make the backtracking step clearer and less error-prone
### Impact
- No change in algorithm logic or time/space complexity
- Output remains identical
Co-authored-by: Swati Vusurumarthi
---
.../com/thealgorithms/backtracking/ArrayCombination.java | 6 +++---
1 file changed, 3 insertions(+), 3 deletions(-)
diff --git a/src/main/java/com/thealgorithms/backtracking/ArrayCombination.java b/src/main/java/com/thealgorithms/backtracking/ArrayCombination.java
index f8cd0c40c20e..d05e33a4242f 100644
--- a/src/main/java/com/thealgorithms/backtracking/ArrayCombination.java
+++ b/src/main/java/com/thealgorithms/backtracking/ArrayCombination.java
@@ -20,8 +20,8 @@ private ArrayCombination() {
* @throws IllegalArgumentException if n or k are negative, or if k is greater than n.
*/
public static List> combination(int n, int k) {
- if (n < 0 || k < 0 || k > n) {
- throw new IllegalArgumentException("Invalid input: n must be non-negative, k must be non-negative and less than or equal to n.");
+ if (k < 0 || k > n) {
+ throw new IllegalArgumentException("Invalid input: 0 ≤ k ≤ n is required.");
}
List> combinations = new ArrayList<>();
@@ -48,7 +48,7 @@ private static void combine(List> combinations, List curr
for (int i = start; i < n; i++) {
current.add(i);
combine(combinations, current, i + 1, n, k);
- current.remove(current.size() - 1); // Backtrack
+ current.removeLast(); // Backtrack
}
}
}
From 8403b8feb198ce250a1e2aef4416216154300961 Mon Sep 17 00:00:00 2001
From: Adarsh-Melath
Date: Mon, 9 Feb 2026 23:02:56 +0530
Subject: [PATCH 4/4] fix:shape's dimension constraints added correctyl (issue
id: #7260) (#7261)
* fix:shape's dimension constraints added correctyl (issue id: #7260)
* fix: base changed to baseLength for better understanding
* fix: base changed to baseLength for better understanding
* base changed to baseLength for better understanding
* fix:cleared some format issues
* fix:cleared some format issues
---
.../java/com/thealgorithms/maths/Area.java | 38 +++++++++----------
1 file changed, 19 insertions(+), 19 deletions(-)
diff --git a/src/main/java/com/thealgorithms/maths/Area.java b/src/main/java/com/thealgorithms/maths/Area.java
index 1eba6666dde3..08807580cb03 100644
--- a/src/main/java/com/thealgorithms/maths/Area.java
+++ b/src/main/java/com/thealgorithms/maths/Area.java
@@ -10,17 +10,17 @@ private Area() {
/**
* String of IllegalArgumentException for radius
*/
- private static final String POSITIVE_RADIUS = "Must be a positive radius";
+ private static final String POSITIVE_RADIUS = "Radius must be greater than 0";
/**
* String of IllegalArgumentException for height
*/
- private static final String POSITIVE_HEIGHT = "Must be a positive height";
+ private static final String POSITIVE_HEIGHT = "Height must be greater than 0";
/**
* String of IllegalArgumentException for base
*/
- private static final String POSITIVE_BASE = "Must be a positive base";
+ private static final String POSITIVE_BASE = "Base must be greater than 0";
/**
* Calculate the surface area of a cube.
@@ -30,7 +30,7 @@ private Area() {
*/
public static double surfaceAreaCube(final double sideLength) {
if (sideLength <= 0) {
- throw new IllegalArgumentException("Must be a positive sideLength");
+ throw new IllegalArgumentException("Side length must be greater than 0");
}
return 6 * sideLength * sideLength;
}
@@ -57,10 +57,10 @@ public static double surfaceAreaSphere(final double radius) {
*/
public static double surfaceAreaPyramid(final double sideLength, final double slantHeight) {
if (sideLength <= 0) {
- throw new IllegalArgumentException("Must be a positive sideLength");
+ throw new IllegalArgumentException("");
}
if (slantHeight <= 0) {
- throw new IllegalArgumentException("Must be a positive slantHeight");
+ throw new IllegalArgumentException("slant height must be greater than 0");
}
double baseArea = sideLength * sideLength;
double lateralSurfaceArea = 2 * sideLength * slantHeight;
@@ -76,10 +76,10 @@ public static double surfaceAreaPyramid(final double sideLength, final double sl
*/
public static double surfaceAreaRectangle(final double length, final double width) {
if (length <= 0) {
- throw new IllegalArgumentException("Must be a positive length");
+ throw new IllegalArgumentException("Length must be greater than 0");
}
if (width <= 0) {
- throw new IllegalArgumentException("Must be a positive width");
+ throw new IllegalArgumentException("Width must be greater than 0");
}
return length * width;
}
@@ -109,7 +109,7 @@ public static double surfaceAreaCylinder(final double radius, final double heigh
*/
public static double surfaceAreaSquare(final double sideLength) {
if (sideLength <= 0) {
- throw new IllegalArgumentException("Must be a positive sideLength");
+ throw new IllegalArgumentException("Side Length must be greater than 0");
}
return sideLength * sideLength;
}
@@ -121,14 +121,14 @@ public static double surfaceAreaSquare(final double sideLength) {
* @param height height of triangle
* @return area of given triangle
*/
- public static double surfaceAreaTriangle(final double base, final double height) {
- if (base <= 0) {
+ public static double surfaceAreaTriangle(final double baseLength, final double height) {
+ if (baseLength <= 0) {
throw new IllegalArgumentException(POSITIVE_BASE);
}
if (height <= 0) {
throw new IllegalArgumentException(POSITIVE_HEIGHT);
}
- return base * height / 2;
+ return baseLength * height / 2;
}
/**
@@ -138,14 +138,14 @@ public static double surfaceAreaTriangle(final double base, final double height)
* @param height height of a parallelogram
* @return area of given parallelogram
*/
- public static double surfaceAreaParallelogram(final double base, final double height) {
- if (base <= 0) {
+ public static double surfaceAreaParallelogram(final double baseLength, final double height) {
+ if (baseLength <= 0) {
throw new IllegalArgumentException(POSITIVE_BASE);
}
if (height <= 0) {
throw new IllegalArgumentException(POSITIVE_HEIGHT);
}
- return base * height;
+ return baseLength * height;
}
/**
@@ -156,17 +156,17 @@ public static double surfaceAreaParallelogram(final double base, final double he
* @param height height of trapezium
* @return area of given trapezium
*/
- public static double surfaceAreaTrapezium(final double base1, final double base2, final double height) {
- if (base1 <= 0) {
+ public static double surfaceAreaTrapezium(final double baseLength1, final double baseLength2, final double height) {
+ if (baseLength1 <= 0) {
throw new IllegalArgumentException(POSITIVE_BASE + 1);
}
- if (base2 <= 0) {
+ if (baseLength2 <= 0) {
throw new IllegalArgumentException(POSITIVE_BASE + 2);
}
if (height <= 0) {
throw new IllegalArgumentException(POSITIVE_HEIGHT);
}
- return (base1 + base2) * height / 2;
+ return (baseLength1 + baseLength2) * height / 2;
}
/**