This package provides Pydantic field annotations that encrypt, decrypt, and hash field values.
Install with Pip:
pip install "pydantic_encryption[all]"Install with Poetry:
poetry add pydantic_encryption -E allgenerics: Support for genericssqlalchemy: Built-in SQLAlchemy integrationtest: Test dependenciesall: All optional extras
- Encrypt and decrypt specific fields
- Hash specific fields
- Built-in SQLAlchemy integration
- Support for AWS KMS (Key Management Service) single-region
- Support for Fernet symmetric encryption and Evervault
- Support for generics
from typing import Annotated
from pydantic_encryption import BaseModel, Encrypt, Hash
class User(BaseModel):
name: str
address: Annotated[bytes, Encrypt] # This field will be encrypted
password: Annotated[bytes, Hash] # This field will be hashed
user = User(name="John Doe", address="123456", password="secret123")
print(user.name) # plaintext (untouched)
print(user.address) # encrypted
print(user.password) # hashedIf you install this package with the sqlalchemy extra, you can use the built-in SQLAlchemy integration for the columns.
SQLAlchemy will automatically handle the encryption/decryption of fields with the SQLAlchemyEncrypted type and the hashing of fields with the SQLAlchemyHashed type.
When you create a new instance of the model, the fields will be encrypted and when you query the database, the fields will be decrypted.
import uuid
from pydantic_encryption import SQLAlchemyEncrypted, SQLAlchemyHashed
from sqlmodel import SQLModel, Field
from sqlalchemy import create_engine
from sqlalchemy.orm import sessionmaker
# Define our schema
class User(Base, table=True):
__tablename__ = "users"
username: str = Field(default=None)
email: bytes = Field(
default=None,
sa_type=SQLAlchemyEncrypted(),
)
password: bytes = Field(
sa_type=SQLAlchemyHashed(),
nullable=False,
)
# Create the database
engine = create_engine("sqlite:///:memory:")
SQLModel.metadata.create_all(engine)
Session = sessionmaker(bind=engine)
session = Session()
# Create a user
user = User(username="john_doe", email="john@example.com", password="secret123") # The email and password will be encrypted/hashed automatically
session.add(user)
session.commit()
# Query the user
user = session.query(User).filter_by(username="john_doe").first()
print(user.email) # decrypted
print(user.password) # hashedYou can choose which encryption algorithm to use by setting the ENCRYPTION_METHOD environment variable.
Valid values are:
fernet: Fernet symmetric encryptionaws: AWS KMSevervault: Evervault
See config.py for the possible environment variables.
.env
ENCRYPTION_METHOD=aws
AWS_KMS_KEY_ARN=123
AWS_KMS_REGION=us-east-1
AWS_ACCESS_KEY_ID=123
AWS_SECRET_ACCESS_KEY=123from typing import Annotated
from pydantic_encryption import BaseModel, Encrypt
class User(BaseModel):
name: str
address: Annotated[bytes, Encrypt] # This field will be encrypted by AWS KMSBy default, Fernet will be used for encryption and decryption.
First you need to generate an encryption key. You can use the following command:
openssl rand -base64 32Then set the following environment variable or add it to your .env file:
ENCRYPTION_KEY=your_encryption_keyYou can define your own encryption or hashing methods by subclassing SecureModel. SecureModel provides you with the utilities to handle encryption, decryption, and hashing.
self.pending_encryption_fields, self.pending_decryption_fields, and self.pending_hash_fields are dictionaries of field names to field values that need to be encrypted, decrypted, or hashed, i.e., fields annotated with Encrypt, Decrypt, or Hash.
You can override the encrypt_data, decrypt_data, and hash_data methods to implement your own encryption, decryption, and hashing logic. You then need to override model_post_init to call these methods or use the default implementation accessible via self.default_post_init().
First, define a custom secure model:
from typing import Any, override
from pydantic import BaseModel as PydanticBaseModel
from pydantic_encryption import SecureModel
class MySecureModel(PydanticBaseModel, SecureModel):
@override
def encrypt_data(self) -> None:
# Your encryption logic here
pass
@override
def decrypt_data(self) -> None:
# Your decryption logic here
pass
@override
def hash_data(self) -> None:
# Your hashing logic here
pass
@override
def model_post_init(self, context: Any, /) -> None:
# Either define your own logic, for example:
# if not self._disable:
# if self.pending_decryption_fields:
# self.decrypt_data()
# if self.pending_encryption_fields:
# self.encrypt_data()
# if self.pending_hash_fields:
# self.hash_data()
# Or use the default logic:
self.default_post_init()
super().model_post_init(context)Then use it:
from typing import Annotated
from pydantic import BaseModel # Here, we don't use the BaseModel provided by the library, but the native one from Pydantic
from pydantic_encryption import Encrypt
class MyModel(BaseModel, MySecureModel):
username: str
address: Annotated[bytes, Encrypt]
model = MyModel(username="john_doe", address="123456")
print(model.address) # encryptedYou can encrypt any field by using the Encrypt annotation with Annotated and inheriting from BaseModel.
from typing import Annotated
from pydantic_encryption import Encrypt, BaseModel
class User(BaseModel):
name: str
address: Annotated[bytes, Encrypt] # This field will be encrypted
user = User(name="John Doe", address="123456")
print(user.address) # encrypted
print(user.name) # plaintext (untouched)The fields marked with Encrypt are automatically encrypted during model initialization.
Similar to encryption, you can decrypt any field by using the Decrypt annotation with Annotated and inheriting from BaseModel.
from typing import Annotated
from pydantic_encryption import Decrypt, BaseModel
class UserResponse(BaseModel):
name: str
address: Annotated[bytes, Decrypt] # This field will be decrypted
user = UserResponse(**user_data) # encrypted value
print(user.address) # decrypted
print(user.name) # plaintext (untouched)Fields marked with Decrypt are automatically decrypted during model initialization.
Note: if you use SQLAlchemyEncrypted, then the value will be decrypted automatically when you query the database.
You can hash sensitive data like passwords by using the Hash annotation.
from typing import Annotated
from pydantic_encryption import Hash, BaseModel
class User(BaseModel):
username: str
password: Annotated[bytes, Hash] # This field will be hashed
user = User(username="john_doe", password="secret123")
print(user.password) # hashed valueFields marked with Hash are automatically hashed using bcrypt during model initialization.
You can disable automatic encryption/decryption/hashing by setting disable to True in the class definition.
from typing import Annotated
from pydantic_encryption import Encrypt, BaseModel
class UserResponse(BaseModel, disable=True):
name: str
address: Annotated[bytes, Encrypt]
# To encrypt/decrypt/hash, call the respective methods manually:
user = UserResponse(name="John Doe", address="123 Main St")
# Manual encryption
user.encrypt_data()
print(user.address) # encrypted
# Or user.decrypt_data() to decrypt and user.hash_data() to hashEach BaseModel has an additional helpful method that will tell you its generic type.
To use generics, you must install this package with the generics extra: pip install pydantic_encryption[generics].
from pydantic_encryption import BaseModel
class MyModel[T](BaseModel):
value: T
model = MyModel[str](value="Hello")
print(model.get_type()) # <class 'str'>Install Poetry and run:
poetry install --with test
poetry run coverage run -m pytest -v -sThis is an early development version. I am considering the following features:
- Add optional support for other encryption providers beyond Evervault
- Add support for AWS KMS and other key management services
- Native encryption via PostgreSQL and other databases
- Specifying encryption key per table or row instead of globally
If you have any feature requests, please open an issue.