-
-
Notifications
You must be signed in to change notification settings - Fork 4.5k
feat(encrryption): Add Fernet Key Store #103511
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
+768
−382
Merged
Changes from all commits
Commits
Show all changes
5 commits
Select commit
Hold shift + click to select a range
f7b17a1
feat(encrryption): Add Fernet Key Store
vgrozdanic f59fad4
prevent stale keys
vgrozdanic a322b0c
use option to control encrpytion method
vgrozdanic a175b84
mypy fixes
vgrozdanic 4a2b9ea
default to plaintext if the method from option is unknown
vgrozdanic File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
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
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
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,8 @@ | ||
| from typing import Literal, TypedDict | ||
|
|
||
|
|
||
| class EncryptedFieldSettings(TypedDict): | ||
| method: Literal["fernet"] | Literal["plaintext"] | ||
| # fernet config | ||
| fernet_primary_key_id: str | None | ||
| fernet_keys_location: str | None |
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
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
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,102 @@ | ||
| from logging import getLogger | ||
| from pathlib import Path | ||
|
|
||
| import sentry_sdk | ||
| from cryptography.fernet import Fernet | ||
| from django.conf import settings | ||
| from django.core.exceptions import ImproperlyConfigured | ||
|
|
||
| logger = getLogger(__name__) | ||
|
|
||
|
|
||
| class FernetKeyStore: | ||
| _keys: dict[str, Fernet] | None = {} | ||
| _is_loaded = False | ||
|
|
||
| @classmethod | ||
| def _path_to_keys(cls) -> Path | None: | ||
| settings_path = settings.DATABASE_ENCRYPTION_SETTINGS.get("fernet_keys_location") | ||
|
|
||
| return Path(settings_path) if settings_path is not None else None | ||
|
|
||
| @classmethod | ||
| def load_keys(cls) -> None: | ||
| """ | ||
| Reads all files in the given directory. | ||
| Filename = Key ID | ||
| File Content = Fernet Key | ||
| """ | ||
| path = cls._path_to_keys() | ||
|
|
||
| if path is None: | ||
| # No keys directory is configured, so we don't need to load any keys. | ||
| cls._keys = None | ||
| cls._is_loaded = True | ||
| return | ||
|
|
||
| if not path.exists() or not path.is_dir(): | ||
| raise ImproperlyConfigured(f"Key directory not found: {path}") | ||
|
|
||
| # Clear the keys dictionary to avoid stale data | ||
| cls._keys = {} | ||
|
|
||
| for file_path in path.iterdir(): | ||
| if file_path.is_file(): | ||
| # Skip hidden files | ||
| if file_path.name.startswith("."): | ||
| continue | ||
|
|
||
| try: | ||
| with open(file_path) as f: | ||
| key_content = f.read().strip() | ||
|
|
||
| if not key_content: | ||
| logger.warning("Empty key file found: %s", file_path.name) | ||
| continue | ||
|
|
||
| # Store Fernet object in the dictionary | ||
| # Objects are stored instead of keys for performance optimization. | ||
| cls._keys[file_path.name] = Fernet(key_content.encode("utf-8")) | ||
|
|
||
| except Exception as e: | ||
| logger.exception("Error reading key %s", file_path.name) | ||
| sentry_sdk.capture_exception(e) | ||
|
|
||
| cls._is_loaded = True | ||
| logger.info("Successfully loaded %d Fernet encryption keys.", len(cls._keys)) | ||
|
|
||
| @classmethod | ||
| def get_fernet_for_key_id(cls, key_id: str) -> Fernet: | ||
| """Retrieves a Fernet object for a specific key ID""" | ||
| if not cls._is_loaded: | ||
| # Fallback: if the keys are not already loaded, load them on first access. | ||
| logger.warning("Loading Fernet encryption keys on first access instead of on startup.") | ||
| cls.load_keys() | ||
|
|
||
| if cls._keys is None: | ||
| # This can happen only if the keys settings are misconfigured | ||
| raise ValueError( | ||
| "Fernet encryption keys are not loaded. Please configure the keys directory." | ||
| ) | ||
|
|
||
| fernet = cls._keys.get(key_id) | ||
| if not fernet: | ||
| raise ValueError(f"Encryption key with ID '{key_id}' not found.") | ||
|
|
||
| return fernet | ||
|
|
||
| @classmethod | ||
| def get_primary_fernet(cls) -> tuple[str, Fernet]: | ||
| """ | ||
| Reads the configuration and returns the primary key ID and the Fernet object | ||
| initialized with the primary Fernet key. | ||
|
|
||
| The primary Fernet is the one that is used to encrypt the data, while | ||
| decryption can be done with any of the registered Fernet keys. | ||
| """ | ||
|
|
||
| primary_key_id = settings.DATABASE_ENCRYPTION_SETTINGS.get("fernet_primary_key_id") | ||
| if primary_key_id is None: | ||
| raise ValueError("Fernet primary key ID is not configured.") | ||
|
|
||
| return primary_key_id, cls.get_fernet_for_key_id(primary_key_id) | ||
Oops, something went wrong.
Oops, something went wrong.
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.
Uh oh!
There was an error while loading. Please reload this page.