FastAPI Payload Shield
Pluggable FastAPI decorators for encrypting and decrypting request and
response payloads. Configure your keys once, then annotate any route with
@PayloadShield.encrypt, @PayloadShield.decrypt, or @PayloadShield.crypt.
Key Features
- Pluggable encryption: base64, Fernet, AES-GCM-256, ChaCha20-Poly1305,
Hybrid RSA+AES, ECDH+AES-GCM, ECIES, and HPKE (RFC 9180) ship out of the
box; register your own with
register_handler(...).- One-time key configuration:PayloadShieldEnc.init({...})sets keys globally for all decorators. - Route-agnostic: no changes needed to your route logic besides adding a decorator.
- Async-friendly: works with FastAPI's async route handlers.
Installation
pip install fastapi_payloadshield
Quick Start
from fastapi import FastAPI
from fastapi_payloadshield import PayloadShield, PayloadShieldEnc
# Configure encryption keys once, at startup.
PayloadShieldEnc.init({
"Key": "my-symmetric-key",
})
app = FastAPI()
@app.get("/api/data")
@PayloadShield.encrypt("base64")
async def get_data():
return {"message": "hello", "data": "world"}
@app.post("/api/process")
@PayloadShield.decrypt("base64")
async def process_data(data: dict):
return {"received": data, "status": "success"}
@app.post("/api/secure")
@PayloadShield.crypt("base64")
async def secure_endpoint(data: dict):
return {"processed": data}
Initialization: PayloadShieldEnc.init(...)
Call once before serving requests. Every decorator reads this shared configuration at call time.
PayloadShieldEnc.init({
"Key": key, # symmetric key: fernet, aes-gcm-256, chacha20-poly1305
"PrivateKey": "string", # RSA/hybrid private key (file path or PEM content)
"PublicKey": "string", # RSA/hybrid public key (file path or PEM content)
"ECPrivateKey": "string", # EC (P-256) private key (file path or PEM content)
"ECPublicKey": "string", # EC (P-256) public key (file path or PEM content)
"HPKEPrivateKey": "string", # X25519 private key (file path or PEM content)
"HPKEPublicKey": "string", # X25519 public key (file path or PEM content)
})
| Field | Used by | Accepts |
|---|---|---|
Key |
fernet, aes-gcm-256, chacha20-poly1305 |
Raw key string. aes-gcm-256 and chacha20-poly1305 require the key to resolve to exactly 32 bytes (UTF-8 or base64 encoded). |
PrivateKey |
rsa-hybrid (decrypt) |
File path to a PEM file, or the raw PEM content (RSA key). |
PublicKey |
rsa-hybrid (encrypt) |
File path to a PEM file, or the raw PEM content (RSA key). |
ECPrivateKey |
ecdh-aes-gcm, ecies (decrypt) |
File path to a PEM file, or the raw PEM content (EC P-256 key). |
ECPublicKey |
ecdh-aes-gcm, ecies (encrypt) |
File path to a PEM file, or the raw PEM content (EC P-256 key). |
HPKEPrivateKey |
hpke (decrypt) |
File path to a PEM file, or the raw PEM content (X25519 key). |
HPKEPublicKey |
hpke (encrypt) |
File path to a PEM file, or the raw PEM content (X25519 key). |
Only set the fields required by the encryption types you actually use.
Decorators
All three live on the PayloadShield class and take an encryption_type
(default "base64").
@PayloadShield.encrypt(encryption_type)
Encrypts the response payload only.
@app.get("/api/users")
@PayloadShield.encrypt("base64")
async def get_users():
return [{"id": 1, "name": "Alice"}]
# Response: {"encrypted": "W3siaWQiOiAxLCAibmFtZSI6ICJBbGljZSJ9XQ=="}
@PayloadShield.decrypt(encryption_type)
Decrypts the request payload only; the route receives the decrypted dict.
@app.post("/api/login")
@PayloadShield.decrypt("base64")
async def login(credentials: dict):
return {"status": "success"}
# Expects: {"encrypted": "base64_encoded_json"}
@PayloadShield.crypt(encryption_type)
Decrypts the request and encrypts the response.
@app.post("/api/secure")
@PayloadShield.crypt("base64")
async def secure_endpoint(data: dict):
return {"processed": data}
# Expects: {"encrypted": "encrypted_data"}
# Returns: {"encrypted": "encrypted_data"}
Built-in Encryption Handlers
| Name | Algorithm | Keys required | Security |
|---|---|---|---|
base64 |
Base64 encoding | none | None — obfuscation only |
fernet |
Fernet (AES-128-CBC + HMAC) | Key |
Symmetric, authenticated |
aes-gcm-256 |
AES-256-GCM | Key (32 bytes) |
Symmetric, authenticated |
chacha20-poly1305 |
ChaCha20-Poly1305 | Key (32 bytes) |
Symmetric, authenticated |
rsa-hybrid |
RSA-OAEP + AES-256-GCM | PublicKey (encrypt), PrivateKey (decrypt) |
Asymmetric/hybrid |
ecdh-aes-gcm |
Ephemeral-static ECDH (P-256) + HKDF-SHA256 + AES-256-GCM | ECPublicKey (encrypt), ECPrivateKey (decrypt) |
Asymmetric/hybrid, authenticated |
ecies |
ECIES: ECDH (P-256) + HKDF-SHA256 + AES-256-CTR + HMAC-SHA256 (encrypt-then-MAC) | ECPublicKey (encrypt), ECPrivateKey (decrypt) |
Asymmetric/hybrid, authenticated |
hpke |
HPKE (RFC 9180) base mode: DHKEM(X25519, HKDF-SHA256) + HKDF-SHA256 + ChaCha20-Poly1305 | HPKEPublicKey (encrypt), HPKEPrivateKey (decrypt) |
Asymmetric/hybrid, authenticated |
Custom Handlers
Implement EncryptionHandler and register it — every decorator can then
use it by name.
from typing import Any, Dict, Optional
from fastapi_payloadshield import EncryptionHandler, register_handler, PayloadShield
class MyHandler(EncryptionHandler):
def encode(self, data: Any, config: Optional[Dict[str, Any]] = None) -> str:
...
def decode(self, encoded_data: str, config: Optional[Dict[str, Any]] = None) -> Any:
...
register_handler("my-handler", MyHandler())
@app.post("/api/custom")
@PayloadShield.crypt("my-handler")
async def custom_endpoint(data: dict):
return data
config is the dict returned by PayloadShieldEnc.get_config() — pull out
whatever keys your handler needs (Key, PrivateKey, PublicKey).
How It Works
Request decryption: client sends {"encrypted": "..."} → decorator
decodes it with the configured handler → route receives the plain dict.
Response encryption: route returns a dict → decorator encodes it with
the configured handler → client receives {"encrypted": "..."}.
Errors
| Situation | Behavior |
|---|---|
| Request decryption fails | 400 response: {"error": "Failed to decrypt request: ..."} |
Unknown encryption_type |
ValueError raised when the decorator is applied: Encryption handler '<name>' not found. Available handlers: ... |
Missing required key (e.g. no Key set for fernet) |
ValueError raised when encoding/decoding: ... requires 'Key' to be set via PayloadShieldEnc.init(...) |
Testing
# Run the example app (also prints Postman-ready request examples)
cd examples
python -m uvicorn main:app --reload
# Run the test suite
pytest
Requirements
- Python 3.7+
- FastAPI 0.68+
- Starlette 0.19+
- cryptography 41+
Project Layout
fastapi_payloadshield/- Main package__init__.py- Public exportsconfig.py-PayloadShieldEnckey configurationdecorators.py-PayloadShielddecoratorscrypto.py- Handler registry (register_handler,get_handler)EncryptionHandler.py,Base64EncryptionHandler.py,FernetEncryptionHandler.py,AESGCM256EncryptionHandler.py,ChaChaEncryptionHandler.py,HybridRSAEncryptionHandler.py,ECDHAESGCMEncryptionHandler.py,ECIESEncryptionHandler.py,HPKEEncryptionHandler.py- Built-in handlers
examples/-main.pydemo app (routes for every handler, auto-generates PEM keys, prints Postman request examples on startup)tests/- pytest suite for handlers, config, and decoratorsdocument/- Additional guides (see document/)
License
Apache-2.0 - See LICENSE for details.
📞 Support
- GitHub Issues: https://github.com/PayloadShield/FastAPIPS/issues
- PyPI Page: https://pypi.org/project/fastapi_payloadshield/
- Author: Ganesh Kandu kanduganesh@gmail.com
Release files for fastapi-payloadshield 1.2.3
For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.
Source distribution (sdist)
| File | Size | Uploaded | |
|---|---|---|---|
| fastapi_payloadshield-1.2.3.tar.gz | 30.5 kB | Details |
Built distribution (wheel)
| File | Interpreter | ABI | Platform | Reset |
|---|---|---|---|---|
| fastapi_payloadshield-1.2.3-py3-none-any.whl | Python 3 | none | any | Details |
Total release size: 53.3 kB
Release files / fastapi_payloadshield-1.2.3.tar.gz
| Download URL | fastapi_payloadshield-1.2.3.tar.gz |
|---|---|
| Size | 30.5 kB |
| Tags | Source |
|
SHA-256 checksum How to use checksums |
acb825cb781202436759e479e9e67fcb83ad1b99f46aa841d002fece46b19eb0
|
|
BLAKE2b-256 checksum How to use checksums |
26b2bbf38b7d4e756722bee6ceb18c31a055e01caf7c2fc45e211df66e732dbc
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
No |
| Uploaded via |
twine/7.0.0 CPython/3.13.14
|
Release files / fastapi_payloadshield-1.2.3-py3-none-any.whl
| Download URL | fastapi_payloadshield-1.2.3-py3-none-any.whl |
|---|---|
| Size | 22.8 kB |
| Tags | Python 3 |
|
SHA-256 checksum How to use checksums |
3612b772da25867c9f6e5336f205f27fdb808c3d0dd206b7590965927cdc96d0
|
|
BLAKE2b-256 checksum How to use checksums |
f287f7eea409db78f185df57a20cc8d776e0e96b5527cc0dcf37f9e0e49bf08f
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
No |
| Uploaded via |
twine/7.0.0 CPython/3.13.14
|