Slide 1:
Introduction to Cryptography with Python
Cryptography is the practice of secure communication in the presence of adversaries. Python provides powerful libraries for implementing cryptographic algorithms. In this presentation, we'll explore various cryptographic concepts and their implementation using Python.
from cryptography.fernet import Fernet
# Generate a random key
key = Fernet.generate_key()
# Create a Fernet instance
f = Fernet(key)
# Encrypt a message
message = b"Hello, Cryptography!"
encrypted = f.encrypt(message)
# Decrypt the message
decrypted = f.decrypt(encrypted)
print(f"Original: {message}")
print(f"Encrypted: {encrypted}")
print(f"Decrypted: {decrypted}")Slide 2:
Symmetric Encryption: AES
Advanced Encryption Standard (AES) is a widely used symmetric encryption algorithm. It uses the same key for both encryption and decryption. Let's implement AES encryption using Python's cryptography library.
from cryptography.hazmat.primitives.ciphers import Cipher, algorithms, modes
from cryptography.hazmat.backends import default_backend
import os
def encrypt_aes(plaintext, key):
iv = os.urandom(16)
cipher = Cipher(algorithms.AES(key), modes.CFB(iv), backend=default_backend())
encryptor = cipher.encryptor()
ciphertext = encryptor.update(plaintext) + encryptor.finalize()
return iv + ciphertext
def decrypt_aes(ciphertext, key):
iv = ciphertext[:16]
cipher = Cipher(algorithms.AES(key), modes.CFB(iv), backend=default_backend())
decryptor = cipher.decryptor()
return decryptor.update(ciphertext[16:]) + decryptor.finalize()
# Example usage
key = os.urandom(32) # 256-bit key
plaintext = b"AES encryption in Python"
encrypted = encrypt_aes(plaintext, key)
decrypted = decrypt_aes(encrypted, key)
print(f"Original: {plaintext}")
print(f"Encrypted: {encrypted}")
print(f"Decrypted: {decrypted}")Slide 3:
Asymmetric Encryption: RSA
RSA is a popular asymmetric encryption algorithm that uses a pair of keys: public key for encryption and private key for decryption. Let's implement RSA encryption using Python's cryptography library.
from cryptography.hazmat.primitives.asymmetric import rsa, padding
from cryptography.hazmat.primitives import hashes
def generate_rsa_keys():
private_key = rsa.generate_private_key(
public_exponent=65537,
key_size=2048
)
public_key = private_key.public_key()
return private_key, public_key
def encrypt_rsa(message, public_key):
ciphertext = public_key.encrypt(
message,
padding.OAEP(
mgf=padding.MGF1(algorithm=hashes.SHA256()),
algorithm=hashes.SHA256(),
label=None
)
)
return ciphertext
def decrypt_rsa(ciphertext, private_key):
plaintext = private_key.decrypt(
ciphertext,
padding.OAEP(
mgf=padding.MGF1(algorithm=hashes.SHA256()),
algorithm=hashes.SHA256(),
label=None
)
)
return plaintext
# Example usage
private_key, public_key = generate_rsa_keys()
message = b"RSA encryption in Python"
encrypted = encrypt_rsa(message, public_key)
decrypted = decrypt_rsa(encrypted, private_key)
print(f"Original: {message}")
print(f"Encrypted: {encrypted}")
print(f"Decrypted: {decrypted}")Slide 4:
Hashing: SHA-256
Hashing is a one-way function that converts data of arbitrary size into a fixed-size output. SHA-256 is a widely used cryptographic hash function. Let's implement SHA-256 hashing using Python's hashlib library.
import hashlib
def sha256_hash(data):
sha256 = hashlib.sha256()
sha256.update(data.encode())
return sha256.hexdigest()
# Example usage
message = "Hello, SHA-256!"
hashed = sha256_hash(message)
print(f"Original message: {message}")
print(f"SHA-256 hash: {hashed}")
# Verify that the same input always produces the same hash
print(f"Hash of 'Hello, SHA-256!': {sha256_hash('Hello, SHA-256!')}")
print(f"Hash of 'Different message': {sha256_hash('Different message')}")Slide 5:
Digital Signatures
Digital signatures provide authenticity, integrity, and non-repudiation. They are created using the sender's private key and verified using the sender's public key. Let's implement digital signatures using RSA.
from cryptography.hazmat.primitives import hashes
from cryptography.hazmat.primitives.asymmetric import padding, rsa
def generate_keys():
private_key = rsa.generate_private_key(
public_exponent=65537,
key_size=2048
)
public_key = private_key.public_key()
return private_key, public_key
def sign_message(message, private_key):
signature = private_key.sign(
message,
padding.PSS(
mgf=padding.MGF1(hashes.SHA256()),
salt_length=padding.PSS.MAX_LENGTH
),
hashes.SHA256()
)
return signature
def verify_signature(message, signature, public_key):
try:
public_key.verify(
signature,
message,
padding.PSS(
mgf=padding.MGF1(hashes.SHA256()),
salt_length=padding.PSS.MAX_LENGTH
),
hashes.SHA256()
)
return True
except:
return False
# Example usage
private_key, public_key = generate_keys()
message = b"This is a signed message."
signature = sign_message(message, private_key)
is_valid = verify_signature(message, signature, public_key)
print(f"Message: {message}")
print(f"Signature: {signature}")
print(f"Signature valid: {is_valid}")
# Try to verify with a tampered message
tampered_message = b"This is a tampered message."
is_valid_tampered = verify_signature(tampered_message, signature, public_key)
print(f"Tampered message signature valid: {is_valid_tampered}")Slide 6:
Key Derivation Functions (KDF)
Key Derivation Functions are used to derive one or more secret keys from a master secret. They are often used to convert passwords into cryptographic keys. Let's implement PBKDF2 (Password-Based Key Derivation Function 2) using Python.
import os
from cryptography.hazmat.primitives import hashes
from cryptography.hazmat.primitives.kdf.pbkdf2 import PBKDF2HMAC
def derive_key(password, salt=None, iterations=100000):
if salt is None:
salt = os.urandom(16)
kdf = PBKDF2HMAC(
algorithm=hashes.SHA256(),
length=32,
salt=salt,
iterations=iterations,
)
key = kdf.derive(password.encode())
return key, salt
# Example usage
password = "mysecretpassword"
derived_key, salt = derive_key(password)
print(f"Password: {password}")
print(f"Derived key: {derived_key.hex()}")
print(f"Salt: {salt.hex()}")
# Verify that the same password and salt produce the same key
verified_key, _ = derive_key(password, salt)
print(f"Key verification: {derived_key == verified_key}")
# Different password produces a different key
different_key, _ = derive_key("differentpassword", salt)
print(f"Different password key: {different_key.hex()}")
print(f"Keys match: {derived_key == different_key}")Slide 7:
Secure Random Number Generation
Cryptographically secure random number generation is crucial for many cryptographic operations, such as generating keys and initialization vectors. Python's secrets module provides functions for generating secure random numbers.
import secrets
def generate_random_bytes(n):
return secrets.token_bytes(n)
def generate_random_integer(a, b):
return secrets.randbelow(b - a + 1) + a
def generate_random_string(length):
return secrets.token_urlsafe(length)
# Example usage
random_bytes = generate_random_bytes(16)
random_int = generate_random_integer(1, 100)
random_string = generate_random_string(12)
print(f"Random bytes: {random_bytes.hex()}")
print(f"Random integer between 1 and 100: {random_int}")
print(f"Random URL-safe string: {random_string}")
# Demonstrate uniqueness
print("\nGenerating multiple random values:")
for _ in range(5):
print(f"Random bytes: {generate_random_bytes(8).hex()}")
print(f"Random integer: {generate_random_integer(1, 10)}")
print(f"Random string: {generate_random_string(8)}")
print()Slide 8:
Message Authentication Codes (MAC)
Message Authentication Codes provide integrity and authenticity of messages. HMAC (Hash-based Message Authentication Code) is a widely used MAC algorithm. Let's implement HMAC using Python's hmac module.
import hmac
import hashlib
def create_hmac(key, message):
h = hmac.new(key.encode(), message.encode(), hashlib.sha256)
return h.hexdigest()
def verify_hmac(key, message, received_hmac):
calculated_hmac = create_hmac(key, message)
return hmac.compare_digest(calculated_hmac, received_hmac)
# Example usage
secret_key = "mysecretkey"
message = "Hello, HMAC!"
# Create HMAC
hmac_result = create_hmac(secret_key, message)
print(f"Message: {message}")
print(f"HMAC: {hmac_result}")
# Verify HMAC
is_valid = verify_hmac(secret_key, message, hmac_result)
print(f"HMAC is valid: {is_valid}")
# Try to verify with a tampered message
tampered_message = "Hello, tampered HMAC!"
is_valid_tampered = verify_hmac(secret_key, tampered_message, hmac_result)
print(f"Tampered message HMAC is valid: {is_valid_tampered}")Slide 9:
Password Hashing with Salting
Proper password storage is crucial for application security. We'll use the bcrypt algorithm, which automatically handles salting and is designed to be slow and resist brute-force attacks. Let's implement password hashing and verification using the bcrypt library.
import bcrypt
def hash_password(password):
salt = bcrypt.gensalt()
hashed = bcrypt.hashpw(password.encode(), salt)
return hashed
def verify_password(password, hashed):
return bcrypt.checkpw(password.encode(), hashed)
# Example usage
password = "mysecretpassword"
# Hash the password
hashed_password = hash_password(password)
print(f"Original password: {password}")
print(f"Hashed password: {hashed_password}")
# Verify the password
is_correct = verify_password(password, hashed_password)
print(f"Password is correct: {is_correct}")
# Try an incorrect password
wrong_password = "wrongpassword"
is_wrong = verify_password(wrong_password, hashed_password)
print(f"Wrong password is correct: {is_wrong}")
# Demonstrate that hashing the same password twice produces different results
hashed_password2 = hash_password(password)
print(f"\nHashing the same password again: {hashed_password2}")
print(f"Hashes are identical: {hashed_password == hashed_password2}")
print(f"But both verify correctly: {verify_password(password, hashed_password2)}")Slide 10:
Elliptic Curve Cryptography (ECC)
Elliptic Curve Cryptography is an approach to public-key cryptography based on the algebraic structure of elliptic curves over finite fields. It offers smaller key sizes compared to RSA for equivalent security. Let's implement ECC key generation and ECDH (Elliptic Curve Diffie-Hellman) key exchange using Python.
from cryptography.hazmat.primitives.asymmetric import ec
from cryptography.hazmat.primitives import serialization
def generate_ecc_key_pair():
private_key = ec.generate_private_key(ec.SECP256R1())
public_key = private_key.public_key()
return private_key, public_key
def perform_ecdh(private_key, peer_public_key):
shared_key = private_key.exchange(ec.ECDH(), peer_public_key)
return shared_key
# Example usage
alice_private, alice_public = generate_ecc_key_pair()
bob_private, bob_public = generate_ecc_key_pair()
# Perform ECDH key exchange
alice_shared_key = perform_ecdh(alice_private, bob_public)
bob_shared_key = perform_ecdh(bob_private, alice_public)
print("Alice's public key:")
print(alice_public.public_bytes(
encoding=serialization.Encoding.PEM,
format=serialization.PublicFormat.SubjectPublicKeyInfo
).decode())
print("\nBob's public key:")
print(bob_public.public_bytes(
encoding=serialization.Encoding.PEM,
format=serialization.PublicFormat.SubjectPublicKeyInfo
).decode())
print(f"\nAlice's shared key: {alice_shared_key.hex()}")
print(f"Bob's shared key: {bob_shared_key.hex()}")
print(f"Shared keys match: {alice_shared_key == bob_shared_key}")Slide 11:
Authenticated Encryption
Authenticated Encryption provides both confidentiality and integrity/authenticity of data. We'll use the ChaCha20-Poly1305 algorithm, which is a modern authenticated encryption algorithm. Let's implement encryption and decryption using ChaCha20-Poly1305 in Python.
from cryptography.hazmat.primitives.ciphers.aead import ChaCha20Poly1305
import os
def encrypt_chacha20_poly1305(key, plaintext, associated_data):
nonce = os.urandom(12)
chacha = ChaCha20Poly1305(key)
ciphertext = chacha.encrypt(nonce, plaintext, associated_data)
return nonce + ciphertext
def decrypt_chacha20_poly1305(key, ciphertext, associated_data):
nonce = ciphertext[:12]
ciphertext = ciphertext[12:]
chacha = ChaCha20Poly1305(key)
plaintext = chacha.decrypt(nonce, ciphertext, associated_data)
return plaintext
# Example usage
key = os.urandom(32) # ChaCha20-Poly1305 uses a 256-bit key
plaintext = b"Secret message"
associated_data = b"Additional data"
encrypted = encrypt_chacha20_poly1305(key, plaintext, associated_data)
decrypted = decrypt_chacha20_poly1305(key, encrypted, associated_data)
print(f"Original: {plaintext}")
print(f"Encrypted: {encrypted.hex()}")
print(f"Decrypted: {decrypted}")
# Attempt to decrypt with modified associated data
try:
decrypt_chacha20_poly1305(key, encrypted, b"Modified data")
print("Decryption succeeded (shouldn't happen)")
except Exception as e:
print(f"Decryption failed as expected: {e}")Slide 12:
Zero-Knowledge Proofs: Schnorr Protocol
Zero-knowledge proofs allow one party (the prover) to prove to another party (the verifier) that a statement is true, without revealing any information beyond the validity of the statement. The Schnorr protocol is a simple zero-knowledge proof for proving knowledge of a discrete logarithm.
import random
from cryptography.hazmat.primitives import hashes
from cryptography.hazmat.primitives.asymmetric import ec
def generate_keypair():
private_key = ec.generate_private_key(ec.SECP256R1())
public_key = private_key.public_key()
return private_key, public_key
def schnorr_prove(private_key, public_key):
curve = public_key.curve
G = public_key.public_numbers().y
q = curve.order
# Prover generates a random value
r = random.randrange(1, q)
R = r * G
# Prover computes the challenge
h = hashes.Hash(hashes.SHA256())
h.update(str(R.x).encode() + str(R.y).encode())
e = int.from_bytes(h.finalize(), 'big') % q
# Prover computes the response
s = (r + e * private_key.private_numbers().private_value) % q
return (R, s)
def schnorr_verify(public_key, proof):
R, s = proof
curve = public_key.curve
G = public_key.public_numbers().y
q = curve.order
# Verifier computes the challenge
h = hashes.Hash(hashes.SHA256())
h.update(str(R.x).encode() + str(R.y).encode())
e = int.from_bytes(h.finalize(), 'big') % q
# Verifier checks if sG = R + eY
sG = s * G
eY = e * public_key.public_numbers().y
RY = R + eY
return sG == RY
# Example usage
private_key, public_key = generate_keypair()
# Prover generates a proof
proof = schnorr_prove(private_key, public_key)
# Verifier checks the proof
is_valid = schnorr_verify(public_key, proof)
print(f"Proof is valid: {is_valid}")
# Try to verify with a different public key
_, fake_public_key = generate_keypair()
is_valid_fake = schnorr_verify(fake_public_key, proof)
print(f"Proof is valid with fake key: {is_valid_fake}")Slide 13:
Homomorphic Encryption: Paillier Cryptosystem
Homomorphic encryption allows computations on encrypted data without decrypting it. The Paillier cryptosystem is a partially homomorphic encryption scheme that supports addition of encrypted values. Let's implement a simple version of the Paillier cryptosystem.
import random
from math import gcd
def generate_paillier_keypair(bits):
p = generate_prime(bits // 2)
q = generate_prime(bits // 2)
n = p * q
g = n + 1
lambda_n = (p - 1) * (q - 1) // gcd(p - 1, q - 1)
mu = mod_inverse(lambda_n, n)
return ((n, g), (lambda_n, mu))
def paillier_encrypt(public_key, m):
n, g = public_key
r = random.randrange(1, n)
c = (pow(g, m, n**2) * pow(r, n, n**2)) % (n**2)
return c
def paillier_decrypt(private_key, c):
lambda_n, mu = private_key
n = (lambda_n * mu) // gcd(lambda_n, mu) + 1
x = (pow(c, lambda_n, n**2) - 1) // n
m = (x * mu) % n
return m
def paillier_add(public_key, c1, c2):
n, _ = public_key
return (c1 * c2) % (n**2)
# Helper functions
def generate_prime(bits):
while True:
n = random.getrandbits(bits)
if n % 2 != 0 and is_prime(n):
return n
def is_prime(n, k=5):
if n < 2: return False
for p in [2, 3, 5, 7, 11, 13, 17, 19, 23, 29]:
if n % p == 0: return n == p
s, d = 0, n - 1
while d % 2 == 0:
s, d = s + 1, d // 2
for _ in range(k):
a = random.randrange(2, n - 1)
x = pow(a, d, n)
if x != 1 and x != n - 1:
for _ in range(s - 1):
x = pow(x, 2, n)
if x == n - 1: break
else: return False
return True
def mod_inverse(a, m):
g, x, y = extended_gcd(a, m)
if g != 1: raise Exception('Modular inverse does not exist')
return x % m
def extended_gcd(a, b):
if a == 0: return (b, 0, 1)
else:
g, y, x = extended_gcd(b % a, a)
return (g, x - (b // a) * y, y)
# Example usage
public_key, private_key = generate_paillier_keypair(1024)
m1, m2 = 42, 73
c1 = paillier_encrypt(public_key, m1)
c2 = paillier_encrypt(public_key, m2)
c_sum = paillier_add(public_key, c1, c2)
decrypted_sum = paillier_decrypt(private_key, c_sum)
print(f"m1: {m1}, m2: {m2}")
print(f"Encrypted sum: {c_sum}")
print(f"Decrypted sum: {decrypted_sum}")
print(f"Actual sum: {m1 + m2}")
print(f"Homomorphic property holds: {decrypted_sum == (m1 + m2)}")Slide 14:
Quantum-Resistant Cryptography: Lattice-Based Encryption
As quantum computers advance, there's a need for cryptographic algorithms resistant to quantum attacks. Lattice-based cryptography is a promising post-quantum approach. Let's implement a simple Learning With Errors (LWE) encryption scheme, which is a foundational lattice-based primitive.
import numpy as np
def generate_lwe_keypair(n, q, sigma):
s = np.random.randint(0, q, size=n) # Secret key
A = np.random.randint(0, q, size=(n, n)) # Public matrix
e = np.random.normal(0, sigma, size=n).astype(int) % q # Error vector
b = (A.dot(s) + e) % q # Public vector
return (A, b), s
def lwe_encrypt(public_key, m, q):
A, b = public_key
n = len(b)
r = np.random.randint(0, 2, size=n)
c1 = r.dot(A) % q
c2 = (r.dot(b) + m * (q // 2)) % q
return c1, c2
def lwe_decrypt(private_key, ciphertext, q):
s = private_key
c1, c2 = ciphertext
z = (c2 - c1.dot(s)) % q
if z > q // 2:
return 1
else:
return 0
# Parameters
n = 10 # Dimension
q = 97 # Modulus
sigma = 1 # Standard deviation for error distribution
# Key generation
public_key, private_key = generate_lwe_keypair(n, q, sigma)
# Encryption and decryption
message = 1
ciphertext = lwe_encrypt(public_key, message, q)
decrypted = lwe_decrypt(private_key, ciphertext, q)
print(f"Original message: {message}")
print(f"Decrypted message: {decrypted}")
# Test multiple messages
for _ in range(10):
m = np.random.randint(0, 2)
c = lwe_encrypt(public_key, m, q)
d = lwe_decrypt(private_key, c, q)
print(f"Message: {m}, Decrypted: {d}, Correct: {m == d}")Slide 15:
Additional Resources
For those interested in diving deeper into cryptography and its implementation in Python, here are some valuable resources:
- "A Graduate Course in Applied Cryptography" by Dan Boneh and Victor Shoup ArXiv link: https://arxiv.org/abs/2008.01580
- "Post-Quantum Cryptography" by Daniel J. Bernstein, Johannes Buchmann, and Erik Dahmen ArXiv link: https://arxiv.org/abs/0809.2789
- "Lattice-based Cryptography" by Daniele Micciancio and Oded Regev ArXiv link: https://arxiv.org/abs/0902.3383
- Python Cryptography Library documentation: https://cryptography.io/en/latest/
- PyCryptodome Library documentation: https://pycryptodome.readthedocs.io/en/latest/
These resources provide in-depth explanations of cryptographic concepts and advanced techniques, as well as practical implementations in Python.