pydantic-encryption 0.0.2__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- pydantic_encryption-0.0.2/LICENSE +21 -0
- pydantic_encryption-0.0.2/PKG-INFO +361 -0
- pydantic_encryption-0.0.2/README.md +327 -0
- pydantic_encryption-0.0.2/pydantic_encryption/__init__.py +3 -0
- pydantic_encryption-0.0.2/pydantic_encryption/annotations.py +29 -0
- pydantic_encryption-0.0.2/pydantic_encryption/config.py +33 -0
- pydantic_encryption-0.0.2/pydantic_encryption/lib/__init__.py +2 -0
- pydantic_encryption-0.0.2/pydantic_encryption/lib/adapters/encryption/__init__.py +3 -0
- pydantic_encryption-0.0.2/pydantic_encryption/lib/adapters/encryption/aws.py +91 -0
- pydantic_encryption-0.0.2/pydantic_encryption/lib/adapters/encryption/evervault.py +47 -0
- pydantic_encryption-0.0.2/pydantic_encryption/lib/adapters/encryption/fernet.py +56 -0
- pydantic_encryption-0.0.2/pydantic_encryption/lib/adapters/hashing/__init__.py +1 -0
- pydantic_encryption-0.0.2/pydantic_encryption/lib/adapters/hashing/argon2.py +20 -0
- pydantic_encryption-0.0.2/pydantic_encryption/models/__init__.py +3 -0
- pydantic_encryption-0.0.2/pydantic_encryption/models/adapters/__init__.py +4 -0
- pydantic_encryption-0.0.2/pydantic_encryption/models/adapters/sqlalchemy.py +143 -0
- pydantic_encryption-0.0.2/pydantic_encryption/models/base.py +16 -0
- pydantic_encryption-0.0.2/pydantic_encryption/models/encryptable.py +30 -0
- pydantic_encryption-0.0.2/pydantic_encryption/models/secure_model.py +202 -0
- pydantic_encryption-0.0.2/pyproject.toml +57 -0
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2025 Julien Kmec
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
|
@@ -0,0 +1,361 @@
|
|
|
1
|
+
Metadata-Version: 2.3
|
|
2
|
+
Name: pydantic_encryption
|
|
3
|
+
Version: 0.0.2
|
|
4
|
+
Summary: Encryption and Hashing Models for Pydantic
|
|
5
|
+
Author: Julien Kmec
|
|
6
|
+
Author-email: me@julien.dev
|
|
7
|
+
Requires-Python: >=3.11.0,<3.14
|
|
8
|
+
Classifier: Programming Language :: Python :: 3
|
|
9
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
10
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
11
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
12
|
+
Provides-Extra: dev
|
|
13
|
+
Provides-Extra: sqlalchemy
|
|
14
|
+
Requires-Dist: argon2-cffi (>=23.1.0,<24.0.0)
|
|
15
|
+
Requires-Dist: aws-encryption-sdk[mpl] (>=4.0.1,<5.0.0)
|
|
16
|
+
Requires-Dist: boto3 (>=1.38.8,<2.0.0)
|
|
17
|
+
Requires-Dist: coverage (>=7.8.0) ; extra == "dev"
|
|
18
|
+
Requires-Dist: evervault (>=4.4.1)
|
|
19
|
+
Requires-Dist: psycopg2-binary (>=2.9.10,<3.0.0) ; extra == "dev"
|
|
20
|
+
Requires-Dist: pydantic (>=2.10.6)
|
|
21
|
+
Requires-Dist: pydantic-settings (>=2.9.1)
|
|
22
|
+
Requires-Dist: pydantic-super-model (>=1.0.2,<2.0.0)
|
|
23
|
+
Requires-Dist: pytest (>=8.3.5) ; extra == "dev"
|
|
24
|
+
Requires-Dist: pytest-asyncio (>=0.26.0,<0.27.0) ; extra == "dev"
|
|
25
|
+
Requires-Dist: pytest-cov (>=6.1.1,<7.0.0) ; extra == "dev"
|
|
26
|
+
Requires-Dist: pytest-docker (>=3.2.1,<4.0.0) ; extra == "dev"
|
|
27
|
+
Requires-Dist: pytest-env (>=1.1.5,<2.0.0) ; extra == "dev"
|
|
28
|
+
Requires-Dist: pytest-sqlalchemy (>=0.3.0,<0.4.0) ; extra == "dev"
|
|
29
|
+
Requires-Dist: sqlalchemy (>=2.0.40,<3.0.0) ; extra == "sqlalchemy"
|
|
30
|
+
Requires-Dist: sqlalchemy-utils (>=0.41.2,<0.42.0) ; extra == "dev"
|
|
31
|
+
Requires-Dist: sqlmodel (>=0.0.24,<0.0.25) ; extra == "sqlalchemy"
|
|
32
|
+
Description-Content-Type: text/markdown
|
|
33
|
+
|
|
34
|
+
# Encryption and Hashing Models for Pydantic
|
|
35
|
+
|
|
36
|
+
This package provides Pydantic field annotations that encrypt, decrypt, and hash field values.
|
|
37
|
+
|
|
38
|
+
## Installation
|
|
39
|
+
|
|
40
|
+
Install with [pip](https://pip.pypa.io/en/stable/):
|
|
41
|
+
```bash
|
|
42
|
+
pip install "pydantic_encryption[sqlalchemy]"
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
Install with [Poetry](https://python-poetry.org/docs/):
|
|
46
|
+
```bash
|
|
47
|
+
poetry add pydantic_encryption --E sqlalchemy
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
### Optional extras
|
|
51
|
+
|
|
52
|
+
- `sqlalchemy`: Built-in SQLAlchemy integration
|
|
53
|
+
- `dev`: Development and test dependencies
|
|
54
|
+
|
|
55
|
+
## Features
|
|
56
|
+
|
|
57
|
+
- Encrypt and decrypt specific fields
|
|
58
|
+
- Hash specific fields
|
|
59
|
+
- Built-in SQLAlchemy integration
|
|
60
|
+
- Support for AWS KMS (Key Management Service) single-region
|
|
61
|
+
- Support for Fernet symmetric encryption and Evervault
|
|
62
|
+
- Support for generics
|
|
63
|
+
|
|
64
|
+
## Example
|
|
65
|
+
|
|
66
|
+
```python
|
|
67
|
+
from typing import Annotated
|
|
68
|
+
from pydantic_encryption import BaseModel, Encrypt, Hash
|
|
69
|
+
|
|
70
|
+
class User(BaseModel):
|
|
71
|
+
name: str
|
|
72
|
+
address: Annotated[bytes, Encrypt] # This field will be encrypted
|
|
73
|
+
password: Annotated[bytes, Hash] # This field will be hashed
|
|
74
|
+
|
|
75
|
+
user = User(name="John Doe", address="123456", password="secret123")
|
|
76
|
+
|
|
77
|
+
print(user.name) # plaintext (untouched)
|
|
78
|
+
print(user.address) # encrypted
|
|
79
|
+
print(user.password) # hashed
|
|
80
|
+
```
|
|
81
|
+
|
|
82
|
+
## SQLAlchemy Integration
|
|
83
|
+
|
|
84
|
+
If you install this package with the `sqlalchemy` extra, you can use the built-in SQLAlchemy integration for the columns.
|
|
85
|
+
|
|
86
|
+
SQLAlchemy will automatically handle the encryption/decryption of fields with the `SQLAlchemyEncrypted` type and the hashing of fields with the `SQLAlchemyHashed` type.
|
|
87
|
+
|
|
88
|
+
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.
|
|
89
|
+
|
|
90
|
+
### Example:
|
|
91
|
+
|
|
92
|
+
```python
|
|
93
|
+
import uuid
|
|
94
|
+
from pydantic_encryption import SQLAlchemyEncrypted, SQLAlchemyHashed
|
|
95
|
+
from sqlmodel import SQLModel, Field
|
|
96
|
+
from sqlalchemy import create_engine
|
|
97
|
+
from sqlalchemy.orm import sessionmaker
|
|
98
|
+
|
|
99
|
+
# Define our schema
|
|
100
|
+
class User(Base, table=True):
|
|
101
|
+
__tablename__ = "users"
|
|
102
|
+
|
|
103
|
+
username: str = Field(default=None)
|
|
104
|
+
email: bytes = Field(
|
|
105
|
+
default=None,
|
|
106
|
+
sa_type=SQLAlchemyEncrypted(),
|
|
107
|
+
)
|
|
108
|
+
password: bytes = Field(
|
|
109
|
+
sa_type=SQLAlchemyHashed(),
|
|
110
|
+
nullable=False,
|
|
111
|
+
)
|
|
112
|
+
|
|
113
|
+
# Create the database
|
|
114
|
+
engine = create_engine("sqlite:///:memory:")
|
|
115
|
+
SQLModel.metadata.create_all(engine)
|
|
116
|
+
Session = sessionmaker(bind=engine)
|
|
117
|
+
session = Session()
|
|
118
|
+
|
|
119
|
+
# Create a user
|
|
120
|
+
user = User(username="john_doe", email="john@example.com", password="secret123") # The email and password will be encrypted/hashed automatically
|
|
121
|
+
|
|
122
|
+
session.add(user)
|
|
123
|
+
session.commit()
|
|
124
|
+
|
|
125
|
+
# Query the user
|
|
126
|
+
user = session.query(User).filter_by(username="john_doe").first()
|
|
127
|
+
|
|
128
|
+
print(user.email) # decrypted
|
|
129
|
+
print(user.password) # hashed
|
|
130
|
+
```
|
|
131
|
+
|
|
132
|
+
## Choose an Encryption Method
|
|
133
|
+
|
|
134
|
+
You can choose which encryption algorithm to use by setting the `ENCRYPTION_METHOD` environment variable.
|
|
135
|
+
|
|
136
|
+
Valid values are:
|
|
137
|
+
- `fernet`: Fernet symmetric encryption
|
|
138
|
+
- `aws`: AWS KMS
|
|
139
|
+
- `evervault`: [Evervault](https://evervault.com/)
|
|
140
|
+
|
|
141
|
+
See [config.py](https://github.com/julien777z/pydantic-encryption/blob/main/pydantic_encryption/config.py) for the possible environment variables.
|
|
142
|
+
|
|
143
|
+
### Example:
|
|
144
|
+
|
|
145
|
+
`.env`
|
|
146
|
+
```env
|
|
147
|
+
ENCRYPTION_METHOD=aws
|
|
148
|
+
AWS_KMS_KEY_ARN=123
|
|
149
|
+
AWS_KMS_REGION=us-east-1
|
|
150
|
+
AWS_ACCESS_KEY_ID=123
|
|
151
|
+
AWS_SECRET_ACCESS_KEY=123
|
|
152
|
+
```
|
|
153
|
+
|
|
154
|
+
```python
|
|
155
|
+
from typing import Annotated
|
|
156
|
+
from pydantic_encryption import BaseModel, Encrypt
|
|
157
|
+
|
|
158
|
+
class User(BaseModel):
|
|
159
|
+
name: str
|
|
160
|
+
address: Annotated[bytes, Encrypt] # This field will be encrypted by AWS KMS
|
|
161
|
+
```
|
|
162
|
+
|
|
163
|
+
### Default Encryption (Fernet Symmetric Encryption)
|
|
164
|
+
|
|
165
|
+
By default, Fernet will be used for encryption and decryption.
|
|
166
|
+
|
|
167
|
+
First you need to generate an encryption key. You can use the following command:
|
|
168
|
+
|
|
169
|
+
```bash
|
|
170
|
+
openssl rand -base64 32
|
|
171
|
+
```
|
|
172
|
+
|
|
173
|
+
Then set the following environment variable or add it to your `.env` file:
|
|
174
|
+
|
|
175
|
+
```bash
|
|
176
|
+
ENCRYPTION_KEY=your_encryption_key
|
|
177
|
+
```
|
|
178
|
+
|
|
179
|
+
### Custom Encryption or Hashing
|
|
180
|
+
|
|
181
|
+
You can define your own encryption or hashing methods by subclassing `SecureModel`. `SecureModel` provides you with the utilities to handle encryption, decryption, and hashing.
|
|
182
|
+
|
|
183
|
+
`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`.
|
|
184
|
+
|
|
185
|
+
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()`.
|
|
186
|
+
|
|
187
|
+
First, define a custom secure model:
|
|
188
|
+
|
|
189
|
+
```python
|
|
190
|
+
from typing import Any, override
|
|
191
|
+
from pydantic import BaseModel as PydanticBaseModel
|
|
192
|
+
from pydantic_encryption import SecureModel
|
|
193
|
+
|
|
194
|
+
class MySecureModel(PydanticBaseModel, SecureModel):
|
|
195
|
+
@override
|
|
196
|
+
def encrypt_data(self) -> None:
|
|
197
|
+
# Your encryption logic here
|
|
198
|
+
pass
|
|
199
|
+
|
|
200
|
+
@override
|
|
201
|
+
def decrypt_data(self) -> None:
|
|
202
|
+
# Your decryption logic here
|
|
203
|
+
pass
|
|
204
|
+
|
|
205
|
+
@override
|
|
206
|
+
def hash_data(self) -> None:
|
|
207
|
+
# Your hashing logic here
|
|
208
|
+
pass
|
|
209
|
+
|
|
210
|
+
@override
|
|
211
|
+
def model_post_init(self, context: Any, /) -> None:
|
|
212
|
+
# Either define your own logic, for example:
|
|
213
|
+
|
|
214
|
+
# if not self._disable:
|
|
215
|
+
# if self.pending_decryption_fields:
|
|
216
|
+
# self.decrypt_data()
|
|
217
|
+
|
|
218
|
+
# if self.pending_encryption_fields:
|
|
219
|
+
# self.encrypt_data()
|
|
220
|
+
|
|
221
|
+
# if self.pending_hash_fields:
|
|
222
|
+
# self.hash_data()
|
|
223
|
+
|
|
224
|
+
# Or use the default logic:
|
|
225
|
+
self.default_post_init()
|
|
226
|
+
|
|
227
|
+
super().model_post_init(context)
|
|
228
|
+
```
|
|
229
|
+
|
|
230
|
+
Then use it:
|
|
231
|
+
|
|
232
|
+
```python
|
|
233
|
+
from typing import Annotated
|
|
234
|
+
from pydantic import BaseModel # Here, we don't use the BaseModel provided by the library, but the native one from Pydantic
|
|
235
|
+
from pydantic_encryption import Encrypt
|
|
236
|
+
|
|
237
|
+
class MyModel(BaseModel, MySecureModel):
|
|
238
|
+
username: str
|
|
239
|
+
address: Annotated[bytes, Encrypt]
|
|
240
|
+
|
|
241
|
+
model = MyModel(username="john_doe", address="123456")
|
|
242
|
+
print(model.address) # encrypted
|
|
243
|
+
```
|
|
244
|
+
|
|
245
|
+
## Encryption
|
|
246
|
+
|
|
247
|
+
You can encrypt any field by using the `Encrypt` annotation with `Annotated` and inheriting from `BaseModel`.
|
|
248
|
+
|
|
249
|
+
```python
|
|
250
|
+
from typing import Annotated
|
|
251
|
+
from pydantic_encryption import Encrypt, BaseModel
|
|
252
|
+
|
|
253
|
+
class User(BaseModel):
|
|
254
|
+
name: str
|
|
255
|
+
address: Annotated[bytes, Encrypt] # This field will be encrypted
|
|
256
|
+
|
|
257
|
+
user = User(name="John Doe", address="123456")
|
|
258
|
+
print(user.address) # encrypted
|
|
259
|
+
print(user.name) # plaintext (untouched)
|
|
260
|
+
```
|
|
261
|
+
|
|
262
|
+
The fields marked with `Encrypt` are automatically encrypted during model initialization.
|
|
263
|
+
|
|
264
|
+
## Decryption
|
|
265
|
+
|
|
266
|
+
Similar to encryption, you can decrypt any field by using the `Decrypt` annotation with `Annotated` and inheriting from `BaseModel`.
|
|
267
|
+
|
|
268
|
+
```python
|
|
269
|
+
from typing import Annotated
|
|
270
|
+
from pydantic_encryption import Decrypt, BaseModel
|
|
271
|
+
|
|
272
|
+
class UserResponse(BaseModel):
|
|
273
|
+
name: str
|
|
274
|
+
address: Annotated[bytes, Decrypt] # This field will be decrypted
|
|
275
|
+
|
|
276
|
+
user = UserResponse(**user_data) # encrypted value
|
|
277
|
+
print(user.address) # decrypted
|
|
278
|
+
print(user.name) # plaintext (untouched)
|
|
279
|
+
```
|
|
280
|
+
|
|
281
|
+
Fields marked with `Decrypt` are automatically decrypted during model initialization.
|
|
282
|
+
|
|
283
|
+
Note: if you use `SQLAlchemyEncrypted`, then the value will be decrypted automatically when you query the database.
|
|
284
|
+
|
|
285
|
+
|
|
286
|
+
## Hashing
|
|
287
|
+
|
|
288
|
+
You can hash sensitive data like passwords by using the `Hash` annotation.
|
|
289
|
+
|
|
290
|
+
```python
|
|
291
|
+
from typing import Annotated
|
|
292
|
+
from pydantic_encryption import Hash, BaseModel
|
|
293
|
+
|
|
294
|
+
class User(BaseModel):
|
|
295
|
+
username: str
|
|
296
|
+
password: Annotated[bytes, Hash] # This field will be hashed
|
|
297
|
+
|
|
298
|
+
user = User(username="john_doe", password="secret123")
|
|
299
|
+
print(user.password) # hashed value
|
|
300
|
+
```
|
|
301
|
+
|
|
302
|
+
Fields marked with `Hash` are automatically hashed using bcrypt during model initialization.
|
|
303
|
+
|
|
304
|
+
## Disable Auto Processing
|
|
305
|
+
|
|
306
|
+
You can disable automatic encryption/decryption/hashing by setting `disable` to `True` in the class definition.
|
|
307
|
+
|
|
308
|
+
```python
|
|
309
|
+
from typing import Annotated
|
|
310
|
+
from pydantic_encryption import Encrypt, BaseModel
|
|
311
|
+
|
|
312
|
+
class UserResponse(BaseModel, disable=True):
|
|
313
|
+
name: str
|
|
314
|
+
address: Annotated[bytes, Encrypt]
|
|
315
|
+
|
|
316
|
+
# To encrypt/decrypt/hash, call the respective methods manually:
|
|
317
|
+
user = UserResponse(name="John Doe", address="123 Main St")
|
|
318
|
+
|
|
319
|
+
# Manual encryption
|
|
320
|
+
user.encrypt_data()
|
|
321
|
+
print(user.address) # encrypted
|
|
322
|
+
|
|
323
|
+
# Or user.decrypt_data() to decrypt and user.hash_data() to hash
|
|
324
|
+
```
|
|
325
|
+
|
|
326
|
+
## Generics
|
|
327
|
+
|
|
328
|
+
Each BaseModel has an additional helpful method that will tell you its generic type.
|
|
329
|
+
|
|
330
|
+
```py
|
|
331
|
+
from pydantic_encryption import BaseModel
|
|
332
|
+
|
|
333
|
+
class MyModel[T](BaseModel):
|
|
334
|
+
value: T
|
|
335
|
+
|
|
336
|
+
model = MyModel[str](value="Hello")
|
|
337
|
+
print(model.get_type()) # <class 'str'>
|
|
338
|
+
```
|
|
339
|
+
|
|
340
|
+
## Run Tests
|
|
341
|
+
|
|
342
|
+
Install [Poetry](https://python-poetry.org/docs/) and run:
|
|
343
|
+
|
|
344
|
+
```bash
|
|
345
|
+
poetry install --with test
|
|
346
|
+
poetry run coverage run -m pytest -v -s
|
|
347
|
+
```
|
|
348
|
+
|
|
349
|
+
## Roadmap
|
|
350
|
+
|
|
351
|
+
This is an early development version. I am considering the following features:
|
|
352
|
+
|
|
353
|
+
- [ ] Add optional support for other encryption providers beyond Evervault
|
|
354
|
+
- [x] Add support for AWS KMS and other key management services
|
|
355
|
+
- [ ] Native encryption via PostgreSQL and other databases
|
|
356
|
+
- [ ] Specifying encryption key per table or row instead of globally
|
|
357
|
+
|
|
358
|
+
## Feature Requests
|
|
359
|
+
|
|
360
|
+
If you have any feature requests, please open an issue.
|
|
361
|
+
|
|
@@ -0,0 +1,327 @@
|
|
|
1
|
+
# Encryption and Hashing Models for Pydantic
|
|
2
|
+
|
|
3
|
+
This package provides Pydantic field annotations that encrypt, decrypt, and hash field values.
|
|
4
|
+
|
|
5
|
+
## Installation
|
|
6
|
+
|
|
7
|
+
Install with [pip](https://pip.pypa.io/en/stable/):
|
|
8
|
+
```bash
|
|
9
|
+
pip install "pydantic_encryption[sqlalchemy]"
|
|
10
|
+
```
|
|
11
|
+
|
|
12
|
+
Install with [Poetry](https://python-poetry.org/docs/):
|
|
13
|
+
```bash
|
|
14
|
+
poetry add pydantic_encryption --E sqlalchemy
|
|
15
|
+
```
|
|
16
|
+
|
|
17
|
+
### Optional extras
|
|
18
|
+
|
|
19
|
+
- `sqlalchemy`: Built-in SQLAlchemy integration
|
|
20
|
+
- `dev`: Development and test dependencies
|
|
21
|
+
|
|
22
|
+
## Features
|
|
23
|
+
|
|
24
|
+
- Encrypt and decrypt specific fields
|
|
25
|
+
- Hash specific fields
|
|
26
|
+
- Built-in SQLAlchemy integration
|
|
27
|
+
- Support for AWS KMS (Key Management Service) single-region
|
|
28
|
+
- Support for Fernet symmetric encryption and Evervault
|
|
29
|
+
- Support for generics
|
|
30
|
+
|
|
31
|
+
## Example
|
|
32
|
+
|
|
33
|
+
```python
|
|
34
|
+
from typing import Annotated
|
|
35
|
+
from pydantic_encryption import BaseModel, Encrypt, Hash
|
|
36
|
+
|
|
37
|
+
class User(BaseModel):
|
|
38
|
+
name: str
|
|
39
|
+
address: Annotated[bytes, Encrypt] # This field will be encrypted
|
|
40
|
+
password: Annotated[bytes, Hash] # This field will be hashed
|
|
41
|
+
|
|
42
|
+
user = User(name="John Doe", address="123456", password="secret123")
|
|
43
|
+
|
|
44
|
+
print(user.name) # plaintext (untouched)
|
|
45
|
+
print(user.address) # encrypted
|
|
46
|
+
print(user.password) # hashed
|
|
47
|
+
```
|
|
48
|
+
|
|
49
|
+
## SQLAlchemy Integration
|
|
50
|
+
|
|
51
|
+
If you install this package with the `sqlalchemy` extra, you can use the built-in SQLAlchemy integration for the columns.
|
|
52
|
+
|
|
53
|
+
SQLAlchemy will automatically handle the encryption/decryption of fields with the `SQLAlchemyEncrypted` type and the hashing of fields with the `SQLAlchemyHashed` type.
|
|
54
|
+
|
|
55
|
+
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.
|
|
56
|
+
|
|
57
|
+
### Example:
|
|
58
|
+
|
|
59
|
+
```python
|
|
60
|
+
import uuid
|
|
61
|
+
from pydantic_encryption import SQLAlchemyEncrypted, SQLAlchemyHashed
|
|
62
|
+
from sqlmodel import SQLModel, Field
|
|
63
|
+
from sqlalchemy import create_engine
|
|
64
|
+
from sqlalchemy.orm import sessionmaker
|
|
65
|
+
|
|
66
|
+
# Define our schema
|
|
67
|
+
class User(Base, table=True):
|
|
68
|
+
__tablename__ = "users"
|
|
69
|
+
|
|
70
|
+
username: str = Field(default=None)
|
|
71
|
+
email: bytes = Field(
|
|
72
|
+
default=None,
|
|
73
|
+
sa_type=SQLAlchemyEncrypted(),
|
|
74
|
+
)
|
|
75
|
+
password: bytes = Field(
|
|
76
|
+
sa_type=SQLAlchemyHashed(),
|
|
77
|
+
nullable=False,
|
|
78
|
+
)
|
|
79
|
+
|
|
80
|
+
# Create the database
|
|
81
|
+
engine = create_engine("sqlite:///:memory:")
|
|
82
|
+
SQLModel.metadata.create_all(engine)
|
|
83
|
+
Session = sessionmaker(bind=engine)
|
|
84
|
+
session = Session()
|
|
85
|
+
|
|
86
|
+
# Create a user
|
|
87
|
+
user = User(username="john_doe", email="john@example.com", password="secret123") # The email and password will be encrypted/hashed automatically
|
|
88
|
+
|
|
89
|
+
session.add(user)
|
|
90
|
+
session.commit()
|
|
91
|
+
|
|
92
|
+
# Query the user
|
|
93
|
+
user = session.query(User).filter_by(username="john_doe").first()
|
|
94
|
+
|
|
95
|
+
print(user.email) # decrypted
|
|
96
|
+
print(user.password) # hashed
|
|
97
|
+
```
|
|
98
|
+
|
|
99
|
+
## Choose an Encryption Method
|
|
100
|
+
|
|
101
|
+
You can choose which encryption algorithm to use by setting the `ENCRYPTION_METHOD` environment variable.
|
|
102
|
+
|
|
103
|
+
Valid values are:
|
|
104
|
+
- `fernet`: Fernet symmetric encryption
|
|
105
|
+
- `aws`: AWS KMS
|
|
106
|
+
- `evervault`: [Evervault](https://evervault.com/)
|
|
107
|
+
|
|
108
|
+
See [config.py](https://github.com/julien777z/pydantic-encryption/blob/main/pydantic_encryption/config.py) for the possible environment variables.
|
|
109
|
+
|
|
110
|
+
### Example:
|
|
111
|
+
|
|
112
|
+
`.env`
|
|
113
|
+
```env
|
|
114
|
+
ENCRYPTION_METHOD=aws
|
|
115
|
+
AWS_KMS_KEY_ARN=123
|
|
116
|
+
AWS_KMS_REGION=us-east-1
|
|
117
|
+
AWS_ACCESS_KEY_ID=123
|
|
118
|
+
AWS_SECRET_ACCESS_KEY=123
|
|
119
|
+
```
|
|
120
|
+
|
|
121
|
+
```python
|
|
122
|
+
from typing import Annotated
|
|
123
|
+
from pydantic_encryption import BaseModel, Encrypt
|
|
124
|
+
|
|
125
|
+
class User(BaseModel):
|
|
126
|
+
name: str
|
|
127
|
+
address: Annotated[bytes, Encrypt] # This field will be encrypted by AWS KMS
|
|
128
|
+
```
|
|
129
|
+
|
|
130
|
+
### Default Encryption (Fernet Symmetric Encryption)
|
|
131
|
+
|
|
132
|
+
By default, Fernet will be used for encryption and decryption.
|
|
133
|
+
|
|
134
|
+
First you need to generate an encryption key. You can use the following command:
|
|
135
|
+
|
|
136
|
+
```bash
|
|
137
|
+
openssl rand -base64 32
|
|
138
|
+
```
|
|
139
|
+
|
|
140
|
+
Then set the following environment variable or add it to your `.env` file:
|
|
141
|
+
|
|
142
|
+
```bash
|
|
143
|
+
ENCRYPTION_KEY=your_encryption_key
|
|
144
|
+
```
|
|
145
|
+
|
|
146
|
+
### Custom Encryption or Hashing
|
|
147
|
+
|
|
148
|
+
You can define your own encryption or hashing methods by subclassing `SecureModel`. `SecureModel` provides you with the utilities to handle encryption, decryption, and hashing.
|
|
149
|
+
|
|
150
|
+
`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`.
|
|
151
|
+
|
|
152
|
+
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()`.
|
|
153
|
+
|
|
154
|
+
First, define a custom secure model:
|
|
155
|
+
|
|
156
|
+
```python
|
|
157
|
+
from typing import Any, override
|
|
158
|
+
from pydantic import BaseModel as PydanticBaseModel
|
|
159
|
+
from pydantic_encryption import SecureModel
|
|
160
|
+
|
|
161
|
+
class MySecureModel(PydanticBaseModel, SecureModel):
|
|
162
|
+
@override
|
|
163
|
+
def encrypt_data(self) -> None:
|
|
164
|
+
# Your encryption logic here
|
|
165
|
+
pass
|
|
166
|
+
|
|
167
|
+
@override
|
|
168
|
+
def decrypt_data(self) -> None:
|
|
169
|
+
# Your decryption logic here
|
|
170
|
+
pass
|
|
171
|
+
|
|
172
|
+
@override
|
|
173
|
+
def hash_data(self) -> None:
|
|
174
|
+
# Your hashing logic here
|
|
175
|
+
pass
|
|
176
|
+
|
|
177
|
+
@override
|
|
178
|
+
def model_post_init(self, context: Any, /) -> None:
|
|
179
|
+
# Either define your own logic, for example:
|
|
180
|
+
|
|
181
|
+
# if not self._disable:
|
|
182
|
+
# if self.pending_decryption_fields:
|
|
183
|
+
# self.decrypt_data()
|
|
184
|
+
|
|
185
|
+
# if self.pending_encryption_fields:
|
|
186
|
+
# self.encrypt_data()
|
|
187
|
+
|
|
188
|
+
# if self.pending_hash_fields:
|
|
189
|
+
# self.hash_data()
|
|
190
|
+
|
|
191
|
+
# Or use the default logic:
|
|
192
|
+
self.default_post_init()
|
|
193
|
+
|
|
194
|
+
super().model_post_init(context)
|
|
195
|
+
```
|
|
196
|
+
|
|
197
|
+
Then use it:
|
|
198
|
+
|
|
199
|
+
```python
|
|
200
|
+
from typing import Annotated
|
|
201
|
+
from pydantic import BaseModel # Here, we don't use the BaseModel provided by the library, but the native one from Pydantic
|
|
202
|
+
from pydantic_encryption import Encrypt
|
|
203
|
+
|
|
204
|
+
class MyModel(BaseModel, MySecureModel):
|
|
205
|
+
username: str
|
|
206
|
+
address: Annotated[bytes, Encrypt]
|
|
207
|
+
|
|
208
|
+
model = MyModel(username="john_doe", address="123456")
|
|
209
|
+
print(model.address) # encrypted
|
|
210
|
+
```
|
|
211
|
+
|
|
212
|
+
## Encryption
|
|
213
|
+
|
|
214
|
+
You can encrypt any field by using the `Encrypt` annotation with `Annotated` and inheriting from `BaseModel`.
|
|
215
|
+
|
|
216
|
+
```python
|
|
217
|
+
from typing import Annotated
|
|
218
|
+
from pydantic_encryption import Encrypt, BaseModel
|
|
219
|
+
|
|
220
|
+
class User(BaseModel):
|
|
221
|
+
name: str
|
|
222
|
+
address: Annotated[bytes, Encrypt] # This field will be encrypted
|
|
223
|
+
|
|
224
|
+
user = User(name="John Doe", address="123456")
|
|
225
|
+
print(user.address) # encrypted
|
|
226
|
+
print(user.name) # plaintext (untouched)
|
|
227
|
+
```
|
|
228
|
+
|
|
229
|
+
The fields marked with `Encrypt` are automatically encrypted during model initialization.
|
|
230
|
+
|
|
231
|
+
## Decryption
|
|
232
|
+
|
|
233
|
+
Similar to encryption, you can decrypt any field by using the `Decrypt` annotation with `Annotated` and inheriting from `BaseModel`.
|
|
234
|
+
|
|
235
|
+
```python
|
|
236
|
+
from typing import Annotated
|
|
237
|
+
from pydantic_encryption import Decrypt, BaseModel
|
|
238
|
+
|
|
239
|
+
class UserResponse(BaseModel):
|
|
240
|
+
name: str
|
|
241
|
+
address: Annotated[bytes, Decrypt] # This field will be decrypted
|
|
242
|
+
|
|
243
|
+
user = UserResponse(**user_data) # encrypted value
|
|
244
|
+
print(user.address) # decrypted
|
|
245
|
+
print(user.name) # plaintext (untouched)
|
|
246
|
+
```
|
|
247
|
+
|
|
248
|
+
Fields marked with `Decrypt` are automatically decrypted during model initialization.
|
|
249
|
+
|
|
250
|
+
Note: if you use `SQLAlchemyEncrypted`, then the value will be decrypted automatically when you query the database.
|
|
251
|
+
|
|
252
|
+
|
|
253
|
+
## Hashing
|
|
254
|
+
|
|
255
|
+
You can hash sensitive data like passwords by using the `Hash` annotation.
|
|
256
|
+
|
|
257
|
+
```python
|
|
258
|
+
from typing import Annotated
|
|
259
|
+
from pydantic_encryption import Hash, BaseModel
|
|
260
|
+
|
|
261
|
+
class User(BaseModel):
|
|
262
|
+
username: str
|
|
263
|
+
password: Annotated[bytes, Hash] # This field will be hashed
|
|
264
|
+
|
|
265
|
+
user = User(username="john_doe", password="secret123")
|
|
266
|
+
print(user.password) # hashed value
|
|
267
|
+
```
|
|
268
|
+
|
|
269
|
+
Fields marked with `Hash` are automatically hashed using bcrypt during model initialization.
|
|
270
|
+
|
|
271
|
+
## Disable Auto Processing
|
|
272
|
+
|
|
273
|
+
You can disable automatic encryption/decryption/hashing by setting `disable` to `True` in the class definition.
|
|
274
|
+
|
|
275
|
+
```python
|
|
276
|
+
from typing import Annotated
|
|
277
|
+
from pydantic_encryption import Encrypt, BaseModel
|
|
278
|
+
|
|
279
|
+
class UserResponse(BaseModel, disable=True):
|
|
280
|
+
name: str
|
|
281
|
+
address: Annotated[bytes, Encrypt]
|
|
282
|
+
|
|
283
|
+
# To encrypt/decrypt/hash, call the respective methods manually:
|
|
284
|
+
user = UserResponse(name="John Doe", address="123 Main St")
|
|
285
|
+
|
|
286
|
+
# Manual encryption
|
|
287
|
+
user.encrypt_data()
|
|
288
|
+
print(user.address) # encrypted
|
|
289
|
+
|
|
290
|
+
# Or user.decrypt_data() to decrypt and user.hash_data() to hash
|
|
291
|
+
```
|
|
292
|
+
|
|
293
|
+
## Generics
|
|
294
|
+
|
|
295
|
+
Each BaseModel has an additional helpful method that will tell you its generic type.
|
|
296
|
+
|
|
297
|
+
```py
|
|
298
|
+
from pydantic_encryption import BaseModel
|
|
299
|
+
|
|
300
|
+
class MyModel[T](BaseModel):
|
|
301
|
+
value: T
|
|
302
|
+
|
|
303
|
+
model = MyModel[str](value="Hello")
|
|
304
|
+
print(model.get_type()) # <class 'str'>
|
|
305
|
+
```
|
|
306
|
+
|
|
307
|
+
## Run Tests
|
|
308
|
+
|
|
309
|
+
Install [Poetry](https://python-poetry.org/docs/) and run:
|
|
310
|
+
|
|
311
|
+
```bash
|
|
312
|
+
poetry install --with test
|
|
313
|
+
poetry run coverage run -m pytest -v -s
|
|
314
|
+
```
|
|
315
|
+
|
|
316
|
+
## Roadmap
|
|
317
|
+
|
|
318
|
+
This is an early development version. I am considering the following features:
|
|
319
|
+
|
|
320
|
+
- [ ] Add optional support for other encryption providers beyond Evervault
|
|
321
|
+
- [x] Add support for AWS KMS and other key management services
|
|
322
|
+
- [ ] Native encryption via PostgreSQL and other databases
|
|
323
|
+
- [ ] Specifying encryption key per table or row instead of globally
|
|
324
|
+
|
|
325
|
+
## Feature Requests
|
|
326
|
+
|
|
327
|
+
If you have any feature requests, please open an issue.
|