Number-Theoretic Algorithms & RSA

By the end of this lesson you will be able to trace euclid, extendedEuclid (including its back-substitution of x and y), modularLinearEquationSolver, the Chinese remainder theorem, modularExponentiation (bit by bit), RSA key generation / encryption / decryption, pseudoprime, witness, millerRabin and pollardRho line-by-line with every number visible; read a modular clock, a CRT table and the group Zn* with the order of each element; implement each procedure in idiomatic Dart (and know exactly where 64-bit int overflows and BigInt is needed); and explain why fast gcd, fast powers and fast primality testing are what make public-key cryptography practical while factoring stays hard. Nothing is assumed beyond school arithmetic and the earlier lessons: What an algorithm is (and its cost), Insertion and merge sort (loop invariants) and Growth of functions (O-notation).

Divisibility, primes and the modular clock (Zn)

A number-theoretic algorithm is a procedure whose inputs are whole numbers (integers) and whose cleverness comes from properties of divisibility and remainders. Before any procedure, four words: divides, prime, gcd and modulus.

Think of a clock. On a 12-hour clock, 10 o'clock plus 5 hours is 3 o'clock, not 15: you only care about the remainder after dividing by 12. Number theory is arithmetic on clocks of any size n — and the clever part is that the clock face still lets you add, subtract and multiply (sometimes even divide) consistently.

Divisibility. We write d | a ("d divides a") when a = k·d for some integer k; for example 3 | 12 because 12 = 4·3. The greatest common divisor gcd(a, b) is the largest integer dividing both (gcd(12, 8) = 4). A number p > 1 is prime if its only positive divisors are 1 and p; otherwise it is composite (12 = 3·4). Two numbers are relatively prime (coprime) when gcd(a, b) = 1.

The remainder operator. a mod n is the remainder r in 0 ≤ r < n with a = ⌊a/n⌋·n + r (⌊x⌋ means "round down"). Two numbers are congruent modulo n, written a ≡ b (mod n), when they have the same remainder, that is when n | (a − b). The number n is called the modulus and the set of clock positions {0, 1, …, n−1} is written Zn.

Small example. On the 12-clock: 10 + 5 = 15 ≡ 3 and 7 · 8 = 56 ≡ 8 (56 = 4·12 + 8). Also 15 ≡ 27 ≡ 3 (mod 12) because 15, 27 and 3 all leave remainder 3. Try the player below: it walks around a clock in jumps of a, which is exactly the picture behind modularLinearEquationSolver later.

The same walk in Dart (the loop ends when the hand returns to the start, which happens after n / gcd(step, n) jumps):

What to notice. Jumping a hours at a time around an n-hour clock visits exactly n / gcd(a, n) different hours, and then repeats. Jump 5 on the 12-clock: gcd(5, 12) = 1, so all 12 hours are visited. Jump 4: gcd(4, 12) = 4, so only 3 hours (0, 4, 8). The animation's last frame states that count; it is also the "order" of a in the additive group Zn.

In Dart. % always returns a value in 0 ≤ r < n for n > 0 (so -7 % 5 == 3), whereas .remainder and ~/ truncate toward zero ((-7).remainder(5) == -2, -7 ~/ 5 == -1) — the maths "mod" of this lesson is Dart's %.

Overflow: the Dart code in this lesson uses 64-bit int (native VM/AOT; on the web ints are JS doubles and lose exactness above 253). Multiplying two numbers below n needs n² < 263, so the panels are exact for n below about 3·109. For bigger moduli the product d * d silently wraps around and the answer is garbage without any error; use BigInt (or the doubling mulMod of the question bank). The page's animations use JavaScript BigInt so any input you type is exact. Also, do not use .remainder where the maths means mod.
Costs in this chapter are counted in arithmetic operations on β-bit numbers: an addition or subtraction costs O(β), a multiplication or division O(β²) with grade-school methods. Every algorithm below is measured by how many such operations it needs — polynomial in β = lg n, not in n itself. That is what "efficient" means here: an algorithm that loops n times is exponential in β, because n = 2β.
Working "mod n" is clock arithmetic. a ≡ b (mod n) means n | (a − b). Everything in this chapter is a fast way of doing something with remainders; "fast" means polynomial in the number of digits of n, never in n itself.

The group Zn* and the order of an element

On a clock you can always add and undo the addition (subtract). Multiplication is trickier: on the 12-clock "times 4" is not undoable (4·0 = 4·3 = 4·6 = 4·9 = 0), like a blurry photo you cannot un-blur. But "times 5" IS undoable (multiply again by 5 and you are back, since 25 ≡ 1). The numbers that can be undone form a small private club, Zn*, and multiplying two members always gives another member.

