Introduction
File encryption has a reputation for being arcane. In practice, modern cryptography libraries compress it to a disciplined recipe: derive a key from a password, encrypt with AES, and store the salt and nonce alongside the ciphertext. This tutorial builds a command-line tool that does exactly that — encrypt file.pdf asks for a password, and the file becomes unreadable without it.
The security lesson matters as much as the code: we use PBKDF2 key derivation with a random salt, a fresh nonce per file, and authenticated encryption — each choice defends against a specific attack. This is the security-serious sibling of the password generator.
Features
cryptography library.Prerequisites
pip install cryptographyStep 1: Create the Script
Save as cryptool.py:
import os
import sys
import getpass
from cryptography.hazmat.primitives.kdf.pbkdf2 import PBKDF2HMAC
from cryptography.hazmat.primitives import hashes
from cryptography.hazmat.primitives.ciphers.aead import AESGCM
SALT_SIZE = 16
NONCE_SIZE = 12
ITERATIONS = 480_000
MAGIC = b"ENC1"
def derive_key(password: str, salt: bytes) -> bytes:
kdf = PBKDF2HMAC(
algorithm=hashes.SHA256(),
length=32,
salt=salt,
iterations=ITERATIONS,
)
return kdf.derive(password.encode())
def encrypt_file(path: str, password: str):
with open(path, "rb") as f:
plaintext = f.read()
salt = os.urandom(SALT_SIZE)
nonce = os.urandom(NONCE_SIZE)
key = derive_key(password, salt)
ciphertext = AESGCM(key).encrypt(nonce, plaintext, None)
with open(path + ".enc", "wb") as f:
f.write(MAGIC + salt + nonce + ciphertext)
print(f"Encrypted -> {path}.enc")
def decrypt_file(path: str, password: str):
with open(path, "rb") as f:
blob = f.read()
if not blob.startswith(MAGIC):
sys.exit("Not an encrypted file (missing header).")
salt = blob[4:4 + SALT_SIZE]
nonce = blob[4 + SALT_SIZE:4 + SALT_SIZE + NONCE_SIZE]
ciphertext = blob[4 + SALT_SIZE + NONCE_SIZE:]
key = derive_key(password, salt)
try:
plaintext = AESGCM(key).decrypt(nonce, ciphertext, None)
except Exception:
sys.exit("Wrong password or corrupted file.")
out_path = path.replace(".enc", "") + ".dec"
with open(out_path, "wb") as f:
f.write(plaintext)
print(f"Decrypted -> {out_path}")
if __name__ == "__main__":
if len(sys.argv) != 3 or sys.argv[1] not in ("encrypt", "decrypt"):
sys.exit(f"Usage: python {sys.argv[0]} encrypt|decrypt <file>")
action, path = sys.argv[1], sys.argv[2]
password = getpass.getpass("Password: ")
(encrypt_file if action == "encrypt" else decrypt_file)(path, password)Step 2: Run the Tool
python cryptool.py encrypt report.pdf
python cryptool.py decrypt report.pdf.encTry decrypting with the wrong password — you get a clean rejection, not garbage output. That is the authenticated encryption working.
How It Works
Never encrypt with a password directly — passwords have too little entropy. PBKDF2 stretches the password into a 256-bit key by hashing it 480,000 times with a random salt, making brute-force guessing expensive for attackers while costing you one honest second. The salt guarantees the same password produces different keys on different files, defeating precomputed rainbow tables.
AESGCM is authenticated encryption: it encrypts *and* produces an authentication tag. Decryption with a wrong password (wrong key) fails the tag check instead of returning garbage — you learn immediately that the password was wrong, rather than silently using a corrupted file. The nonce must be unique per encryption; os.urandom handles that, and reusing a nonce with the same key is the one catastrophic mistake in GCM.
The file format is deliberately simple: MAGIC + salt + nonce + ciphertext. The magic bytes let the tool recognize its own files; salt and nonce ride in the open — both are designed to be public. Only the ciphertext is secret.
Common Errors & Fixes
ChunkedEOF handling, or accept the memory cost for files under a few hundred MB.Key Concepts
What to Try Next
argon2-cffi) — the newer key-derivation standard.FAQ
Is this actually secure?
The primitives are the same ones protecting HTTPS and password managers. Real-world failures come from weak passwords and leaked keys — use a strong passphrase.
Why not just ZIP with a password?
Classic ZIP encryption is weak or proprietary depending on the variant. AES-encrypted ZIPs exist but depend on the recipient's tooling — this format is under your control.
Where should I store the salt and nonce?
In the file, exactly as done — both are designed to be public. Never reuse a salt across files, and never reuse a nonce with the same derived key.