No description
  • Python 92.1%
  • C++ 7.9%
Find a file
Repository files (latest commit first)
Filename Latest commit message Latest commit date
Azer 4bf63df731 Fix the primality tests that called composites prime, and make RSA runnable
congruences.py reported 9, 15, 21, 25 and 49 as prime. The loop was

    for i in range(2, int(a**0.5)+1):
        if a % 2 == 0: return False

and the loop variable is never used - the body tests a % 2 on every pass - so
nothing but even numbers was ever rejected. The range also starts at 2, so it
is empty for 0, 1 and every negative, all of which came back prime too.

The same edge went wrong in the other three checkers: range(2, n) and
range(2, isqrt(n)+1) are empty below 2, so prime_check_native, prime_check_smart
and the sieve all called 1 prime. prime_check_smart did worse on negatives -
sqrt(-5) raises ValueError: math domain error rather than answering no - and
eratosthenes_sieve raised IndexError for a limit of 0, because `sieve[1] = 0`
ran before anything checked the length.

generate_keys.py could not run at all: it imported primeNum and cryptomath,
neither of which was in the repository, and carried its own
"# add primeNum implementation" note. Both modules are here now -
Miller-Rabin and prime generation in one, Euclid and the modular inverse in
the other - and the generator around them was corrected:

- Randomness comes from `secrets`. It used `random`, a Mersenne Twister whose
  future output follows from enough of its past, which for key material
  means the key does.
- p and q are checked to differ. Nothing checked before, and p == q gives
  n = p^2, whose factors fall out of a square root.
- The private exponent is no longer printed to standard output, where it
  went into terminal scrollback and any log that captured the run.
- The private key file is created with mode 0600 in a single open, rather
  than written and left at the default - and rather than chmod-ed afterwards,
  which leaves a window.
- keySize now means the size of the modulus. It asked for two 1024-bit
  primes, which is a 2048-bit modulus, and then wrote 1024 into the file as
  the size.
- e is 65537 rather than a random 1024-bit number, which is valid but makes
  every encryption a full-width exponentiation for nothing.
- The pair is checked to round-trip before any file is written, the files
  are opened with O_EXCL and closed by a context manager, and the name and
  key size are arguments instead of 'al_sweigart' and 1024 in the source.

