Cryptography — hashes, signatures, keys
Be able to explain hash functions, signatures and why API keys should never live in the code.
Prerequisites
- BBinary numbers and bitsrequired
Intuition
Three building blocks that cover most things:
A hash (SHA-256): a one-way function from arbitrary data to 256 bits. The same input always gives the same hash; the smallest change gives an entirely different one. It cannot be reversed. Used for fingerprints (has the file changed?) and for passwords — then with a slow hash (bcrypt, argon2) plus a salt, never plain SHA-256.
A signature (HMAC or asymmetric): proves that the data comes from someone with the key and has not been changed. AI-grafen's certificates are signed with HMAC — which is why they can be verified without being forgeable.
Encryption: symmetric (the same key, fast, AES) or asymmetric (a public/private key pair, slow, used to exchange symmetric keys — which is what TLS does).
Formal
Why API keys should never live in the code:
- Git does not forget. A key that has been committed remains in the history even after it has been removed from the file. The only remedy is to revoke the key.
- The repo spreads — forks, backups, CI logs, a laptop that goes missing.
- Automatic scanners find keys in public repos within minutes. There are documented cases where a leaked cloud key ran up tens of thousands of kronor in costs overnight.
Where they should live instead, in ascending order:
| Place | Suitable for |
|---|---|
| An environment variable | local development |
A .env outside git (in .gitignore) | local development |
| A secrets manager (Vault, a cloud KMS) | production |
| Short-lived tokens with automatic rotation | the best — the leak has an expiry date |
AI-grafen fetches the LLM key out of Vault at bootstrap and writes it to an environment file that is never in the repo; only the first twelve characters are ever logged.
If a key has leaked: revoke first, investigate afterwards. The order is not negotiable.
Code
import hashlib, hmac, os, secrets
# A fingerprint — the same input, the same hash
print(hashlib.sha256(b"hello").hexdigest()[:16]) # 2cf2...
# Passwords: NEVER raw SHA-256. A slow hash plus a salt.
salt = secrets.token_bytes(16)
hashed = hashlib.scrypt(b"password", salt=salt, n=2**14, r=8, p=1)
# A signature (the same principle as the platform's certificates)
KEY = os.environ["SIGNING_KEY"].encode() # from the environment, not the code
def sign(data: bytes) -> str:
return hmac.new(KEY, data, hashlib.sha256).hexdigest()
def verify(data: bytes, sig: str) -> bool:
return hmac.compare_digest(sign(data), sig) # a constant-time comparison
# The key from the environment, with a clear error if it is missing
API_KEY = os.environ.get("LLM_API_KEY")
if not API_KEY:
raise RuntimeError("LLM_API_KEY is missing — set it in the environment, not in the code")
print("key:", API_KEY[:12] + "…") # never log the whole thing
hmac.compare_digest instead of == is not pedantry: an ordinary comparison stops at the first wrong character, which leaks information about the signature through the response time.
Mastery means
- Explains hash functions, signatures and keys
- Knows why API keys should never live in the code
- Handles secrets correctly
Sign in to do the exercises and build your mastery up.
Sources
- OWASP — Secrets Management Cheat Sheet — CC BY-SA 4.0
- The Python documentation (PSF licence) — PSF