- Python 92.1%
- C++ 7.9%
| Filename | Latest commit message | Latest commit date |
|---|---|---|
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.
|
||
| .forgejo/workflows | ||
| .gitignore | ||
| congruences.py | ||
| cryptomath.py | ||
| eratosthenes_sieve.py | ||
| fermat_primality_test.py | ||
| generate_keys.py | ||
| mod_finding.cpp | ||
| modular_exponentiation.py | ||
| Presentation.pptx | ||
| Presnetation.pdf | ||
| prime_check_native.py | ||
| prime_check_smart.py | ||
| primeNum.py | ||
| README.md | ||
| test_lesson.py | ||
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.
keySizeis the size of the modulus, sopandqget half each. The previous version asked for two 1024-bit primes — a 2048-bit modulus — and then wrote1024into 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, notrandom. A Mersenne Twister's output can be reconstructed from enough of it, which for key material means the key can be. pandqare checked to differ.p == qgivesn = p², whose factors fall out of a square root.eis 65537 unless that shares a factor with the totient. The previous version dreweat 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.