Loading content...
We've established RSA's mathematical foundations. Now we transform that theory into a practical cryptosystem capable of securing real communications. But this transformation is not straightforward—naive RSA encryption is insecure, and the way we prepare messages for encryption can make the difference between an unbreakable system and one that falls to standard attacks.
This page covers the complete encryption pipeline:
Every HTTPS connection, every digital signature, every encrypted email relies on these processes. Understanding them is essential for anyone working with network security.
The basic RSA operation (c = m^e mod n) is called 'textbook RSA' and is NOT secure for direct use. Without proper padding, RSA is vulnerable to: chosen plaintext attacks, adaptive chosen ciphertext attacks, small message attacks, and more. Always use standardized padding schemes like OAEP (for encryption) or PSS (for signatures). This page explains why and how.
RSA operates on integers, but messages are typically text, files, or binary data. Before encryption, we must convert our message into an integer m where 0 ≤ m < n. This section covers the encoding process and its constraints.
Step 1: Convert Message to Bytes
Any digital data is already a sequence of bytes (8-bit values from 0-255). Text is encoded using UTF-8 or another character encoding. Files are already byte sequences.
Step 2: Interpret Bytes as an Integer
A byte sequence [b₀, b₁, b₂, ..., bₖ] can be interpreted as an integer:
m = b₀ × 256^k + b₁ × 256^(k-1) + ... + bₖ × 256^0
This is simply the byte sequence as a big-endian unsigned integer.
Example:
Constraint: m < n
For 2048-bit RSA, n is a 2048-bit number (≈ 617 decimal digits). The message integer must be smaller than n, limiting single-block messages to at most 256 bytes for 2048-bit RSA (minus padding overhead).
For longer messages, we either:
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566
def bytes_to_int(data: bytes) -> int: """ Convert a byte sequence to an integer. Uses big-endian byte order (most significant byte first). This is the standard for cryptographic operations. """ return int.from_bytes(data, byteorder='big') def int_to_bytes(n: int, length: int = None) -> bytes: """ Convert an integer to bytes. If length is specified, pads with leading zeros to reach that length. """ if length is None: # Compute required length length = (n.bit_length() + 7) // 8 if length == 0: length = 1 return n.to_bytes(length, byteorder='big') def encode_message(message: str, encoding: str = 'utf-8') -> int: """ Encode a string message as an integer for RSA. Process: 1. Convert string to bytes using specified encoding 2. Interpret bytes as a big-endian integer """ message_bytes = message.encode(encoding) return bytes_to_int(message_bytes) def decode_message(m: int, encoding: str = 'utf-8') -> str: """ Decode an integer back to a string after RSA decryption. """ message_bytes = int_to_bytes(m) return message_bytes.decode(encoding) # Demonstrationif __name__ == "__main__": # Example: Encode "Hello, RSA!" message = "Hello, RSA!" m = encode_message(message) print(f"Original message: '{message}'") print(f"As bytes: {message.encode('utf-8').hex()}") print(f"As integer: {m}") print(f"Bit length: {m.bit_length()} bits") # For 2048-bit RSA, n has ~2048 bits # m must be < n, so m can have at most ~2047 bits # That's approximately 256 bytes maximum (before padding) # Verify round-trip decoded = decode_message(m) print(f"Decoded: '{decoded}'") assert decoded == message # Larger example showing bit length constraints large_message = "A" * 200 # 200 bytes = 1600 bits m_large = encode_message(large_message) print(f"\n200-byte message needs {m_large.bit_length()} bits") print(f"Fits in 2048-bit RSA: {m_large.bit_length() < 2048}")While 2048-bit RSA can theoretically encrypt ~256 bytes, padding schemes reduce this. OAEP with SHA-256 requires 66 bytes of overhead, leaving only 190 bytes for the actual message. This is why RSA is typically used only to encrypt symmetric keys (16-32 bytes), not bulk data.
At its mathematical core, RSA encryption and decryption are simple modular exponentiations.
Encryption (using public key):
c = m^e mod n
where:
Decryption (using private key):
m = c^d mod n
where:
Implementation: Efficient Modular Exponentiation
Computing m^e mod n directly is infeasible—65537 multiplications of 2048-bit numbers. Instead, we use the square-and-multiply algorithm:
This requires only O(log e) multiplications. For e = 65537 = 2^16 + 1 (17 bits, only two 1-bits), RSA encryption needs just 17 squarings and 2 multiplications.
| Operation | Exponent | Multiplications | Relative Speed |
|---|---|---|---|
| Encryption (e=65537) | 17 bits, 2 set bits | ~17 squarings + 2 multiplies | Fast |
| Encryption (e=3) | 2 bits, 2 set bits | ~2 squarings + 2 multiplies | Very fast (but less secure) |
| Decryption (standard) | ~2048 bits | ~3000 operations | Slow |
| Decryption (CRT) | ~1024 bits × 2 | ~2 × 1500 operations | ~4x faster than standard |
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596
def rsa_encrypt_raw(message: int, e: int, n: int) -> int: """ Raw RSA encryption: c = m^e mod n WARNING: This is TEXTBOOK RSA and is NOT secure for direct use! Always use padding (OAEP) in production. Args: message: Integer representation of message, must be 0 ≤ m < n e: Public exponent n: RSA modulus Returns: Ciphertext as integer """ if not (0 <= message < n): raise ValueError(f"Message must be in range [0, n). Got {message}, n={n}") # Python's pow() with 3 arguments uses efficient modular exponentiation ciphertext = pow(message, e, n) return ciphertext def rsa_decrypt_raw(ciphertext: int, d: int, n: int) -> int: """ Raw RSA decryption: m = c^d mod n Args: ciphertext: Encrypted message as integer d: Private exponent n: RSA modulus Returns: Decrypted message as integer """ message = pow(ciphertext, d, n) return message def rsa_decrypt_crt(ciphertext: int, p: int, q: int, dP: int, dQ: int, qInv: int) -> int: """ RSA decryption with Chinese Remainder Theorem optimization. Approximately 4x faster than standard decryption. Precomputed values: dP = d mod (p-1) dQ = d mod (q-1) qInv = q^(-1) mod p """ # Partial decryptions with smaller moduli m1 = pow(ciphertext, dP, p) m2 = pow(ciphertext, dQ, q) # Combine using Garner's formula h = (qInv * (m1 - m2)) % p message = m2 + h * q return message # Complete example with small numbersif __name__ == "__main__": # Generate a small RSA key pair for demonstration p, q = 61, 53 n = p * q # 3233 e = 17 phi_n = (p - 1) * (q - 1) # 3120 d = pow(e, -1, phi_n) # 2753 # Precompute CRT parameters dP = d % (p - 1) dQ = d % (q - 1) qInv = pow(q, -1, p) print(f"RSA Key Pair (demonstration only - keys are too small!):") print(f" Public: (e={e}, n={n})") print(f" Private: (d={d}, n={n})") print(f" CRT params: dP={dP}, dQ={dQ}, qInv={qInv}") print() # Encrypt and decrypt message = 123 print(f"Original message: m = {message}") # Encrypt ciphertext = rsa_encrypt_raw(message, e, n) print(f"Ciphertext: c = m^e mod n = {message}^{e} mod {n} = {ciphertext}") # Decrypt (standard method) decrypted_standard = rsa_decrypt_raw(ciphertext, d, n) print(f"Decrypted (standard): m = c^d mod n = {ciphertext}^{d} mod {n} = {decrypted_standard}") # Decrypt (CRT method) decrypted_crt = rsa_decrypt_crt(ciphertext, p, q, dP, dQ, qInv) print(f"Decrypted (CRT): m = {decrypted_crt}") print(f"\nAll methods agree: {message == decrypted_standard == decrypted_crt}")The raw RSA operation c = m^e mod n is called textbook RSA. Despite its mathematical elegance, it is fundamentally insecure for direct use. Understanding these vulnerabilities is essential for appreciating why padding schemes are mandatory.
Vulnerability 1: Deterministic Encryption
Textbook RSA is deterministic—the same plaintext always produces the same ciphertext. This seemingly minor property has devastating consequences:
Vulnerability 2: Malleability
An attacker can manipulate ciphertexts to produce predictable changes in plaintexts:
If c = m^e mod n, then (c × 2^e) mod n = (2m)^e mod n.
The attacker has doubled the message without knowing what it was! This enables:
In 1998, Daniel Bleichenbacher demonstrated a practical attack against PKCS#1 v1.5 padding in SSL 3.0, extracting the premaster secret in under a million queries. This led to the development of OAEP. In 2017, the ROBOT attack showed that many TLS implementations were still vulnerable to Bleichenbacher-style attacks 19 years later. Never assume textbook RSA is safe, and verify that your implementations use proper padding.
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156
# Demonstrations of Textbook RSA Vulnerabilities# These are educational examples - NEVER use textbook RSA in production! import secretsfrom math import gcd def demo_deterministic_attack(): """ Demonstrate how deterministic encryption allows confirmation attacks. Scenario: Alice encrypts either "yes" or "no" and sends to Bob. Eve intercepts the ciphertext but can test which it is. """ print("=== Deterministic Encryption Attack ===") # Small RSA for demonstration p, q = 61, 53 n = p * q e = 17 # Alice encrypts her message (let's say "yes" = 121 in our encoding) alice_message = 121 # "yes" ciphertext = pow(alice_message, e, n) print(f"Eve intercepts ciphertext: {ciphertext}") # Eve doesn't know the message, but she can guess! guess_yes = 121 guess_no = 110 encrypted_yes = pow(guess_yes, e, n) encrypted_no = pow(guess_no, e, n) print(f"Eve encrypts 'yes': {encrypted_yes}") print(f"Eve encrypts 'no': {encrypted_no}") if ciphertext == encrypted_yes: print("Eve concludes: Alice sent 'yes'!") elif ciphertext == encrypted_no: print("Eve concludes: Alice sent 'no'!") print() def demo_malleability_attack(): """ Demonstrate how malleability allows ciphertext manipulation. Scenario: Attacker can double the encrypted amount without decrypting. """ print("=== Malleability Attack ===") p, q = 61, 53 n = p * q e = 17 d = pow(e, -1, (p-1)*(q-1)) # Bank encrypts transaction: $100 amount = 100 ciphertext = pow(amount, e, n) print(f"Original transaction: ${amount}") print(f"Encrypted ciphertext: {ciphertext}") # Attacker modifies ciphertext to double the amount # If c = m^e, then c' = c * 2^e = (2m)^e factor = 2 modified_ciphertext = (ciphertext * pow(factor, e, n)) % n print(f"Modified ciphertext: {modified_ciphertext}") # Bank decrypts decrypted = pow(modified_ciphertext, d, n) print(f"Bank decrypts: ${decrypted}") print(f"Attacker doubled the amount without knowing it!") print() def demo_small_message_attack(): """ Demonstrate the small message attack with e=3. If m^e < n, there's no modular reduction, so c = m^e. Attacker simply takes the e-th root. """ print("=== Small Message Attack (e=3) ===") # Use e=3 and 2048-bit n n = 2 ** 2048 # Simplified; in practice n would be a semiprime e = 3 # Small message (like a 256-bit AES key) message = 2 ** 128 # 128-bit value # Encrypt ciphertext = pow(message, e, n) # Since m^3 < n, ciphertext = message^3 exactly (no mod reduction) # Attacker computes cube root recovered = round(ciphertext ** (1/3)) print(f"Message: {message}") print(f"m^3: {message**3}") print(f"n: {n}") print(f"m^3 < n: {message**3 < n}") print(f"Recovered message (cube root): {recovered}") print(f"Attack successful: {recovered == message}") print() def demo_hastad_broadcast_attack(): """ Demonstrate Håstad's broadcast attack. If the same message is encrypted to 3 recipients with different n's and e=3, CRT can recover the message. """ print("=== Håstad's Broadcast Attack (e=3) ===") # Three recipients with different moduli n1, n2, n3 = 3233, 2773, 1711 # Small primes for demo e = 3 # Same small exponent # Same message sent to all three message = 10 # Small for demonstration c1 = pow(message, e, n1) c2 = pow(message, e, n2) c3 = pow(message, e, n3) print(f"Message: {message}") print(f"c1 = {message}^3 mod {n1} = {c1}") print(f"c2 = {message}^3 mod {n2} = {c2}") print(f"c3 = {message}^3 mod {n3} = {c3}") # CRT: find x such that x ≡ c1 mod n1, x ≡ c2 mod n2, x ≡ c3 mod n3 # Then x = m^3 (exactly, since m^3 < n1*n2*n3) N = n1 * n2 * n3 N1, N2, N3 = N // n1, N // n2, N // n3 y1 = pow(N1, -1, n1) y2 = pow(N2, -1, n2) y3 = pow(N3, -1, n3) x = (c1 * N1 * y1 + c2 * N2 * y2 + c3 * N3 * y3) % N # x = m^3, so take cube root recovered = round(x ** (1/3)) print(f"CRT solution: {x}") print(f"Cube root: {recovered}") print(f"Attack successful: {recovered == message}") if __name__ == "__main__": demo_deterministic_attack() demo_malleability_attack() demo_small_message_attack() demo_hastad_broadcast_attack()The solution to textbook RSA's vulnerabilities is padding—transforming the message before encryption in a way that introduces randomness, structured redundancy, and resistance to manipulation. The modern standard for RSA encryption padding is OAEP (Optimal Asymmetric Encryption Padding).
OAEP: Designed for Provable Security
OAEP was designed by Bellare and Rogaway in 1994 specifically to provide IND-CCA2 security (indistinguishability under adaptive chosen-ciphertext attack) when combined with RSA. This is the gold standard for encryption security.
How OAEP Works:
Encode the message with structured padding:
Generate random seed:
Apply a Feistel-like structure:
Encrypt with RSA:
where MGF is a Mask Generation Function (typically MGF1 based on SHA-256).
Why OAEP Defeats the Attacks:
| Attack | Why OAEP Prevents It |
|---|---|
| Deterministic encryption | Random seed makes each encryption unique |
| Malleability | Structural redundancy (hash, 0x01 marker) detected after manipulation |
| Small message | Padding fills the block regardless of message size |
| Broadcast attack | Different random seed means different encodings |
| CCA2 attacks | Decryption fails unless padding structure is correct |
The 0x00 || maskedSeed || maskedDB Structure:
For 2048-bit RSA with SHA-256:
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183
"""RSA-OAEP Implementation (Educational) OAEP provides IND-CCA2 security when used with RSA.Always use a vetted library (like PyCryptodome) for production.""" import hashlibimport secrets def mgf1(seed: bytes, length: int, hash_func=hashlib.sha256) -> bytes: """ Mask Generation Function 1 (MGF1). Generates a mask of specified length from a seed using a hash function. Used in OAEP to create pseudorandom masks. """ hash_len = hash_func().digest_size mask = b'' counter = 0 while len(mask) < length: C = counter.to_bytes(4, byteorder='big') mask += hash_func(seed + C).digest() counter += 1 return mask[:length] def xor_bytes(a: bytes, b: bytes) -> bytes: """XOR two byte strings of equal length.""" return bytes(x ^ y for x, y in zip(a, b)) def oaep_encode(message: bytes, k: int, label: bytes = b"", hash_func=hashlib.sha256) -> bytes: """ OAEP encoding as per PKCS#1 v2.2. Args: message: Message to encode (mLen <= k - 2*hLen - 2) k: RSA modulus byte length (e.g., 256 for 2048-bit RSA) label: Optional label (default empty) hash_func: Hash function to use (default SHA-256) Returns: Encoded message EM of length k """ hLen = hash_func().digest_size # 32 for SHA-256 mLen = len(message) # Check message length if mLen > k - 2 * hLen - 2: raise ValueError(f"Message too long. Max: {k - 2*hLen - 2}, got: {mLen}") # Step 1: Construct data block DB = lHash || PS || 0x01 || M lHash = hash_func(label).digest() PS = b'\x00' * (k - mLen - 2 * hLen - 2) # Zero padding DB = lHash + PS + b'\x01' + message # Step 2: Generate random seed seed = secrets.token_bytes(hLen) # Step 3: Let dbMask = MGF(seed, k - hLen - 1) dbMask = mgf1(seed, k - hLen - 1, hash_func) # Step 4: Let maskedDB = DB ⊕ dbMask maskedDB = xor_bytes(DB, dbMask) # Step 5: Let seedMask = MGF(maskedDB, hLen) seedMask = mgf1(maskedDB, hLen, hash_func) # Step 6: Let maskedSeed = seed ⊕ seedMask maskedSeed = xor_bytes(seed, seedMask) # Step 7: EM = 0x00 || maskedSeed || maskedDB EM = b'\x00' + maskedSeed + maskedDB return EM def oaep_decode(EM: bytes, k: int, label: bytes = b"", hash_func=hashlib.sha256) -> bytes: """ OAEP decoding as per PKCS#1 v2.2. Returns the original message, or raises ValueError if decoding fails. SECURITY NOTE: In production, all checks must be done in constant time to prevent timing attacks. This implementation is for education only. """ hLen = hash_func().digest_size # Check lengths if len(EM) != k: raise ValueError("Incorrect encoded message length") if k < 2 * hLen + 2: raise ValueError("Modulus too short") # Split EM into components Y = EM[0:1] maskedSeed = EM[1:1+hLen] maskedDB = EM[1+hLen:] # Check Y == 0x00 if Y != b'\x00': raise ValueError("Decoding error: Y != 0x00") # Recover seed seedMask = mgf1(maskedDB, hLen, hash_func) seed = xor_bytes(maskedSeed, seedMask) # Recover DB dbMask = mgf1(seed, k - hLen - 1, hash_func) DB = xor_bytes(maskedDB, dbMask) # Parse DB = lHash' || PS || 0x01 || M lHash_prime = DB[:hLen] # Verify lHash lHash = hash_func(label).digest() if lHash_prime != lHash: raise ValueError("Decoding error: label hash mismatch") # Find 0x01 separator rest = DB[hLen:] separator_index = rest.find(b'\x01') if separator_index == -1: raise ValueError("Decoding error: separator not found") # Verify PS is all zeros PS = rest[:separator_index] if PS != b'\x00' * len(PS): raise ValueError("Decoding error: invalid padding") # Extract message message = rest[separator_index + 1:] return message def rsa_oaep_encrypt(message: bytes, e: int, n: int) -> int: """Complete RSA-OAEP encryption.""" k = (n.bit_length() + 7) // 8 # Modulus byte length EM = oaep_encode(message, k) m = int.from_bytes(EM, byteorder='big') c = pow(m, e, n) return c def rsa_oaep_decrypt(ciphertext: int, d: int, n: int) -> bytes: """Complete RSA-OAEP decryption.""" k = (n.bit_length() + 7) // 8 m = pow(ciphertext, d, n) EM = m.to_bytes(k, byteorder='big') message = oaep_decode(EM, k) return message # Demonstrationif __name__ == "__main__": # Simulated 2048-bit RSA key (in practice, use proper key generation) from demo_rsa_key import generate_rsa_keypair # For this demo, use smaller key p, q = 0xd4bcd52406f2264b4587e6f8b43fa66d, 0xc3e7f82a9314db1b6a77298a1552f7e1 n = p * q e = 65537 d = pow(e, -1, (p-1)*(q-1)) k = (n.bit_length() + 7) // 8 max_message_len = k - 2 * 32 - 2 # SHA-256 has 32-byte digest print(f"RSA modulus: {n.bit_length()} bits ({k} bytes)") print(f"Maximum message length with SHA-256: {max_message_len} bytes") print() # Encrypt a message message = b"Secret AES key!" print(f"Original message: {message}") ciphertext = rsa_oaep_encrypt(message, e, n) print(f"OAEP ciphertext: {ciphertext}") decrypted = rsa_oaep_decrypt(ciphertext, d, n) print(f"Decrypted message: {decrypted}") assert decrypted == message, "Decryption failed!" print("\n✓ RSA-OAEP encryption/decryption successful!")The OAEP code above is educational. Production implementations require: constant-time comparisons to prevent timing attacks, proper entropy sources, side-channel resistant operations, and compatibility testing. Use PyCryptodome, OpenSSL, or your platform's cryptographic library.
In the real world, RSA is almost never used to encrypt data directly. The message size limit (~190 bytes with OAEP) and computational cost (~1000x slower than AES) make direct RSA encryption impractical for arbitrary data.
Instead, modern systems use hybrid encryption:
This gives the best of both worlds:
This is exactly how HTTPS works:
During the TLS handshake:
(Modern TLS 1.3 prefers ECDHE key exchange, but the hybrid principle remains.)
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130
"""Hybrid Encryption: RSA + AES This is how real systems (TLS, S/MIME, PGP) actually use RSA.RSA encrypts a symmetric key; AES encrypts the data.""" import secretsimport hashlibfrom cryptography.hazmat.primitives.ciphers.aead import AESGCM # Assume we have RSA functions from previous examples# from rsa_oaep import rsa_oaep_encrypt, rsa_oaep_decrypt def hybrid_encrypt(plaintext: bytes, rsa_public_key: tuple) -> dict: """ Hybrid encryption using RSA + AES-GCM. Args: plaintext: Data to encrypt (any size) rsa_public_key: (e, n) tuple Returns: Dictionary with encrypted_key, nonce, and ciphertext """ e, n = rsa_public_key # Step 1: Generate random AES-256 key (32 bytes) aes_key = secrets.token_bytes(32) # Step 2: Encrypt data with AES-GCM nonce = secrets.token_bytes(12) # 96-bit nonce for GCM aesgcm = AESGCM(aes_key) ciphertext = aesgcm.encrypt(nonce, plaintext, None) # GCM appends authentication tag to ciphertext # Step 3: Encrypt AES key with RSA-OAEP encrypted_key = rsa_oaep_encrypt(aes_key, e, n) return { 'encrypted_key': encrypted_key, # RSA-encrypted AES key 'nonce': nonce, # AES-GCM nonce 'ciphertext': ciphertext, # AES-encrypted data + auth tag } def hybrid_decrypt(encrypted_data: dict, rsa_private_key: tuple) -> bytes: """ Hybrid decryption using RSA + AES-GCM. Args: encrypted_data: Dictionary from hybrid_encrypt rsa_private_key: (d, n) tuple Returns: Original plaintext """ d, n = rsa_private_key # Step 1: Decrypt AES key with RSA-OAEP aes_key = rsa_oaep_decrypt(encrypted_data['encrypted_key'], d, n) # Step 2: Decrypt data with AES-GCM aesgcm = AESGCM(aes_key) plaintext = aesgcm.decrypt( encrypted_data['nonce'], encrypted_data['ciphertext'], None ) return plaintext # Demonstrationdef demo_hybrid_encryption(): """ Demonstrate hybrid encryption with a 10MB file. RSA alone would require ~50,000 blocks and take minutes. Hybrid encryption takes milliseconds for any size data. """ import time # Generate RSA key pair (use proper generation in practice) p, q = generate_large_primes() # Assume this exists n = p * q e = 65537 d = pow(e, -1, (p-1)*(q-1)) public_key = (e, n) private_key = (d, n) # Generate a 10 MB random "file" large_data = secrets.token_bytes(10 * 1024 * 1024) # 10 MB print(f"Encrypting {len(large_data):,} bytes of data...") start = time.time() encrypted = hybrid_encrypt(large_data, public_key) encrypt_time = time.time() - start print(f"Encryption took {encrypt_time:.3f} seconds") print(f" RSA-encrypted key: {encrypted['encrypted_key']}") print(f" Nonce: {encrypted['nonce'].hex()}") print(f" Ciphertext length: {len(encrypted['ciphertext']):,} bytes") start = time.time() decrypted = hybrid_decrypt(encrypted, private_key) decrypt_time = time.time() - start print(f"Decryption took {decrypt_time:.3f} seconds") print(f"Decryption successful: {decrypted == large_data}") # Performance comparisondef compare_rsa_vs_hybrid(): """ Compare encrypting 1000 bytes with pure RSA vs hybrid. """ # RSA alone: Need 6 blocks of 190 bytes = 6 RSA operations # Each RSA encryption ~1ms = 6ms total # Hybrid: 1 RSA operation (32-byte key) + negligible AES time # Total: ~1ms print("\nPerformance comparison for 1KB data:") print(" Pure RSA (6 blocks): ~6ms") print(" Hybrid (RSA+AES): ~1ms") print(" Speedup: 6x") print("\nFor 10MB data:") print(" Pure RSA: ~50,000 blocks = ~50 seconds") print(" Hybrid: 1 RSA + ~10ms AES = ~11ms") print(" Speedup: ~4500x")| Protocol | RSA/Asymmetric Use | Symmetric Cipher | Notes |
|---|---|---|---|
| TLS 1.2 | RSA key transport or DHE/ECDHE | AES-GCM, AES-CBC, ChaCha20 | RSA key transport deprecated |
| TLS 1.3 | ECDHE only (RSA for signatures) | AES-GCM, ChaCha20-Poly1305 | No RSA key transport |
| S/MIME | RSA key encryption | AES-CBC, 3DES | Email encryption standard |
| PGP/GPG | RSA or ECDH key encryption | AES, Twofish, CAST5 | File/email encryption |
| SSH | RSA/ECDSA for auth, DH for key | AES-CTR, ChaCha20 | Secure shell protocol |
Even with correct padding, RSA implementations can be vulnerable to side-channel attacks—attacks that extract secrets from observable physical properties of the computation rather than cryptographic weaknesses.
Timing Attacks
Different private key bits can cause different execution times in modular exponentiation. An attacker measuring decryption time over many operations can statistically extract the entire private key.
Countermeasures:
Power Analysis
Different operations consume different amounts of power. Simple Power Analysis (SPA) can distinguish squaring from multiplication. Differential Power Analysis (DPA) uses statistical analysis of many traces.
Countermeasures:
Fault Attacks (Bellcore Attack)
If an attacker can induce computation errors (via voltage glitching, laser injection, etc.) during CRT-optimized RSA, a single faulty signature reveals the factorization of n.
Countermeasures:
Writing a secure RSA implementation from scratch is extremely difficult. Even experienced cryptographers discover new attacks regularly. The OpenSSL team has patched RSA-related vulnerabilities for decades. Use established libraries that receive ongoing security scrutiny and updates. Your job as a practitioner is to use these tools correctly, not to reinvent them.
We've covered the complete RSA encryption pipeline—from raw operations through secure padding to practical hybrid encryption. Understanding this pipeline is essential for anyone working with network security or cryptographic systems.
What's Next:
The final page of this module explores RSA Security Strength—analyzing what makes RSA secure, current and future threats (including quantum computing), recommended key sizes for different security levels, and the ongoing evolution of cryptographic standards.
You now understand the complete RSA encryption process: message encoding, the core encryption operation, why padding is essential, how OAEP provides provable security, how hybrid encryption works in practice, and the side-channel considerations for secure implementation. You're prepared to explore RSA's security strength and future challenges.