In an era of interconnected systems, protecting information is paramount. Whether you are transmitting data over an insecure network or storing it on a portable device, you need to be able to lock down with data encryption. Encryption is the process of converting data into an unreadable format to ensure that only authorized parties can access it. While the mathematics behind it are complex, understanding the basic principles is crucial for any developer.
Table of Contents
- 1. Lock Down with Data Encryption
- 1.1.1 💻 The First Rule of Encryption
- 1.1.2 💻 Symmetric vs. Asymmetric Encryption
- 1.1.3 💻 Certificates and Trust
- 1.2 TL;DR
- 1.3 0) Setup
- 1.4 1) Symmetric Encryption (files, blobs, tokens)
- 1.5 2) Deriving Keys from Passwords (do not use raw passwords as keys)
- 1.6 3) Public-Key Basics
- 1.7 4) File Encryption Mini-CLI (password-based AES-GCM)
- 1.8 5) Secrets Management 101
- 1.9 6) Key Rotation Strategy
- 1.10 7) Hashing vs. HMAC vs. Encryption
- 1.11 8) Common Faceplants to Avoid
- 1.12 9) Quick “What should I use?”
- 1.13 10) Minimal, Production-ish Patterns
- 1.14 More Topics
Lock Down with Data Encryption
💻 The First Rule of Encryption
Before diving into the mechanics, it’s vital to remember the first rule of data encryption: never try to create your own encryption algorithm. Cryptography is an incredibly specialized field, and homemade algorithms are almost certain to contain vulnerabilities. Instead, always rely on well-known, peer-reviewed standards and use established libraries in your programming language that implement them correctly. This guide uses a simple cipher for educational purposes only, to illustrate how encryption works.
💻 Symmetric vs. Asymmetric Encryption
Encryption methods are generally divided into two types. Symmetric key encryption uses the same secret key to both encrypt and decrypt the data. It’s fast and efficient, making it great for securing files on your own machine. A simple example is the XOR cipher, where each bit of data is combined with a bit from the key. Applying the same key a second time restores the original data.
Asymmetric key encryption (or public key encryption) uses two different keys: a public key for encrypting data and a private key for decrypting it. This is the foundation of secure communication online (like HTTPS), as you can share your public key freely without compromising your private key.
💻 Certificates and Trust
A key challenge with asymmetric encryption is knowing you can trust a public key. How do you know the public key for your bank’s website actually belongs to your bank? This is solved using digital certificates.
A trusted Certificate Authority (CA) signs a website’s public key, creating a certificate that your web browser can verify. Your browser comes pre-installed with the public keys of major CAs. When you connect to a secure site, it presents this certificate, allowing your browser to verify its authenticity before establishing a secure, encrypted connection.
TL;DR
- Use authenticated encryption (AES-GCM or ChaCha20-Poly1305).
- Derive keys from passwords with a slow KDF (Argon2 or PBKDF2).
- Never reuse nonces. Never roll your own crypto.
- Store keys outside your code. Rotate them.
- Sign things you care about with Ed25519.
- Use
cryptography
for serious stuff; stdlib for hashes/HMAC.
0) Setup
python -m venv .venv && source .venv/bin/activate # Windows: .venv\Scripts\activate
pip install cryptography argon2-cffi python-dotenv
1) Symmetric Encryption (files, blobs, tokens)
Option A: Quick wins with Fernet (AES128 + HMAC, safe defaults)
from cryptography.fernet import Fernet
# Generate and store this once (e.g., in a secret manager)
key = Fernet.generate_key() # base64 urlsafe
f = Fernet(key)
token = f.encrypt(b"hello, secrets") # bytes in, bytes out
plain = f.decrypt(token)
Use Fernet when you want batteries-included integrity and you’re not picky about algorithms.
Option B: AES-256-GCM (manual but modern)
from cryptography.hazmat.primitives.ciphers.aead import AESGCM
from secrets import token_bytes
key = token_bytes(32) # 256-bit key
nonce = token_bytes(12) # 96-bit nonce; NEVER reuse with same key
aad = b"metadata: v1" # optional associated data
aesgcm = AESGCM(key)
ciphertext = aesgcm.encrypt(nonce, b"secret payload", aad)
plaintext = aesgcm.decrypt(nonce, ciphertext, aad)
Rules:
- 12-byte random nonce per message. Don’t repeat.
- AAD lets you bind extra context (headers, IDs) without encrypting it.
2) Deriving Keys from Passwords (do not use raw passwords as keys)
Argon2 (preferred)
from argon2.low_level import Type, hash_secret_raw
from secrets import token_bytes
password = b"correct horse battery staple"
salt = token_bytes(16)
key = hash_secret_raw(
secret=password,
salt=salt,
time_cost=3, # bump for more security if acceptable
memory_cost=64*1024, # in KiB (64 MiB)
parallelism=2,
hash_len=32,
type=Type.ID
)
PBKDF2-HMAC (fallback, widely supported)
from hashlib import pbkdf2_hmac
from secrets import token_bytes
password = b"passphrase"
salt = token_bytes(16)
key = pbkdf2_hmac("sha256", password, salt, 200_000, dklen=32)
Store salt
with the ciphertext. Never store the password.
3) Public-Key Basics
(share secrets with people you don’t trust, which is… everyone)
Key exchange / encryption: RSA hybrid (RSA for the small key, AES-GCM for data)
# Generate keys (once)
from cryptography.hazmat.primitives.asymmetric import rsa, padding
from cryptography.hazmat.primitives import serialization, hashes
from cryptography.hazmat.primitives.ciphers.aead import AESGCM
from secrets import token_bytes
private_key = rsa.generate_private_key(public_exponent=65537, key_size=3072)
public_key = private_key.public_key()
# Sender side: generate data key, wrap with RSA-OAEP, encrypt data with AES-GCM
data_key = token_bytes(32)
wrapped = public_key.encrypt(
data_key,
padding.OAEP(mgf=padding.MGF1(algorithm=hashes.SHA256()), algorithm=hashes.SHA256(), label=None)
)
nonce = token_bytes(12)
ciphertext = AESGCM(data_key).encrypt(nonce, b"big secret file bytes...", None)
# Receiver side: unwrap then decrypt
unwrapped = private_key.decrypt(
wrapped,
padding.OAEP(mgf=padding.MGF1(algorithm=hashes.SHA256()), algorithm=hashes.SHA256(), label=None)
)
plaintext = AESGCM(unwrapped).decrypt(nonce, ciphertext, None)
Signatures: Ed25519 (simple, fast, not cringe)
from cryptography.hazmat.primitives.asymmetric.ed25519 import Ed25519PrivateKey
sk = Ed25519PrivateKey.generate()
pk = sk.public_key()
msg = b"pay me back"
sig = sk.sign(msg)
pk.verify(sig, msg) # raises if tampered
4) File Encryption Mini-CLI (password-based AES-GCM)
Drop this in locker.py
. It’s tiny but safe.
import argparse, json, sys
from secrets import token_bytes
from hashlib import pbkdf2_hmac
from cryptography.hazmat.primitives.ciphers.aead import AESGCM
def derive_key(password: bytes, salt: bytes) -> bytes:
return pbkdf2_hmac("sha256", password, salt, 300_000, dklen=32)
def encrypt_file(inp, outp, password: bytes):
salt = token_bytes(16)
key = derive_key(password, salt)
nonce = token_bytes(12)
data = inp.read()
ct = AESGCM(key).encrypt(nonce, data, None)
out = {"v":1, "kdf":"pbkdf2-sha256", "salt":salt.hex(), "nonce":nonce.hex(), "ct":ct.hex()}
outp.write(json.dumps(out).encode())
def decrypt_file(inp, outp, password: bytes):
obj = json.loads(inp.read())
key = derive_key(password, bytes.fromhex(obj["salt"]))
pt = AESGCM(key).decrypt(bytes.fromhex(obj["nonce"]), bytes.fromhex(obj["ct"]), None)
outp.write(pt)
if __name__ == "__main__":
p = argparse.ArgumentParser()
p.add_argument("mode", choices=["enc","dec"])
p.add_argument("infile")
p.add_argument("outfile")
p.add_argument("--password", required=True)
args = p.parse_args()
with open(args.infile, "rb") as inp, open(args.outfile, "wb") as outp:
if args.mode == "enc":
encrypt_file(inp, outp, args.password.encode())
else:
decrypt_file(inp, outp, args.password.encode())
Usage:
python locker.py enc secrets.pdf secrets.enc --password "passphrase"
python locker.py dec secrets.enc secrets.pdf --password "passphrase"
Please don’t use “passphrase” in production unless you enjoy breaches.
5) Secrets Management 101
- Never hardcode keys. Use env vars,
.env
(with.gitignore
), or a real secret manager. - Separate keys per environment (dev/stage/prod).
- Limit blast radius: different keys per tenant or data domain.
.env
example with python-dotenv
:
from dotenv import load_dotenv
import os
load_dotenv()
db_key = os.environ["DB_AES_KEY"] # base64 or hex; rotate sensibly
6) Key Rotation Strategy
- Tag ciphertext with a key ID:
{"kid":"2025-08-prod-a", "algo":"AES-256-GCM", "...": "..."}
- On decrypt: pick key by
kid
. - On encrypt: use newest active key.
- Run a background re-encrypt job for old blobs if required by policy.
7) Hashing vs. HMAC vs. Encryption
- Hash (SHA-256): one-way. Use for file integrity checks.
- HMAC: hash with a secret to verify authenticity of data.
- Encryption: keeps content secret; use AEAD to also get integrity.
HMAC example:
import hmac, hashlib
secret = b"k"
msg = b"message"
tag = hmac.new(secret, msg, hashlib.sha256).digest()
hmac.new(secret, msg, hashlib.sha256).verify(tag) # raises if wrong
8) Common Faceplants to Avoid
- Reusing a nonce with AES-GCM or ChaCha20-Poly1305. That’s game over.
- ECB mode. If you see it, delete it.
- Homegrown padding or “just XORing.” No.
- Encrypting without integrity. Use AEAD or add HMAC.
- Storing keys right next to ciphertext in the same repo.
- Skipping randomness checks. Use
secrets
, notrandom
.
9) Quick “What should I use?”
Scenario | Pick |
---|---|
Encrypt small tokens/config | Fernet |
File/blob encryption at rest | AES-256-GCM |
Password-based vault | Argon2id → AES-GCM |
Cross-user secure sharing | RSA/ECIES hybrid + AES-GCM |
Tamper-proof releases | Ed25519 signatures |
Low-end devices or speed focus | ChaCha20-Poly1305 |
10) Minimal, Production-ish Patterns
- Wrap every decrypt in try/except and treat failure as “data untrusted.”
- Log key IDs, never keys.
- Use constant-time compares for MACs/signatures (libs already do).
- Add version fields to every encrypted envelope so you can evolve.
More Topics
- Python Coding Essentials: Reliability by Abstraction
- Python Coding Essentials: Different Types of Data
- Python Coding Essentials: Embrace Storage and Persistence
- Python Coding Essentials: Neater Code with Modules
- Python Coding Essentials: Files and Modules Done Quickly
- Python’s Itertools Module – How to Loop More Efficiently
- Python Multithreading – How to Handle Concurrent Tasks