Skip to content
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

ADD crypto-fields #160

Open
wants to merge 19 commits into
base: master
Choose a base branch
from
Open

Conversation

kamil1marczak
Copy link

Hi I have added package of encrypted fields,

@codecov-commenter
Copy link

codecov-commenter commented Apr 24, 2021

Codecov Report

Merging #160 (714ecae) into master (252e681) will increase coverage by 0.01%.
The diff coverage is 93.16%.

Impacted file tree graph

@@            Coverage Diff             @@
##           master     #160      +/-   ##
==========================================
+ Coverage   93.02%   93.04%   +0.01%     
==========================================
  Files           9       11       +2     
  Lines         789      949     +160     
==========================================
+ Hits          734      883     +149     
- Misses         55       66      +11     
Impacted Files Coverage Δ
drf_extra_fields/crypto_fields.py 88.40% <88.40%> (ø)
tests/test_crypto_fields.py 96.70% <96.70%> (ø)
drf_extra_fields/runtests/settings.py 86.66% <100.00%> (ø)

Continue to review full report at Codecov.

Legend - Click here to learn more
Δ = absolute <relative> (impact), ø = not affected, ? = missing data
Powered by Codecov. Last update 252e681...714ecae. Read the comment docs.

@yigitguler
Copy link
Member

Hi @kamil1marczak, thank you for this detailed PR.

We will review it and get back to you.

