Source code for codegen_database.types.encrypted
from __future__ import annotations
import base64
import hashlib
from typing import TYPE_CHECKING, Any
from sqlalchemy import Text
from sqlalchemy import types as sa_types
if TYPE_CHECKING:
from collections.abc import Callable
from cryptography.fernet import Fernet
def _load_fernet() -> type[Fernet]:
try:
# Deferred on purpose (not a circular import): cryptography is
# the optional ``encrypted`` extra, and this module is imported
# by the package root -- a top-level import would make plain
# ``import codegen_database`` require it.
from cryptography.fernet import Fernet # noqa: PLC0415
except ImportError as exc: # pragma: no cover -- env-dependent
msg = (
"EncryptedText requires the 'cryptography' package -- "
"install the extra: pip install 'codegen-database[encrypted]'"
)
raise ImportError(msg) from exc
return Fernet
[docs]
class EncryptedText(sa_types.TypeDecorator[str]):
impl = Text
cache_ok = True
def __init__(self, key: str | Callable[[], str]) -> None:
self.key = key
super().__init__()
def _fernet(self) -> Fernet:
fernet_cls = _load_fernet()
material = self.key() if callable(self.key) else self.key
digest = hashlib.sha256(material.encode("utf-8")).digest()
return fernet_cls(base64.urlsafe_b64encode(digest))
[docs]
def process_bind_param(
self,
value: Any, # noqa: ANN401
dialect: Any, # noqa: ANN401, ARG002
) -> str | None:
if value is None:
return None
return (
self._fernet()
.encrypt(str(value).encode("utf-8"))
.decode(
"ascii",
)
)
[docs]
def process_result_value(
self,
value: Any, # noqa: ANN401
dialect: Any, # noqa: ANN401, ARG002
) -> str | None:
if value is None:
return None
return self._fernet().decrypt(value.encode("ascii")).decode("utf-8")