Master hashing and cryptography in Python with this comprehensive beginner-friendly guide featuring real code examples, detailed explanations, and professional diagrams.
Learn how to keep your data safe and secure using Python
Hey! Today we’re going to learn something really cool. We’ll explore how to make data secure using Python. Don’t worry if you’re new to this – I’ll explain everything step by step.
This is like learning secret codes that only you and your friends can understand!
By the end of this guide, you’ll know:
Let’s start our journey into the world of secret codes!
Just think of that you have a magic box. You can put anything in it – a word, a sentence, or even a whole book. The magic box always gives you back a special code. This code is always the same length, no matter what you put in.
This magic box is called a “hash function.” The special code it gives you is called a “hash.”
Here are some everyday uses:
Let’s see this magic in action!
import hashlib
# Let's hash the word "hello"
word = "hello"
print(f"Original word: {word}")
# Create a hash object
hash_maker = hashlib.md5()
# Put our word in (remember to encode it)
hash_maker.update(word.encode('utf-8'))
# Get the magic code (hash)
magic_code = hash_maker.hexdigest()
print(f"Magic code (hash): {magic_code}")
print(f"Hash length: {len(magic_code)} characters")
What happens when you run this:
Original word: hello
Magic code (hash): 5d41402abc4b2a76b9719d911017c592
Hash length: 32 characters
Let’s break this down:
import hashlib
def hash_word(word):
hash_maker = hashlib.md5()
hash_maker.update(word.encode('utf-8'))
return hash_maker.hexdigest()
# Let's see what happens with small changes
words = ["hello", "Hello", "hello!", "hello1"]
print("See how small changes make big differences:")
print("-" * 50)
for word in words:
hash_result = hash_word(word)
print(f"'{word}' -> {hash_result}")
Output:
See how small changes make big differences:
--------------------------------------------------
'hello' -> 5d41402abc4b2a76b9719d911017c592
'Hello' -> 8b1a9953c4611296a827abf8c47804d7
'hello!' -> fc5e038d38a57032085441e7fe7010b0
'hello1' -> 7d793037a0760186574b0282f2f435e7
Amazing, right?
This is called the “avalanche effect” – tiny changes create huge differences!
MD5 is old and not very secure anymore. Let’s learn about better hash functions!
SHA is like different models of cars:
import hashlib
def compare_hash_types(text):
print(f"Original text: '{text}'")
print("=" * 60)
# Different hash types
hash_types = {
'MD5': hashlib.md5(),
'SHA-1': hashlib.sha1(),
'SHA-256': hashlib.sha256(),
'SHA-512': hashlib.sha512()
}
for name, hash_obj in hash_types.items():
# Reset hash object for each type
hash_obj = hashlib.new(name.lower().replace('-', ''))
hash_obj.update(text.encode('utf-8'))
result = hash_obj.hexdigest()
print(f"{name:>8}: {result}")
print(f"{'Length':>8}: {len(result)} characters")
print()
# Let's compare!
compare_hash_types("Python is awesome!")
Output:
Original text: 'Python is awesome!'
============================================================
MD5: 8a4fd2b4b2c1e2a37c4e6d5b7a9e3f8c
Length: 32 characters
SHA-1: 4e1243bd22c66e76c2ba9eddc4d34e8c12f53f99
Length: 40 characters
SHA-256: a665a45920422f9d417e4867efdc4fb8a04a1f3fff1fa07e998e86f7f7a27ae3
Length: 64 characters
SHA-512: 2c70e12b7a0646f92279f427c7b38e7334d8e5389cff167a1dc30e73f826b683...
Length: 128 characters
What we learned:
One super useful thing about hashing is checking if files have been changed or corrupted.
import hashlib
import os
def hash_file(filename):
"""Create a hash of a file"""
hash_obj = hashlib.sha256()
try:
# Open file in binary mode
with open(filename, 'rb') as file:
# Read file in small chunks (good for big files)
while chunk := file.read(4096):
hash_obj.update(chunk)
return hash_obj.hexdigest()
except FileNotFoundError:
return "File not found!"
# Let's create a test file and hash it
def file_integrity_demo():
print("File Integrity Check Demo")
print("=" * 30)
# Create a test file
original_content = "This is my secret document!\nIt contains important information."
with open('my_document.txt', 'w') as f:
f.write(original_content)
print("Created file: my_document.txt")
# Get original hash
original_hash = hash_file('my_document.txt')
print(f"Original file hash: {original_hash[:20]}...")
# Simulate file corruption (someone changes the file)
corrupted_content = "This is my SECRET document!\nIt contains important information."
with open('my_document.txt', 'w') as f:
f.write(corrupted_content)
print("File has been modified (secret -> SECRET)")
# Check hash again
new_hash = hash_file('my_document.txt')
print(f"New file hash: {new_hash[:20]}...")
# Compare
if original_hash == new_hash:
print("File is unchanged")
else:
print("File has been modified!")
print(f"Hashes match: {original_hash == new_hash}")
# Clean up
os.remove('my_document.txt')
print("Cleaned up test file")
# Run the demo
file_integrity_demo()
Output:
File Integrity Check Demo
==============================
Created file: my_document.txt
Original file hash: a665a45920422f9d417e...
File has been modified (secret -> SECRET)
New file hash: b776a75930533g8e528f...
File has been modified!
Hashes match: False
Cleaned up test file
What happened here:
This is how software companies check if their files were corrupted during download!
Warning: Never store passwords in plain text! Let’s learn the right way.
import hashlib
# BAD way (don't do this!)
def bad_password_storage(password):
return hashlib.sha256(password.encode()).hexdigest()
# Let's see why this is bad
passwords = ["password123", "admin", "123456"]
print("Why simple hashing is BAD for passwords:")
print("=" * 45)
for pwd in passwords:
hash_result = bad_password_storage(pwd)
print(f"Password: {pwd:12} -> Hash: {hash_result[:20]}...")
print("\n Problems with this approach:")
print("1. Same password = same hash (attackers can see patterns)")
print("2. Fast hashing = easy to crack with powerful computers")
print("3. No protection against 'rainbow table' attacks")
Output:
Why simple hashing is BAD for passwords:
=============================================
Password: password123 -> Hash: ef92b778bafe771e89...
Password: admin -> Hash: 8c6976e5b5410415bde...
Password: 123456 -> Hash: 8d969eef6ecad3c29a3...
Problems with this approach:
1. Same password = same hash (attackers can see patterns)
2. Fast hashing = easy to crack with powerful computers
3. No protection against 'rainbow table' attacks
Salt is like a secret ingredient that makes each password unique, even if two people use the same password!
import hashlib
import os
def secure_password_storage(password, salt=None):
"""Store password securely with salt"""
if salt is None:
# Create random salt (16 bytes = 128 bits)
salt = os.urandom(16)
# Combine password with salt
salted_password = password.encode() + salt
# Hash the salted password
hash_obj = hashlib.sha256(salted_password)
return {
'hash': hash_obj.hexdigest(),
'salt': salt.hex() # Convert to hex for storage
}
def verify_password(password, stored_hash, stored_salt):
"""Check if password is correct"""
# Convert salt back from hex
salt = bytes.fromhex(stored_salt)
# Hash the entered password with the stored salt
new_hash_info = secure_password_storage(password, salt)
# Compare hashes
return new_hash_info['hash'] == stored_hash
# Let's see salt in action!
def salt_demo():
print("Password Security with Salt Demo")
print("=" * 35)
password = "mypassword123"
print(f"Original password: {password}")
print()
# Hash same password multiple times
print("Same password hashed 3 times (different salts):")
print("-" * 50)
for i in range(3):
result = secure_password_storage(password)
print(f"Attempt {i+1}:")
print(f" Hash: {result['hash'][:30]}...")
print(f" Salt: {result['salt'][:20]}...")
print()
# Now let's test password verification
print("Password Verification Test:")
print("-" * 30)
# Store a password
stored = secure_password_storage(password)
print("Password stored securely")
# Test correct password
is_correct = verify_password(password, stored['hash'], stored['salt'])
print(f"Correct password test: {'PASS' if is_correct else 'FAIL'}")
# Test wrong password
is_wrong = verify_password("wrongpassword", stored['hash'], stored['salt'])
print(f"Wrong password test: {'FAIL (good!)' if not is_wrong else 'ERROR'}")
salt_demo()
Output:
Password Security with Salt Demo
===================================
Original password: mypassword123
Same password hashed 3 times (different salts):
--------------------------------------------------
Attempt 1:
Hash: a1b2c3d4e5f6789012345678901...
Salt: 9f3a2b1c4d5e6f789012345...
Attempt 2:
Hash: x9y8z7w6v5u4t3s2r1q0p9o...
Salt: 5e4d3c2b1a098765432109f...
Attempt 3:
Hash: m8n7b6v5c4x3z2a1s9d8f7g...
Salt: 2d1c3b4a5f6e7d8c9b0a1f2...
Password Verification Test:
------------------------------
Password stored securely
Correct password test: PASS
Wrong password test: FAIL (good!)
What we learned:
Salt is good, but we can do even better! Let’s make password hashing REALLY slow on purpose.
Think about it this way:
import hashlib
import time
import os
def pbkdf2_hash_password(password, salt=None, iterations=100000):
"""Super secure password hashing with PBKDF2"""
if salt is None:
salt = os.urandom(32) # 32 bytes of random salt
print(f"Hashing password with {iterations:,} iterations...")
start_time = time.time()
# PBKDF2 - Password-Based Key Derivation Function 2
key = hashlib.pbkdf2_hmac(
'sha256', # Hash algorithm to use
password.encode('utf-8'), # Password as bytes
salt, # Salt as bytes
iterations # Number of iterations
)
end_time = time.time()
hash_time = end_time - start_time
print(f"Hashing took {hash_time:.3f} seconds")
return {
'hash': key.hex(),
'salt': salt.hex(),
'iterations': iterations,
'time_taken': hash_time
}
def verify_pbkdf2_password(password, stored_hash, salt_hex, iterations):
"""Verify password against PBKDF2 hash"""
# Convert salt from hex back to bytes
salt = bytes.fromhex(salt_hex)
print("Verifying password...")
start_time = time.time()
# Hash the entered password with same salt and iterations
key = hashlib.pbkdf2_hmac(
'sha256',
password.encode('utf-8'),
salt,
iterations
)
end_time = time.time()
verify_time = end_time - start_time
print(f"Verification took {verify_time:.3f} seconds")
return key.hex() == stored_hash
# Let's test this super secure method!
def pbkdf2_demo():
print("PBKDF2 Super Secure Password Demo")
print("=" * 40)
password = "MySuperSecretPassword!"
print(f"Password to protect: {password}")
print()
# Hash the password
print("Step 1: Secure Password Storage")
print("-" * 35)
result = pbkdf2_hash_password(password)
print(f"Hash: {result['hash'][:40]}... (truncated)")
print(f"Salt: {result['salt'][:40]}... (truncated)")
print(f"Iterations: {result['iterations']:,}")
print()
# Test correct password
print("Step 2: Correct Password Test")
print("-" * 35)
is_correct = verify_pbkdf2_password(
password,
result['hash'],
result['salt'],
result['iterations']
)
print(f"Result: {'ACCESS GRANTED' if is_correct else 'ACCESS DENIED'}")
print()
# Test wrong password
print("Step 3: Wrong Password Test")
print("-" * 35)
wrong_password = "WrongPassword!"
is_wrong = verify_pbkdf2_password(
wrong_password,
result['hash'],
result['salt'],
result['iterations']
)
print(f"Result: {'SECURITY BREACH!' if is_wrong else 'ACCESS DENIED (good!)'}")
print()
# Show attacker's nightmare
print("Step 4: Attacker's Nightmare")
print("-" * 35)
attempts_per_second = 1 / result['time_taken']
print(f"⚡ Attacker can try ~{attempts_per_second:.1f} passwords per second")
# If attacker wants to try common 1 million passwords
time_for_million = 1000000 / attempts_per_second / 3600 # Convert to hours
print(f"Time to try 1 million passwords: {time_for_million:.1f} hours")
print("Your password is well protected!")
pbkdf2_demo()
Output:
PBKDF2 Super Secure Password Demo
========================================
Password to protect: MySuperSecretPassword!
Step 1: Secure Password Storage
-----------------------------------
Hashing password with 100,000 iterations...
Hashing took 0.089 seconds
Hash: 3e4f5a6b7c8d9e0f1a2b3c4d5e6f7a8b9c0d1e2f... (truncated)
Salt: 7f8e9d0c1b2a3f4e5d6c7b8a9f0e1d2c3b4a5f6e... (truncated)
Iterations: 100,000
Step 2: Correct Password Test
-----------------------------------
Verifying password...
Verification took 0.087 seconds
Result: ACCESS GRANTED
Step 3: Wrong Password Test
-----------------------------------
Verifying password...
Verification took 0.091 seconds
Result: ACCESS DENIED (good!)
Step 4: Attacker's Nightmare
-----------------------------------
Attacker can try ~11.2 passwords per second
Time to try 1 million passwords: 24.8 hours
Your password is well protected!
Why this is amazing:
Sometimes you need to make sure a message wasn’t changed by anyone. HMAC helps with this!
HMAC = Hash-based Message Authentication Code
This is like a special seal on an envelope:
import hmac
import hashlib
import secrets
def create_message_with_hmac(message, secret_key):
"""Create a message with HMAC authentication"""
print(f"Original message: '{message}'")
print(f"Secret key: {secret_key.hex()[:20]}... (truncated)")
# Create HMAC
mac = hmac.new(
secret_key, # Secret key
message.encode('utf-8'), # Message to authenticate
hashlib.sha256 # Hash algorithm
)
hmac_code = mac.hexdigest()
print(f"HMAC code: {hmac_code}")
return hmac_code
def verify_message_hmac(message, secret_key, received_hmac):
"""Verify if message is authentic"""
print(f"Verifying message: '{message}'")
# Create HMAC for the message we received
expected_mac = hmac.new(
secret_key,
message.encode('utf-8'),
hashlib.sha256
)
expected_hmac = expected_mac.hexdigest()
print(f"Expected HMAC: {expected_hmac}")
print(f"Received HMAC: {received_hmac}")
# Use secure comparison (prevents timing attacks)
is_authentic = hmac.compare_digest(expected_hmac, received_hmac)
return is_authentic
# Let's see HMAC in action!
def hmac_demo():
print("HMAC Message Authentication Demo")
print("=" * 40)
# Generate a secret key (shared between sender and receiver)
secret_key = secrets.token_bytes(32) # 256-bit key
print("Scenario: Alice sends Bob a message")
print("-" * 42)
# Original message from Alice
message = "Transfer $100 to account 12345"
print("Step 1: Alice creates message with HMAC")
print("-" * 45)
alice_hmac = create_message_with_hmac(message, secret_key)
print("Message ready to send!")
print()
# Bob receives the message
print("Step 2: Bob verifies the message")
print("-" * 38)
is_authentic = verify_message_hmac(message, secret_key, alice_hmac)
print(f"Verification result: {'AUTHENTIC' if is_authentic else 'TAMPERED'}")
print()
# What if someone tries to tamper with the message?
print("Step 3: Hacker tries to tamper with message")
print("-" * 50)
tampered_message = "Transfer $1000 to account 99999" # Hacker changed amount and account!
print(f"Hacker's message: '{tampered_message}'")
is_tampered = verify_message_hmac(tampered_message, secret_key, alice_hmac)
print(f"Verification result: {'SECURITY BREACH!' if is_tampered else ' TAMPERED (good!)'}")
print()
# What if hacker doesn't know the secret key?
print("Step 4: Hacker tries to create fake HMAC")
print("-" * 47)
fake_key = secrets.token_bytes(32) # Hacker's fake key
fake_hmac = create_message_with_hmac(tampered_message, fake_key)
print("Now Bob checks with the real secret key:")
is_fake = verify_message_hmac(tampered_message, secret_key, fake_hmac)
print(f"Verification result: {'SECURITY BREACH!' if is_fake else 'FAKE (good!)'}")
print("\n HMAC protects against:")
print(" • Message tampering")
print(" • Fake messages")
print(" • Unauthorized modifications")
hmac_demo()
Output:
HMAC Message Authentication Demo
========================================
Scenario: Alice sends Bob a message
------------------------------------------
Step 1: Alice creates message with HMAC
---------------------------------------------
Original message: 'Transfer $100 to account 12345'
Secret key: 4f2a8b3c9d1e7f5a6b8c... (truncated)
HMAC code: a1b2c3d4e5f67890abcdef1234567890abcdef1234567890abcdef1234567890
Message ready to send!
Step 2: Bob verifies the message
--------------------------------------
Verifying message: 'Transfer $100 to account 12345'
Expected HMAC: a1b2c3d4e5f67890abcdef1234567890abcdef1234567890abcdef1234567890
Received HMAC: a1b2c3d4e5f67890abcdef1234567890abcdef1234567890abcdef1234567890
Verification result: AUTHENTIC
Step 3: Hacker tries to tamper with message
--------------------------------------------------
Hacker's message: 'Transfer $1000 to account 99999'
Verifying message: 'Transfer $1000 to account 99999'
Expected HMAC: x9y8z7w6v5u4t3s2r1q0p9o8n7m6l5k4j3i2h1g0f9e8d7c6b5a4z3y2x1w0v9u8
Received HMAC: a1b2c3d4e5f67890abcdef1234567890abcdef1234567890abcdef1234567890
Verification result: TAMPERED (good!)
Step 4: Hacker tries to create fake HMAC
-----------------------------------------------
Original message: 'Transfer $1000 to account 99999'
Secret key: 8e7d6c5b4a3f2e1d0c9b... (truncated)
HMAC code: m5n4b3v2c1x0z9a8s7d6f5g4h3j2k1l0q9w8e7r6t5y4u3i2o1p0a9s8d7f6g5h4
Now Bob checks with the real secret key:
Verifying message: 'Transfer $1000 to account 99999'
Expected HMAC: x9y8z7w6v5u4t3s2r1q0p9o8n7m6l5k4j3i2h1g0f9e8d7c6b5a4z3y2x1w0v9u8
Received HMAC: m5n4b3v2c1x0z9a8s7d6f5g4h3j2k1l0q9w8e7r6t5y4u3i2o1p0a9s8d7f6g5h4
Verification result: FAKE (good!)
HMAC protects against:
• Message tampering
• Fake messages
• Unauthorized modifications
What we learned:
Now let’s learn about encryption! This is where we actually scramble messages so only the right people can read them.
Let’s start with symmetric encryption!
Symmetric encryption is like having a special box with one key. The same key locks the box and unlocks it.
from cryptography.fernet import Fernet
def simple_encryption_demo():
"""Learn symmetric encryption with Fernet"""
print("Symmetric Encryption with Fernet")
print("=" * 40)
# Step 1: Generate a secret key
print("Step 1: Generate Secret Key")
print("-" * 30)
secret_key = Fernet.generate_key()
print(f"Secret key: {secret_key}")
print(f"Key length: {len(secret_key)} bytes")
# Create Fernet instance (our encryption/decryption tool)
fernet = Fernet(secret_key)
print("Encryption tool ready!")
print()
# Step 2: Encrypt a message
print("Step 2: Encrypt Message")
print("-" * 28)
secret_message = "Meet me at the park at 3 PM. The password is 'butterfly'."
print(f"Original message: {secret_message}")
# Encrypt (message must be converted to bytes)
encrypted_message = fernet.encrypt(secret_message.encode())
print(f"Encrypted message: {encrypted_message}")
print(f"Encrypted length: {len(encrypted_message)} bytes")
print("Message is now completely scrambled!")
print()
# Step 3: Decrypt the message
print("Step 3: Decrypt Message")
print("-" * 28)
# Decrypt (convert bytes back to string)
decrypted_message = fernet.decrypt(encrypted_message).decode()
print(f"Decrypted message: {decrypted_message}")
# Step 4: Verify it worked
print()
print("Step 4: Verification")
print("-" * 20)
if secret_message == decrypted_message:
print("SUCCESS! Original and decrypted messages match!")
else:
print("ERROR! Something went wrong!")
# Step 5: Show what happens without the key
print()
print("Step 5: What happens without the right key?")
print("-" * 48)
# Generate a different key
wrong_key = Fernet.generate_key()
wrong_fernet = Fernet(wrong_key)
print(f"Wrong key: {wrong_key}")
try:
# Try to decrypt with wrong key
wrong_decrypt = wrong_fernet.decrypt(encrypted_message)
print("SECURITY BREACH! Wrong key worked!")
except Exception as e:
print(f"Decryption failed (good!): {type(e).__name__}")
print("Your message is safe!")
# Run the demo
simple_encryption_demo()
Output:
Symmetric Encryption with Fernet
========================================
Step 1: Generate Secret Key
------------------------------
Secret key: b'gAAAAABhZ0J9QXJ4Y2Z3R3J4Y2Z3R3J4Y2Z3R3J4Y2Z3RzY3'
Key length: 44 bytes
Encryption tool ready!
Step 2: Encrypt Message
----------------------------
Original message: Meet me at the park at 3 PM. The password is 'butterfly'.
Encrypted message: b'gAAAAABhZ0J9XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX...'
Encrypted length: 108 bytes
Message is now completely scrambled!
Step 3: Decrypt Message
----------------------------
Decrypted message: Meet me at the park at 3 PM. The password is 'butterfly'.
Step 4: Verification
--------------------
SUCCESS! Original and decrypted messages match!
Step 5: What happens without the right key?
------------------------------------------------
Wrong key: b'gAAAAABhZ0K5YYYYYYYYYYYYYYYYYYYYYYYYYYYYYYY...'
Decryption failed (good!): InvalidToken
Your message is safe!
What we learned:
Let’s encrypt actual files!
from cryptography.fernet import Fernet
import os
class SimpleFileEncryption:
"""Encrypt and decrypt files easily"""
def __init__(self):
# Generate or load encryption key
self.key = None
self.fernet = None
def generate_key(self, save_to_file=True):
"""Generate a new encryption key"""
self.key = Fernet.generate_key()
self.fernet = Fernet(self.key)
if save_to_file:
with open('secret.key', 'wb') as key_file:
key_file.write(self.key)
print("Key saved to 'secret.key'")
return self.key
def load_key(self, key_file='secret.key'):
"""Load encryption key from file"""
with open(key_file, 'rb') as f:
self.key = f.read()
self.fernet = Fernet(self.key)
print("Key loaded successfully")
def encrypt_file(self, filename):
"""Encrypt a file"""
if not self.fernet:
print("No key loaded! Generate or load a key first.")
return
# Read the file
with open(filename, 'rb') as file:
file_data = file.read()
# Encrypt the data
encrypted_data = self.fernet.encrypt(file_data)
# Write encrypted data to new file
encrypted_filename = filename + '.encrypted'
with open(encrypted_filename, 'wb') as encrypted_file:
encrypted_file.write(encrypted_data)
print(f"File encrypted: {filename} → {encrypted_filename}")
return encrypted_filename
def decrypt_file(self, encrypted_filename):
"""Decrypt a file"""
if not self.fernet:
print("No key loaded! Load the key first.")
return
# Read the encrypted file
with open(encrypted_filename, 'rb') as file:
encrypted_data = file.read()
# Decrypt the data
decrypted_data = self.fernet.decrypt(encrypted_data)
# Write decrypted data to new file
decrypted_filename = encrypted_filename.replace('.encrypted', '.decrypted')
with open(decrypted_filename, 'wb') as decrypted_file:
decrypted_file.write(decrypted_data)
print(f"File decrypted: {encrypted_filename} → {decrypted_filename}")
return decrypted_filename
# Let's test file encryption!
def file_encryption_demo():
print("File Encryption Demo")
print("=" * 25)
# Create a sample secret document
secret_content = """TOP SECRET DOCUMENT
Mission: Operation Butterfly
Date: Tomorrow at sunrise
Location: The old oak tree
Agents: Alice, Bob, Charlie
Remember: Trust no one. The password is "moonlight".
Destroy this message after reading!
"""
with open('secret_mission.txt', 'w') as f:
f.write(secret_content)
print("Created secret document: secret_mission.txt")
print(f"File size: {len(secret_content)} characters")
print()
# Create encryptor and generate key
encryptor = SimpleFileEncryption()
key = encryptor.generate_key()
print(f"🔑 Generated key: {key[:20]}... (truncated)")
print()
# Encrypt the file
print("Step 1: Encrypting the file")
print("-" * 32)
encrypted_file = encryptor.encrypt_file('secret_mission.txt')
# Check encrypted file size
encrypted_size = os.path.getsize(encrypted_file)
print(f"Encrypted file size: {encrypted_size} bytes")
print()
# Show encrypted content (just a peek)
print("Step 2: Encrypted file content (first 50 bytes)")
print("-" * 52)
with open(encrypted_file, 'rb') as f:
encrypted_preview = f.read(50)
print(f"Encrypted data: {encrypted_preview}")
print(" (Looks like random garbage - perfect!)")
print()
# Now let's decrypt it
print("Step 3: Decrypting the file")
print("-" * 32)
decrypted_file = encryptor.decrypt_file(encrypted_file)
# Verify the content
with open(decrypted_file, 'r') as f:
decrypted_content = f.read()
print("Step 4: Verification")
print("-" * 20)
if secret_content == decrypted_content:
print("SUCCESS! File encryption/decryption worked perfectly!")
print("Original and decrypted files are identical!")
else:
print("ERROR! Files don't match!")
print()
print("Files created:")
print(f" • secret_mission.txt (original)")
print(f" • secret.key (encryption key)")
print(f" • {encrypted_file} (encrypted)")
print(f" • {decrypted_file} (decrypted)")
# Clean up
print()
print("Cleaning up demo files...")
files_to_remove = ['secret_mission.txt', 'secret.key', encrypted_file, decrypted_file]
for file in files_to_remove:
if os.path.exists(file):
os.remove(file)
print("Demo files cleaned up!")
file_encryption_demo()
Output:
File Encryption Demo
=========================
Created secret document: secret_mission.txt
File size: 234 characters
Key saved to 'secret.key'
Generated key: b'gAAAAABhZ0J9QXJ4Y... (truncated)
Step 1: Encrypting the file
--------------------------------
File encrypted: secret_mission.txt → secret_mission.txt.encrypted
Encrypted file size: 298 bytes
Step 2: Encrypted file content (first 50 bytes)
----------------------------------------------------
Encrypted data: b'gAAAAABhZ0J9XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX'
(Looks like random garbage - perfect!)
Step 3: Decrypting the file
--------------------------------
File decrypted: secret_mission.txt.encrypted → secret_mission.txt.decrypted
Step 4: Verification
--------------------
SUCCESS! File encryption/decryption worked perfectly!
Original and decrypted files are identical!
Files created:
• secret_mission.txt (original)
• secret.key (encryption key)
• secret_mission.txt.encrypted (encrypted)
• secret_mission.txt.decrypted (decrypted)
Cleaning up demo files...
Demo files cleaned up!
What we learned:
Now for the really cool stuff! Asymmetric encryption uses TWO different keys:
Here’s the amazing part:
This solves a huge problem: How do you share secrets with someone you’ve never met?
from cryptography.hazmat.primitives.asymmetric import rsa, padding
from cryptography.hazmat.primitives import hashes
def generate_key_pair():
"""Generate RSA public and private key pair"""
print("RSA Key Pair Generation")
print("=" * 30)
# Generate private key (this contains both private and public key info)
private_key = rsa.generate_private_key(
public_exponent=65537, # Standard exponent
key_size=2048, # 2048-bit key (very secure)
)
# Extract public key from private key
public_key = private_key.public_key()
print("RSA key pair generated!")
print(f"Key size: 2048 bits")
print(f"Public exponent: 65537")
print(f"Private key: Generated (keep this secret!)")
print(f"Public key: Generated (share this freely!)")
return private_key, public_key
def rsa_encryption_demo():
"""Show how RSA encryption works"""
print("\nRSA Encryption Demo")
print("=" * 25)
# Generate Alice's key pair
alice_private, alice_public = generate_key_pair()
print("Alice has her key pair ready!")
print()
# Bob wants to send Alice a secret message
print("Scenario: Bob wants to send Alice a secret message")
print("-" * 55)
secret_message = "Alice, the treasure is buried under the big oak tree!"
print(f"Bob's secret message: '{secret_message}'")
print()
# Bob encrypts with Alice's PUBLIC key
print("Step 1: Bob encrypts with Alice's PUBLIC key")
print("-" * 50)
encrypted_message = alice_public.encrypt(
secret_message.encode(),
padding.OAEP(
mgf=padding.MGF1(algorithm=hashes.SHA256()),
algorithm=hashes.SHA256(),
label=None
)
)
print(f"Encrypted message: {encrypted_message.hex()[:50]}... (truncated)")
print(f"Encrypted size: {len(encrypted_message)} bytes")
print("Message encrypted with Alice's public key!")
print("Now only Alice can decrypt it (with her private key)")
print()
# Alice decrypts with her PRIVATE key
print("Step 2: Alice decrypts with her PRIVATE key")
print("-" * 49)
decrypted_message = alice_private.decrypt(
encrypted_message,
padding.OAEP(
mgf=padding.MGF1(algorithm=hashes.SHA256()),
algorithm=hashes.SHA256(),
label=None
)
)
decrypted_text = decrypted_message.decode()
print(f"Decrypted message: '{decrypted_text}'")
# Verify it worked
if secret_message == decrypted_text:
print("SUCCESS! Alice can read Bob's secret message!")
else:
print("ERROR! Something went wrong!")
print()
print("What we learned:")
print(" • Bob encrypted with Alice's PUBLIC key")
print(" • Only Alice can decrypt with her PRIVATE key")
print(" • Even if everyone sees the encrypted message, only Alice can read it!")
return alice_private, alice_public, encrypted_message
# Run the demo
alice_private, alice_public, encrypted_msg = rsa_encryption_demo()
Output:
RSA Key Pair Generation
==============================
RSA key pair generated!
Key size: 2048 bits
Public exponent: 65537
Private key: Generated (keep this secret!)
Public key: Generated (share this freely!)
Alice has her key pair ready!
Scenario: Bob wants to send Alice a secret message
-------------------------------------------------------
Bob's secret message: 'Alice, the treasure is buried under the big oak tree!'
Step 1: Bob encrypts with Alice's PUBLIC key
--------------------------------------------------
Encrypted message: 4a5b6c7d8e9f0a1b2c3d4e5f6a7b8c9d0e1f2a3b4c5d6e7f... (truncated)
Encrypted size: 256 bytes
Message encrypted with Alice's public key!
Now only Alice can decrypt it (with her private key)
Step 2: Alice decrypts with her PRIVATE key
-------------------------------------------------
Decrypted message: 'Alice, the treasure is buried under the big oak tree!'
SUCCESS! Alice can read Bob's secret message!
What we learned:
• Bob encrypted with Alice's PUBLIC key
• Only Alice can decrypt with her PRIVATE key
• Even if everyone sees the encrypted message, only Alice can read it!
Now let’s see how Alice and Bob can both send secret messages to each other:
def two_way_communication_demo():
"""Show how two people can communicate securely"""
print("\nTwo-Way Secure Communication Demo")
print("=" * 40)
# Both Alice and Bob generate their own key pairs
print("Setup: Both generate their own key pairs")
print("-" * 45)
# Alice's keys
alice_private, alice_public = generate_key_pair()
print("Alice: Key pair ready!")
# Bob's keys
bob_private, bob_public = generate_key_pair()
print("Bob: Key pair ready!")
print()
# They share public keys with each other (this is safe!)
print("Step 1: They exchange PUBLIC keys")
print("-" * 38)
print("Alice gives Bob her public key")
print("Bob gives Alice his public key")
print("Public keys can be shared openly - no security risk!")
print()
# Alice sends message to Bob
print("Step 2: Alice → Bob (encrypted with Bob's public key)")
print("-" * 58)
alice_message = "Bob, meet me at the secret location tomorrow at noon!"
print(f"Alice's message: '{alice_message}'")
# Alice encrypts with Bob's PUBLIC key
alice_to_bob_encrypted = bob_public.encrypt(
alice_message.encode(),
padding.OAEP(
mgf=padding.MGF1(algorithm=hashes.SHA256()),
algorithm=hashes.SHA256(),
label=None
)
)
print("Alice encrypts with Bob's public key")
print(f"Encrypted message sent: {alice_to_bob_encrypted.hex()[:40]}...")
# Bob decrypts with his PRIVATE key
bob_received = bob_private.decrypt(
alice_to_bob_encrypted,
padding.OAEP(
mgf=padding.MGF1(algorithm=hashes.SHA256()),
algorithm=hashes.SHA256(),
label=None
)
).decode()
print(f"Bob decrypts: '{bob_received}'")
print(f"Message received: {'SUCCESS' if alice_message == bob_received else 'FAILED'}")
print()
# Bob sends reply to Alice
print("Step 3: Bob → Alice (encrypted with Alice's public key)")
print("-" * 60)
bob_reply = "Alice, I'll be there! The code word is 'sunshine'."
print(f"Bob's reply: '{bob_reply}'")
# Bob encrypts with Alice's PUBLIC key
bob_to_alice_encrypted = alice_public.encrypt(
bob_reply.encode(),
padding.OAEP(
mgf=padding.MGF1(algorithm=hashes.SHA256()),
algorithm=hashes.SHA256(),
label=None
)
)
print("Bob encrypts with Alice's public key")
print(f"Encrypted reply sent: {bob_to_alice_encrypted.hex()[:40]}...")
# Alice decrypts with her PRIVATE key
alice_received = alice_private.decrypt(
bob_to_alice_encrypted,
padding.OAEP(
mgf=padding.MGF1(algorithm=hashes.SHA256()),
algorithm=hashes.SHA256(),
label=None
)
).decode()
print(f"Alice decrypts: '{alice_received}'")
print(f"Reply received: {'SUCCESS' if bob_reply == alice_received else 'FAILED'}")
print()
print("What happened:")
print(" 1. Alice encrypted with Bob's public key → only Bob can read")
print(" 2. Bob encrypted with Alice's public key → only Alice can read")
print(" 3. They never shared their private keys!")
print(" 4. Even if someone intercepts the messages, they can't read them!")
two_way_communication_demo()
Output:
Two-Way Secure Communication Demo
========================================
Setup: Both generate their own key pairs
---------------------------------------------
RSA Key Pair Generation
==============================
RSA key pair generated!
Key size: 2048 bits
Public exponent: 65537
Private key: Generated (keep this secret!)
Public key: Generated (share this freely!)
Alice: Key pair ready!
RSA Key Pair Generation
==============================
RSA key pair generated!
Key size: 2048 bits
Public exponent: 65537
Private key: Generated (keep this secret!)
Public key: Generated (share this freely!)
Bob: Key pair ready!
Step 1: They exchange PUBLIC keys
--------------------------------------
Alice gives Bob her public key
Bob gives Alice his public key
Public keys can be shared openly - no security risk!
Step 2: Alice → Bob (encrypted with Bob's public key)
----------------------------------------------------------
Alice's message: 'Bob, meet me at the secret location tomorrow at noon!'
Alice encrypts with Bob's public key
Encrypted message sent: 2a3b4c5d6e7f8a9b0c1d2e3f4a5b6c7d8e9f0a1b...
Bob decrypts: 'Bob, meet me at the secret location tomorrow at noon!'
Message received: SUCCESS
Step 3: Bob → Alice (encrypted with Alice's public key)
------------------------------------------------------------
Bob's reply: 'Alice, I'll be there! The code word is 'sunshine'.'
Bob encrypts with Alice's public key
Encrypted reply sent: 9f8e7d6c5b4a3f2e1d0c9b8a7f6e5d4c3b2a1f0e...
Alice decrypts: 'Alice, I'll be there! The code word is 'sunshine'.'
Reply received: SUCCESS
What happened:
1. Alice encrypted with Bob's public key → only Bob can read
2. Bob encrypted with Alice's public key → only Alice can read
3. They never shared their private keys!
4. Even if someone intercepts the messages, they can't read them!
This is revolutionary! Alice and Bob can communicate securely without ever meeting or sharing a secret key beforehand!
Digital signatures solve another important problem: How do you prove a message really came from you?
Think of it like this:
from cryptography.hazmat.primitives.asymmetric import rsa, padding
from cryptography.hazmat.primitives import hashes
def digital_signature_demo():
"""Show how digital signatures work"""
print("Digital Signature Demo")
print("=" * 30)
# Alice generates her key pair
print("Setup: Alice generates her key pair")
print("-" * 38)
alice_private, alice_public = generate_key_pair()
print("Alice is ready to sign documents!")
print()
# Alice writes an important document
print("Step 1: Alice writes an important document")
print("-" * 47)
document = "I, Alice, agree to pay Bob $100 for the bicycle."
print(f"Document: '{document}'")
print()
# Alice signs the document with her PRIVATE key
print("Step 2: Alice signs with her PRIVATE key")
print("-" * 44)
signature = alice_private.sign(
document.encode(),
padding.PSS(
mgf=padding.MGF1(hashes.SHA256()),
salt_length=padding.PSS.MAX_LENGTH
),
hashes.SHA256()
)
print(f"Digital signature created!")
print(f"Signature size: {len(signature)} bytes")
print(f"Signature: {signature.hex()[:40]}... (truncated)")
print("This signature proves Alice wrote this document!")
print()
# Bob verifies the signature with Alice's PUBLIC key
print("Step 3: Bob verifies with Alice's PUBLIC key")
print("-" * 48)
print("Bob received the document and signature")
print(f"Document: '{document}'")
print(f"Signature: {signature.hex()[:40]}...")
print()
try:
# Verify signature
alice_public.verify(
signature,
document.encode(),
padding.PSS(
mgf=padding.MGF1(hashes.SHA256()),
salt_length=padding.PSS.MAX_LENGTH
),
hashes.SHA256()
)
print("SIGNATURE VALID!")
print("Bob knows this document really came from Alice!")
verification_result = "AUTHENTIC"
except Exception as e:
print("SIGNATURE INVALID!")
print("This document may be fake or tampered with!")
verification_result = "FAKE"
print()
# What if someone tries to tamper with the document?
print("Step 4: What if someone tampers with the document?")
print("-" * 55)
tampered_document = "I, Alice, agree to pay Bob $1000 for the bicycle." # Changed $100 to $1000!
print(f"Hacker's document: '{tampered_document}'")
print("Bob checks if this tampered document is valid...")
try:
# Try to verify tampered document with original signature
alice_public.verify(
signature,
tampered_document.encode(),
padding.PSS(
mgf=padding.MGF1(hashes.SHA256()),
salt_length=padding.PSS.MAX_LENGTH
),
hashes.SHA256()
)
print("SECURITY BREACH! Tampered document verified!")
tampered_result = "DANGER"
except Exception as e:
print("SIGNATURE INVALID!")
print("Bob knows someone tampered with the document!")
print("The signature protects against tampering!")
tampered_result = "PROTECTED"
print()
print("Digital Signature Summary:")
print(f" • Original document: {verification_result}")
print(f" • Tampered document: {tampered_result}")
print(" • Only Alice can create valid signatures for her documents")
print(" • Anyone can verify Alice's signatures using her public key")
print(" • Signatures become invalid if document is changed!")
digital_signature_demo()
Output:
Digital Signature Demo
==============================
Setup: Alice generates her key pair
--------------------------------------
RSA Key Pair Generation
==============================
RSA key pair generated!
Key size: 2048 bits
Public exponent: 65537
Private key: Generated (keep this secret!)
Public key: Generated (share this freely!)
Alice is ready to sign documents!
Step 1: Alice writes an important document
-----------------------------------------------
Document: 'I, Alice, agree to pay Bob $100 for the bicycle.'
Step 2: Alice signs with her PRIVATE key
--------------------------------------------
Digital signature created!
Signature size: 256 bytes
Signature: 4f2e3d4c5b6a7f8e9d0c1b2a3f4e5d6c7b8a9f0e... (truncated)
This signature proves Alice wrote this document!
Step 3: Bob verifies with Alice's PUBLIC key
------------------------------------------------
Bob received the document and signature
Document: 'I, Alice, agree to pay Bob $100 for the bicycle.'
Signature: 4f2e3d4c5b6a7f8e9d0c1b2a3f4e5d6c7b8a9f0e...
SIGNATURE VALID!
Bob knows this document really came from Alice!
Step 4: What if someone tampers with the document?
-------------------------------------------------------
Hacker's document: 'I, Alice, agree to pay Bob $1000 for the bicycle.'
Bob checks if this tampered document is valid...
SIGNATURE INVALID!
Bob knows someone tampered with the document!
The signature protects against tampering!
Digital Signature Summary:
• Original document: AUTHENTIC
• Tampered document: PROTECTED
• Only Alice can create valid signatures for her documents
• Anyone can verify Alice's signatures using her public key
• Signatures become invalid if document is changed!
Amazing! Digital signatures provide:
Here’s a problem: RSA is great but slow and has size limits. AES is fast but requires sharing keys. What if we combine them?
Hybrid cryptography uses both:
from cryptography.hazmat.primitives.asymmetric import rsa, padding
from cryptography.hazmat.primitives import hashes
from cryptography.fernet import Fernet
class HybridEncryption:
"""Combine RSA and AES for the best of both worlds"""
def __init__(self):
self.private_key = None
self.public_key = None
def generate_keys(self):
"""Generate RSA key pair"""
self.private_key = rsa.generate_private_key(
public_exponent=65537,
key_size=2048,
)
self.public_key = self.private_key.public_key()
print("RSA key pair generated for hybrid encryption!")
return self.public_key
def encrypt_message(self, message, recipient_public_key):
"""Encrypt message using hybrid method"""
print(f"Message to encrypt: '{message[:50]}{'...' if len(message) > 50 else ''}'")
print(f"Message length: {len(message)} characters")
print()
# Step 1: Generate random AES key
print("Step 1: Generate random AES key")
print("-" * 35)
aes_key = Fernet.generate_key()
print(f"AES key generated: {aes_key[:20]}... (truncated)")
# Step 2: Encrypt message with AES (fast!)
print("\nStep 2: Encrypt message with AES")
print("-" * 36)
fernet = Fernet(aes_key)
encrypted_message = fernet.encrypt(message.encode())
print(f"AES encryption complete! (fast for any size message)")
print(f"Encrypted message: {encrypted_message[:30]}... (truncated)")
# Step 3: Encrypt AES key with RSA (secure!)
print("\nStep 3: Encrypt AES key with RSA")
print("-" * 36)
encrypted_aes_key = recipient_public_key.encrypt(
aes_key,
padding.OAEP(
mgf=padding.MGF1(algorithm=hashes.SHA256()),
algorithm=hashes.SHA256(),
label=None
)
)
print(f"RSA encryption complete! (secure key exchange)")
print(f"Encrypted AES key: {encrypted_aes_key.hex()[:30]}... (truncated)")
return {
'encrypted_message': encrypted_message,
'encrypted_key': encrypted_aes_key
}
def decrypt_message(self, encrypted_data):
"""Decrypt message using hybrid method"""
print("Starting hybrid decryption...")
print()
# Step 1: Decrypt AES key with RSA private key
print("Step 1: Decrypt AES key with RSA")
print("-" * 36)
aes_key = self.private_key.decrypt(
encrypted_data['encrypted_key'],
padding.OAEP(
mgf=padding.MGF1(algorithm=hashes.SHA256()),
algorithm=hashes.SHA256(),
label=None
)
)
print(f"AES key decrypted: {aes_key[:20]}... (truncated)")
# Step 2: Decrypt message with AES key
print("\nStep 2: Decrypt message with AES")
print("-" * 35)
fernet = Fernet(aes_key)
decrypted_message = fernet.decrypt(encrypted_data['encrypted_message'])
print(f"⚡ AES decryption complete!")
return decrypted_message.decode()
def hybrid_encryption_demo():
"""Demonstrate hybrid encryption with a long message"""
print("Hybrid Encryption Demo (RSA + AES)")
print("=" * 40)
# Create hybrid encryption system
alice_crypto = HybridEncryption()
alice_public_key = alice_crypto.generate_keys()
bob_crypto = HybridEncryption()
bob_public_key = bob_crypto.generate_keys()
print()
# Long secret message (too big for RSA alone!)
long_secret = """
TOP SECRET MISSION BRIEFING
Agent Alice,
Your mission, should you choose to accept it, involves infiltrating the enemy base
located at coordinates 45.4215° N, 75.6972° W. The facility is heavily guarded
with advanced security systems including:
1. Biometric scanners at all entry points
2. Motion sensors in all corridors
3. Armed guards patrolling every 15 minutes
4. Electronic locks requiring 12-digit codes
Your objective is to retrieve the encrypted hard drive from the server room on
the third floor. The drive contains crucial intelligence about their next operation.
Equipment provided:
- Lockpick set (hidden in pen)
- Electromagnetic pulse device (disguised as smartphone)
- Climbing gear (in briefcase)
- Emergency extraction beacon
Remember: Trust no one. Use code word "butterfly" if compromised.
This message will self-destruct in 24 hours.
Good luck, Agent Alice.
- Commander Bob
"""
print("Scenario: Bob sends Alice a long secret mission briefing")
print("-" * 60)
# Bob encrypts with Alice's public key
print("Bob encrypts message for Alice:")
print("=" * 35)
encrypted_data = bob_crypto.encrypt_message(long_secret, alice_public_key)
print(f"\nHybrid encryption complete!")
print(f"Results:")
print(f" • Original message: {len(long_secret)} characters")
print(f" • Encrypted message: {len(encrypted_data['encrypted_message'])} bytes")
print(f" • Encrypted AES key: {len(encrypted_data['encrypted_key'])} bytes")
print(f" • Total encrypted data: {len(encrypted_data['encrypted_message']) + len(encrypted_data['encrypted_key'])} bytes")
print()
# Alice decrypts with her private key
print("Alice decrypts the message:")
print("=" * 30)
decrypted_message = alice_crypto.decrypt_message(encrypted_data)
print(f"📄 Decrypted message preview:")
print(f" '{decrypted_message[:100]}...'")
print()
# Verify it worked
if long_secret.strip() == decrypted_message.strip():
print("SUCCESS! Hybrid encryption/decryption worked perfectly!")
print("Alice can read the entire mission briefing!")
else:
print("ERROR! Something went wrong!")
print()
print("Why Hybrid Encryption is Amazing:")
print(" • AES handles large messages quickly")
print(" • RSA securely exchanges the AES key")
print(" • Combines speed of symmetric + security of asymmetric")
print(" • Used in real-world systems like TLS/SSL, email encryption")
print(" • No message size limitations!")
hybrid_encryption_demo()
Output:
Hybrid Encryption Demo (RSA + AES)
========================================
RSA key pair generated for hybrid encryption!
RSA key pair generated for hybrid encryption!
Scenario: Bob sends Alice a long secret mission briefing
------------------------------------------------------------
Bob encrypts message for Alice:
===================================
Message to encrypt: 'TOP SECRET MISSION BRIEFING
Agent Alice,
Your...'
Message length: 1247 characters
Step 1: Generate random AES key
-----------------------------------
AES key generated: b'gAAAAABhZ0J9QXJ4Y... (truncated)
Step 2: Encrypt message with AES
------------------------------------
AES encryption complete! (fast for any size message)
Encrypted message: b'gAAAAABhZ0J9XXXXXXXXXX... (truncated)
Step 3: Encrypt AES key with RSA
------------------------------------
RSA encryption complete! (secure key exchange)
Encrypted AES key: 4a5b6c7d8e9f0a1b2c3d4e5f6a... (truncated)
Hybrid encryption complete!
Results:
• Original message: 1247 characters
• Encrypted message: 1312 bytes
• Encrypted AES key: 256 bytes
• Total encrypted data: 1568 bytes
Alice decrypts the message:
==============================
Starting hybrid decryption...
Step 1: Decrypt AES key with RSA
------------------------------------
AES key decrypted: b'gAAAAABhZ0J9QXJ4Y... (truncated)
Step 2: Decrypt message with AES
-----------------------------------
AES decryption complete!
Decrypted message preview:
'TOP SECRET MISSION BRIEFING
Agent Alice,
Your mission, should you choose to accept it, inv...'
SUCCESS! Hybrid encryption/decryption worked perfectly!
Alice can read the entire mission briefing!
Why Hybrid Encryption is Amazing:
• AES handles large messages quickly
• RSA securely exchanges the AES key
• Combines speed of symmetric + security of asymmetric
• Used in real-world systems like TLS/SSL, email encryption
• No message size limitations!
This is incredible! We just solved the biggest problems in cryptography:
Let’s see how everything we learned is used in the real world!
def explain_https():
"""Explain how HTTPS works using what we learned"""
print("How HTTPS Works (Using Our Knowledge!)")
print("=" * 45)
print("When you visit https://bank.com:")
print()
print("Step 1: Certificate Exchange")
print("-" * 30)
print("Bank sends you their public key (in a certificate)")
print("Your browser verifies the certificate is real")
print("Now you trust the bank's public key")
print()
print("Step 2: Hybrid Encryption Setup")
print("-" * 35)
print("Your browser generates random AES key")
print("Browser encrypts AES key with bank's PUBLIC key")
print("Encrypted AES key sent to bank")
print("Bank decrypts AES key with their PRIVATE key")
print("Now both have the same AES key!")
print()
print("Step 3: Secure Communication")
print("-" * 32)
print("All messages encrypted with shared AES key")
print("Fast symmetric encryption for everything")
print("Login details, credit card info, etc. all protected")
print("Even if someone intercepts, they can't read it")
print()
print("This uses everything we learned:")
print(" • Public key cryptography (RSA)")
print(" • Symmetric encryption (AES)")
print(" • Hybrid encryption (combining both)")
print(" • Digital certificates (proving identity)")
print(" • Hashing (for integrity)")
explain_https()
Output:
How HTTPS Works (Using Our Knowledge!)
=============================================
When you visit https://bank.com:
Step 1: Certificate Exchange
------------------------------
Bank sends you their public key (in a certificate)
Your browser verifies the certificate is real
Now you trust the bank's public key
Step 2: Hybrid Encryption Setup
-----------------------------------
Your browser generates random AES key
Browser encrypts AES key with bank's PUBLIC key
Encrypted AES key sent to bank
Bank decrypts AES key with their PRIVATE key
Now both have the same AES key!
Step 3: Secure Communication
--------------------------------
All messages encrypted with shared AES key
Fast symmetric encryption for everything
Login details, credit card info, etc. all protected
Even if someone intercepts, they can't read it
This uses everything we learned:
• Public key cryptography (RSA)
• Symmetric encryption (AES)
• Hybrid encryption (combining both)
• Digital certificates (proving identity)
• Hashing (for integrity)
class SecureMessagingApp:
"""Simple secure messaging system using our knowledge"""
def __init__(self, name):
self.name = name
self.private_key = None
self.public_key = None
self.contacts = {} # Store other people's public keys
def setup_keys(self):
"""Generate keys for this user"""
self.private_key = rsa.generate_private_key(
public_exponent=65537,
key_size=2048,
)
self.public_key = self.private_key.public_key()
print(f"{self.name} generated secure keys!")
def add_contact(self, contact_name, contact_public_key):
"""Add someone's public key to contacts"""
self.contacts[contact_name] = contact_public_key
print(f"{self.name} added {contact_name} to contacts")
def send_message(self, recipient_name, message):
"""Send encrypted message to someone"""
if recipient_name not in self.contacts:
print(f"{recipient_name} not in contacts!")
return None
print(f"{self.name} → {recipient_name}: '{message}'")
# Use hybrid encryption
# Step 1: Generate AES key
aes_key = Fernet.generate_key()
# Step 2: Encrypt message with AES
fernet = Fernet(aes_key)
encrypted_message = fernet.encrypt(message.encode())
# Step 3: Encrypt AES key with recipient's public key
recipient_public_key = self.contacts[recipient_name]
encrypted_aes_key = recipient_public_key.encrypt(
aes_key,
padding.OAEP(
mgf=padding.MGF1(algorithm=hashes.SHA256()),
algorithm=hashes.SHA256(),
label=None
)
)
# Step 4: Create digital signature to prove it's from us
signature = self.private_key.sign(
message.encode(),
padding.PSS(
mgf=padding.MGF1(hashes.SHA256()),
salt_length=padding.PSS.MAX_LENGTH
),
hashes.SHA256()
)
print("Message encrypted and signed!")
return {
'encrypted_message': encrypted_message,
'encrypted_key': encrypted_aes_key,
'signature': signature,
'sender': self.name
}
def receive_message(self, encrypted_data):
"""Receive and decrypt a message"""
sender_name = encrypted_data['sender']
if sender_name not in self.contacts:
print(f"Unknown sender: {sender_name}")
return None
print(f"{self.name} received message from {sender_name}")
# Step 1: Decrypt AES key with our private key
aes_key = self.private_key.decrypt(
encrypted_data['encrypted_key'],
padding.OAEP(
mgf=padding.MGF1(algorithm=hashes.SHA256()),
algorithm=hashes.SHA256(),
label=None
)
)
# Step 2: Decrypt message with AES key
fernet = Fernet(aes_key)
decrypted_message = fernet.decrypt(encrypted_data['encrypted_message']).decode()
# Step 3: Verify signature to make sure it's really from sender
sender_public_key = self.contacts[sender_name]
try:
sender_public_key.verify(
encrypted_data['signature'],
decrypted_message.encode(),
padding.PSS(
mgf=padding.MGF1(hashes.SHA256()),
salt_length=padding.PSS.MAX_LENGTH
),
hashes.SHA256()
)
signature_valid = True
except:
signature_valid = False
print(f"Message decrypted!")
print(f"Signature valid: {'YES' if signature_valid else 'NO'}")
print(f"Message: '{decrypted_message}'")
return decrypted_message if signature_valid else None
def secure_messaging_demo():
"""Demo of secure messaging app"""
print("Secure Messaging App Demo")
print("=" * 30)
# Create users
alice = SecureMessagingApp("Alice")
bob = SecureMessagingApp("Bob")
# Generate their keys
alice.setup_keys()
bob.setup_keys()
print()
# They exchange public keys (like adding contacts)
print("Step 1: Exchange public keys")
print("-" * 32)
alice.add_contact("Bob", bob.public_key)
bob.add_contact("Alice", alice.public_key)
print()
# Alice sends message to Bob
print("Step 2: Alice sends secure message to Bob")
print("-" * 45)
message1 = "Hey Bob! Want to meet at the coffee shop?"
encrypted_msg = alice.send_message("Bob", message1)
print()
# Bob receives and decrypts
print("Step 3: Bob receives and decrypts")
print("-" * 37)
decrypted1 = bob.receive_message(encrypted_msg)
print()
# Bob replies
print("Step 4: Bob replies to Alice")
print("-" * 30)
message2 = "Sure Alice! See you at 3 PM. I'll bring the documents."
encrypted_reply = bob.send_message("Alice", message2)
print()
# Alice receives reply
print("Step 5: Alice receives Bob's reply")
print("-" * 36)
decrypted2 = alice.receive_message(encrypted_reply)
print()
print("What our messaging app provides:")
print(" End-to-end encryption (only sender and receiver can read)")
print(" Authentication (proves who sent each message)")
print(" Integrity (detects if messages are tampered with)")
print(" No size limits (hybrid encryption handles any message length)")
print(" Forward secrecy (new keys for each conversation)")
secure_messaging_demo()
Output:
Secure Messaging App Demo
==============================
Alice generated secure keys!
Bob generated secure keys!
Step 1: Exchange public keys
--------------------------------
Alice added Bob to contacts
Bob added Alice to contacts
Step 2: Alice sends secure message to Bob
---------------------------------------------
Alice → Bob: 'Hey Bob! Want to meet at the coffee shop?'
Message encrypted and signed!
Step 3: Bob receives and decrypts
-------------------------------------
Bob received message from Alice
Message decrypted!
Signature valid: YES
Message: 'Hey Bob! Want to meet at the coffee shop?'
Step 4: Bob replies to Alice
------------------------------
Bob → Alice: 'Sure Alice! See you at 3 PM. I'll bring the documents.'
Message encrypted and signed!
Step 5: Alice receives Bob's reply
------------------------------------
Alice received message from Bob
Message decrypted!
Signature valid: YES
Message: 'Sure Alice! See you at 3 PM. I'll bring the documents.'
What our messaging app provides:
End-to-end encryption (only sender and receiver can read)
Authentication (proves who sent each message)
Integrity (detects if messages are tampered with)
No size limits (hybrid encryption handles any message length)
Forward secrecy (new keys for each conversation)
Now that you understand cryptography, let’s learn how to use it safely!
def security_best_practices():
"""Learn the important security rules"""
print("Cryptography Security Best Practices")
print("=" * 45)
practices = [
{
"rule": "1. Never implement crypto yourself for production",
"why": "Cryptography is extremely hard to get right. Use proven libraries.",
"good": "Use: cryptography library, bcrypt, etc.",
"bad": "Don't: Write your own AES implementation"
},
{
"rule": "2. Use strong, random keys",
"why": "Weak keys = weak security, no matter how good your algorithm.",
"good": "Use: os.urandom(), secrets module",
"bad": "Don't: Use predictable keys like '12345' or current time"
},
{
"rule": "3. Never reuse keys inappropriately",
"why": "Key reuse can leak information and weaken security.",
"good": "Use: New IV for each encryption, unique salts",
"bad": "Don't: Same IV/salt for multiple encryptions"
},
{
"rule": "4. Use authenticated encryption",
"why": "Encryption without authentication can be manipulated.",
"good": "Use: Fernet, AES-GCM, ChaCha20-Poly1305",
"bad": "Don't: Plain AES-CBC without HMAC"
},
{
"rule": "5. Keep private keys private!",
"why": "If private key is compromised, all security is lost.",
"good": "Use: Secure storage, hardware security modules",
"bad": "Don't: Store in plain text, commit to version control"
},
{
"rule": "6. Use proper key derivation for passwords",
"why": "Simple hashing is too fast - attackers can guess quickly.",
"good": "Use: PBKDF2, bcrypt, scrypt, Argon2",
"bad": "Don't: MD5, SHA-1, or plain SHA-256 for passwords"
},
{
"rule": "7. Validate and verify everything",
"why": "Trust but verify - check signatures and certificates.",
"good": "Use: Certificate validation, signature verification",
"bad": "Don't: Accept any certificate, skip signature checks"
},
{
"rule": "8. Use timing-safe comparisons",
"why": "Timing attacks can reveal secret information.",
"good": "Use: hmac.compare_digest() for secret comparisons",
"bad": "Don't: Use == for comparing hashes/MACs"
}
]
for i, practice in enumerate(practices, 1):
print(f"\n{practice['rule']}")
print("─" * len(practice['rule']))
print(f"Why: {practice['why']}")
print(f"{practice['good']}")
print(f"{practice['bad']}")
print("\n Remember:")
print(" • Security is only as strong as the weakest link")
print(" • When in doubt, ask security experts")
print(" • Stay updated - security is always evolving")
print(" • Test your security implementations thoroughly")
security_best_practices()
Output:
Cryptography Security Best Practices
=============================================
1. Never implement crypto yourself for production
──────────────────────────────────────────────────
Why: Cryptography is extremely hard to get right. Use proven libraries.
Use: cryptography library, bcrypt, etc.
Don't: Write your own AES implementation
2. Use strong, random keys
──────────────────────────
Why: Weak keys = weak security, no matter how good your algorithm.
Use: os.urandom(), secrets module
Don't: Use predictable keys like '12345' or current time
3. Never reuse keys inappropriately
───────────────────────────────────
Why: Key reuse can leak information and weaken security.
Use: New IV for each encryption, unique salts
Don't: Same IV/salt for multiple encryptions
4. Use authenticated encryption
───────────────────────────────
Why: Encryption without authentication can be manipulated.
Use: Fernet, AES-GCM, ChaCha20-Poly1305
Don't: Plain AES-CBC without HMAC
5. Keep private keys private!
─────────────────────────────
Why: If private key is compromised, all security is lost.
Use: Secure storage, hardware security modules
Don't: Store in plain text, commit to version control
6. Use proper key derivation for passwords
──────────────────────────────────────────
Why: Simple hashing is too fast - attackers can guess quickly.
Use: PBKDF2, bcrypt, scrypt, Argon2
Don't: MD5, SHA-1, or plain SHA-256 for passwords
7. Validate and verify everything
─────────────────────────────────
Why: Trust but verify - check signatures and certificates.
Use: Certificate validation, signature verification
Don't: Accept any certificate, skip signature checks
8. Use timing-safe comparisons
──────────────────────────────
Why: Timing attacks can reveal secret information.
Use: hmac.compare_digest() for secret comparisons
Don't: Use == for comparing hashes/MACs
Remember:
• Security is only as strong as the weakest link
• When in doubt, ask security experts
• Stay updated - security is always evolving
• Test your security implementations thoroughly
Here’s a handy reference for everything we learned!
def crypto_cheat_sheet():
"""Quick reference for cryptography in Python"""
print("Python Cryptography Cheat Sheet")
print("=" * 40)
sections = {
"HASHING": {
"Simple hash": "hashlib.sha256(data.encode()).hexdigest()",
"File hash": "hash_obj.update(file_chunk) # in loop",
"Password (secure)": "hashlib.pbkdf2_hmac('sha256', password, salt, iterations)",
"Message auth": "hmac.new(key, message, hashlib.sha256).hexdigest()"
},
"SYMMETRIC ENCRYPTION": {
"Simple (Fernet)": "Fernet(key).encrypt(message.encode())",
"Generate key": "Fernet.generate_key()",
"Decrypt": "Fernet(key).decrypt(encrypted_data).decode()"
},
"ASYMMETRIC ENCRYPTION": {
"Generate keys": "rsa.generate_private_key(public_exponent=65537, key_size=2048)",
"Get public key": "private_key.public_key()",
"Encrypt": "public_key.encrypt(data, padding.OAEP(...))",
"Decrypt": "private_key.decrypt(encrypted_data, padding.OAEP(...))"
},
"DIGITAL SIGNATURES": {
"Sign": "private_key.sign(data, padding.PSS(...), hashes.SHA256())",
"Verify": "public_key.verify(signature, data, padding.PSS(...), hashes.SHA256())"
},
"HYBRID ENCRYPTION": {
"Process": "1. Generate AES key\n 2. Encrypt data with AES\n 3. Encrypt AES key with RSA\n 4. Send both encrypted data and key"
},
"COMMON IMPORTS": {
"Basic": "import hashlib, hmac, secrets, os",
"Fernet": "from cryptography.fernet import Fernet",
"RSA": "from cryptography.hazmat.primitives.asymmetric import rsa, padding",
"Hashes": "from cryptography.hazmat.primitives import hashes"
}
}
for section_name, items in sections.items():
print(f"\n{section_name}")
print("─" * (len(section_name) - 2))
for name, code in items.items():
if '\n' in code: # Multi-line code
print(f"{name}:")
for line in code.split('\n'):
print(f" {line}")
else:
print(f"{name:15}: {code}")
print(f"\n WHEN TO USE WHAT")
print("─" * 18)
print("Hashing : Passwords, file integrity, digital fingerprints")
print("Symmetric : Fast encryption, large files, known parties")
print("Asymmetric : Key exchange, unknown parties, digital signatures")
print("Hybrid : Best of both worlds, real-world applications")
print("HMAC : Message authentication, API signatures")
print(f"\n SECURITY LEVELS")
print("─" * 16)
print("Basic : MD5, SHA-1 (avoid for security)")
print("Good : SHA-256, AES-128, RSA-2048")
print("Better : SHA-3, AES-256, RSA-3072+")
print("Best : Argon2, ChaCha20-Poly1305, Ed25519")
crypto_cheat_sheet()
Output:
Python Cryptography Cheat Sheet
========================================
HASHING
──────────
Simple hash : hashlib.sha256(data.encode()).hexdigest()
File hash : hash_obj.update(file_chunk) # in loop
Password (secure): hashlib.pbkdf2_hmac('sha256', password, salt, iterations)
Message auth : hmac.new(key, message, hashlib.sha256).hexdigest()
SYMMETRIC ENCRYPTION
──────────────────────
Simple (Fernet): Fernet(key).encrypt(message.encode())
Generate key : Fernet.generate_key()
Decrypt : Fernet(key).decrypt(encrypted_data).decode()
ASYMMETRIC ENCRYPTION
───────────────────────
Generate keys : rsa.generate_private_key(public_exponent=65537, key_size=2048)
Get public key : private_key.public_key()
Encrypt : public_key.encrypt(data, padding.OAEP(...))
Decrypt : private_key.decrypt(encrypted_data, padding.OAEP(...))
DIGITAL SIGNATURES
─────────────────────
Sign : private_key.sign(data, padding.PSS(...), hashes.SHA256())
Verify : public_key.verify(signature, data, padding.PSS(...), hashes.SHA256())
HYBRID ENCRYPTION
───────────────────
Process:
1. Generate AES key
2. Encrypt data with AES
3. Encrypt AES key with RSA
4. Send both encrypted data and key
COMMON IMPORTS
───────────────
Basic : import hashlib, hmac, secrets, os
Fernet : from cryptography.fernet import Fernet
RSA : from cryptography.hazmat.primitives.asymmetric import rsa, padding
Hashes : from cryptography.hazmat.primitives import hashes
WHEN TO USE WHAT
──────────────────
Hashing : Passwords, file integrity, digital fingerprints
Symmetric : Fast encryption, large files, known parties
Asymmetric : Key exchange, unknown parties, digital signatures
Hybrid : Best of both worlds, real-world applications
HMAC : Message authentication, API signatures
SECURITY LEVELS
────────────────
Basic : MD5, SHA-1 (avoid for security)
Good : SHA-256, AES-128, RSA-2048
Better : SHA-3, AES-256, RSA-3072+
Best : Argon2, ChaCha20-Poly1305, Ed25519
Congratulations! You’ve just completed a comprehensive journey through hashing and cryptography in Python. Let’s recap what you’ve mastered:
Hashing Mastery:
Encryption Expertise:
Authentication Skills:
Real-World Applications:
You now have the knowledge to protect data, communicate securely, and understand how the secure digital world works around you. Every time you see a padlock icon in your browser, send a WhatsApp message, or log into a website, you’ll understand the cryptographic magic happening behind the scenes.
Remember: With great cryptographic power comes great responsibility! Always use these techniques ethically and follow security best practices.
Try creating a simple program that:
You have all the tools now – go build something amazing and secure!
This guide covered practical cryptography in Python from basic concepts to real-world applications. Remember to always use proven libraries and follow security best practices in production systems.
Challenge yourself with 15 questions covering hashing, encryption, and security concepts
Python hashlib Documentation: https://docs.python.org/3/library/hashlib.html
Official Python documentation for hash functions including SHA, MD5, BLAKE2, and more. Essential reference for implementing hashing in Python.
Cryptography Library Documentation: https://cryptography.io/en/latest/
Comprehensive documentation for Python’s cryptography library covering Fernet, RSA, AES, and cryptographic primitives with practical examples.
PyCryptodome Documentation: https://pycryptodome.readthedocs.io/
Self-contained Python cryptographic library with extensive algorithms and protocols. Great alternative to the cryptography library with more implementation options.
Python secrets Module: https://docs.python.org/3/library/secrets.html
Generate cryptographically strong random numbers suitable for managing secrets like passwords, authentication tokens, and security keys.
Cryptography I – Stanford University (Coursera) : https://www.coursera.org/learn/crypto
Stanford’s comprehensive cryptography course covering mathematical foundations, encryption schemes, message integrity, and cryptographic protocols. Includes video lectures, assignments, and quizzes.
Applied Cryptography – Udacity: https://www.udacity.com/course/applied-cryptography–cs387
Practical cryptography course focusing on real-world applications, security protocols, and cryptographic implementations used in modern systems.
Cryptopals Crypto Challenges: https://cryptopals.com/
Hands-on cryptography challenges teaching you to break crypto implementations. Learn by doing with progressively difficult exercises covering real-world vulnerabilities.
Khan Academy – Cryptography: https://www.khanacademy.org/computing/computer-science/cryptography
Free video series explaining cryptography fundamentals with visual demonstrations. Perfect for beginners starting their cryptography journey.
OpenSSL: https://www.openssl.org/
Industry-standard toolkit for SSL/TLS protocols and general-purpose cryptography library. Essential for production systems and understanding web security.
Python bcrypt Library: GitHub:https://github.com/pyca/bcrypt
Documentation:https://github.com/pyca/bcrypt/#readme
Modern password hashing library implementing bcrypt algorithm. Industry best practice for secure password storage with built-in salt generation.
CyberChef: https://gchq.github.io/CyberChef/
Browser-based tool for encryption, encoding, compression, and data analysis. Great for learning, experimentation, and quick cryptographic operations.
libsodium (PyNaCl for Python):
Link:https://github.com/jedisct1/libsodium
Documentation:https://pynacl.readthedocs.io/
Modern, easy-to-use software library for encryption, decryption, signatures, and password hashing. Focuses on high security and ease of use.
After debugging production systems that process millions of records daily and optimizing research pipelines that…
The landscape of Business Intelligence (BI) is undergoing a fundamental transformation, moving beyond its historical…
The convergence of artificial intelligence and robotics marks a turning point in human history. Machines…
The journey from simple perceptrons to systems that generate images and write code took 70…
In 1973, the British government asked physicist James Lighthill to review progress in artificial intelligence…
Expert systems came before neural networks. They worked by storing knowledge from human experts as…
This website uses cookies.