Automatic field encryption and decryption
FabricioPatrocinio opened this issue · comments
Initial Checks
- I have searched Google & GitHub for similar requests and couldn't find anything
- I have read and followed the docs and still think this feature is missing
Description
Idea
Automatically perform encryption for data entry schemes with their natural values and another scheme being inherited from the previous one this time for data presentation. I have more or less a good idea of how to do it, I'll demonstrate it using your tool.
Example of the code I use today
from datetime import datetime
from uuid import UUID
from pydantic import EmailStr, Field, field_validator
from schemas import BaseSchema
from utils.cryptographic import decrypt, encrypt
class EnrollmentStudentRequestSchema(BaseSchema):
name: str
address: str
email: EmailStr
student_cpf: str = Field(min_length=11, max_length=14)
student_rg: str = Field(min_length=7, max_length=9)
grade: int
school_name: str
financial_responsible_name: str | None = None
financial_responsible_cpf: str | None = None
@field_validator(
"email",
"student_cpf",
"student_rg",
"financial_responsible_name",
"financial_responsible_cpf",
mode="after",
)
@classmethod
def parse_encrypt(cls, value: str | EmailStr) -> str | EmailStr:
return encrypt(value)
class EnrollmentStudentResponseSchema(EnrollmentStudentRequestSchema):
created_at: datetime
updated_at: datetime | None = None
@field_validator(
"email",
"student_cpf",
"student_rg",
"financial_responsible_name",
"financial_responsible_cpf",
mode="before",
)
@classmethod
def parse_decrypt(cls, value: str | EmailStr) -> str | EmailStr:
return decrypt(value)
I believe that this should already be embedded in the FIELD field, using the same logic that I showed above, you could provide a new parameter, which would receive an inheritance by composition with two arguments, Field(encryptar=True or False, encryption_key=ENCRYPTION_KEY)
or using a class to configure, example:
from datetime import datetime
from uuid import UUID
from pydantic import EmailStr, Field
from schemas import BaseSchema
class EnrollmentStudentRequestSchema(BaseSchema):
name: str
address: str
email: EmailStr = Field(encryptar=True, encryption_key=ENCRYPTION_KEY)
student_cpf: str = Field(min_length=11, max_length=14, encryptar=True, encryption_key=ENCRYPTION_KEY)
student_rg: str = Field(min_length=7, max_length=9, encryptar=True, encryption_key=ENCRYPTION_KEY)
grade: int
school_name: str
financial_responsible_name: str | None = Field(encryptar=True, encryption_key=ENCRYPTION_KEY)
financial_responsible_cpf: str | None = Field(encryptar=True, encryption_key=ENCRYPTION_KEY)
class EnrollmentStudentResponseSchema(EnrollmentStudentRequestSchema):
email: EmailStr = Field(encryptar=False, encryption_key=ENCRYPTION_KEY)
student_cpf: str = Field(min_length=11, max_length=14, encryptar=False, encryption_key=ENCRYPTION_KEY)
student_rg: str = Field(min_length=7, max_length=9, encryptar=False, encryption_key=ENCRYPTION_KEY)
financial_responsible_name: str | None = Field(encryptar=False, encryption_key=ENCRYPTION_KEY)
financial_responsible_cpf: str | None = Field(encryptar=False, encryption_key=ENCRYPTION_KEY)
created_at: datetime
updated_at: datetime | None = None
# ----------------------------- OR -----------------------------
class EnrollmentStudentRequestSchema(BaseSchema):
name: str
address: str
email: EmailStr
student_cpf: str = Field(min_length=11, max_length=14)
student_rg: str = Field(min_length=7, max_length=9)
grade: int
school_name: str
financial_responsible_name: str | None = None
financial_responsible_cpf: str | None = None
class Config:
encryptar_list = [
"email",
"student_cpf",
"student_rg",
"financial_responsible_name",
"financial_responsible_cpf",
] # Defaul null
encryptar = True
encryption_key = ENCRYPTION_KEY # If there are values in the config above, this must be mandatory
class EnrollmentStudentResponseSchema(EnrollmentStudentRequestSchema):
created_at: datetime
updated_at: datetime | None = None
class Config:
decrypt = True
I prefer using field ;) But they could make both available!
I hope you understand the idea, I can try to implement something like that one day and contribute with you! :)
My cryptographic functions
from cryptography.fernet import Fernet
from settings import settings
cipher_suite = Fernet(settings.ENCRYPTION_KEY.encode())
def encrypt(value: str) -> str:
v = cipher_suite.encrypt(value.encode())
return v.decode()
def decrypt(value: str) -> str:
v = cipher_suite.decrypt(value.encode())
return v.decode()
Affected Components
- Compatibility between releases
- Data validation/parsing
- Data serialization -
.model_dump()
and.model_dump_json()
- JSON Schema
- Dataclasses
- Model Config
- Field Types - adding or changing a particular data type
- Function validation decorator
- Generic Models
- Other Model behaviour -
model_construct()
, pickling, private attributes, ORM mode - Plugins and integration with other tools - mypy, FastAPI, python-devtools, Hypothesis, VS Code, PyCharm, etc.
This sounds cool!
Probably too specific to be included in Pydantic, especially in the Field
signature. Perhaps in pydantic-extra-types?
We could even receive the function itself instead of encryption_key, so everyone would use their chosen encryption lib. My point is that it should be as simple as possible to do end-to-end data encryption
Probably too specific to be included in Pydantic, especially in the Field signature. Perhaps in pydantic-extra-types?
I like @Viicos suggestion here. Moving this issue to pydantic-extra-types
.