Encrypt Data at Rest
Learn how to encrypt data at rest using cryptography in this Secure development tutorial. Includes hands-on steps, troubleshooting, and what to study next.
Focus: encrypt data at rest using cryptography
Your database just got stolen. The attacker has every row, every column, every byte of your customers' personal data — but because you encrypted the fields at rest, they walk away with nothing but gibberish. That's the difference between a headline breach and a footnote. In this lesson, you'll learn how to encrypt data at rest using cryptography — not with vague theory, but with practical Python code you can run today, using a battle-tested library and proven algorithms.
The problem this lesson solves
Most applications protect data in transit with TLS, but leave data at rest — sitting in databases, on disk, in backups — as plaintext. If an attacker gains access to the storage layer (a leaked backup, a compromised server, a misconfigured S3 bucket), they can exfiltrate everything. The pain is real: massive fines, lawsuits, and permanent trust damage.
Encryption at rest is your last line of defense. When every other control fails, it's the ciphertext that keeps your data safe.
Ignoring this is a compliance and legal risk too. Regulations like GDPR, HIPAA, and PCI-DSS explicitly require encryption of sensitive data at rest. Failing to encrypt can turn a security incident into a regulatory catastrophe.
Core concept / mental model
Think of encryption as a vault with two keys. The vault is the algorithm — like AES-256 — that scrambles your data into unreadable ciphertext. The keys are what unlock it. You encrypt data with one key, and decrypt it with the same key (that's symmetric encryption). Unlike a password you can memorize, these are long, random, binary keys (32 bytes for AES-256).
A crucial idea is the nonce (number used once). For most modern modes (like AES-GCM), you generate a random nonce each time you encrypt. It's not secret, but it must never repeat for the same key. If two messages are encrypted with the same key and nonce, an attacker can recover the encryption key. The nonce is often prepended to the ciphertext for convenience.
Another key concept is authentication. Encrypting without authenticating allows an attacker to tamper with ciphertext, potentially causing silent data corruption. The cryptography library's AESGCM mode provides encrypt-then-MAC, which authenticates the ciphertext, so you detect any tampering.
Here's a simple mental diagram:
plaintext --(encrypt with key + nonce)--> ciphertext
ciphertext --(decrypt with key + nonce)--> plaintext
The nonce is stored alongside the ciphertext — it's not secret, just unique.
How it works step by step
Let's break down the process of encrypting data at rest using cryptography:
- Choose your algorithm and mode. For most use cases, use AES-256 in GCM mode (authenticated encryption). It's fast, secure, and available in the
cryptographylibrary. - Generate or obtain a random 256-bit key. Use
secrets.token_bytes(32)for maximum entropy. - Generate a fresh random nonce (12 bytes) for each encryption operation.
- Encrypt the plaintext bytes, producing a ciphertext that includes the authentication tag (GCM appends it automatically).
- Store the nonce and ciphertext together — e.g.,
nonce + ciphertextin a database column. - Decrypt by splitting the stored value, extracting the nonce, and calling
decrypt. - Manage your keys securely. Store keys in a secure location — environment variables, a key management service (KMS) like AWS KMS or HashiCorp Vault — and rotate them regularly.
This ensures that even if the database is compromised, the data is unreadable without the key.
Hands-on walkthrough
Let's put this into practice. You'll need the cryptography library. Install it if you haven't:
pip install cryptography
Basic encryption and decryption
The following script demonstrates encrypting and decrypting a password string:
from cryptography.hazmat.primitives.ciphers.aead import AESGCM
import secrets
# Generate a random 256-bit key (keep this secret and safe!)
key = secrets.token_bytes(32)
# Generate a random 12-byte nonce
nonce = secrets.token_bytes(12)
# Your plaintext data (must be bytes)
plaintext = b"my super secret password: hunter2"
aesgcm = AESGCM(key)
# Encrypt: returns ciphertext + tag combined
ciphertext = aesgcm.encrypt(nonce, plaintext, None)
# For storage, combine nonce and ciphertext
stored = nonce + ciphertext
print(f"Stored value (hex): {stored.hex()[:64]}...")
# Decrypt
nonce_from_stored = stored[:12]
ciphertext_from_stored = stored[12:]
decrypted = aesgcm.decrypt(nonce_from_stored, ciphertext_from_stored, None)
assert decrypted == plaintext
print(f"Decrypted: {decrypted.decode()}")
Expected output:
Stored value (hex): a1b2c3d4... (random)
Decrypted: my super secret password: hunter2
Adding associated data
GCM supports Associated Data (AAD) — data that is authenticated but not encrypted. This is useful for binding a record to its context, like a user ID, so you can detect if the ciphertext is swapped between records.
from cryptography.hazmat.primitives.ciphers.aead import AESGCM
import secrets
key = secrets.token_bytes(32)
nonce = secrets.token_bytes(12)
aesgcm = AESGCM(key)
plaintext = b"SSN: 123-45-6789"
aad = b"user_id=42"
ciphertext = aesgcm.encrypt(nonce, plaintext, aad)
stored = nonce + ciphertext
# Later, during decryption, provide the same AAD
nonce_from_stored = stored[:12]
ciphertext_from_stored = stored[12:]
try:
decrypted = aesgcm.decrypt(nonce_from_stored, ciphertext_from_stored, aad)
print(f"Decrypted: {decrypted.decode()}")
except Exception as e:
print(f"Decryption failed: {e}")
Expected output:
Decrypted: SSN: 123-45-6789
If you use the wrong AAD, decryption will raise an InvalidTag exception.
Encrypting a file
For files (like configuration or backup files), you can encrypt the whole file:
import secrets
from cryptography.hazmat.primitives.ciphers.aead import AESGCM
key = secrets.token_bytes(32)
nonce = secrets.token_bytes(12)
aesgcm = AESGCM(key)
# Write encrypted file
plaintext = b"backup config: api_key=...\n"
ciphertext = aesgcm.encrypt(nonce, plaintext, None)
with open("config.enc", "wb") as f:
f.write(nonce + ciphertext)
# Read and decrypt
with open("config.enc", "rb") as f:
data = f.read()
nonce_from_file = data[:12]
ciphertext_from_file = data[12:]
decrypted = aesgcm.decrypt(nonce_from_file, ciphertext_from_file, None)
print(f"Decrypted file content: {decrypted.decode()}")
Now you've encrypted data at rest — whether in a database or a file — using cryptography.
Compare options / when to choose what
There are several ways to encrypt data at rest. Here's a comparison of popular approaches:
| Approach | Pros | Cons | Best for |
|---|---|---|---|
| Application-level (this lesson) | Full control over what's encrypted; works anywhere | Need to manage keys in your code | Sensitive fields like SSNs, passwords, tokens |
| Database-level (e.g., TDE) | Transparent, no app changes | Encrypts whole DB; limited granularity | Existing apps with compliance requirements |
| Storage-level (LUKS, BitLocker) | Encrypts entire disk; OS handles it | Coarse-grained; not per-record | On-prem servers, VM images |
| Cloud KMS + envelope encryption | Centralized key management, audit logs | More setup, vendor lock-in | Modern cloud-native apps |
For most web applications, application-level encryption is the best balance of control and security. It lets you encrypt only sensitive fields — like credit card numbers — without impacting performance on non-sensitive data.
A common pattern is envelope encryption: encrypt your data with a data key, and encrypt that data key with a master key stored in a KMS. This gives you flexibility and easy rotation.
Variations to consider:
- ChaCha20-Poly1305: Another authenticated encryption algorithm, often faster on mobile devices, and available in the
cryptographylibrary. - Fernet: A higher-level symmetric encryption format that is password-based, but not recommended for sensitive data without a proper key derivation function.
- Transit encryption in HashiCorp Vault: Lets you encrypt data with a key managed by Vault, without ever exposing the key to your app.
Troubleshooting & edge cases
Let's address common pitfalls you might encounter.
Nonce reuse — the silent killer
If you accidentally reuse a nonce with the same key for two different messages, an attacker can recover the keystream and decrypt both. Always generate a fresh nonce using secrets.token_bytes(12) for each encryption.
Never hard-code a nonce. The nonce is not a password — it's a uniqueness guarantee.
Decryption fails with InvalidTag
This error means the authentication tag didn't match. Causes:
- The nonce you supplied is wrong.
- The ciphertext was tampered with.
- You provided different associated data than when encrypting.
- You mismatched the key.
Fix: Carefully split the stored value into its nonce and ciphertext parts. Ensure you're using the exact same key and AAD. If you're storing nonce + ciphertext, remember the nonce length (12 bytes).
Key loss or corruption
If you lose the key, you lose the data permanently. There's no backdoor. Mitigation:
- Use a secure key management system (KMS) to store and back up keys.
- Set up key rotation policies.
- Test decryption regularly in a disaster recovery drill.
Unsupported mode or missing library
The cryptography library uses OpenSSL under the hood. If you get an UnsupportedAlgorithm error, update the library or your OpenSSL. Always use the latest version of the cryptography package.
Encoding issues
You must encrypt bytes, not strings. Convert strings to bytes with .encode('utf-8') before encryption, and back with .decode('utf-8') after decryption.
What you learned & what's next
You now understand the core principle behind encrypting data at rest using cryptography: you use a symmetric key to encrypt data, store the nonce with the ciphertext, and use the same key to decrypt. You implemented this with the cryptography library's AESGCM, learned how to add associated data for tamper detection, and saw how to apply the pattern to files and records.
You're ready to place this into your secure development toolkit. In the next lesson, we'll explore secure key management — how to store, rotate, and distribute those keys without shooting yourself in the foot. Encryption is only as strong as your key management, so stay tuned.
Keep practicing — your data's safety depends on it.
Practice recap
Now try it yourself: write a function that encrypts a dictionary of user data (e.g., email, phone) into a JSON file, with each field encrypted separately using a fresh nonce. Then write a companion function that decrypts the file and verifies the data integrity. Run it and make sure the round-trip works. Once you're comfortable, experiment with adding associated data like a user ID.
Common mistakes
- Reusing a nonce with the same key for multiple encryptions — this can expose the key to attackers.
- Storing the nonce and ciphertext separately without any way to associate them, causing decryption failures.
- Hardcoding encryption keys in source code or committing them to git — keys should be in environment variables or a KMS.
- Using a weak or outdated algorithm like DES or ECB mode, which is not authenticated and vulnerable to pattern leaks.
- Trying to encrypt strings directly without encoding them to bytes, causing
TypeError. - Forgetting to use associated data to bind records to their context, allowing ciphertext swapping attacks.
Variations
- Use
ChaCha20Poly1305for mobile or embedded environments where AES hardware acceleration is unavailable. - Adopt envelope encryption with a cloud KMS (AWS KMS, Google Cloud KMS) for centralized key management and rotation.
- Consider database-level transparent data encryption (TDE) if you need whole-database encryption with minimal app changes.
Real-world use cases
- A healthcare app encrypting patient PII (SSN, diagnosis) in its PostgreSQL database to meet HIPAA compliance.
- A SaaS platform encrypting user API tokens and OAuth refresh tokens before storing them in a Redis cache.
- A fintech service using envelope encryption with AWS KMS to encrypt customer card numbers at rest in S3 backups.
Key takeaways
- Encrypt data at rest using symmetric cryptography (AES-GCM) to protect sensitive fields in databases and files.
- Always generate a fresh random nonce (12 bytes) for each encryption operation and store it alongside the ciphertext.
- Use authenticated encryption (GCM) to ensure both confidentiality and integrity — tampering is detected.
- Manage keys securely: use a KMS or secure environment, rotate keys regularly, and never hard-code them.
- Application-level encryption gives you the most control over what to encrypt and how, balancing security and performance.
- Test decryption in your disaster recovery process — losing your key means losing your data.
Keep learning
Related tutorials, quizzes, and articles for this topic.
Discussion
Questions, corrections, and tips help everyone reading this page.
0 comments
Add a comment
No comments yet — start the thread.