Skillversitas

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.