feat(security): encryption key versioning + AAD binding for EncryptedJSONField (#87)#171
Open
b3lz3but wants to merge 2 commits intocaptainpragmatic:masterfrom
Open
Conversation
…JSONField (captainpragmatic#87) Close two security gaps in the AES-256-GCM encryption layer: 1. Key versioning with keyring fallback: - New wire format: aes:v1:<payload> (no AAD) / aes:v2:<payload> (AAD-bound) - ENCRYPTION_KEYS setting: ordered list [current, ...previous] - Decryption tries all keys in keyring — enables zero-downtime key rotation - Legacy "aes:<payload>" format (pre-versioning) still decrypts - Prod: DJANGO_ENCRYPTION_KEY_PREVIOUS env var for comma-separated old keys 2. AAD (Associated Authenticated Data) context binding: - EncryptedJSONField encrypts with aad=b"table:field:pk" - v2 wire format embeds AAD in payload for self-contained verification - Cross-table ciphertext transplant detected via AAD prefix check - GCM authentication fails if embedded AAD is tampered 3. Management command for data migration: - reencrypt_with_aad: re-encrypt v1 → v2 with AAD context - --dry-run flag, batch processing, idempotent Backward compatible: all existing callsites (40+) use no aad= parameter and produce v1 format. AAD is opt-in via EncryptedJSONField only. Closes captainpragmatic#87 Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> Signed-off-by: Ciprian Radulescu <craps2003@gmail.com>
…patibility encrypt_sensitive_data() was calling get_encryption_keys()[0] directly, bypassing the get_encryption_key() wrapper. This broke existing test patches that mock get_encryption_key (e.g. test_mfa_with_encryption_key_missing). Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> Signed-off-by: Ciprian Radulescu <craps2003@gmail.com>
Contributor
Author
|
@mostlyvirtual ready for review 🙏 |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Closes #87 — implements the Better tier (key versioning + AAD context binding) for the AES-256-GCM encryption layer.
1. Key versioning with keyring fallback
aes:v1:<payload>(no AAD) /aes:v2:<payload>(AAD-bound)ENCRYPTION_KEYSsetting: ordered list[current, ...previous]aes:<payload>format (pre-versioning) still decryptsDJANGO_ENCRYPTION_KEY_PREVIOUSenv var for comma-separated old keysv1format (noaad=parameter) — zero breaking changes2. AAD (Associated Authenticated Data) context binding
EncryptedJSONFieldencrypts withaad=b"table:field:pk"via thread-local frompre_saveaes:v2:<base64url(aad_len[2B] + aad + nonce[12B] + ciphertext + tag[16B])>from_db_value3. Data migration command
python manage.py reencrypt_with_aad— re-encrypt v1 → v2 with AAD context--dry-runflag to preview,--batchfor batch size, idempotent (skips v2)EncryptedJSONField(currentlyCustomerPaymentMethod.bank_details)Test plan
aes:<payload>format still decrypts (backward compat)ENCRYPTION_KEYSaes:v2:wire formatCloses #87
🤖 Generated with Claude Code