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
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.
There is a package that I have used successfully for encrypted fields.
django-encrypted-model-fields
My model looks like this:
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.