Security fundamentals · Beginner

Encrypting sensitive fields and managing secrets

3 min read

Why it matters

Some data must be readable again, such as a customer's PAN or bank account number, so it cannot be hashed. It must be encrypted, and the encryption is only as good as the handling of the key. Leaked keys and secrets in source code are among the most common causes of breaches.

The concept

  • Encryption in transit (TLS) protects data on the network. Encryption at rest protects stored data: disk or database encryption by the platform, and field-level encryption by the application for the most sensitive values.

  • Use authenticated encryption (for example AES-GCM, or Fernet, which combines AES with an HMAC). It protects confidentiality and also detects any change to the ciphertext.

  • Key management. Keys live in a key management service (AWS KMS, Azure Key Vault, Google Cloud KMS) or a secrets manager, never in code or in the database next to the data. Envelope encryption: data is encrypted with a data key, and the data key is encrypted by a master key held in the KMS.

  • Secrets (API keys, database passwords) go in a secrets manager, are injected at runtime, are rotated, and are scanned for in repositories.

  • Defence in depth: assume any single control can fail, and layer them.

Worked example: an encrypted field

encrypt_field.py
import os

from cryptography.fernet import Fernet, InvalidToken

# In production the key comes from a secrets manager or KMS, never from source code.
key = os.environ.get("FIELD_KEY") or Fernet.generate_key().decode()
f = Fernet(key.encode())

token = f.encrypt(b"PAN: ABCDE1234F")
print("stored in database:", token[:28].decode() + "...")
print("decrypted:", f.decrypt(token).decode())

tampered = token[:-6] + (b"AAAAAA" if token[-6:] != b"AAAAAA" else b"BBBBBB")
try:
    f.decrypt(tampered)
except InvalidToken:
    print("tampered token rejected")

other = Fernet(Fernet.generate_key())
try:
    other.decrypt(token)
except InvalidToken:
    print("wrong key rejected")

print("same plaintext twice gives different tokens:", f.encrypt(b"x") != f.encrypt(b"x"))
shellsession
$ pip install cryptography
$ python3 encrypt_field.py
stored in database: gAAAAABqvOzCK0nOwf0WtxUKuAB6...
decrypted: PAN: ABCDE1234F
tampered token rejected
wrong key rejected
same plaintext twice gives different tokens: True

The token is what the database stores. Changing any bytes makes decryption fail instead of returning corrupted data, and a different key cannot decrypt it. Encrypting the same value twice gives different tokens because each one uses a random initialisation vector, so an attacker cannot tell which rows hold the same value. (The flip side: you cannot search for an encrypted value with a simple WHERE; systems that need lookups store a separate keyed hash.) The PAN shown is the standard example format, not a real number.

Common mistakes

  • Keys in source code or environment files committed to Git. Scan repositories with a tool such as gitleaks, and rotate any key that ever leaked, even briefly.

  • Unauthenticated modes such as AES-CBC without a MAC, or ECB, which leaks patterns.

  • Rolling your own cryptography. Use high-level libraries (Fernet, libsodium, the cloud KMS SDKs).

  • Encrypting everything with one key forever. Plan key rotation from the start.

Interview angle

"How would you store customers' bank account numbers?" (field-level authenticated encryption, keys in a KMS with envelope encryption, least-privilege access to decrypt, audit logs, and masking in the UI). "A developer committed an API key to a public repository. What do you do?" (revoke and rotate it first, then remove it from history, then check logs for misuse).

Practice task

  1. Implement key rotation with MultiFernet: decrypt with the old or new key, re-encrypt with the new one, and prove old tokens still work during the transition.

  2. Run gitleaks on one of your repositories and fix anything it finds.

Further reading