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.
- 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.
pip install fastapi_payloadshieldfrom 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}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.
All three live on the PayloadShield class and take an encryption_type
(default "base64").
Encrypts the response payload only.
@app.get("/api/users")
@PayloadShield.encrypt("base64")
async def get_users():
return [{"id": 1, "name": "Alice"}]
# Response: {"encrypted": "W3siaWQiOiAxLCAibmFtZSI6ICJBbGljZSJ9XQ=="}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"}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"}| 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 |
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 dataconfig is the dict returned by PayloadShieldEnc.get_config() — pull out
whatever keys your handler needs (Key, PrivateKey, PublicKey).
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": "..."}.
| 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(...) |
# Run the example app (also prints Postman-ready request examples)
cd examples
python -m uvicorn main:app --reload
# Run the test suite
pytest- Python 3.7+
- FastAPI 0.68+
- Starlette 0.19+
- cryptography 41+
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/)
Apache-2.0 - See LICENSE for details.
- GitHub Issues: https://github.com/PayloadShield/FastAPIPS/issues
- PyPI Page: https://pypi.org/project/fastapi_payloadshield/
- Author: Ganesh Kandu kanduganesh@gmail.com