RSA Encryption
RSA can encrypt directly: data encrypted with a public key can only be decrypted with the matching private key. That is how TLS bootstraps session secrets and how you can send a secret to someone whose public key you found on a website. But RSA encryption has real limits - small plaintexts, padding requirements, and speed - so in production it is usually used to encrypt a symmetric key, not the data itself.
Encrypting with OAEP Padding
Raw RSA (textbook RSA) is malleable and leaks information; the standard padding is OAEP (Optimal Asymmetric Encryption Padding), which randomizes each encryption:
import os
from cryptography.hazmat.primitives.asymmetric import rsa, padding
from cryptography.hazmat.primitives import hashes
public_key = ... # from the previous chapter
private_key = ...
message = b"session key material"
ciphertext = public_key.encrypt(
message,
padding.OAEP(
mgf=padding.MGF1(algorithm=hashes.SHA256()),
algorithm=hashes.SHA256(),
label=None,
),
)
print(len(ciphertext)) # 256 bytes for a 2048-bit key
plaintext = private_key.decrypt(ciphertext, padding.OAEP(
mgf=padding.MGF1(algorithm=hashes.SHA256()),
algorithm=hashes.SHA256(),
label=None,
))
print(plaintext) # b"session key material"
For a 2048-bit key, OAEP with SHA-256 accepts at most 190 bytes of plaintext (256-byte modulus minus padding and hash overhead). Exceeding the limit raises a ValueError - a sign that the data belongs in a hybrid scheme, not in the RSA call.
The Hybrid Pattern
import os
from cryptography.hazmat.primitives.ciphers.aead import AESGCM
def wrap_for(recipient_public, data: bytes):
aes_key = AESGCM.generate_key(bit_length=256)
wrapped = recipient_public.encrypt(aes_key, oaep_padding) # small
ct = AESGCM(aes_key).encrypt(os.urandom(12), data, None) # fast
return wrapped, ct # send both
def unwrap(private_key, wrapped, ct):
aes_key = private_key.decrypt(wrapped, oaep_padding)
nonce, payload = ct[:12], ct[12:]
return AESGCM(aes_key).decrypt(nonce, payload, None)
RSA carries the 32-byte AES key; AES-GCM carries the file, record, or message body. This is exactly the structure inside TLS, PGP, and S/MIME.
Pitfalls
- Key size vs plaintext: remember the overhead math; chunking raw RSA over a big buffer is a performance trap.
- Padding oracle: if your code distinguishes "bad padding" from "wrong key" to an attacker, PKCS#1 v1.5 becomes attackable (Bleichenbacher) - OAEP plus a uniform error is the fix.
- Speed: a 2048-bit RSA encryption is roughly a millisecond; thousands per second is the ceiling, so keep it at the bootstrap step.
- Wrong key pair: encrypting with the private key is not encryption - that operation is a signature (next chapter).
Summary
Encrypt small secrets with RSA-OAEP-SHA256, never bulk data. For anything larger, wrap a random AES key with RSA and encrypt the payload with AES-GCM - the hybrid pattern that makes RSA's slowness irrelevant and its key-exchange strength the foundation of the scheme.