skip to Main Content

This is my custom field that I call in a model. Migrations succeed and I’m able to insert an encrypted value into the data table. However, it will not decrypt.

Here’s the custom field class:

class SecureString(CharField):
    """Custom Encrypted Field"""

    #kdf = X963KDF(algorithm=hashes.SHA256(),
                    # length=32,
                     #sharedinfo=None,
                    # backend=default_backend())

    key = bytes(settings.FERNET_KEY,'utf-8')
    f = Fernet(key)

    def from_db_value(self, value, expression, connection):

        return self.f.decrypt(str.encode(value))

    def get_prep_value(self, value):

        return self.f.encrypt(bytes(value, 'utf-8'))

And here is the error when trying to retrieve values from the data table:

File /usr/local/lib/python3.8/site-packages/IPython/core/formatters.py:706, in PlainTextFormatter.__call__(self, obj)
    699 stream = StringIO()
    700 printer = pretty.RepresentationPrinter(stream, self.verbose,
    701     self.max_width, self.newline,
    702     max_seq_length=self.max_seq_length,
    703     singleton_pprinters=self.singleton_printers,
    704     type_pprinters=self.type_printers,
    705     deferred_pprinters=self.deferred_printers)
--> 706 printer.pretty(obj)
    707 printer.flush()
    708 return stream.getvalue()

File /usr/local/lib/python3.8/site-packages/IPython/lib/pretty.py:410, in RepresentationPrinter.pretty(self, obj)
    407                         return meth(obj, self, cycle)
    408                 if cls is not object 
    409                         and callable(cls.__dict__.get('__repr__')):
--> 410                     return _repr_pprint(obj, self, cycle)
    412     return _default_pprint(obj, self, cycle)
    413 finally:

File /usr/local/lib/python3.8/site-packages/IPython/lib/pretty.py:778, in _repr_pprint(obj, p, cycle)
    776 """A pprint that just redirects to the normal repr function."""
    777 # Find newlines and replace them with p.break_()
--> 778 output = repr(obj)
    779 lines = output.splitlines()
    780 with p.group():

File /usr/local/lib/python3.8/site-packages/django/db/models/query.py:370, in QuerySet.__repr__(self)
    369 def __repr__(self):
--> 370     data = list(self[: REPR_OUTPUT_SIZE + 1])
    371     if len(data) > REPR_OUTPUT_SIZE:
    372         data[-1] = "...(remaining elements truncated)..."

File /usr/local/lib/python3.8/site-packages/django/db/models/query.py:376, in QuerySet.__len__(self)
    375 def __len__(self):
--> 376     self._fetch_all()
    377     return len(self._result_cache)

File /usr/local/lib/python3.8/site-packages/django/db/models/query.py:1867, in QuerySet._fetch_all(self)
   1865 def _fetch_all(self):
   1866     if self._result_cache is None:
-> 1867         self._result_cache = list(self._iterable_class(self))
   1868     if self._prefetch_related_lookups and not self._prefetch_done:
   1869         self._prefetch_related_objects()

File /usr/local/lib/python3.8/site-packages/django/db/models/query.py:204, in ValuesIterable.__iter__(self)
    198 names = [
    199     *query.extra_select,
    200     *query.values_select,
    201     *query.annotation_select,
    202 ]
    203 indexes = range(len(names))
--> 204 for row in compiler.results_iter(
    205     chunked_fetch=self.chunked_fetch, chunk_size=self.chunk_size
    206 ):
    207     yield {names[i]: row[i] for i in indexes}

File /usr/local/lib/python3.8/site-packages/django/db/models/sql/compiler.py:1336, in SQLCompiler.apply_converters(self, rows, converters)
   1334     value = row[pos]
   1335     for converter in convs:
-> 1336         value = converter(value, expression, connection)
   1337     row[pos] = value
   1338 yield row

File /code/server/identity/model_mixins.py:61, in SecureString.from_db_value(self, value, expression, connection)
     59 key = bytes(settings.FERNET_KEY, 'utf-8')
     60 f = Fernet(key)
---> 61 return f.decrypt(str.encode(value))

