5

I'm playing around with a project that is python backend-based. I'll have Django for the "core" stuff and FastAPI for some crawlers. I'm encrpting some data to the DB with Django using the Fernet module and a custom Field.

class EncryptedField(models.CharField):
    description = "Save encrypted data to DB an read as string on application level."

    def __init__(self, *args, **kwargs):
        kwargs["max_length"] = 1000
        super().__init__(*args, **kwargs)

    @cached_property
    def fernet(self) -> Fernet:
        return Fernet(key=settings.FERNET_KEY)

    def get_internal_type(self) -> str:
        return "BinaryField"

    def get_db_prep_save(
        self, value: Any, connection: BaseDatabaseWrapper
    ) -> Union[memoryview, None]:
        value = super().get_db_prep_save(value, connection)
        if value is not None:
            encrypted_value = self.fernet.encrypt(data=force_bytes(s=value))
            return connection.Database.Binary(encrypted_value)

    def from_db_value(self, value: bytes, *args) -> Union[str, None]:
        if value is not None:
            decrypted_value = self.fernet.decrypt(token=force_bytes(s=value))
            return self.to_python(value=force_str(s=decrypted_value))

Everything work as expected, the problem is when I try to decrypt the value on FastAPI side:

def decrypt(value: bytes):
    return Fernet(FERNET_KEY).decrypt(token=value)

Some important information:

  • I double check and settings.FERNET_KEY == FERNET_KEY, ie, I'm using the same key on both sides.
  • Both services share the same DB and the function are receiving different values when reading for it.
    • Django -> from_db_value -> value -> b"gAAAAABhSm94ADjyQES3JL-EiEX4pH2odwJnJe2qsuGk_K685vseoVNN6kuoF9CRdf2GxiIViOgiKVcZMk5olg7FrJL2cmMFvg=="
    • FastAPI -> user.encrypted_field -> value -> b"pbkdf2_sha256$260000$RzIJ5Vg3Yx8JTz4y5ZHttZ$0z9CuQiPCJrBZqc/5DvxiEcbNHZpu8hAZgmibAe7nrQ=". I actually enter inside the DB and checked that this is the value stored there. user comes from here:
      • from sqlmodel import Session, select
        
        from .models import User
        
        
        async def get_user(db: Session, username: str) -> str:
           statement = select(User).where(User.username == username)
           return db.exec(statement).first()
        

So I'm wondering there's something before from_db_value that's converting the value somehow?!

One final alternative would be to decrypt the value on Django and send it directly to FastAPI, but I'd prefer not to do so.

How can I decrypt the value on FastAPI?

Murilo Sitonio
  • 270
  • 7
  • 30
  • I think you could return the `force_str(s=decrypted_value)` directly in `from_db_value` as it's a python `str` object there. Also, I don't know that I would have `get_internal_type` return `"BinaryField'`. IMO, I'll leave that unimplemented. The custom class is afterall subclassing `CharField` – Oluwafemi Sule Sep 22 '21 at 14:59
  • @OluwafemiSule Yeah I think both suggestions would work, but they are not related to the actual problem... – Murilo Sitonio Sep 22 '21 at 16:24
  • One of this is indeed related. Marking the internal type value as BinaryField when you mean to store it as a string means that depending on the backing service it gets stored as the type supported there. For example, in [postgresql](https://github.com/django/django/blob/ca9872905559026af82000e46cde6f7dedc897b6/django/db/backends/postgresql/base.py#L75) this is stored as [`bytea`](https://www.db-fiddle.com/f/djdHbvCssCtkp2kU39AsiV/0) and you lose the original value stored there when you read it. – Oluwafemi Sule Sep 22 '21 at 19:19
  • @OluwafemiSule But I'm not losing the original value: I can read it perfectly in django. – Murilo Sitonio Sep 23 '21 at 11:16
  • I could not reproduce the hashed value (`pbkdf2_sha256$260000$...`) in DB. I get `gAAAAA...` as expected. – aaron Sep 28 '21 at 06:25

2 Answers2

1

It is enough to decode the value:

def decrypt(value: bytes):
    return Fernet(FERNET_KEY).decrypt(token=value)

Are you sure you are trying to decrypt the same data? To me, this looks like an encrypted password field and some other field.

mysql> select first_name, password from users_user where login='test'
+------------------------------------------------------------------------------------------------------+------------------------------------------------------------------------------------------+
| first_name                                                                                           | password                                                                                 |
| gAAAAABhSm94ADjyQES3JL-EiEX4pH2odwJnJe2qsuGk_K685vseoVNN6kuoF9CRdf2GxiIViOgiKVcZMk5olg7FrJL2cmMFvg== | pbkdf2_sha256$260000$RzIJ5Vg3Yx8JTz4y5ZHttZ$0z9CuQiPCJrBZqc/5DvxiEcbNHZpu8hAZgmibAe7nrQ= |
kjaw
  • 515
  • 2
  • 9
  • You can try `Fernet(FERNET_KEY).decrypt(token=b'gAAAAABhSm94ADjyQES3JL-EiEX4pH2odwJnJe2qsuGk_K685vseoVNN6kuoF9CRdf2GxiIViOgiKVcZMk5olg7FrJL2cmMFvg==')` to make sure – kjaw Oct 01 '21 at 12:49
0

In FastAPI you don't force bytes.

def decrypt(value: bytes):
    return Fernet(FERNET_KEY).decrypt(token=value)

#instead do
def decrypt(value: bytes):
    return Fernet(FERNET_KEY).decrypt(token=force_bytes(s=value))

Then the decryption function on Django and FastAPI will look the same. force_bytes might be the place where data is getting changed.

I would recommend you check the force bytes function. Show us the code in force bytes function if possible.

codefire
  • 393
  • 1
  • 3
  • 13
  • this could also depend on your models used in FastAPI. The result you get is a hash as denoted by pbkdf2_sha256 in the beginning. – codefire Oct 01 '21 at 12:05
  • `force_bytes` it's a django built-in function (`from django.utils.encoding import force_bytes`). About the FastAPI model, I'm using `SecretBytes` typing. Would you recommend something different? – Murilo Sitonio Oct 01 '21 at 13:07
  • then I suggest to not use force_bytes function. Instead use a helper function that checks if an objects is of type bytes. If it is not bytes, then convert it to string using str() and then using python builtin function bytes(string, 'utf8') obtain the bytes. This will mean your data pipelines remain identical in django and fastapi. Can you try that and see if it helps. I don't see another reason why this shouldn't work identically as long as your data is not getting mangled in database or during database fetch. Also see if database fetch value is same in django and fastapi before decryption. – codefire Oct 01 '21 at 13:19
  • If input can be guaranteed to be same and functions used are identical, then both should provide exact same output. 1. Make sure to log and see data before decryption on both django and fastapi. if data before decryption is different, that is the problem 2. If that is not the problem, then change and see the output of force bytes. You dont need to do that when decrypting in django since you have already forced bytes when encrypting. I have a feeling FastAPI is showing correct decrypted output unless you are sure "gAAA" is the correct output. – codefire Oct 01 '21 at 13:20
  • What is the schema type of encrypted data storage column stored? Depending on that and model fields used, the data before decryption might change in Django and FastAPI. – codefire Oct 01 '21 at 13:26