0

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==

Tyler
  • 33
  • 5
  • I wanted to add to the above error I'm getting since it definitely helps with Diagnosing. Its an error earlier on that says `Invalid base64-encoded string: number of data characters (201) cannot be 1 more than a multiple of 4` So is this an issue when it's being encrypted rather than decrypted? – Tyler Aug 31 '23 at 16:58

1 Answers1

0

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.

Charlie G
  • 538
  • 5
  • 14
  • In postgres, if you write a query to decrypt the results, are you able to do so with the decrypt function? I'm interested in AES encrpytion provided by my custom class above for this reason. – Tyler Aug 31 '23 at 16:54