File /usr/local/lib/python3.8/site-packages/cryptography/fernet.py:86, in Fernet.decrypt(self, token, ttl)
     83 def decrypt(
     84     self, token: typing.Union[bytes, str], ttl: typing.Optional[int] = None
     85 ) -> bytes:
---> 86     timestamp, data = Fernet._get_unverified_token_data(token)
     87     if ttl is None:
     88         time_info = None

File /usr/local/lib/python3.8/site-packages/cryptography/fernet.py:119, in Fernet._get_unverified_token_data(token)
    117     data = base64.urlsafe_b64decode(token)
    118 except (TypeError, binascii.Error):
--> 119     raise InvalidToken
    121 if not data or data[0] != 0x80:
    122     raise InvalidToken

InvalidToken: 

Been scratching my head at this for hours. Works perfectly fine in a standalone python script. Is django doing something to the fernet key upon decrypting I’m unaware of?

Database is Postgres 12.4

EDIT:

The database value appears as this: x674141414141426b384d34596e535f6d6a6b3173556b5344455f57585a58326851374b35576c56536938676d3358556165515a767a677a754f6c4b7a646c613276397644455874675276795971724b794e616744596f6d516d78694a5167335334654d526b784d495156566d7344575242776e3639487143515044334f676e65334e524268624a5742786f544258336f435930675f6c646c6c524a44714b56466d4f7836575f5f6c5772556e4a6d4f3372325372636b33536a6a346d7a536c56415635386f474c44596d646962774c42384e4e4d645657464a3743567a55597854673d3d

while the encrypted value with a standalone encryption with fernet shows this (same key used):
gAAAAABk8M1BhMeOF10wcEZ7U6zb_vQpaQ8zHxlDuGLyFCA6JQu0NSYfshulqorntWQS4OF7PAyyQ7BCJZ2r0QNt7e8FBZbjTQ==

2

Answers


  1. Chosen as BEST ANSWER

    I know not the most secure, but entire database is encrypted at rest as is. This is to obfuscate data from other employees at the company. I know CBC is a better AES mode.

    Using AES encryption, I can write encrypted values and read them from Django this way. I can also decrypt with pgcrypto extension in postgres with a sql query. Still working on an issue with that which will be posted as another question.

    class EncryptField(models.BinaryField):
    description = "Encrypted column"
    
    def __init__(self, *args, **kwargs):
        kwargs['max_length'] = 255
        kwargs['blank'] = True
        kwargs['null'] = True
        super().__init__(*args, **kwargs)
    
    def deconstruct(self):
        name, path, args, kwargs = super().deconstruct()
        del kwargs["max_length"]
        del kwargs["blank"]
        del kwargs["null"]
        return name, path, args, kwargs
    
    def get_prep_value(self, value):
        if value is None:
            return value
        enc_secret = AES.new(settings.AES_KEY.encode("utf-8"), AES.MODE_ECB)
        cipher_text = base64.b64encode(enc_secret.encrypt(pad(value.encode('utf-8'), AES.block_size)))
        return cipher_text
    
    def from_db_value(self, value, expression, connection):
        if value is None:
            return value
        raw = base64.b64decode(value)
        dec_secret = AES.new(settings.AES_KEY.encode('utf-8'),AES.MODE_ECB)
        clear_val = unpad(dec_secret.decrypt(raw), AES.block_size)
        return clear_val
    
    def to_python(self, value):
        if isinstance(value, Identity):
            return value
        if value is None:
            return value
        dec_secret = AES.new(settings.AES_KEY.encode('utf-8'),AES.MODE_ECB)
        clear_val = unpad(dec_secret.decrypt(value), AES.block_size)
        return clear_val
    

  2. There is a package that I have used successfully for encrypted fields. django-encrypted-model-fields

    My model looks like this:

    from django.db import models
    from encrypted_model_fields.fields import EncryptedCharField
    
    
    class MyModel(models.Model):
        name = models.CharField(max_length=50, unique=True)
        password = EncryptedCharField(max_length=100)
    
        def __str__(self):
            return self.name

    And add in settings.py

    INSTALLED_APPS = [
    ….
    ‘encrypted_model_fields’,

    I set the encryption key with the environment variable FIELD_ENCRYPTION_KEY

    I do not need to do anything else.

    Login or Signup to reply.
Please signup or login to give your own answer.
Back To Top
Search