pydantic / pydantic-extra-types

Extra Pydantic types.

Geek Repo:Geek Repo

Github PK Tool:Github PK Tool

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

commented

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.