A Python port of the Invisible Internet Project (I2P)
at main 152 lines 5.0 kB view raw
1"""ElGamal — 2048-bit ElGamal encryption. 2 3Ported from net.i2p.crypto.ElGamalEngine. 4Uses Python's native int arithmetic (arbitrary precision). 5 6I2P-specific format: 7- Max plaintext: 222 bytes 8- Ciphertext: always 514 bytes (two 257-byte components) 9- Prepends random nonzero byte + SHA-256 hash before encryption 10- Verifies hash on decryption 11""" 12 13import hashlib 14import os 15 16# I2P's 2048-bit ElGamal prime (from CryptoConstants.java) 17ELGAMAL_PRIME = int( 18 "FFFFFFFFFFFFFFFFC90FDAA22168C234C4C6628B80DC1CD1" 19 "29024E088A67CC74020BBEA63B139B22514A08798E3404DD" 20 "EF9519B3CD3A431B302B0A6DF25F14374FE1356D6D51C245" 21 "E485B576625E7EC6F44C42E9A637ED6B0BFF5CB6F406B7ED" 22 "EE386BFB5A899FA5AE9F24117C4B1FE649286651ECE45B3D" 23 "C2007CB8A163BF0598DA48361C55D39A69163FA8FD24CF5F" 24 "83655D23DCA3AD961C62F356208552BB9ED529077096966D" 25 "670C354E4ABC9804F1746C08CA18217C32905E462E36CE3B" 26 "E39E772C180E86039B2783A2EC07A28FB5C55DF06F4C52C9" 27 "DE2BCBF6955817183995497CEA956AE515D2261898FA0510" 28 "15728E5A8AACAA68FFFFFFFFFFFFFFFF", 16 29) 30 31# Generator 32ELGAMAL_GENERATOR = 2 33 34# Max plaintext size: prime_size(256) - 1(random) - 32(hash) - 1(format) = 222 35MAX_CLEARTEXT = 222 36 37# Component size in output (257 bytes each, zero-padded) 38COMPONENT_SIZE = 257 39 40# Total ciphertext size 41CIPHERTEXT_SIZE = COMPONENT_SIZE * 2 # 514 42 43 44class ElGamalEngine: 45 """ElGamal encryption/decryption with I2P-specific formatting. 46 47 Uses 2048-bit prime from CryptoConstants. 48 Not recommended for new applications — prefer X25519/ChaCha20. 49 """ 50 51 @staticmethod 52 def encrypt(data: bytes, public_key: bytes) -> bytes: 53 """Encrypt up to 222 bytes using ElGamal. 54 55 Args: 56 data: plaintext, max 222 bytes 57 public_key: 256-byte ElGamal public key 58 59 Returns: 60 514-byte ciphertext (two 257-byte components) 61 62 Raises: 63 ValueError: if data exceeds 222 bytes 64 """ 65 if len(data) > MAX_CLEARTEXT: 66 raise ValueError(f"Data too long: {len(data)} > {MAX_CLEARTEXT}") 67 68 # Build plaintext: [random_nonzero_byte][sha256(data)][data] 69 rand_byte = 0 70 while rand_byte == 0: 71 rand_byte = os.urandom(1)[0] 72 hash_val = hashlib.sha256(data).digest() 73 plaintext = bytes([rand_byte]) + hash_val + data 74 75 # Pad to 256 bytes 76 plaintext = plaintext.ljust(256, b"\x00") 77 78 # Convert to integer (big-endian) 79 m = int.from_bytes(plaintext, "big") 80 81 # Convert public key to integer 82 y = int.from_bytes(public_key[:256], "big") 83 84 # Pick random k, compute y_val = g^k mod p, d = m * y^k mod p 85 k = int.from_bytes(os.urandom(256), "big") % (ELGAMAL_PRIME - 2) + 1 86 y_val = pow(ELGAMAL_GENERATOR, k, ELGAMAL_PRIME) 87 d = (m * pow(y, k, ELGAMAL_PRIME)) % ELGAMAL_PRIME 88 89 # Encode as 257-byte components (zero-padded big-endian) 90 y_bytes = y_val.to_bytes(COMPONENT_SIZE, "big") 91 d_bytes = d.to_bytes(COMPONENT_SIZE, "big") 92 93 return y_bytes + d_bytes 94 95 @staticmethod 96 def decrypt(encrypted: bytes, private_key: bytes) -> bytes | None: 97 """Decrypt ElGamal ciphertext. 98 99 Args: 100 encrypted: 514-byte ciphertext 101 private_key: 256-byte ElGamal private key 102 103 Returns: 104 Decrypted data, or None if hash verification fails. 105 """ 106 if len(encrypted) != CIPHERTEXT_SIZE: 107 return None 108 109 # Extract components 110 y_bytes = encrypted[:COMPONENT_SIZE] 111 d_bytes = encrypted[COMPONENT_SIZE:] 112 113 y_val = int.from_bytes(y_bytes, "big") 114 d = int.from_bytes(d_bytes, "big") 115 116 # Convert private key to integer 117 x = int.from_bytes(private_key[:256], "big") 118 119 # Compute m = d * y^(-x) mod p = d * y^(p-1-x) mod p 120 m = (d * pow(y_val, ELGAMAL_PRIME - 1 - x, ELGAMAL_PRIME)) % ELGAMAL_PRIME 121 122 # Convert back to bytes (256 bytes) 123 plaintext = m.to_bytes(256, "big") 124 125 # Parse: [random_byte][32-byte hash][data_with_padding] 126 hash_val = plaintext[1:33] 127 padded_data = plaintext[33:] 128 129 # Try all possible data lengths (longest first) using the hash 130 # to find the correct boundary between data and zero-padding. 131 for length in range(len(padded_data), -1, -1): 132 candidate = padded_data[:length] 133 if hashlib.sha256(candidate).digest() == hash_val: 134 return candidate 135 136 return None 137 138 @staticmethod 139 def generate_keypair() -> tuple[bytes, bytes]: 140 """Generate ElGamal key pair. 141 142 Returns: 143 (public_key, private_key) — both 256 bytes 144 """ 145 # Random private key x in [1, p-2] 146 x = int.from_bytes(os.urandom(256), "big") % (ELGAMAL_PRIME - 2) + 1 147 # Public key y = g^x mod p 148 y = pow(ELGAMAL_GENERATOR, x, ELGAMAL_PRIME) 149 150 pub = y.to_bytes(256, "big") 151 priv = x.to_bytes(256, "big") 152 return pub, priv