Skip to content

Commit

Permalink
Merge pull request #45 from C0D1UM/40-rotate-keys
Browse files Browse the repository at this point in the history
Support for key rotation
  • Loading branch information
earthpyy authored Nov 1, 2024
2 parents b787bb7 + 996a079 commit 38e3d0e
Show file tree
Hide file tree
Showing 3 changed files with 68 additions and 6 deletions.
31 changes: 27 additions & 4 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -41,13 +41,20 @@ pip install django-secured-fields
HASH_SALT: 500d492e
```

3. Put generated key and hash salt in settings
3. Put generated key(s) and hash salt in settings

```python
# settings.py

SECURED_FIELDS_KEY = 'TtY8MAeXuhdKDd1HfGUwim-vQ8H7fXyRQ9J8pTi_-lg='
SECURED_FIELDS_HASH_SALT = '500d492e' # optional
# or multiple keys for rotation
SECURED_FIELDS_KEY = [
'TtY8MAeXuhdKDd1HfGUwim-vQ8H7fXyRQ9J8pTi_-lg=',
'...',
]

# optional
SECURED_FILDS_HASH_SALT = '500d492e'
```

## Usage
Expand Down Expand Up @@ -88,7 +95,7 @@ id_card_number = secured_fields.EncryptedCharField(max_length=18, searchable=Tru

| Key | Required | Default | Description |
| --- | -------- | ------- | ----------- |
| `SECURED_FIELDS_KEY` | Yes | | Key for using in encryption/decryption with Fernet. Usually generated from `python manage.py generate_key`. |
| `SECURED_FIELDS_KEY` | Yes | | Key(s) for using in encryption/decryption with Fernet. Usually generated from `python manage.py generate_key`. For rotation keys, use a list of keys instead (see [MultiFernet](https://cryptography.io/en/latest/fernet/#cryptography.fernet.MultiFernet)). |
| `SECURED_FIELDS_HASH_SALT` | No | `''` | Salt to append after the field value before hashing. Usually generated from `python manage.py generate_key`. |
| `SECURED_FIELDS_FILE_STORAGE` | No | `'secured_fields.storage.EncryptedFileSystemStorage'` | File storage class used for storing encrypted file/image fields. See [EncryptedStorageMixin](#encryptedstoragemixin) |

Expand All @@ -115,6 +122,22 @@ b'gAAAAABh2_Ry_thxLTuFFXeMc9hNttah82979JPuMSjnssRB0DmbgwdtEU5dapBgISOST_a_egDc66
b'test'
```

### Rotate Keys

```python
> from secured_fields.fernet import get_fernet

> encrypted_data = get_fernet().encrypt(b'test')
> encrypted_data
b'gAAAAABh2_Ry_thxLTuFFXeMc9hNttah82979JPuMSjnssRB0DmbgwdtEU5dapBgISOST_a_egDc66EG_ZtVu_EqF_69djJwuA=='

> rotated_encrypted_data = get_fernet().rotate(encrypted_data)
> get_fernet().decrypt(rotated_encrypted_data)
b'test'
```

See more details in [MultiFernet.rotate](https://cryptography.io/en/latest/fernet/#cryptography.fernet.MultiFernet.rotate).

### `EncryptedMixin`

If you have a field which is not supported by the package, you can use `EncryptedMixin` to enable encryption and search functionality for that custom field.
Expand Down Expand Up @@ -180,7 +203,7 @@ make lint
### Testing

```bash
make test
make test-pg # or make test-mysql, make test-sqlite
```

### Fix Formatting
Expand Down
10 changes: 8 additions & 2 deletions secured_fields/fernet.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
from cryptography import fernet
from django.conf import settings

fernet_client: typing.Optional[fernet.Fernet] = None
fernet_client: typing.Optional[fernet.MultiFernet] = None


def get_fernet():
Expand All @@ -13,6 +13,12 @@ def get_fernet():
fernet_key = getattr(settings, 'SECURED_FIELDS_KEY', None)
assert fernet_key is not None, '`SECURED_FIELDS_KEY` is required when using django-secured-fields'

fernet_client = fernet.Fernet(fernet_key)
if isinstance(fernet_key, str):
fernet_keys = [fernet_key]
else:
fernet_keys = fernet_key

fernet_instances = [fernet.Fernet(key) for key in fernet_keys]
fernet_client = fernet.MultiFernet(fernet_instances)

return fernet_client
33 changes: 33 additions & 0 deletions test_secured_fields/main/tests/test_fernet.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
from cryptography.fernet import Fernet

from django import test

from secured_fields import fernet as fernet_module
from secured_fields.fernet import get_fernet


class FernetTestCase(test.TestCase):
def setUp(self):
fernet_module.fernet_client = None

def tearDown(self) -> None:
fernet_module.fernet_client = None

def test_simple(self):
fernet = get_fernet()
encrypted = fernet.encrypt(b'test')
self.assertEqual(fernet.decrypt(encrypted), b'test')

def test_rotation_keys(self):
key1 = Fernet.generate_key()
fernet = Fernet(key1)
encrypted = fernet.encrypt(b'test')
self.assertEqual(fernet.decrypt(encrypted), b'test')

key2 = Fernet.generate_key()
with test.override_settings(SECURED_FIELDS_KEY=[key2, key1]):
fernet = get_fernet()
self.assertEqual(fernet.decrypt(encrypted), b'test')

encrypted_2 = fernet.encrypt(b'test')
self.assertEqual(fernet.decrypt(encrypted_2), b'test')

0 comments on commit 38e3d0e

Please sign in to comment.