Metadata-Version: 2.3
Name: pydantic_encryption
Version: 0.0.2a9
Summary: Encryption and hashing models for Pydantic
Author: Julien Kmec
Author-email: me@julien.dev
Requires-Python: >=3.10,<4.0.0
Classifier: Programming Language :: Python :: 3
Classifier: Programming Language :: Python :: 3.10
Classifier: Programming Language :: Python :: 3.11
Classifier: Programming Language :: Python :: 3.12
Classifier: Programming Language :: Python :: 3.13
Provides-Extra: all
Provides-Extra: generics
Provides-Extra: test
Requires-Dist: argon2-cffi (>=23.1.0,<24.0.0)
Requires-Dist: boto3 (>=1.38.3,<2.0.0)
Requires-Dist: coverage (>=7.8.0) ; extra == "all"
Requires-Dist: coverage (>=7.8.0) ; extra == "test"
Requires-Dist: evervault (>=4.4.1)
Requires-Dist: psycopg2-binary (>=2.9.10,<3.0.0) ; extra == "all"
Requires-Dist: psycopg2-binary (>=2.9.10,<3.0.0) ; extra == "test"
Requires-Dist: pydantic (>=2.10.6)
Requires-Dist: pydantic-settings (>=2.9.1)
Requires-Dist: pytest (>=8.3.5) ; extra == "all"
Requires-Dist: pytest (>=8.3.5) ; extra == "test"
Requires-Dist: pytest-asyncio (>=0.26.0,<0.27.0) ; extra == "all"
Requires-Dist: pytest-asyncio (>=0.26.0,<0.27.0) ; extra == "test"
Requires-Dist: pytest-cov (>=6.1.1,<7.0.0) ; extra == "all"
Requires-Dist: pytest-cov (>=6.1.1,<7.0.0) ; extra == "test"
Requires-Dist: pytest-docker (>=3.2.1,<4.0.0) ; extra == "all"
Requires-Dist: pytest-docker (>=3.2.1,<4.0.0) ; extra == "test"
Requires-Dist: pytest-env (>=1.1.5,<2.0.0) ; extra == "all"
Requires-Dist: pytest-env (>=1.1.5,<2.0.0) ; extra == "test"
Requires-Dist: pytest-sqlalchemy (>=0.3.0,<0.4.0) ; extra == "all"
Requires-Dist: pytest-sqlalchemy (>=0.3.0,<0.4.0) ; extra == "test"
Requires-Dist: python-generics (>=0.2.3) ; extra == "all"
Requires-Dist: python-generics (>=0.2.3) ; extra == "generics"
Requires-Dist: sqlalchemy (>=2.0.40,<3.0.0)
Requires-Dist: sqlalchemy-utils (>=0.41.2,<0.42.0) ; extra == "all"
Requires-Dist: sqlalchemy-utils (>=0.41.2,<0.42.0) ; extra == "test"
Requires-Dist: sqlmodel (>=0.0.24,<0.0.25) ; extra == "all"
Requires-Dist: sqlmodel (>=0.0.24,<0.0.25) ; extra == "test"
Description-Content-Type: text/markdown

# Encryption and hashing models for Pydantic

This package provides Pydantic field annotations that encrypt, decrypt, and hash field values.

## Installation

Install with Pip:
```bash
pip install pydantic_encryption[all]
```

### Optional extras

- `generics`: Support for generics
- `sqlalchemy`: Built-in SQLAlchemy integration
- `test`: Test dependencies
- `all`: All optional extras

## Features

- Encrypt and decrypt specific fields
- Hash specific fields
- Built-in SQLAlchemy integration
- Support for Fernet symmetric encryption and Evervault
- Support for generics

## Example

```python
from pydantic_encryption import BaseModel, Encrypt, Hash, Annotated

class User(BaseModel):
    name: str
    address: Annotated[str, Encrypt] # This field will be encrypted
    password: Annotated[str, 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 `SQLAlchemyEncryptedString` type and the hashing of fields with the `SQLAlchemyHashedString` 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

```python
from pydantic_encryption import SQLAlchemyEncryptedString, SQLAlchemyHashedString, EncryptionMethod
from sqlalchemy import Column, String, Integer
from sqlalchemy.orm import declarative_base
from sqlalchemy import create_engine
from sqlalchemy.orm import sessionmaker

