File size: 3,186 Bytes
f8b03c1 | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 | """Module for encrypted."""
from __future__ import annotations
import base64
import logging
from typing import Any
from cryptography.fernet import Fernet, InvalidToken
from django.conf import settings
from django.db import models
logger = logging.getLogger(__name__)
class EncryptedTextField(models.TextField[str, str]):
prefix: str = "enc:v1:"
def __init__(self, *args: Any, **kwargs: Any) -> None:
super().__init__(*args, **kwargs)
def _resolve_keys(self) -> list[bytes]:
primary = getattr(settings, "CREDENTIAL_ENCRYPTION_KEY", None) or getattr(
settings, "SCRAPER_ENCRYPTION_KEY", None
)
previous = getattr(settings, "CREDENTIAL_ENCRYPTION_KEY_PREVIOUS", None) or getattr(
settings, "SCRAPER_ENCRYPTION_KEY_PREVIOUS", None
)
keys: list[bytes] = []
for k in (primary, previous):
if not k:
continue
if isinstance(k, str):
k = k.encode()
keys.append(k)
return keys
def _get_ciphers(self) -> list[Fernet]:
keys = self._resolve_keys()
if not keys:
raise RuntimeError("missing encryption key")
return [Fernet(k) for k in keys]
def _encrypt(self, plain_text: str) -> str:
cipher = self._get_ciphers()[0]
token = cipher.encrypt(plain_text.encode())
return f"{self.prefix}{base64.urlsafe_b64encode(token).decode()}"
def _decrypt(self, encrypted_value: str) -> str:
token_b64 = encrypted_value[len(self.prefix) :]
token = base64.urlsafe_b64decode(token_b64.encode())
last_error: Exception | None = None
for cipher in self._get_ciphers():
try:
decrypted: bytes = cipher.decrypt(token)
return decrypted.decode()
except Exception as e:
logger.debug("解密尝试失败,继续下一个密钥: %s", e)
last_error = e
continue
if last_error:
raise last_error
raise InvalidToken()
def get_prep_value(self, value: Any) -> Any:
plain_value = super().get_prep_value(value)
if plain_value is None or plain_value == "":
return plain_value
if isinstance(plain_value, str) and plain_value.startswith(self.prefix):
return plain_value
if not isinstance(plain_value, str):
plain_value = str(plain_value)
return self._encrypt(plain_value)
def to_python(self, value: Any) -> Any:
if value is None or value == "":
return value
if not isinstance(value, str):
value = str(value)
if not value.startswith(self.prefix):
return value
try:
return self._decrypt(value)
except (InvalidToken, ValueError):
if getattr(settings, "DEBUG", False):
logger.warning("解密失败,返回原始加密值(DEBUG 模式): %s...", value[:20])
return value
raise
def from_db_value(self, value: Any, expression: Any, connection: Any) -> Any:
return self.to_python(value)
|