fermat_primality_test.py raised ValueError for every n below 4 and for
negatives, because random.randint(2, n - 2) has an empty range there. Its
header comment described a different program ("find the smallest twin in
given range"). It now also demonstrates what the test is actually for: with
k=5 over 200 runs it calls the Carmichael numbers 561, 1105 and 1729 prime a
good fraction of the time, which is the reason primeNum.py uses Miller-Rabin.

modular_exponentiation.py had two problems. powerAndMod(7, 0, 7) answered 0
where 7^0 % 7 is 1 - the `if x == 0: return 0` shortcut ignores a zero
exponent - and for p == 1 it answered 1 where every result is 0. And
findPower called the modulus-free power() and reduced afterwards, so it built
a^i in full: searching n = 5000 constructed 3^4999, a 2386-digit integer, and
took 37 times longer than pow(a, i, n).

mod_finding.cpp was wrong twice over. int everywhere meant
123456789^13 mod 1000000007 came out as -405786624 instead of 354411480, and
`mod *= arr[i]` accumulated the whole product before taking the modulus once
at the end, which answered 0 for 65537^65536 mod 4294967291. `int s;` was
also read uninitialised whenever b was below 2. int64_t alone does not fix
it either - with a modulus near 2^32 the product of two values below it still
overflows - so the multiplication doubles instead, which needs no compiler
extension and stays correct up to a modulus of 2^62. It compiles clean under
-Wall -Wextra -Wpedantic -Werror and agrees with Python's pow() on every case
in the suite.

Added test_lesson.py: 32 tests, standard library only. Every primality
implementation is compared against a deliberately dull reference over
-20..2000, the modular arithmetic against pow(), the modular inverse against
its own definition, the key pair for a round trip and its file mode, the
interactive scripts through piped stdin, and mod_finding.cpp by compiling it
and comparing against pow(). Added a README, which there was none of, and a
CI workflow that runs all of it with warnings as errors plus a real 1024-bit
key generation.
2026-10-07 03:19:30 +04:00
.forgejo/workflows Fix the primality tests that called composites prime, and make RSA runnable 2026-10-07 03:19:30 +04:00
.gitignore Fix the primality tests that called composites prime, and make RSA runnable 2026-10-07 03:19:30 +04:00
congruences.py Fix the primality tests that called composites prime, and make RSA runnable 2026-10-07 03:19:30 +04:00
cryptomath.py Fix the primality tests that called composites prime, and make RSA runnable 2026-10-07 03:19:30 +04:00
eratosthenes_sieve.py Fix the primality tests that called composites prime, and make RSA runnable 2026-10-07 03:19:30 +04:00
fermat_primality_test.py Fix the primality tests that called composites prime, and make RSA runnable 2026-10-07 03:19:30 +04:00
generate_keys.py Fix the primality tests that called composites prime, and make RSA runnable 2026-10-07 03:19:30 +04:00
mod_finding.cpp Fix the primality tests that called composites prime, and make RSA runnable 2026-10-07 03:19:30 +04:00
modular_exponentiation.py Fix the primality tests that called composites prime, and make RSA runnable 2026-10-07 03:19:30 +04:00
Presentation.pptx ffsdf 2022-04-19 09:57:26 +04:00
Presnetation.pdf ffsdf 2022-04-19 09:57:26 +04:00
prime_check_native.py Fix the primality tests that called composites prime, and make RSA runnable 2026-10-07 03:19:30 +04:00
prime_check_smart.py Fix the primality tests that called composites prime, and make RSA runnable 2026-10-07 03:19:30 +04:00
primeNum.py Fix the primality tests that called composites prime, and make RSA runnable 2026-10-07 03:19:30 +04:00
README.md Fix the primality tests that called composites prime, and make RSA runnable 2026-10-07 03:19:30 +04:00
test_lesson.py Fix the primality tests that called composites prime, and make RSA runnable 2026-10-07 03:19:30 +04:00

Cryptography lesson

Worked examples for a cryptography lesson: primality testing, modular arithmetic and RSA key generation. Python 3.9 or newer, standard library only — nothing to install.

python -m unittest -v        # 32 tests over everything here

The slides are Presentation.pptx / Presnetation.pdf.

The files

File What it shows
prime_check_native.py Primality by trying every divisor up to n - 1.
prime_check_smart.py The same, stopping at sqrt(n) — a divisor above the root is always paired with one below it.
eratosthenes_sieve.py The sieve of Eratosthenes: every prime up to n at once.
fermat_primality_test.py Fermat's test, and where it fails.
congruences.py Whether two integers are congruent modulo a divisor.
modular_exponentiation.py Exponentiation by squaring, with and without a modulus, plus a brute-force discrete log.
mod_finding.cpp (a^b) % n in C++. Build with g++ -std=c++17 -O2 mod_finding.cpp -o mod_finding.
primeNum.py Miller-Rabin, and generating a prime of a given bit width.
cryptomath.py Euclid's algorithm, extended Euclid, modular inverse.
generate_keys.py RSA key generation on top of the two modules above.

primeNum.py and cryptomath.py are new. generate_keys.py imported both and neither existed here, so it could not be run at all.

Most files run on their own and print a short demonstration:

python primeNum.py
python cryptomath.py
python fermat_primality_test.py
python generate_keys.py mykey --key-size 2048

Two things worth taking away

Fermat's test has a blind spot; Miller-Rabin does not

A Carmichael number is composite but satisfies Fermat's congruence for every base coprime to it, so Fermat's test can only catch one by stumbling onto a base that shares a factor. python fermat_primality_test.py measures it: with k = 5 rounds over 200 runs, 561 (= 3·11·17), 1105 and 1729 are each called prime a good fraction of the time.

That is why primeNum.py uses Miller-Rabin to generate RSA primes. Every round rejects a composite with probability at least 3/4, independently, so 40 rounds leave a false positive far less likely than the hardware getting the arithmetic wrong.

Reducing late overflows

mod_finding.cpp used int and took the modulus once at the end. 123456789^13 mod 1000000007 came out as -405786624 instead of 354411480. Widening to int64_t is not enough either: with a modulus near 2^32 the product of two values below it still overflows, which is why the multiplication there doubles rather than multiplies.

Key generation

generate_keys.py writes <name>_pubkey.txt and <name>_privkey.txt, each holding keySize,modulus,exponent.

  • keySize is the size of the modulus, so p and q get half each. The previous version asked for two 1024-bit primes — a 2048-bit modulus — and then wrote 1024 into the file as the size.
  • The private key file is created with mode 0600, and the private exponent is not printed. It used to go to standard output, and from there into terminal scrollback and any log that captured it.
  • Randomness comes from secrets, not random. A Mersenne Twister's output can be reconstructed from enough of it, which for key material means the key can be.
  • p and q are checked to differ. p == q gives n = p², whose factors fall out of a square root.
  • e is 65537 unless that shares a factor with the totient. The previous version drew e at random from [2^(keySize-1), 2^keySize), which is valid but leaves every encryption doing a full-width exponentiation for nothing.
  • The pair is checked to round-trip before anything is written.

None of which makes this a production RSA implementation. It has no padding, so it is textbook RSA — deterministic, malleable, and not safe for real messages. Use your platform's library for that.