Skip to content

refactor: Enhance docs, add more tests in RSA #5898

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 6 commits into from
Oct 26, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
79 changes: 66 additions & 13 deletions src/main/java/com/thealgorithms/ciphers/RSA.java
Original file line number Diff line number Diff line change
Expand Up @@ -4,63 +4,116 @@
import java.security.SecureRandom;

/**
* @author Nguyen Duy Tiep on 23-Oct-17.
* RSA is an asymmetric cryptographic algorithm used for secure data encryption and decryption.
* It relies on a pair of keys: a public key (used for encryption) and a private key
* (used for decryption). The algorithm is based on the difficulty of factoring large prime numbers.
*
* This implementation includes key generation, encryption, and decryption methods that can handle both
* text-based messages and BigInteger inputs. For more details on RSA:
* <a href="https://en.wikipedia.org/wiki/RSA_(cryptosystem)">RSA Cryptosystem - Wikipedia</a>.
*
* Example Usage:
* <pre>
* RSA rsa = new RSA(1024);
* String encryptedMessage = rsa.encrypt("Hello RSA!");
* String decryptedMessage = rsa.decrypt(encryptedMessage);
* System.out.println(decryptedMessage); // Output: Hello RSA!
* </pre>
*
* Note: The key size directly affects the security and performance of the RSA algorithm.
* Larger keys are more secure but slower to compute.
*
* @author Nguyen Duy Tiep
* @version 23-Oct-17
*/
public class RSA {

private BigInteger modulus;
private BigInteger privateKey;
private BigInteger publicKey;

/**
* Constructor that generates RSA keys with the specified number of bits.
*
* @param bits The bit length of the keys to be generated. Common sizes include 512, 1024, 2048, etc.
*/
public RSA(int bits) {
generateKeys(bits);
}

/**
* @return encrypted message
* Encrypts a text message using the RSA public key.
*
* @param message The plaintext message to be encrypted.
* @throws IllegalArgumentException If the message is empty.
* @return The encrypted message represented as a String.
*/
public synchronized String encrypt(String message) {
if (message.isEmpty()) {
throw new IllegalArgumentException("Message is empty");
}
return (new BigInteger(message.getBytes())).modPow(publicKey, modulus).toString();
}

/**
* @return encrypted message as big integer
* Encrypts a BigInteger message using the RSA public key.
*
* @param message The plaintext message as a BigInteger.
* @return The encrypted message as a BigInteger.
*/
public synchronized BigInteger encrypt(BigInteger message) {
return message.modPow(publicKey, modulus);
}

/**
* @return plain message
* Decrypts an encrypted message (as String) using the RSA private key.
*
* @param encryptedMessage The encrypted message to be decrypted, represented as a String.
* @throws IllegalArgumentException If the message is empty.
* @return The decrypted plaintext message as a String.
*/
public synchronized String decrypt(String encryptedMessage) {
if (encryptedMessage.isEmpty()) {
throw new IllegalArgumentException("Message is empty");
}
return new String((new BigInteger(encryptedMessage)).modPow(privateKey, modulus).toByteArray());
}

/**
* @return plain message as big integer
* Decrypts an encrypted BigInteger message using the RSA private key.
*
* @param encryptedMessage The encrypted message as a BigInteger.
* @return The decrypted plaintext message as a BigInteger.
*/
public synchronized BigInteger decrypt(BigInteger encryptedMessage) {
return encryptedMessage.modPow(privateKey, modulus);
}

/**
* Generate a new public and private key set.
* Generates a new RSA key pair (public and private keys) with the specified bit length.
* Steps:
* 1. Generate two large prime numbers p and q.
* 2. Compute the modulus n = p * q.
* 3. Compute Euler's totient function: φ(n) = (p-1) * (q-1).
* 4. Choose a public key e (starting from 3) that is coprime with φ(n).
* 5. Compute the private key d as the modular inverse of e mod φ(n).
* The public key is (e, n) and the private key is (d, n).
*
* @param bits The bit length of the keys to be generated.
*/
public final synchronized void generateKeys(int bits) {
SecureRandom r = new SecureRandom();
BigInteger p = new BigInteger(bits / 2, 100, r);
BigInteger q = new BigInteger(bits / 2, 100, r);
SecureRandom random = new SecureRandom();
BigInteger p = new BigInteger(bits / 2, 100, random);
BigInteger q = new BigInteger(bits / 2, 100, random);
modulus = p.multiply(q);

BigInteger m = (p.subtract(BigInteger.ONE)).multiply(q.subtract(BigInteger.ONE));
BigInteger phi = (p.subtract(BigInteger.ONE)).multiply(q.subtract(BigInteger.ONE));

publicKey = BigInteger.valueOf(3L);

while (m.gcd(publicKey).intValue() > 1) {
while (phi.gcd(publicKey).intValue() > 1) {
publicKey = publicKey.add(BigInteger.TWO);
}

privateKey = publicKey.modInverse(m);
privateKey = publicKey.modInverse(phi);
}
}
59 changes: 50 additions & 9 deletions src/test/java/com/thealgorithms/ciphers/RSATest.java
Original file line number Diff line number Diff line change
@@ -1,23 +1,64 @@
package com.thealgorithms.ciphers;

import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertThrows;

import java.math.BigInteger;
import org.junit.jupiter.api.Test;

class RSATest {

RSA rsa = new RSA(1024);
private final RSA rsa = new RSA(1024);

@Test
void testRSA() {
// given
String textToEncrypt = "Such secure";
void testEncryptDecryptString() {
String originalMessage = "Such secure";
String encryptedMessage = rsa.encrypt(originalMessage);
String decryptedMessage = rsa.decrypt(encryptedMessage);
assertEquals(originalMessage, decryptedMessage);
}

@Test
void testEncryptDecryptBigInteger() {
BigInteger originalMessage = new BigInteger("12345678901234567890");
BigInteger encryptedMessage = rsa.encrypt(originalMessage);
BigInteger decryptedMessage = rsa.decrypt(encryptedMessage);
assertEquals(originalMessage, decryptedMessage);
}

// when
String cipherText = rsa.encrypt(textToEncrypt);
String decryptedText = rsa.decrypt(cipherText);
@Test
void testEmptyMessage() {
String originalMessage = "";
assertThrows(IllegalArgumentException.class, () -> rsa.encrypt(originalMessage));
assertThrows(IllegalArgumentException.class, () -> rsa.decrypt(originalMessage));
}

@Test
void testDifferentKeySizes() {
// Testing with 512-bit RSA keys
RSA smallRSA = new RSA(512);
String originalMessage = "Test with smaller key";

// then
assertEquals("Such secure", decryptedText);
String encryptedMessage = smallRSA.encrypt(originalMessage);
String decryptedMessage = smallRSA.decrypt(encryptedMessage);

assertEquals(originalMessage, decryptedMessage);

// Testing with 2048-bit RSA keys
RSA largeRSA = new RSA(2048);
String largeOriginalMessage = "Test with larger key";

String largeEncryptedMessage = largeRSA.encrypt(largeOriginalMessage);
String largeDecryptedMessage = largeRSA.decrypt(largeEncryptedMessage);

assertEquals(largeOriginalMessage, largeDecryptedMessage);
}

@Test
void testSpecialCharacters() {
String originalMessage = "Hello, RSA! @2024#";
String encryptedMessage = rsa.encrypt(originalMessage);
String decryptedMessage = rsa.decrypt(encryptedMessage);
assertEquals(originalMessage, decryptedMessage);
}
}