Encryption and Hashing Models for Pydantic
Project description
Encryption and Hashing Models for Pydantic
This package provides Pydantic field annotations that encrypt, decrypt, and hash field values.
Installation
Install with pip:
pip install pydantic_encryption
Install with Poetry:
poetry add pydantic_encryption
Optional extras
aws: AWS KMS encryption supportevervault: Evervault encryption supportsqlalchemy: Built-in SQLAlchemy integrationall: All optional dependenciesdev: Development and test dependencies
# Install with specific extras
pip install "pydantic_encryption[sqlalchemy]"
pip install "pydantic_encryption[aws]"
pip install "pydantic_encryption[all]"
Features
- 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
Example
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) # hashed
SQLAlchemy Integration
If 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.
Example:
import uuid
from pydantic_encryption.integrations.sqlalchemy 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) # hashed
Supported Types
SQLAlchemyEncrypted automatically detects and preserves the Python type of your data:
| Type | Example |
|---|---|
str |
"hello@example.com" |
bytes |
b"binary data" |
bool |
True, False |
int |
42 |
float |
3.14159 |
Decimal |
Decimal("99999.99") |
UUID |
UUID("12345678-1234-5678-1234-567812345678") |
date |
date(1990, 5, 15) |
datetime |
datetime(2025, 1, 21, 14, 30, 45) |
time |
time(14, 30, 45) |
timedelta |
timedelta(hours=2, minutes=30) |
import uuid
from datetime import date, datetime, time, timedelta
from decimal import Decimal
class User(Base, table=True):
email: str = Field(sa_type=SQLAlchemyEncrypted())
birth_date: date = Field(sa_type=SQLAlchemyEncrypted())
last_login: datetime = Field(sa_type=SQLAlchemyEncrypted())
age: int = Field(sa_type=SQLAlchemyEncrypted())
balance: float = Field(sa_type=SQLAlchemyEncrypted())
salary: Decimal = Field(sa_type=SQLAlchemyEncrypted())
external_id: uuid.UUID = Field(sa_type=SQLAlchemyEncrypted())
login_time: time = Field(sa_type=SQLAlchemyEncrypted())
session_duration: timedelta = Field(sa_type=SQLAlchemyEncrypted())
Choose an Encryption Method
You 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.
Example:
.env
ENCRYPTION_METHOD=aws
AWS_KMS_KEY_ARN=123
AWS_KMS_REGION=us-east-1
AWS_KMS_ACCESS_KEY_ID=123
AWS_KMS_SECRET_ACCESS_KEY=123
from 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 KMS
Separate Encrypt/Decrypt Keys (AWS KMS)
You can use different KMS keys for encryption and decryption by setting separate ARNs:
.env
ENCRYPTION_METHOD=aws
AWS_KMS_ENCRYPT_KEY_ARN=arn:aws:kms:us-east-1:123456789:key/encrypt-key-id
AWS_KMS_DECRYPT_KEY_ARN=arn:aws:kms:us-east-1:123456789:key/decrypt-key-id
AWS_KMS_REGION=us-east-1
AWS_KMS_ACCESS_KEY_ID=123
AWS_KMS_SECRET_ACCESS_KEY=123
For read-only scenarios where you only need to decrypt data, you can specify just the decrypt key:
.env
ENCRYPTION_METHOD=aws
AWS_KMS_DECRYPT_KEY_ARN=arn:aws:kms:us-east-1:123456789:key/decrypt-key-id
AWS_KMS_REGION=us-east-1
AWS_KMS_ACCESS_KEY_ID=123
AWS_KMS_SECRET_ACCESS_KEY=123
Note: You cannot mix AWS_KMS_KEY_ARN with the separate key settings. Use either the global key or the separate encrypt/decrypt keys. If you specify AWS_KMS_ENCRYPT_KEY_ARN, you must also specify AWS_KMS_DECRYPT_KEY_ARN.
Default Encryption (Fernet Symmetric Encryption)
By 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 32
Then set the following environment variable or add it to your .env file:
ENCRYPTION_KEY=your_encryption_key
Custom Encryption or Hashing
You 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) # encrypted
Encryption
You 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.
Decryption
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.
Hashing
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 value
Fields marked with Hash are automatically hashed using Argon2 during model initialization.
Disable Auto Processing
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 hash
Generics
Each BaseModel has an additional helpful method that will tell you its generic type.
from pydantic_encryption import BaseModel
class MyModel[T](BaseModel):
value: T
model = MyModel[str](value="Hello")
print(model.get_type()) # <class 'str'>
Run Tests
Install Poetry and run:
poetry install --all-extras
poetry run pytest -v
Roadmap
This 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
Feature Requests
If you have any feature requests, please open an issue.
Project details
Release history Release notifications | RSS feed
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
Built Distribution
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
File details
Details for the file pydantic_encryption-0.3.2.tar.gz.
File metadata
- Download URL: pydantic_encryption-0.3.2.tar.gz
- Upload date:
- Size: 14.7 kB
- Tags: Source
- Uploaded using Trusted Publishing? Yes
- Uploaded via: twine/6.1.0 CPython/3.13.7
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
8f95b33d570709de55d600bc88931f53398794fddb5189da80f74a2637bd7e21
|
|
| MD5 |
054a071073cdd4e763c8526a06068c7b
|
|
| BLAKE2b-256 |
b3ad56ac01768d9bb41ef69e64d1b108a2b7027269a55a174463bc2605d2ddac
|
Provenance
The following attestation bundles were made for pydantic_encryption-0.3.2.tar.gz:
Publisher:
publish-to-pypi.yml on julien777z/pydantic-encryption
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
pydantic_encryption-0.3.2.tar.gz -
Subject digest:
8f95b33d570709de55d600bc88931f53398794fddb5189da80f74a2637bd7e21 - Sigstore transparency entry: 1059139886
- Sigstore integration time:
-
Permalink:
julien777z/pydantic-encryption@bb48b4fb9160c0cdf51c488bef88dacb9d7ec4ec -
Branch / Tag:
refs/heads/main - Owner: https://github.com/julien777z
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
publish-to-pypi.yml@bb48b4fb9160c0cdf51c488bef88dacb9d7ec4ec -
Trigger Event:
workflow_dispatch
-
Statement type:
File details
Details for the file pydantic_encryption-0.3.2-py3-none-any.whl.
File metadata
- Download URL: pydantic_encryption-0.3.2-py3-none-any.whl
- Upload date:
- Size: 17.8 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? Yes
- Uploaded via: twine/6.1.0 CPython/3.13.7
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
23b5f508bf5cee6081c579b8bfec4a693134eec5c94998c0458d78e91e89cf6a
|
|
| MD5 |
316637b5a961c5fba64b4d447730871f
|
|
| BLAKE2b-256 |
560d0754e23b63a2f93ed3457fe1d07598559f5eb1031476c9d860b8f71dd00c
|
Provenance
The following attestation bundles were made for pydantic_encryption-0.3.2-py3-none-any.whl:
Publisher:
publish-to-pypi.yml on julien777z/pydantic-encryption
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
pydantic_encryption-0.3.2-py3-none-any.whl -
Subject digest:
23b5f508bf5cee6081c579b8bfec4a693134eec5c94998c0458d78e91e89cf6a - Sigstore transparency entry: 1059139888
- Sigstore integration time:
-
Permalink:
julien777z/pydantic-encryption@bb48b4fb9160c0cdf51c488bef88dacb9d7ec4ec -
Branch / Tag:
refs/heads/main - Owner: https://github.com/julien777z
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
publish-to-pypi.yml@bb48b4fb9160c0cdf51c488bef88dacb9d7ec4ec -
Trigger Event:
workflow_dispatch
-
Statement type: