This article has a more math-focused version with formal proofs on Folio.
In Part 1, we explored prime numbers — the raw material of RSA. Now we need the machinery that makes RSA work: modular arithmetic.
You already know modular arithmetic. Every time you look at a clock and say "3 hours after 11 is 2," you're doing it. The twist is that this simple idea — numbers that wrap around — turns out to be the foundation of modern cryptography.
The Clock Analogy
On a 12-hour clock, there's no "13 o'clock." After 12, you wrap back to 1. In math, we write:
This reads "13 is congruent to 1, modulo 12." It means 13 and 1 have the same remainder when divided by 12.
# Modular arithmetic in Python is just the % operator
print(13 % 12) # 1
print(27 % 12) # 3
print(48 % 12) # 0 (exactly on the 12)
More formally: means divides . That's it. That's the whole definition.
The beautiful thing is that modular arithmetic preserves addition and multiplication:
# These properties let us keep numbers small during computation
n = 17
a, b = 100, 200
# Direct computation
print((a * b) % n) # 13
# Reducing first, same result
print(((a % n) * (b % n)) % n) # 13
This property is critical for RSA. Without it, we'd need to compute numbers with thousands of digits before taking the modulus. Instead, we reduce at every step.
Fast Modular Exponentiation: The Speed Trick
RSA needs to compute things like where might be 65537 and might be a 2048-bit number. Computing directly? Your computer would run out of memory (and patience).
The trick is repeated squaring. Instead of 65537 multiplications, we need about
squarings.
def fast_mod_exp(base, exponent, mod):
"""Compute base^exponent mod mod using repeated squaring."""
result = 1
base = base % mod
while exponent > 0:
# If exponent is odd, multiply result with base
if exponent % 2 == 1:
result = (result * base) % mod
# Square the base, halve the exponent
exponent = exponent >> 1
base = (base * base) % mod
return result
# Python has this built in!
print(pow(7, 65537, 143)) # 7^65537 mod 143 = 123
print(fast_mod_exp(7, 65537, 143)) # Same: 123
The key insight: we never let any intermediate value exceed because we reduce modulo after every multiplication. This is what makes RSA practical.
One-Way Functions: Easy Forward, Hard Backward
Here's where cryptography enters the picture. Modular exponentiation is a one-way function:
- Forward (easy): Given , , and , compute . This takes milliseconds.
- Backward (hard): Given , , and , find . This is the discrete logarithm problem — no efficient algorithm is known.
# Forward: trivial
m, e, n = 42, 65537, 3233
c = pow(m, e, n)
print(f"Encrypted: {c}") # Encrypted: 2557
# Backward: you'd need to try all possible m values
# For real RSA with 2048-bit n, this is computationally infeasible
RSA's genius is that it finds a trapdoor — a secret piece of information that makes the backward direction easy too. That trapdoor is the modular inverse, and to understand it, we need the Euclidean algorithm.
The Euclidean Algorithm: 2300 Years Old and Still Useful
Euclid's algorithm finds the greatest common divisor (GCD) of two numbers. It's one of the oldest algorithms still in active use today.
The idea is simple:
, and
.
def gcd(a, b):
"""Euclid's algorithm — over 2300 years old."""
while b != 0:
a, b = b, a % b
return a
print(gcd(48, 18)) # 6
print(gcd(17, 13)) # 1 (coprime — important for RSA!)
print(gcd(65537, 3120)) # 1 (this is a real RSA check)
Two numbers with are called coprime. RSA requires that the public exponent is coprime with a certain special number. We'll get to that number shortly.
The Extended Euclidean Algorithm: Finding the Trapdoor
The extended version doesn't just find — it finds integers and such that:
This is called Bezout's identity, and it's the key to computing modular inverses.
def extended_gcd(a, b):
"""Returns (gcd, x, y) such that a*x + b*y = gcd(a, b)."""
if a == 0:
return b, 0, 1
gcd_val, x1, y1 = extended_gcd(b % a, a)
x = y1 - (b // a) * x1
y = x1
return gcd_val, x, y
g, x, y = extended_gcd(35, 15)
print(f"gcd={g}, 35*({x}) + 15*({y}) = {35*x + 15*y}")
# gcd=5, 35*(1) + 15*(-2) = 5
Modular Inverse: The RSA Private Key
The modular inverse of modulo is the number such that:
Think of it as "division in modular arithmetic." It exists if and only if .
Why does the extended Euclidean algorithm give us this? If , then we can find where . Taking both sides modulo , the term vanishes:
So
is the modular inverse of
.
def mod_inverse(a, n):
"""Find a^(-1) mod n using extended Euclidean algorithm."""
g, x, _ = extended_gcd(a % n, n)
if g != 1:
raise ValueError(f"No inverse: gcd({a}, {n}) = {g}")
return x % n
# Example: inverse of 7 mod 26
inv = mod_inverse(7, 26)
print(f"7^(-1) mod 26 = {inv}") # 15
print(f"Verify: 7 * {inv} mod 26 = {(7 * inv) % 26}") # 1
# Python 3.8+ has this built in:
print(pow(7, -1, 26)) # 15
This is exactly how RSA computes the private key. Given the public exponent , the private key is:
But what is ? For that, we need two more theorems.
Fermat's Little Theorem: The Warm-Up
Fermat discovered that for any prime and any not divisible by :
# Verify Fermat's little theorem
p = 17
for a in range(1, p):
assert pow(a, p - 1, p) == 1
print(f"Fermat's little theorem holds for all a in [1, {p-1}] mod {p}")
# Useful corollary: a^(-1) ≡ a^(p-2) (mod p)
a, p = 7, 17
print(f"Inverse via Fermat: {pow(a, p - 2, p)}") # 5
print(f"Inverse via ext GCD: {mod_inverse(a, p)}") # 5
This gives us a second way to compute modular inverses (when the modulus is prime), but more importantly, it's the stepping stone to Euler's theorem.
Euler's Theorem and the Totient Function
Euler generalized Fermat's result to any modulus, not just primes. First, he defined the totient function
: the count of integers from 1 to
that are coprime with
.
def euler_totient(n):
"""Compute phi(n) by counting coprimes (brute force)."""
from math import gcd
return sum(1 for k in range(1, n + 1) if gcd(k, n) == 1)
print(f"phi(12) = {euler_totient(12)}") # 4 (1, 5, 7, 11)
print(f"phi(7) = {euler_totient(7)}") # 6 (all of 1..6, since 7 is prime)
print(f"phi(15) = {euler_totient(15)}") # 8
Euler's theorem states: if , then:
Notice that when is prime, , and this reduces to Fermat's little theorem. Euler's theorem is the engine that makes RSA's encryption-decryption cycle work.
Why Is the Secret
Here's where everything clicks. RSA chooses for two large primes and . The totient has a beautiful property for products of distinct primes:
Why? Among the numbers , the ones that are not coprime to are exactly the multiples of (there are of them) and multiples of (there are of them), minus the multiples of both (just itself). So:
# Verify with a small example
p, q = 61, 53
n = p * q # 3233
phi_brute = euler_totient(n)
phi_formula = (p - 1) * (q - 1)
print(f"n = {n}")
print(f"phi(n) brute force = {phi_brute}") # 3120
print(f"(p-1)(q-1) = {phi_formula}") # 3120
assert phi_brute == phi_formula
This is the secret of RSA. Anyone can know (it's public), but computing requires knowing and — which requires factoring . And factoring large numbers is computationally infeasible.
Putting It All Together: The RSA Key Relationship
Now we can see the full picture. RSA picks primes , computes and , then chooses coprime to and computes:
This means , or equivalently for some integer . Now watch the magic of decryption:
The last step uses Euler's theorem. The message comes back.
# Complete RSA key generation using everything we've learned
p, q = 61, 53
n = p * q # 3233 (public)
phi_n = (p - 1) * (q - 1) # 3120 (secret!)
e = 17 # public exponent (coprime with phi_n)
assert gcd(e, phi_n) == 1 # Verify e is valid
d = mod_inverse(e, phi_n) # 2753 (private key!)
print(f"Public key: (n={n}, e={e})")
print(f"Private key: (n={n}, d={d})")
# Encrypt and decrypt
message = 42
encrypted = pow(message, e, n)
decrypted = pow(encrypted, d, n)
print(f"Message: {message} -> Encrypted: {encrypted} -> Decrypted: {decrypted}")
# Message: 42 -> Encrypted: 2557 -> Decrypted: 42
Recap: The Modular Arithmetic Toolkit for RSA
| Concept | Role in RSA |
|---|---|
| Modular arithmetic | Keeps numbers bounded, enables wrap-around |
| Fast exponentiation | Makes encryption/decryption practical |
| One-way functions | Security foundation — easy to encrypt, hard to reverse |
| Euclidean algorithm | Validates that is coprime with |
| Extended Euclidean | Computes the private key |
| Modular inverse | — the private key itself |
| Fermat's little theorem | Special case that builds intuition |
| Euler's theorem | Guarantees — why decryption works |
| Totient function | — the trapdoor secret |
Every piece of modular arithmetic we covered plays a specific, essential role. Remove any one, and RSA falls apart.
Up Next: Part 3 — Building RSA From Scratch
We now have all the mathematical tools. In Part 3, we'll implement RSA end-to-end in Python: key generation, encryption, decryption, and digital signatures. We'll also see where textbook RSA is dangerously insecure and what real implementations do differently.
Note: A version of this article with formal theorem/proof environments is available on Folio.
Top comments (0)