Understanding RSA (Draft)
RSA is one of the first public-key encryption algorithms ever used. For this reason, it serves well as a pedagogical example for readers curious about cryptography.
In this post, I’ll explore the mathematical foundations of the system, with a focus on number theory.
Although this article leans toward the mathematical side, it’s written to invite coders and curious readers to engage with the concepts.
A basic understanding of arithmetic and modular arithmetic will be helpful. I'll link to external resources where appropriate, rather than covering every concept in depth.
For a more technical and programmer-oriented point of view, there are other resources out there. Here I've linked some basic implementations in JavaScript.
In mathematics, it's often useful to begin with simple cases to build intuition. For example, prime numbers can be understood at varying levels of complexity.
I’ll start with the most familiar definition, using basic examples to illustrate key ideas.
Another valuable habit is to revisit concepts multiple times. The more deeply you internalize them, the more naturally you’ll be able to apply them later on.
Let’s begin by understanding the system.
RSA relies on a pair of keys: one public, which can be shared openly, and one private, which must be kept secret by its owner.
The standard example involves Bob and Alice exchanging messages.
Since they want to encrypt their communication using RSA, each of them has such a key pair.
Confident in the system, Alice wants to send a message to Bob. She uses Bob’s public key to encrypt the message.
At this point, Bob (and Bob only) can decrypt the message using his private key.
The security of this system relies on the fact that Bob's private key is known only to him.
Using this private key, Bob can decrypt messages that were encrypted with his corresponding public key through a fascinating mathematical process based on modular arithmetic.
Key Generation
Generating such public/private key pairs involves fundamental number theory concepts, particularly prime numbers.
You may recall that a prime number is a positive integer greater than 1, divisible only by 1 and itself.
and are prime numbers, while is not. It can be expressed as a product of primes: .
Prime numbers are the atoms of the integers. Every positive integer can be constructed as a product of primes (factorization).
or
This is formalized in a famous result known as the Fundamental Theorem of Arithmetic, or Unique Factorization Theorem (UFT).
Every positive integer can be uniquely factorized (up to ordering) into its prime factors.
Notice that 1 is not included in the theorem’s statement, as it can be seen as a product of no primes.
Another example of prime factorization:
Now let’s return to RSA and the key generation process.
Bob can generate a pair of keys using the following steps:
- Choose two primes: and
- Compute
- Compute
- Choose such that where is coprime to ()
- Find such that
- The public key is ; the private key consists of
Some of these steps rely on concepts we haven’t yet defined.
Let’s walk through them one by one.
Step 1: Choose two primes
Bob first chooses two distinct prime numbers: and .
These primes should be large enough (at least 20 digits) to ensure the system’s security.
It is notoriously difficult to factor the product of two large primes and this is the basis of RSA’s strength.
Step 2: Compute
This is straightforward.
Step 3: Compute
Here we introduce , the Euler's totient function.
Given a positive integer , the totient function counts the number of integers from to that are coprime to . By convention, .
because the integers , and are coprime to .
But what does “coprime” mean?
Two positive integers are coprime if they have no common prime factors other than 1.
In particular, if is a prime number, then . Why?
It also follows that when and are coprime (distinct primes are coprime) .
Note: computing from just would require factoring , which is copmputationlly hard (recall step 1) if you don't know and .
Step 4: Choose such that where is coprime to ()
Now Bob must choose an exponent .
It should satisfy:
- is coprime to or, algorithmically,
A common choice is (65,537); large enough for security, small enough for efficiency.
To check whether and are coprime, Bob can use the greatest common divisor (gcd).
The greatest common divisor of two integers is the largest positive integer that divides both numbers without leaving a remainder.
The gcd of and is , since it divides both without a remainder.
To compute the gcd, Bob can use the Euclidean algorithm, one of the oldest known algorithms (from Elements, c. 300 BC). The algorithm is based on the following principle:
If and for some and , then
It's extremely easy top write the algorithm in javascript:
- Let and be two positive integers (swap if )
- If , then
- Otherwise, replace with , and with
- Repeat until
is the reminder computed using javascript remainder operator %
.
The last non-zero remainder is the gcd.
function gcd(a, b) {
return b === 0 ? a : gcd(b, a % b);
}
Notice the nice recursion! Now, Bob can algorithmically determine whether two integers are coprime:
Two integers are coprime if their greatest common divisor (gcd) is 1.
The integers 8 and 15 are coprime because their gcd is 1.
Step 5: Find such that
The equation must be read as is congruent modulo .
We now delve into modular arithmetic, starting with its foundation: modular congruence.
Let and be integers, and let be a positive integer. We say that is congruent to modulo (written as ), if there exists an integer such that: .
Nothe: is the remainder when dividing by .
JavaScript doesn’t have a true mod
operator, only a remainder operator %
, which behaves slightly differently for negative numbers.
(See this StackOverflow post).
There is anyway a proposal.
Now, the equation
can be rewritten as:
That is: is the modular inverse of modulo .
Such a modular inverse exists only if and are coprime (and by step 4 they are).
To compute , Bob can use the Extended Euclidean Algorithm, a standard method for finding modular inverses.
For theoretical reasons, I'll introduce a more elegant method based on Euler's Theorem, which provides deeper insight into why RSA encryption\decription works so smootlhy.
Let be a positive integer. For any integer that is coprime to , we have:
Since Bob knows , he can use Euler's Theorem to compute :
This implies:
So Bob can compute:
to obtain the modular inverse.
is calculated through the fast exponentiation algorithm (also known as exponentiation by squaring or binary exponentiation)
Encryption/Decritpion
Encryption
Now Bob have already shared his public-key to Alice.
Alice will be able to send a message to Bob in the following way:
- She converts the message into a series of chuncks, each of wich is represented by an integer ranging from to
- To encrypt each chunk , she compute
Decritpion
Bob can decrypt each received chunk using the formula:
Remember that is the modular inverse () of . Thus , and the equation becomes:
By definition of modular congruence, can be written as for some :
Using the power properties:
We introduced Euler's Theorem for a reason. In fact, in number theory it's used every time when dealing with powers as it simplifies the problem significantly. Euler tells us that , thus:
Thus for Bob, recovering the message for each chunk will simply evaluate to:
Exercise: Rewrite the encryption and decryption process by yourself, focusing only on one chunk as explained.
Conclusion
I hope to have explained the theoretical part of RSA for generating keys and encryption/decryption. For users encountering this concept for the first time, it will certainly be overwhelming and that's normal. Hopefully, you can come back to this while learning the basic concepts.