| 24 | |
| 25 | @dataclass |
| 26 | class EncryptedString: |
| 27 | cipher: bytes |
| 28 | salt: bytes |
| 29 | _fernet: Fernet | None = field(default=None, compare=False, repr=False) |
| 30 | |
| 31 | secret_key: ClassVar[bytes] = settings.SECRET_KEY.encode() |
| 32 | |
| 33 | def __post_init__(self): |
| 34 | if self._fernet is None: |
| 35 | self._fernet = self.get_fernet(base64.urlsafe_b64decode(self.salt)) |
| 36 | |
| 37 | def __len__(self) -> int: |
| 38 | return len(self.cipher) |
| 39 | |
| 40 | @classmethod |
| 41 | def from_plain_text(cls, message: str | bytes, salt: bytes = b""): |
| 42 | message = message.encode() if isinstance(message, str) else message |
| 43 | salt = salt or os.urandom(16) |
| 44 | fernet = cls.get_fernet(salt) |
| 45 | cipher = fernet.encrypt(message) |
| 46 | return cls(cipher, base64.urlsafe_b64encode(salt), fernet) |
| 47 | |
| 48 | @classmethod |
| 49 | def get_fernet(cls, salt: bytes) -> Fernet: |
| 50 | if isinstance(salt, str): |
| 51 | salt = salt.encode() |
| 52 | kdf = PBKDF2HMAC(algorithm=hashes.SHA256(), length=32, salt=salt, iterations=480000, backend=default_backend()) |
| 53 | return Fernet(base64.urlsafe_b64encode(kdf.derive(cls.secret_key))) |
| 54 | |
| 55 | def decrypt(self) -> str: |
| 56 | return self._fernet.decrypt(self.cipher).decode() |
| 57 | |
| 58 | def serialize(self) -> str: |
| 59 | return f"${self.salt.decode()}${self.cipher.decode()}$" |
| 60 | |
| 61 | @classmethod |
| 62 | def deserialize(cls, value: str): |
| 63 | salt, cipher = value.strip("$").split("$") |
| 64 | return cls(cipher.encode(), salt.encode()) |
| 65 | |
| 66 | |
| 67 | class EncryptedObject(EncryptedString): |