Skip to main content

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 support
  • evervault: Evervault encryption support
  • sqlalchemy: Built-in SQLAlchemy integration
  • all: All optional dependencies
  • dev: 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 encryption
  • aws: AWS KMS
  • evervault: 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


Download files

Download the file for your platform. If you're not sure which to choose, learn more about installing packages.

Source Distribution

pydantic_encryption-0.3.2.tar.gz (14.7 kB view details)

Uploaded Source

Built Distribution

If you're not sure about the file name format, learn more about wheel file names.

pydantic_encryption-0.3.2-py3-none-any.whl (17.8 kB view details)

Uploaded Python 3

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

Hashes for pydantic_encryption-0.3.2.tar.gz
Algorithm Hash digest
SHA256 8f95b33d570709de55d600bc88931f53398794fddb5189da80f74a2637bd7e21
MD5 054a071073cdd4e763c8526a06068c7b
BLAKE2b-256 b3ad56ac01768d9bb41ef69e64d1b108a2b7027269a55a174463bc2605d2ddac

See more details on using hashes here.

Provenance

The following attestation bundles were made for pydantic_encryption-0.3.2.tar.gz:

Publisher: publish-to-pypi.yml on julien777z/pydantic-encryption

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

File details

Details for the file pydantic_encryption-0.3.2-py3-none-any.whl.

File metadata

File hashes

Hashes for pydantic_encryption-0.3.2-py3-none-any.whl
Algorithm Hash digest
SHA256 23b5f508bf5cee6081c579b8bfec4a693134eec5c94998c0458d78e91e89cf6a
MD5 316637b5a961c5fba64b4d447730871f
BLAKE2b-256 560d0754e23b63a2f93ed3457fe1d07598559f5eb1031476c9d860b8f71dd00c

See more details on using hashes here.

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

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

Supported by

AWS Cloud computing and Security Sponsor Datadog Monitoring Depot Continuous Integration Fastly CDN Google Download Analytics Pingdom Monitoring Sentry Error logging StatusPage Status page