Cryptography protocol used in mixin: Fernet (symmetric encryption) provided by Cryptography (pyca/cryptography)
"""

def __init__(self, salt_settings_env=None, password=None, *args, **kwargs):
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

salt_settings_env sounds like an environment variable but it's not actually. Why not this is directly provided like password? Why there is an inconsistency?

I suggest to have DEFAULT_CRYPTOGRAPHY_SALT and DEFAULT_CRYPTOGRAPHY_PASSWORD variables in settings.py. These must be defined to be able to use crypto fields. If salt or password is not provided along with field definition these will be used. What do you think?

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

in an updated version, I use Django SECRET_KEY as salt but I kept the password as an optional parameter to make it more dynamic (the whole idea is to have something static - salt and dynamic - password as encryption ingredients)

Copy link
Contributor

@omerfarukabaci omerfarukabaci left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thank you for your PR, we much appreciate it! However, I guess there is a misunderstanding here (and yes, I have realized it now, after reviewing your PR... 😓). This package includes extra fields for DRF (Django Rest Framework) Serializer fields, not for Django model fields (at least until now). I don't know what can/should we do about it. Any thoughts @alicertel?

README.md Outdated
Comment on lines 428 to 431
## CryptoField Package
A set of generic common Djano Fields that automatically encrypt data for database amd encrypt for the purpose of usage in Django.
* *CryptoFieldMixin* - Mixin that implement encrypt/decrypt methods.
Example of how to use to create custom CryptoTextField
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
## CryptoField Package
A set of generic common Djano Fields that automatically encrypt data for database amd encrypt for the purpose of usage in Django.
* *CryptoFieldMixin* - Mixin that implement encrypt/decrypt methods.
Example of how to use to create custom CryptoTextField
## CryptoField Package
A set of generic common Django Fields that automatically encrypts data for the database and for the purpose of usage in Django.
* *CryptoFieldMixin* - A mixin that implements encrypt/decrypt methods.
Example of how to use it to create the CustomCryptoTextField

README.md Outdated
Comment on lines 454 to 457
**Settings.**
each CryptoField has 2 kwargs *salt_settings_env* and *password*.
* *salt_settings_env* - name of variable stored in settings.py file, which will be used as cryptographic salt. default: salt_settings_env = 'SECRET_KEY' if settings not set or no SECRET_KEY in setting default: salt = "Salt123!!!"
* *password* - password to be used in encryption process of given field (together with salt set globally) default = 'password'
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
**Settings.**
each CryptoField has 2 kwargs *salt_settings_env* and *password*.
* *salt_settings_env* - name of variable stored in settings.py file, which will be used as cryptographic salt. default: salt_settings_env = 'SECRET_KEY' if settings not set or no SECRET_KEY in setting default: salt = "Salt123!!!"
* *password* - password to be used in encryption process of given field (together with salt set globally) default = 'password'
### Settings
Each CryptoField has 2 kwargs: `salt_settings_env` and `password`.
* `salt_settings_env`: Name of the variable stored in the settings.py file, which will be used as cryptographic salt.
**Default**: `"SECRET_KEY"`
**Note**: If `salt_settings_env` does not exist in `settings.py` then `salt` will be `Salt123!!!`
* `password`: Password to be used in the encryption process of the given field (together with salt set globally).
**Default**: `"password"`

README.md Outdated
Comment on lines 459 to 470
**Example.**

```python
# models.py

from django.db import models

from drf_extra_fields.crypto_field import *

class TestCryptoEmail(models.Model):

value = CryptoEmailField(salt_settings_env='NEW_SECRET_KEY', password='new_password')
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
**Example.**
```python
# models.py
from django.db import models
from drf_extra_fields.crypto_field import *
class TestCryptoEmail(models.Model):
value = CryptoEmailField(salt_settings_env='NEW_SECRET_KEY', password='new_password')
## Example
```python
# models.py
from django.db import models
from drf_extra_fields.crypto_field import CryptoEmailField
class User(models.Model):
encrypted_email = CryptoEmailField(salt_settings_env='NEW_SECRET_KEY', password='new_password')

Comment on lines 32 to 38
def to_bytes(_obj):
if isinstance(_obj, bytes):
return _obj
elif isinstance(_obj, (bytearray, memoryview)):
return force_bytes(_obj)
else:
return pickle.dumps(_obj)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

  1. This method doesn't work as expected for bytearray:
to_bytes(bytearray([1, 2, 3, 4, 5]))
# b"bytearray(b'\\x01\\x02\\x03\\x04\\x05')"

because force_bytes casts bytearray to str and then convert it to bytes. Here we don't need to use force_bytes, we can cast the object to bytes directly for bytearray and memoryview.

  1. What about calling this method _to_bytes(), since this is a specific method for crypto_field?

  2. What is the purpose of the underscore in _obj here? I guess using _to_bytes(obj) is fine.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In the new version, I use validators rather than force casting types

Comment on lines 1 to 15
try:
from functools import cached_property as property_decorator
except ImportError:
from builtins import property as property_decorator
import base64
import pickle
from django.utils.translation import gettext_lazy as _
from cryptography.fernet import Fernet
from cryptography.hazmat.primitives import hashes
from cryptography.hazmat.primitives.kdf.pbkdf2 import PBKDF2HMAC
from django.conf import settings
from django.core.checks import Error
from django.core.exceptions import ImproperlyConfigured
from django.db import models
from django.utils.encoding import force_bytes
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
try:
from functools import cached_property as property_decorator
except ImportError:
from builtins import property as property_decorator
import base64
import pickle
from django.utils.translation import gettext_lazy as _
from cryptography.fernet import Fernet
from cryptography.hazmat.primitives import hashes
from cryptography.hazmat.primitives.kdf.pbkdf2 import PBKDF2HMAC
from django.conf import settings
from django.core.checks import Error
from django.core.exceptions import ImproperlyConfigured
from django.db import models
from django.utils.encoding import force_bytes
try:
from functools import cached_property as property_decorator
except ImportError:
from builtins import property as property_decorator
import base64
import pickle
from cryptography.fernet import Fernet
from cryptography.hazmat.primitives import hashes
from cryptography.hazmat.primitives.kdf.pbkdf2 import PBKDF2HMAC
from django.conf import settings
from django.core.checks import Error
from django.core.exceptions import ImproperlyConfigured
from django.db import models
from django.utils.encoding import force_bytes
from django.utils.translation import gettext_lazy as _

Comment on lines 135 to 142
def get_db_prep_save(self, value, connection, prepared=False):
if self.empty_strings_allowed and value == bytes():
value = ""
value = super().get_db_prep_value(value, connection, prepared=False)
if value is not None:
encrypted_value = self.encrypt(value)
return encrypted_value
# return connection.Database.Binary(encrypted_value)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What about something like that?

Suggested change
def get_db_prep_save(self, value, connection, prepared=False):
if self.empty_strings_allowed and value == bytes():
value = ""
value = super().get_db_prep_value(value, connection, prepared=False)
if value is not None:
encrypted_value = self.encrypt(value)
return encrypted_value
# return connection.Database.Binary(encrypted_value)
def get_db_prep_save(self, value, connection, prepared=False):
if value:
value = self.encrypt(value)
return super().get_db_prep_value(value, connection, prepared)

requirements.txt Outdated
@@ -1,2 +1,3 @@
Django >= 2.2
djangorestframework >= 3.9.2
cryptography
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Could you please pin the version?

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

version 3.2.1 is the last with python 3.5 support therefore I have pined that one

)


class PersonTest(TestCase):
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
class PersonTest(TestCase):
class CryptoFieldsTests(TestCase):

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Also what about creating a model consisting of all crypto fields and making some insert & update & delete operations to make sure that the whole integration is working?

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I have tested in that matter in updated version

@@ -0,0 +1,100 @@
import datetime
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Could you please rename this file to test_crypto_fields.py?

@@ -0,0 +1,211 @@
try:
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Could you please rename this file to crypto_fields.py? It makes more sense while importing:

from drf_extra_fields.crypto_fields import CryptoTextField

@alicertel
Copy link
Contributor

Thank you for your PR, we much appreciate it! However, I guess there is a misunderstanding here (and yes, I have realized it now, after reviewing your PR... 😓). This package includes extra fields for DRF (Django Rest Framework) Serializer fields, not for Django model fields (at least until now). I don't know what can/should we do about it. Any thoughts @alicertel?

Good catch @omerfarukabaci . I didn't realize that. You are right our package includes only DRF fields. I believe this pr is more suitable to https://django-extensions.readthedocs.io/en/latest/field_extensions.html

@kamil1marczak
Copy link
Author

kamil1marczak commented May 12, 2021 via email

@kamil1marczak
Copy link
Author

@omerfarukabaci @alicertel everything has been rewritten to drf fields. I will be grateful for another review

@alicertel
Copy link
Contributor

Thanks @kamil1marczak I will review this within this week.

@kamil1marczak
Copy link
Author

Thanks @kamil1marczak I will review this within this week.

@alicertel hey any progress?

Copy link
Contributor

@alicertel alicertel left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks @kamil1marczak I will review this within this week.

@alicertel hey any progress?

Apologies for the late review. I've been reviewing your PR but it's not finished. I will send it anyway and continue reviewing this week.

@omerfarukabaci Could you please help me review this PR?

tests/models.py Show resolved Hide resolved
requirements.txt Outdated
@@ -1,2 +1,3 @@
Django >= 2.2
djangorestframework >= 3.9.2
cryptography==3.2.1
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

How about we make cryptography an optional dependency? Please see https://github.com/Hipo/drf-extra-fields/blob/master/setup.py#L19

Pillow is an optional dependency and it's only required if Base64ImageField is used.

We can raise an error if the cryptography is not installed when the one of the crypto fields are initialized. How that sounds?

Comment on lines 104 to 105
except InvalidToken:
return None
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why this is failing silently? There is something wrong here and this code block is hiding it. I think we should raise the error. What do you think?

value = super(CryptoCharField, self).to_internal_value(value)
if value:
return value.decode("utf-8")
self.fail("invalid")
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

README.md Outdated
Comment on lines 453 to 489
## CryptoBinaryField and CryptoCharField

+ A django-rest-framework fields for handling encryption through serialisation. Inputs are String object and internal
python representation is Binary object for CryptoBinaryField and String object for CryptoCharField

+ It takes the optional parameter `salt` (Django SECRET_KEY imported from setting as default). If set it use custom
cryptographic salt
+ It takes the optional parameter `password` ("Non_nobis1solum?nati!sumus" as default). If set it use a custom password
in encryption. **It is highly recommended to use custom one!!**
+ It takes the optional parameter `ttl` (None as default). If set it manage the number of seconds old a message may be
for it to be valid. If the message is older than ttl seconds (from the time it was originally created) field will
return None and encrypted message will not be enabled for decryption.

**Example**

```python
from rest_framework import serializers
from drf_extra_fields.crypto_fields import CryptoCharField


class CryptoSerializer(serializers.Serializer):
crypto_char = CryptoCharField()

```
**Example with parameters**
+ It takes custom salt and password. Once saved it will be aviable for decryption for 1000 seconds.

```python
from rest_framework import serializers
from drf_extra_fields.crypto_fields import CryptoCharField


class CryptoSerializer(serializers.Serializer):
crypto_char = CryptoCharField(salt="custom salt", password="custom password", ttl=1000)

```

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I am not sure why there are bunch of changes in the README file. Could you please revert them back and only have the documentation of the newly added fields.

I assume these changes caused by the IDE/Editor you are using.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Agreed, it might be because of auto formatting or something.

return decrypted_message.decode("utf-8")


class CryptoBinaryField(serializers.Field):
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Could you please tell me your use case for this field? I am curious about why you want to store binary data in DB (judging by your previous PR I am assuming these data will go to db)?

Copy link
Contributor

@omerfarukabaci omerfarukabaci left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thank you for the changes! 🚀 I have made some change requests.

README.md Outdated
Comment on lines 453 to 489
## CryptoBinaryField and CryptoCharField

+ A django-rest-framework fields for handling encryption through serialisation. Inputs are String object and internal
python representation is Binary object for CryptoBinaryField and String object for CryptoCharField

+ It takes the optional parameter `salt` (Django SECRET_KEY imported from setting as default). If set it use custom
cryptographic salt
+ It takes the optional parameter `password` ("Non_nobis1solum?nati!sumus" as default). If set it use a custom password
in encryption. **It is highly recommended to use custom one!!**
+ It takes the optional parameter `ttl` (None as default). If set it manage the number of seconds old a message may be
for it to be valid. If the message is older than ttl seconds (from the time it was originally created) field will
return None and encrypted message will not be enabled for decryption.

**Example**

```python
from rest_framework import serializers
from drf_extra_fields.crypto_fields import CryptoCharField


class CryptoSerializer(serializers.Serializer):
crypto_char = CryptoCharField()

```
**Example with parameters**
+ It takes custom salt and password. Once saved it will be aviable for decryption for 1000 seconds.

```python
from rest_framework import serializers
from drf_extra_fields.crypto_fields import CryptoCharField


class CryptoSerializer(serializers.Serializer):
crypto_char = CryptoCharField(salt="custom salt", password="custom password", ttl=1000)

```

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Agreed, it might be because of auto formatting or something.

README.md Outdated Show resolved Hide resolved
README.md Outdated Show resolved Hide resolved
README.md Outdated Show resolved Hide resolved
README.md Outdated Show resolved Hide resolved
tests/test_crypto_fields.py Outdated Show resolved Hide resolved
Comment on lines +26 to +27
class CryptoSerializer(serializers.Serializer):
message = CryptoBinaryField(required=False)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Let's call this BaseCryptoSerializer and create other serializers in the test methods using this:

def test_serialization_with_salt(self):
    class CryptoSerializer(BaseCryptoSerializer):
        message = CryptoBinaryField(password=..., salt=...)

tests/test_crypto_fields.py Outdated Show resolved Hide resolved
encrypted_message = _encrypt(token, message)
model_data = SaveCrypto(message=encrypted_message, created=now)
serializer = TtlCryptoSerializerSerializer(model_data)
time.sleep(3)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We cannot use time.sleep in tests, we should mock time.time.

created = serializers.DateTimeField()


class PointSerializerTest(TestCase):
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Let's make sure that every AssertionErrror and ValidationError is tested.

kamil1marczak and others added 8 commits August 27, 2021 19:57
Co-authored-by: Ömer Faruk Abacı <[email protected]>
Co-authored-by: Ömer Faruk Abacı <[email protected]>
Co-authored-by: Ömer Faruk Abacı <[email protected]>
Co-authored-by: Ömer Faruk Abacı <[email protected]>
Co-authored-by: Ömer Faruk Abacı <[email protected]>
Co-authored-by: Ömer Faruk Abacı <[email protected]>
Co-authored-by: Ömer Faruk Abacı <[email protected]>
kamil1marczak and others added 3 commits September 20, 2021 20:36
Co-authored-by: Ömer Faruk Abacı <[email protected]>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

5 participants