Mathematical Techniques for Information Security: Cryptography with Python
A practical walkthrough of the number theory and algorithms that keep data safe — with working Python code for each technique.
1. Introduction
Cryptography is applied mathematics. Every secure channel, password hash, and digital signature you rely on is built on a handful of mathematical ideas — modular arithmetic, prime numbers, discrete logarithms, and one-way functions. This post walks through the core techniques used in modern information security, explains the math behind each one, and shows a working Python implementation you can run and experiment with.
We'll cover:
- Modular arithmetic and the Caesar cipher (the foundation)
- RSA — public-key encryption based on prime factorization
- Diffie–Hellman key exchange — based on discrete logarithms
- Cryptographic hash functions (SHA-256)
- AES — symmetric encryption used for bulk data
2. Modular Arithmetic: The Foundation
Almost every cryptographic algorithm relies on modular arithmetic — arithmetic that "wraps around" after reaching a fixed number, called the modulus. We write this as:
a ≡ b (mod n) means a and b leave the same remainder when divided by n
The simplest cipher built on this idea is the Caesar cipher, which shifts each letter by a fixed amount k:
E(x) = (x + k) mod 26 | D(x) = (x - k) mod 26
def caesar_encrypt(text, shift):
result = ""
for ch in text:
if ch.isalpha():
base = ord('A') if ch.isupper() else ord('a')
result += chr((ord(ch) - base + shift) % 26 + base)
else:
result += ch
return result
def caesar_decrypt(text, shift):
return caesar_encrypt(text, -shift)
message = "ATTACK AT DAWN"
encrypted = caesar_encrypt(message, 3)
print("Encrypted:", encrypted) # DWWDFN DW GDZQ
print("Decrypted:", caesar_decrypt(encrypted, 3))
This cipher is trivially breakable (only 25 possible shifts), but it introduces the core idea every modern scheme builds on: transforming data using a reversible mathematical function governed by a secret key.
3. RSA: Public-Key Cryptography
RSA is an asymmetric algorithm — it uses two different keys: a public key to encrypt, and a private key to decrypt. Its security rests on one fact: it is computationally easy to multiply two large primes, but extremely hard to factor their product back into the original primes.
The Math
- Choose two large primes
pandq. Computen = p × q. - Compute Euler's totient:
φ(n) = (p-1)(q-1). - Choose public exponent
esuch thatgcd(e, φ(n)) = 1. - Compute private exponent
d, the modular inverse ofe:d ≡ e⁻¹ (mod φ(n)). - Public key:
(n, e). Private key:(n, d). - Encryption:
c = mᵉ mod n. Decryption:m = c^d mod n.
from sympy import randprime, gcd, mod_inverse
def generate_keys(bits=16):
p = randprime(2**(bits-1), 2**bits)
q = randprime(2**(bits-1), 2**bits)
while q == p:
q = randprime(2**(bits-1), 2**bits)
n = p * q
phi = (p - 1) * (q - 1)
e = 65537
while gcd(e, phi) != 1:
e += 2
d = mod_inverse(e, phi)
return (n, e), (n, d) # public, private
def rsa_encrypt(message, public_key):
n, e = public_key
return [pow(ord(ch), e, n) for ch in message]
def rsa_decrypt(cipher, private_key):
n, d = private_key
return ''.join(chr(pow(c, d, n)) for c in cipher)
public_key, private_key = generate_keys(bits=32)
msg = "HELLO"
cipher = rsa_encrypt(msg, public_key)
print("Ciphertext:", cipher)
print("Decrypted:", rsa_decrypt(cipher, private_key))
Note: real-world RSA uses 2048+ bit primes and proper padding (OAEP). The code above is simplified for teaching the math — for production use, always use a vetted library like cryptography or PyCryptodome.
4. Diffie–Hellman Key Exchange
Diffie–Hellman lets two parties agree on a shared secret over an insecure channel, without ever transmitting the secret itself. It relies on the discrete logarithm problem: given g, p, and gË£ mod p, it's hard to recover x.
Alice picks secret a → sends A = gᵃ mod p
Bob picks secret b → sends B = gᵇ mod p
Shared secret: s = Bᵃ mod p = Aᵇ mod p = g^(ab) mod p
import random
# Publicly agreed values
p = 23 # small prime (use 2048-bit primes in real systems)
g = 5 # primitive root mod p
def generate_private_key():
return random.randint(2, p - 2)
def generate_public_key(private_key):
return pow(g, private_key, p)
def compute_shared_secret(their_public, my_private):
return pow(their_public, my_private, p)
alice_priv = generate_private_key()
bob_priv = generate_private_key()
alice_pub = generate_public_key(alice_priv)
bob_pub = generate_public_key(bob_priv)
alice_shared = compute_shared_secret(bob_pub, alice_priv)
bob_shared = compute_shared_secret(alice_pub, bob_priv)
print("Alice's shared secret:", alice_shared)
print("Bob's shared secret: ", bob_shared)
print("Match:", alice_shared == bob_shared)
5. Cryptographic Hash Functions
A hash function maps data of any size to a fixed-size digest. Good cryptographic hashes are one-way (can't reverse the digest to get the input) and collision-resistant (hard to find two inputs with the same digest). Hashes are used for password storage, integrity checks, and digital signatures.
import hashlib
def sha256_hash(data):
return hashlib.sha256(data.encode()).hexdigest()
password = "MySecretPassword123"
print("SHA-256:", sha256_hash(password))
# Demonstrating the avalanche effect: one character change
print("SHA-256:", sha256_hash("MySecretPassword124"))
Notice how a single-character change produces a completely different digest — this "avalanche effect" is a key design goal of secure hash functions.
For password storage specifically, never use a raw hash like SHA-256 alone — use a slow, salted algorithm designed for passwords, such as bcrypt, scrypt, or Argon2.
6. AES: Symmetric Encryption
Where RSA is slow and used mainly to exchange keys, AES (Advanced Encryption Standard) is fast and used to encrypt the actual bulk data. It operates on fixed-size blocks (128 bits) through multiple rounds of substitution, permutation, and mixing based on operations in a finite field, GF(2⁸).
# pip install pycryptodome
from Crypto.Cipher import AES
from Crypto.Util.Padding import pad, unpad
from Crypto.Random import get_random_bytes
def aes_encrypt(plaintext, key):
cipher = AES.new(key, AES.MODE_CBC)
ct_bytes = cipher.encrypt(pad(plaintext.encode(), AES.block_size))
return cipher.iv, ct_bytes
def aes_decrypt(iv, ct_bytes, key):
cipher = AES.new(key, AES.MODE_CBC, iv)
pt = unpad(cipher.decrypt(ct_bytes), AES.block_size)
return pt.decode()
key = get_random_bytes(32) # AES-256
iv, ciphertext = aes_encrypt("Top secret battle plans", key)
print("Ciphertext (hex):", ciphertext.hex())
decrypted = aes_decrypt(iv, ciphertext, key)
print("Decrypted:", decrypted)
In practice, HTTPS and most secure messaging apps use a hybrid approach: Diffie–Hellman or RSA to establish a shared key, then AES to encrypt the actual traffic — combining the key-exchange strength of asymmetric math with the speed of symmetric ciphers.
7. Summary Table
| Technique | Math Basis | Used For |
|---|---|---|
| Caesar Cipher | Modular addition | Teaching only |
| RSA | Prime factorization, modular exponentiation | Key exchange, digital signatures |
| Diffie–Hellman | Discrete logarithm problem | Establishing shared secrets |
| SHA-256 | One-way compression functions | Integrity checks, signatures |
| AES | Finite field (GF(2⁸)) operations | Bulk data encryption |
8. Closing Thoughts
Every layer of modern digital security — from the padlock icon in your browser to encrypted messaging apps — is built from these same mathematical primitives: modular arithmetic, prime numbers, discrete logarithms, and one-way functions. Understanding the math doesn't just satisfy curiosity — it's what lets you evaluate whether a security claim actually holds up, and avoid classic mistakes like rolling your own crypto.
Disclaimer: The code in this post is written for educational purposes to illustrate the underlying mathematics. For production systems, always use audited, well-maintained libraries (e.g. cryptography, PyCryptodome) rather than custom implementations.

0 Comments