Base = declarative_base()

# Define our schema
class User(Base):
    __tablename__ = "users"

    id = Column(Integer, primary_key=True)
    username = Column(String)
    email = Column(SQLAlchemyEncryptedString(encryption_method=EncryptionMethod.FERNET)) # This field will be encrypted in the database
    password = Column(SQLAlchemyHashedString()) # This field will be hashed in the database

# Create the database
engine = create_engine("sqlite:///:memory:")
Base.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
```

You can also use the `@sqlalchemy_table(...)` decorator to automatically convert `Encrypt` and `Hash` annotations to `SQLAlchemyEncryptedString` and `SQLAlchemyHashedString` types.
Make sure to inherit from `pydantic_encryption.BaseModel` or if inherting from `pydantic_encryption.SecureModel`, make sure to follow [Custom Encryption or Hashing](https://github.com/julien777z/pydantic-encryption?tab=readme-ov-file#custom-encryption-or-hashing).

### Example:

```python
from typing import Annotated
from pydantic_encryption import EncryptionMethod, BaseModel, Encrypt, Hash, sqlalchemy_table
from sqlmodel import SQLModel, Field
import uuid

class Base(SQLModel, table=False):
    """Base model."""

    id: uuid.UUID = Field(default_factory=uuid.uuid4, primary_key=True)

@sqlalchemy_table(use_encryption_method=EncryptionMethod.FERNET)
class User(
    Base,
    BaseModel,
    table=True,
):
    """
    Managed User model. The `Encrypt` and `Hash` annotations are automatically converted to
    `SQLAlchemyEncryptedString` and `SQLAlchemyHashedString` types.
    """

    __tablename__ = "users"

    username: str = Field(default=None)
    email: Annotated[str, Encrypt]
    password: Annotated[str, Hash]
```

## Choose an Encryption Method

You can choose which encryption method to use by setting the `use_encryption_method` parameter in the class definition.

### Example:

```python
from typing import Annotated
from pydantic_encryption import EncryptionMethod, BaseModel, Encrypt

class User(BaseModel, use_encryption_method=EncryptionMethod.EVERVAULT):
    name: str
    address: Annotated[str, Encrypt] # This field will be encrypted by Evervault
```

### 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:

```bash
openssl rand -base64 32
```

Then set the following environment variable or add it to your `.env` file:

```bash
ENCRYPTION_KEY=your_encryption_key
```

### Evervault

You can optionally use [Evervault](https://evervault.com/) to encrypt and decrypt fields.

Set the `use_encryption_method` parameter to `EncryptionMethod.EVERVAULT`.

You need to set the following environment variables or add them to your `.env` file:

```bash
EVERVAULT_APP_ID=your_app_id
EVERVAULT_API_KEY=your_api_key
EVERVAULT_ENCRYPTION_ROLE=your_encryption_role
```

### 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:

```python
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:

```python
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[str, 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`.

```python
from typing import Annotated
from pydantic_encryption import Encrypt, BaseModel

class User(BaseModel):
    name: str
    address: Annotated[str, 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`.

```python
from typing import Annotated
from pydantic_encryption import Decrypt, BaseModel

class UserResponse(BaseModel):
    name: str
    address: Annotated[str, 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.


## Hashing

You can hash sensitive data like passwords by using the `Hash` annotation.

```python
from typing import Annotated
from pydantic_encryption import Hash, BaseModel

class User(BaseModel):
    username: str
    password: Annotated[str, 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 bcrypt during model initialization.

## Disable Auto Processing

You can disable automatic encryption/decryption/hashing by setting `disable` to `True` in the class definition.

```python
from typing import Annotated
from pydantic_encryption import Encrypt, BaseModel

class UserResponse(BaseModel, disable=True):
    name: str
    address: Annotated[str, 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.

To use generics, you must install this package with the `generics` extra: `pip install pydantic_encryption[generics]`.

```py
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](https://python-poetry.org/docs/) and run:

```bash
poetry install --with test
poetry run coverage run -m pytest -v -s
```

## 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.