Formally Zn* = {a in 0..n−1 : gcd(a, n) = 1}. It is closed under multiplication mod n, has identity 1, and every element has a multiplicative inverse (a number x with a·x ≡ 1 mod n), so it is a group of size φ(n) (Euler's totient); Z12* = {1, 5, 7, 11}, φ(12) = 4, and for a prime p, Zp* = {1, …, p−1} has φ(p) = p − 1 members. The order of a, ord(a), is the smallest t ≥ 1 with at ≡ 1 (mod n): the number of multiplications until you are back at 1. Lagrange: ord(a) always divides φ(n) — that is why Euler's theorem holds: aφ(n) ≡ 1 (mod n) (prime special case, Fermat: ap−1 ≡ 1 (mod p)). If one element has order exactly φ(n) it is a generator (primitive root) and the group is cyclic.

Small example. Z7*: powers of 3 are 3, 2, 6, 4, 5, 1 — six steps to reach 1, so ord(3) = 6 = φ(7), and 3 is a generator. Powers of 2 are 2, 4, 1: ord(2) = 3, which divides 6 ✓. In Z12* = {1, 5, 7, 11} every non-identity element has order 2 and none has order 4, so Z12* has no generator.

What to notice. Each frame lists the powers of one element until they return to 1, then colours the element by whether its order equals φ(n) (green = generator). Every order in the last frame divides φ(n).

Elements NOT coprime to n are absent from the group (they have no inverse), and their powers never come back to 1: the powers of 3 mod 12 are 3, 9, 3, 9, … Also the table below is for Zn* under multiplication — the ordinary "Zn under addition" of the clock above is a different group of size n.
Zn* is cyclic exactly when n = 1, 2, 4, pe or 2pe for an odd prime p (cyclic-group theorem). The Chinese remainder theorem below explains why Z12* ≅ Z4* × Z3* = {1,3} × {1,2}: a product of two cyclic groups that is not cyclic because gcd of their orders is 2.
Euler: aφ(n) ≡ 1 (mod n) for gcd(a, n) = 1. That single fact is why RSA decryption undoes encryption, why Fermat's primality test exists and why an inverse can be computed as aφ(n)−1.

Dart (helper used by the group player; the loop stops when the power returns to 1, which is guaranteed for gcd(a, n) = 1):

Zn* is a group of order φ(n), and every element order divides it (group laws and Lagrange's theorem), checked for every n ≤ 60:

The cyclic-group theorem (when is Zn* cyclic?) as two functions that must agree for every n ≤ 300:

euclid(a, b)

You have a 91 cm by 35 cm floor and want the biggest square tile that fits exactly with no cutting. Lay 35 cm squares along the floor: two fit and a 21 cm strip is left over. Now the tile must fit the 35 by 21 strip — lay 21 cm squares: a 14 cm strip is left. Lay 14 cm squares on the 21 by 14 strip: a 7 cm strip is left. Lay 7 cm squares on the 14 by 7 strip: nothing left over. The last non-empty strip size, 7, is the answer. euclid does exactly that, and a mod b is "the leftover strip".

The whole algorithm rests on the gcd recursion theorem: gcd(a, b) = gcd(b, a mod b). Every common divisor of a and b also divides the leftover a mod b, and vice versa, so replacing the pair by a smaller pair never changes the gcd — and when b reaches 0, gcd(a, 0) = a. Dart index note: nothing here is an array, so there is no index shift; the three lines of the panel map to one if and one recursive return.

Small example. gcd(12, 8): 12 mod 8 = 4, so gcd(12, 8) = gcd(8, 4); 8 mod 4 = 0, so = gcd(4, 0) = 4.

What to notice. The second argument shrinks every call (it becomes a mod b, which is always smaller than b) so the recursion must stop, and when b = 0 the first argument is the answer. When a < b (Example 2) the first call just swaps the numbers, since a mod b = a.

Do not stop at "a mod b = 0" and return b by hand, and do not forget the zero case: euclid(a, 0) = a and euclid(0, 0) = 0 by convention. Also do not try Euclid with .remainder on negative numbers — remainders must be non-negative for the strictly-shrinking argument.
Running time (Lamé). If a > b ≥ 1 and b < Fk+1 (a Fibonacci number), euclid(a, b) makes fewer than k recursive calls; the worst case is consecutive Fibonacci numbers, which shrink as slowly as possible (each step's remainder is just the previous term, every quotient is 1). So the number of calls is O(lg b), and on β-bit inputs euclid performs O(β) divisions. The table shows calls needed for euclid(Fk+1, Fk): exactly k − 1.

Correctness idea (loop-invariant style): the invariant "gcd of the current pair = gcd of the original pair" holds initially and is preserved by the gcd recursion theorem; the recursion terminates because b strictly decreases (a mod b < b).

Dart implementation:

Input size → what is feasible. a, b ≤ 1018 → at most ≈ 87 divisions (Lamé), instant; 106 such gcds ≈ 9·107 steps ≈ 1 s. Only the naive divisor scan (1018 steps) is hopeless.

Lamé’s theorem and the O(lg b) bound in code (Fibonacci list indexed by the subscript):

The GCD recursion theorem, Bézout and the common-divisor corollary checked against the definition of gcd:

Euler’s theorem, Fermat’s theorem and φ(n) = n·Π(1 − 1/p) as executable checks:

gcd(a, b) = gcd(b, a mod b), stop at b = 0. O(lg b) steps, worst on consecutive Fibonacci numbers. This is the engine inside every other procedure of the chapter.

extendedEuclid(a, b)

Knowing the tile size is nice, but suppose you want to build a length of exactly 1 cm out of a 77 cm rod and a 30 cm rod, where you may add and subtract copies of each. extendedEuclid returns the recipe: gcd(77, 30) = 1 = 77·(−7) + 30·18 — subtract seven copies of the 77 rod, add eighteen of the 30 rod.

Bézout's identity says gcd(a, b) is always an integer combination d = a·x + b·y. The procedure recurses exactly like euclid, then on the way back up converts the recipe for (b, a mod b) into a recipe for (a, b): since a mod b = a − ⌊a/b⌋·b, we get d = b·x′ + (a − ⌊a/b⌋b)·y′ = a·y′ + b·(x′ − ⌊a/b⌋·y′). Those are lines 4–5, and it is the back-substitution you may know from school: rewrite each remainder in terms of the two numbers before it. This is also how modular inverses are found: if gcd(a, n) = 1 then x (reduced mod n) is a⁻¹ mod n.

Small example. gcd(12, 8) = 4. Calls: (12, 8) → (8, 4) → (4, 0). Bottom: (4, 1, 0). Level (8, 4): ⌊8/4⌋ = 2, so (d, x, y) = (4, 0, 1 − 2·0) = (4, 0, 1), i.e. 4 = 8·0 + 4·1. Level (12, 8): ⌊12/8⌋ = 1 so (4, 1, 0 − 1·1) = (4, 1, −1): 4 = 12·1 + 8·(−1) ✓.

First the recursion itself, with the back-substitution column filled in as each call returns (the last column checks d = a·x + b·y at every level):

Now the same computation read the way it is done on paper: write the division equations rk−1 = qk·rk + rk+1 going down, then start from the last non-zero remainder (the gcd) and substitute upward, keeping the expression in terms of the two most recent remainders until only a and b are left. The coefficient update in each substitution is exactly lines 4–5 of the procedure.

What to notice. The two views give identical (x, y). In the paper view, the pair of coefficients (s, t) moves from (1, −q) to (t, s − q′·t) at each substitution — the same "swap and subtract q times" as lines 4–5.

Two classic mistakes: using .remainder/truncating division with negatives (Dart's ~/ equals ⌊a/b⌋ only for non-negative a, b), and forgetting that x can be negative — reduce it mod n (x % n) before calling it an inverse. The pair (x, y) is not unique: (x + k·b/d, y − k·a/d) also works; the procedure returns one particular small pair.
extendedEuclid makes exactly the same recursive calls as euclid, plus O(1) extra arithmetic per call, so it is also O(lg b) calls (O(β) operations). The returned x and y stay small: for a, b > 0 with d = gcd(a, b) it returns |x| ≤ b/d and |y| ≤ a/d (table below, checked in the verify program), so no big intermediate numbers appear. Correctness idea: the invariant d = a·x + b·y is checked in every row above; lines 4–5 preserve it algebraically as shown.

Dart implementation (returns a record; ~/ is integer division, which equals ⌊a/b⌋ for non-negative a, b) and an iterative form that never recurses (it keeps the two most recent coefficient pairs — the same back-substitution done on the way down):

Input size → what is feasible. a, b ≤ 1018 → ≤ 87 recursion levels, coefficients stay below b, so no overflow; recursion depth 87 is safe.

extendedEuclid = euclid + one line of algebra on the way back: (d, x, y) = (d′, y′, x′ − ⌊a/b⌋·y′). It gives Bézout coefficients, and when d = 1 it gives the modular inverse x mod n.

modularLinearEquationSolver(a, b, n)

A clock has n hours. You start at 0 and jump forward a hours at a time. Which numbers of jumps x land you exactly on hour b? You can only ever reach multiples of g = gcd(a, n) — so if b is not a multiple of g there is no answer; if it is, the answers repeat every n/g jumps, giving exactly g solutions. (The clock players in the modular-clock section showed this "only n/gcd hours are ever visited" picture.)

The equation a·x ≡ b (mod n) has solutions iff d = gcd(a, n) divides b, and then it has exactly d solutions modulo n: x₀, x₀ + n/d, x₀ + 2n/d, … The procedure gets d and the Bézout number x′ (with d = a·x′ + n·y′) from extendedEuclid, then x₀ = x′·(b/d) mod n is the first solution.

Small example. 3x ≡ 6 (mod 9): d = gcd(3, 9) = 3 divides 6, so there are 3 solutions. extendedEuclid(3, 9) = (3, 1, 0), so x₀ = 1·(6/3) mod 9 = 2, and the solutions are 2, 2 + 9/3 = 5, 8 (check: 3·8 = 24 = 2·9 + 6 ✓). When d = 1 there is a single solution, x = a⁻¹·b mod n.

What to notice. In the "a·x mod n" column every solution gives b; solutions are spaced n/d apart; in Example 2 the test "d | b" fails so nothing is printed.

Do not divide: "x = b / a" is meaningless mod n unless a has an inverse (d = 1), and even then the inverse is not the ordinary fraction. Also remember the equation can have several solutions (d of them) — returning just one loses the others.
Running time: O(lg n + gcd(a, n)) arithmetic operations — O(lg n) for extendedEuclid plus d for the printing loop. Correctness: a·x′ ≡ d (mod n), so a·(x′·b/d) ≡ b (mod n); the other d − 1 solutions are the shifts by n/d, and there are no more since the equation is equivalent to (a/d)·x ≡ (b/d) (mod n/d), which has a unique solution mod n/d.

Dart implementation (returns the list instead of printing; empty list means "no solutions"; the multiplication x1 * (b ~/ d) can overflow for n near 231 or more, in which case use BigInt):

Input size → what is feasible. n ≤ 2·109 → one extendedEuclid plus d ≤ n solutions to list; if d is huge (e.g. n = 109, a = 0) the output itself has d numbers.

Count of solutions: exactly d = gcd(a, n) solutions or none, compared with brute force for every n ≤ 40:

a·x ≡ b (mod n): compute d = gcd(a, n); no solution unless d | b; else exactly d solutions spaced n/d apart, the first from extendedEuclid.

The Chinese remainder theorem (the CRT table)

Sun Tsu (about 100 CE) asked: a number leaves remainder 2 when divided by 3, remainder 3 by 5, remainder 2 by 7 — what is it? Think of three clocks with 3, 5 and 7 hours all reading a single hidden number of ticks. If you know each clock's reading, and the sizes share no factors, the total tick count below 3·5·7 = 105 is uniquely determined. The answer is 23.

The Chinese remainder theorem. If n₁, …, nk are pairwise relatively prime and n = n₁n₂⋯nk, then the map a ↦ (a mod n₁, …, a mod nk) is a one-to-one correspondence (a bijection: every input has its own output and every output is hit) between Zn and Zn₁ × ⋯ × Znk. It can be proved constructively with mi = n/ni and ci = mi·(mi−1 mod ni): ci is ≡ 1 mod ni and ≡ 0 mod every other nj, so a = (Σ aici) mod n has the right remainder on every clock. The panel below writes the construction as steps, and the inverse in line 5 comes from extendedEuclid.

Small example. x ≡ 1 (mod 2) and x ≡ 2 (mod 3): n = 6, m₁ = 3, 3⁻¹ mod 2 = 1, c₁ = 3; m₂ = 2, 2⁻¹ mod 3 = 2, c₂ = 4; x = (1·3 + 2·4) mod 6 = 11 mod 6 = 5. Check: 5 mod 2 = 1, 5 mod 3 = 2 ✓.

The CRT table

Picture Zn ↔ Zn₁ × Zn₂ as a grid: row = a mod n₁, column = a mod n₂, and the cell holds the unique a in 0..n−1. Every cell is filled exactly once — that is the bijection. The next players draw the grid (for n ≤ 130), pick an a, show the cell (a mod n₁, a mod n₂) it lands in, then go backwards from the cell to a with the ci formula.

And the Z15 ↔ Z3 × Z5 table in full (computed by the page, not typed): each a appears once, and adding or multiplying a's mod 15 corresponds to adding or multiplying the residue pairs componentwise.

The construction itself, step by step, in the animation below:

What to notice. Each ci is 1 on its own clock and 0 on the others; the answer is a sum of "one clock at a time" contributions. Example 2 (moduli 4 and 6) shows the theorem refusing to apply: they share the factor 2.

Requiring only that the moduli are "different" or "odd" is wrong: they must be pairwise coprime (every pair, not just the whole set). With non-coprime moduli the system may have no solution or many (see the Hard question on merging), and ci cannot be built since mi has no inverse mod ni. Also do not form n = n₁⋯nk in 64-bit int for large moduli: the product can overflow — use BigInt.
Running time: k inverses, each O(lg ni) operations, plus O(k) multiplications mod n — polynomial in the bit-length of n. Why it matters: it lets you compute mod a big n by working mod its small coprime factors (RSA decryption speed-up in the question bank) and then recombining; it also explains why Zn* splits into smaller groups.

Dart implementation (the product big *= ni is where 64-bit overflow would occur if the moduli multiply past 263; the page's own inputs are far smaller):

Input size → what is feasible. moduli ≤ 109 with n = Πni ≤ 9·1018 → fits an int; beyond that use BigInt. Each modulus costs one extendedEuclid.

The CRT isomorphism Z105 ≅ Z3×Z5×Z7 (bijection, respects + and ·) and its failure for the non-coprime moduli 4 and 6:

With pairwise coprime moduli, a number below n = n₁⋯nk and its list of remainders determine each other. a = Σ aici mod n, where ci is 1 on clock i and 0 on the others.

modularExponentiation(a, b, n)

To compute a raised to the power 90 you do not multiply 90 times. Squaring doubles the exponent in one step: a, a², a⁴, a⁸, … Write the exponent in binary and every 1-bit says "also multiply the answer by a once". A seven-bit exponent (90 = 1011010 in binary) costs about seven squarings, not 90 multiplications — and reducing mod n after every step keeps every number smaller than n².

Reading the bits of b from the most significant, each iteration squares the running result d (doubling the exponent seen so far, tracked by c) and, if the bit is 1, multiplies once by a (adding 1 to the exponent). After all bits, d = ab mod n. The variable c is not needed to get the answer — it is kept only to state the loop invariant "d = ac mod n"; the Dart panel drops it. Binary numbers: b = 13 is 1101 because 13 = 8 + 4 + 0 + 1; the leftmost bit is the "most significant".

Small example. 313 mod 7, bits 1101: start d = 1. Bit 1: square (1), multiply by 3 → 3. Bit 1: square 9 mod 7 = 2, multiply by 3 → 6. Bit 0: square 36 mod 7 = 1, no multiply → 1. Bit 1: square 1, multiply by 3 → 3. Answer 3 (four squarings and three multiplies instead of twelve multiplications). The pictures below show the bits of b as boxes; the box being read is highlighted and the S / M strip records "square" and "multiply".

What to notice. Every bit causes exactly one square (S); only the 1-bits also cause a multiply (M) — so the S/M strip is literally the binary expansion of b read left to right. c follows the bits already read, and d always equals ac mod n.

Never compute pow(a, b) % n: the intermediate ab has about b·lg a bits and overflows (or takes forever) long before the mod. Reduce after every multiplication. Also note a zero exponent: the loop sees b = 0 as no bits in the Dart panel (bitLength is 0) and returns 1, and 1 mod n is 0 only if n = 1 — hence the n ≥ 2 guard in the animations.
Running time: with β = number of bits of b, the loop runs β times, each with one squaring and at most one extra multiplication: between β and 2β multiplications mod n, i.e. O(β) arithmetic operations, and O(β·lg²n) bit operations for β-bit n. Best case b = 2k (single 1-bit), worst case b = 2β − 1 (all ones). Loop invariant: before each iteration d = ac mod n, where c is the number formed by the bits already processed. Overflow: d·d is below n², which fits in a 64-bit int only when n < about 3.0·109; the safe Dart panels below use a doubling multiply or BigInt.

Dart implementation (bitLength gives k + 1, the number of bits; for b = 0 the loop simply does not run and d stays 1), and two overflow-safe variants — one for moduli up to 262 using the doubling mulMod from the question bank, and one with BigInt, which has no size limit (BigInt.modPow is the library's own modularExponentiation):

Input size → what is feasible. b ≤ 1018 → ≤ 120 multiplications; but every product must fit: n ≤ 3·109 with plain int, larger n needs mulMod or BigInt (question 20).

The multiplication count β + popcount(b) ≤ 2β, in code:

ab mod n in O(lg b) multiplications: square for every bit, multiply for every 1-bit, reduce mod n each time. It powers RSA, Fermat tests and Miller-Rabin.

The RSA public-key cryptosystem

Imagine a mailbox with a slot: anyone can drop a letter in (the public key), but only the owner holds the key that opens it (the secret key). RSA builds the slot from a fact about numbers: multiplying two big primes is easy, but recovering them from the product is (as far as anyone knows) hard.

Key generation: pick two distinct primes p and q; n = pq; φ(n) = (p−1)(q−1); pick a small odd e with gcd(e, φ(n)) = 1; compute d = e⁻¹ mod φ(n) with extendedEuclid. Public key P = (e, n), secret key S = (d, n). Encrypt message M < n: C = P(M) = Me mod n. Decrypt: S(C) = Cd mod n = Med mod n = M, because ed ≡ 1 (mod φ(n)) and Euler's theorem (the full correctness argument also covers a message M that shares a factor with n). All the heavy lifting is modularExponentiation. The first player below uses p = 17, q = 23, e = 3, d = 235.

Small example. p = 3, q = 11: n = 33, φ = 2·10 = 20, e = 3 (gcd(3, 20) = 1), d = 7 because 3·7 = 21 ≡ 1 (mod 20). Message M = 4: C = 4³ mod 33 = 64 mod 33 = 31. Decrypt: 317 mod 33 = 4 ✓. The players below repeat this with p = 17 and q = 23, an edge case, your own numbers, and finally a whole word one letter at a time.

Round trip on text: each letter A..Z becomes 1..26, is encrypted separately and decrypted back (the modulus must exceed 26). This is only to show the mechanism; separate deterministic letters would be trivially breakable by frequency counting in real life.

What to notice. Only two heavy operations exist: extendedEuclid once (to get d) and modularExponentiation twice per message. Ciphertext letters are unrelated-looking numbers; only d turns them back.

Numbers this small are a classroom toy: n = 3233 can be factored by hand. Real RSA uses 2048-bit or larger n and randomized padding (never raw Me): plain (unpadded) RSA is deterministic (same message, same ciphertext) and malleable. Never roll your own crypto. Also M must be less than n, and d must be reduced mod φ(n) into a non-negative number.
Cost: encryption is O(β) modular multiplications when e is small (e = 3, 17 or 65537 are traditional), decryption O(β) multiplications with a β-bit d; key generation needs random primes found with the primality test of the next section, plus one extendedEuclid. Overflow: with p, q around 231 the product n needs 62 bits and every d·d needs 124 bits, so real key sizes require BigInt; the RSA panel is exact only for n up to about 3·109, and a Dart Expert question uses BigInt with Mersenne primes.

Dart implementation:

Input size → what is feasible. toy primes below 5·104 fit int; real keys (2048-bit n) need BigInt (question 37).

RSA correctness for every message M in Zn, and why n must be squarefree:

RSA = (1) two primes → n and φ(n); (2) d = e⁻¹ mod φ(n) by extendedEuclid; (3) C = Me, M = Cd by modularExponentiation. Correct because ed ≡ 1 mod φ(n) plus Euler; secure only because factoring n is (believed) hard.

Primality testing: pseudoprime, witness and millerRabin

To check whether a stranger is honest you cannot interview them forever, but you can ask trick questions. A liar (composite number) fails a random trick question about half the time or more; an honest person (prime) never fails any. Ask enough questions and if nobody trips the answer, you are almost certainly dealing with an honest number.

Fermat's test says: if n is prime then an−1 ≡ 1 (mod n) for every a not divisible by n. Testing this with a = 2 gives the first procedure, pseudoprime. Its errors are called base-2 pseudoprimes: composite n with 2n−1 ≡ 1 (mod n). The converse fails badly for Carmichael numbers such as 561 = 3·11·17 that satisfy a560 ≡ 1 for every a coprime to 561 yet are composite (for example 2560 mod 561 = 1). witness repairs this with a second check: writing n − 1 = 2t·u with u odd, it computes au, then squares t times. If a squaring produces 1 from a number other than 1 or n − 1, that number is a nontrivial square root of 1, which cannot exist modulo a prime — so n is definitely composite. If the final value is not 1, Fermat's test failed — also definitely composite.

pseudoprime(n)

The procedure is three lines: if 2n−1 mod n ≠ 1 return COMPOSITE (definitely); else return PRIME ("we hope!"). Only one kind of error is possible: answering PRIME for a composite. Small example. n = 15: 214 mod 15 = 4 ≠ 1 → COMPOSITE, certain. n = 341 = 11·31: 2340 mod 341 = 1 → answers PRIME although 341 is composite (the smallest base-2 pseudoprime). The player shows the modular exponentiation of 2n−1 bit by bit.

What to notice. "COMPOSITE" always comes with a proof (the value 2n−1 mod n ≠ 1); "PRIME" is only a guess. The table below (computed by the page) counts the guesses that are wrong.

Passing pseudoprime is not a proof of primality. Trying more bases does not fix it, because Carmichael numbers (561, 1105, 1729, …) pass for every base coprime to n; only a base sharing a factor with n exposes them, and finding one is as hard as factoring.
Only 22 values below 10,000 make pseudoprime err (341, 561, 645, 1105, …; the page counts them itself), and the error probability for a random β-bit number tends to 0. But for adversarially chosen inputs a better test is needed — hence witness and millerRabin.

Dart implementation:

Input size → what is feasible. n < 3·109 with int arithmetic; one modular exponentiation ≈ 60 multiplications.

Prime number theorem π(n) ~ n/ln n and the density of primes among random odd numbers:

How often does pseudoprime lie? Counting base-2 pseudoprimes and Carmichael numbers (Korselt) below 106:

witness(a, n)

Small example. n = 13 gives n − 1 = 12 = 2²·3, so t = 2, u = 3. For a = 5: x₀ = 5³ mod 13 = 8, x₁ = 8² mod 13 = 12 (= n − 1), x₂ = 12² mod 13 = 1 — Fermat holds and the only "1 after a square" came from n − 1 = −1, a legal square root, so a is not a witness. Here is the sequence au, a2u, …, a2tu drawn as boxes:

What to notice. The boxes x₀ … xt are successive squarings. A witness answer true comes from either (1) a box equal to 1 right after a box that is neither 1 nor n − 1 (red pair: nontrivial square root of 1) or (2) a last box not equal to 1 (Fermat fails).

A value of 1 after squaring is only suspicious when the number squared was NOT 1 and NOT n − 1. Checking just "x² ≡ 1" would wrongly flag ±1 themselves. And a = 1 or a = n − 1 are never witnesses (all squarings give 1), so the random choice picks from 1..n−1 but only a in 2..n−2 can ever help.

Dart implementation:

Input size → what is feasible. n < 3·109 in plain int; use BigInt for 64-bit n. Cost ≈ log n multiplications.

Square roots of 1, the fact witness exploits:

millerRabin(n, s)

millerRabin tries s random bases. Witness-count theorem: if n is an odd composite, at least (n − 1)/2 of the possible values of a are witnesses, so each round misses with probability ≤ 1/2 and the answer "PRIME" is wrong with probability ≤ 2−s. "COMPOSITE" is always right. The page uses a small seeded pseudo-random generator, so a given (n, s, seed) always gives the same trace. Small example. n = 15 with a = 2: 14 = 2·7, x₀ = 2⁷ mod 15 = 8, x₁ = 64 mod 15 = 4 ≠ 1, so true at once; millerRabin(15, s) returns COMPOSITE in the first round.

What to notice. A composite is usually exposed in round 1 (see the witness counts below); primes are never exposed, so all s rounds run and the answer is PRIME.

How many witnesses does a composite really have? Counted by brute force by the page (bases 1..n−1):

"PRIME" from millerRabin is a probabilistic statement, and the error bound 2−s holds only when the bases are truly random and independent — a fixed base list (like 2, 3, 5, …) is deterministic and is exact only below a known bound (see the Hard question). Never test n = 2 or even n with this code: the procedure assumes n is odd and greater than 2.
Running time: s rounds, each dominated by one modularExponentiation: O(s·β) arithmetic operations, O(s·β³) bit operations for a β-bit n. Correctness: COMPOSITE is certain because witness returns true only with a proof (a failed Fermat test or a nontrivial square root of 1); PRIME is a probabilistic claim with error ≤ 2−s. In practice the real error is far smaller than the bound for random inputs.

Dart implementation (an exact all-witness check is used in the verify file; here rng supplies the randomness, and the x * x inside witness stays exact only while n < 3·109; the deterministic 64-bit variant in the question bank uses BigInt):

Input size → what is feasible. s = 30 rounds on a 64-bit n ≈ 30·64 ≈ 2000 multiplications; error ≤ 2−30 ≈ 10−9.

Error bound: error ≤ 2−s, from the exact witness counts of every odd composite below 2000, plus a seeded simulation:

pseudoprime: 2n−1 ≡ 1 — errs only on base-2 pseudoprimes. witness adds the square-root-of-1 check and beats Carmichael numbers. millerRabin: s random witness rounds; COMPOSITE is certain, PRIME is wrong with probability ≤ 2−s.

pollardRho factoring (the ρ shape)

Walk through a maze by a fixed pseudo-random rule: each cell tells you where to go next. Since the maze is finite, you must eventually step on a cell you were on before and go round in a loop — the path traces the Greek letter ρ (a tail, then a cycle). Now imagine two mazes at once: one modulo p and one modulo q, where n = pq. The p-maze is smaller, so its walk loops sooner, around √p steps. When it does, two of your x values agree mod p but not mod q, and gcd(difference, n) hands you p.

The sequence is xi = (xi−1² − 1) mod n. Rather than compare every pair, the procedure remembers only y = xk for k = 1, 2, 4, 8, … (lines 2, 12–13) and takes gcd(y − xi, n) each step (line 8). The loop is while true — it may print several factors and never halts on its own; the animations stop after the first printed factor, or after a step cap.

Small example. n = 91 = 7·13, x₁ = 2: the sequence is 2, 3, 8, 63, 55, 21, 76, 42, 34, 63, 55, 21, … — it comes back to 63 at x₁₀ = x₄: a tail 2, 3, 8 and a cycle 63, 55, 21, 76, 42, 34. Modulo 7 the same numbers are 2, 3, 1, 0, 6, 0, 6, … which loops much earlier (tail 2, 3, 1, then the cycle 0, 6), and at that moment the difference is a multiple of 7 while it is not a multiple of 13, so gcd = 7.

First the ρ shape itself: the sequence reduced mod the smallest prime factor p of n, laid out with the tail on a line and the cycle on a circle (nodes appear in order of i). The frame where a value repeats is the moment when pollardRho's gcd can reveal p:

Then the full procedure, with the checkpoint y doubling at i = 2, 4, 8, …:

What to notice. In the ρ picture the loop mod p closes after about √p steps; the checkpoint y at k = 2, 4, 8, … eventually sits inside the cycle so some xi ≡ y (mod p) is found while xi ≢ y (mod n/p) — then d = gcd(y − xi, n) is a proper divisor. For a prime n there is nothing to find.

pollardRho is a heuristic, not a guarantee: d = n can happen when the sequence closes modulo every prime factor at once (retry with a different x₁ or polynomial), and it cannot split a prime — test primality first (millerRabin). A while true loop never ends on a prime input; the code here needs a step cap. Squares of values near n also overflow 64-bit ints for n above about 3·109 (use the doubling mulMod or BigInt).
Running time: heuristically Θ(√p) steps to find the smallest prime factor p of n, hence O(n1/4) arithmetic operations — exponential in β = lg n, so it factors 30-digit numbers in seconds but not 600-digit RSA moduli; that gap is RSA's security. It can also fail (d = n, or a prime n) — then retry with a different x₁ or a different polynomial. It never prints an incorrect divisor. The table (computed by the page) compares steps to √p for several n = p·q.

Dart implementation (maxSteps bounds the otherwise infinite loop and returns null on failure):

Input size → what is feasible. n < 262 → ≈ n1/4 ≈ 4.6·104 iterations (heuristic); a prime n never prints anything, so test primality first.

The √p birthday bound for x ↦ x² − 1 mod p, and why other constants are worse:

pollardRho: iterate x → x² − 1 mod n, take gcd(y − xi, n) against a doubling checkpoint; a repeat mod p (after ≈ √p steps) reveals p. O(n1/4) heuristic, O(1) space, and never outputs a wrong factor.

Quiz

Interview questions

Cheat sheet

AlgorithmBestAverageWorstSpaceWhen to use / notes
euclid(a, b)1 call (b | a)O(β) divisionsO(β): Fibonacci pairs, k − 1 callsO(1) iterative, O(β) recursion depthgcd, lcm = a·b/gcd, coprimality test
extendedEuclid(a, b)1 callO(β)O(β)O(1) iterative, O(β) recursiveBézout d = ax + by; modular inverse when d = 1
modularLinearEquationSolverO(β) (no solution)O(lg n + d)O(lg n + gcd(a, n))O(d) to list solutionsa·x ≡ b (mod n): 0 or exactly d solutions
Chinese remainder theoremk inverses, O(k·β) (β = bits of n)O(k)needs pairwise coprime moduli; unique mod n₁⋯nk; speeds up RSA decryption
modularExponentiationβ mults (b = 2k)≈ 1.5 β2β mults (b all ones)O(1)never form ab first; reduce every step; BigInt if n > 3·109
RSA keygen / encrypt / decryptkeygen: primality tests + 1 extendedEuclid; encrypt O(β) mults (small e); decrypt O(β) multsO(1)toy sizes only here; real use needs padding (OAEP)
pseudoprime(n)1 modularExponentiation: O(β) multsO(1)COMPOSITE certain; PRIME wrong for base-2 pseudoprimes (341, 561, …); fine for random large numbers
witness(a, n)O(β) (exits at first square root of 1)O(β)O(β) (one exponentiation + t squarings)O(1)true = proof of compositeness
millerRabin(n, s)O(β) (witness in round 1)O(β) for composites, O(s·β) for primesO(s·β)O(1)COMPOSITE is certain; PRIME wrong with prob ≤ 2−s; default primality test
pollardRho(n)a few steps (lucky x₁)≈ √p for smallest prime p, O(n1/4)never halts on primes / can fail (d = n)O(1)heuristic; never prints a wrong factor; test primality first