Skip to content

encryption

ormar gives you a way to encrypt only fields in a database. The encryption backend provided allows one-way encryption (HASH backend) as well as two-way encryption/decryption (FERNET backend).

!!!Warning Note that in order for encryption to work, you need to install optional encryption packages.

1
You can do it manually `pip install cryptography` or with ormar by `pip install ormar[crypto]`

!!! Warning Note that adding encrypt_backend changes the database column type to TEXT, which needs to be reflected in db either through migration (alembic) or manual changes

Define field encryption

To encrypt a field, you need to pass at least the encrypt_secret and encrypt_backend parameters.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
base_ormar_config = ormar.OrmarConfig(
    metadata=metadata
    database=database
)

class Filter(ormar.Model):
    ormar_config = base_ormar_config.copy()

    id: int = ormar.Integer(primary_key=True)
    name: str = ormar.String(max_length=100, 
                             encrypt_secret="secret123", 
                             encrypt_backend=ormar.EncryptBackends.FERNET)

!!!warns that you can encrypt all field types except primary_key columns and close series (ForeignKey and ManyToMany). Check backend details for more information.

Available backend

hash

HASH is a one-way hash (such as a password) that is never decrypted when retrieved

To set it, pass the appropriate backend value.

1
2
3
4
... # rest of model definition
password: str = ormar.String(max_length=128,
                         encrypt_secret="secret123", 
                         encrypt_backend=ormar.EncryptBackends.HASH)

Note that since this backend never decrypts stored values, it only applies to string fields. The hash used is a sha512 hash, so the field length must be>=128.

Warning Note that in the HASH backend, you can filter by full values, but filters like contains will not work because encrypted values are compared.

!!!note Note that the encryption_secret provided hashes itself first and acts as salt, so you need to recreate this step in order to compare it with the stored string. Order_by will not work because comparing encrypted strings, so you cannot sort reliably.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
class Hash(ormar.Model):
    ormar_config = base_ormar_config.copy(tablename="hashes")

    id: int = ormar.Integer(primary_key=True)
    name: str = ormar.String(max_length=128,
                             encrypt_secret="udxc32",
                             encrypt_backend=ormar.EncryptBackends.HASH)


await Hash(name='test1').save()

# note the steps to recreate the stored value
# you can use also cryptography package instead of hashlib
secret = hashlib.sha256("udxc32".encode()).digest()
secret = base64.urlsafe_b64encode(secret)
hashed_test1 = hashlib.sha512(secret + 'test1'.encode()).hexdigest()

# full value comparison works
hash1 = await Hash.objects.get(name='test1')
assert hash1.name == hashed_test1

# but partial comparison does not (hashed strings are compared)
with pytest.raises(NoMatch):
    await Filter.objects.get(name__icontains='test')

Fenette

FERNET is a bidirectional encryption/decryption backend

To set it, pass the appropriate backend value.

1
2
3
... # rest of model definition
year: int = ormar.Integer(encrypt_secret="secret123", 
                          encrypt_backend=ormar.EncryptBackends.FERNET)

Values are encrypted on the way to the database and decrypted on the way out. Can be used for all types because the return value is parsed to the corresponding python type.

!!! Warning Note that in the FERNET backend, you completely lose the possibility of filtering because part of the encrypted value is a timestamp. The same is true for order_by, because encrypted strings are compared, so you cannot sort reliably.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
class Filter(ormar.Model):
    ormar_config = base_ormar_config.copy()

    id: int = ormar.Integer(primary_key=True)
    name: str = ormar.String(max_length=100, 
                             encrypt_secret="asd123", 
                             encrypt_backend=ormar.EncryptBackends.FERNET)

await Filter(name='test1').save()
await Filter(name='test1').save()

# values are properly encrypted and later decrypted
filters = await Filter.objects.all()
assert filters[0].name == filters[1].name == 'test1'

# but you cannot filter at all since part of the fernet hash is a timestamp
# which means that even if you encrypt the same string 2 times it will be different
with pytest.raises(NoMatch):
    await Filter.objects.get(name='test1')

custom backend

If you want to support other types of encryption (i.e. AES), you can provide your own EncryptionBackend.

To set up the backend, all you need to do is subclass the ormar.fields.EncryptBackend class and provide the required backend.

An example virtual backend (which does nothing) might look like this:

1
2
3
4
5
6
7
8
9
class DummyBackend(ormar.fields.EncryptBackend):
    def _initialize_backend(self, secret_key: bytes) -> None:
        pass

    def encrypt(self, value: Any) -> str:
        return value

    def decrypt(self, value: Any) -> str:
        return value

To use this backend, set encrypt_backend to CUSTOM and provide your backend as a parameter through encrypt_custom_backend.

1
2
3
4
5
6
7
8
9
class Filter(ormar.Model):
    ormar_config = base_ormar_config.copy()

    id: int = ormar.Integer(primary_key=True)
    name: str = ormar.String(max_length=100, 
                             encrypt_secret="secret123", 
                             encrypt_backend=ormar.EncryptBackends.CUSTOM,
                             encrypt_custom_backend=DummyBackend
                             )