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
Metadata
Release files for fastapi-payloadshield 1.2.4
For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.
Source distribution (sdist)
| File | Size | Uploaded | Provenance | Actions |
|---|---|---|---|---|
| fastapi_payloadshield-1.2.4.tar.gz | 24.9 kB | Without provenance | Details for fastapi_payloadshield-1.2.4.tar.gz |
Built distribution (wheel)
| File | Interpreter | ABI | Platform | Actions Reset |
|---|---|---|---|---|
| fastapi_payloadshield-1.2.4-py3-none-any.whl | Python 3 | none | any | Details for fastapi_payloadshield-1.2.4-py3-none-any.whl |
Total release size: 35.3 kB
Release files / fastapi_payloadshield-1.2.4.tar.gz
| Download URL | fastapi_payloadshield-1.2.4.tar.gz |
|---|---|
| Size | 24.9 kB |
| Tags | Source |
|
SHA-256 checksum How to use checksums |
d8a8ef1f5313561df0c720a272cdb483d18b89ff713674492c6fd16d366ee029
|
|
BLAKE2b-256 checksum How to use checksums |
07f1726d4ace04e2f0d38b2fa93ad2fee181951b8d6014cfeb5fe17afc8be576
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
No |
| Uploaded via |
twine/7.0.0 CPython/3.13.7
|
Release files / fastapi_payloadshield-1.2.4-py3-none-any.whl
| Download URL | fastapi_payloadshield-1.2.4-py3-none-any.whl |
|---|---|
| Size | 10.4 kB |
| Tags | Python 3 |
|
SHA-256 checksum How to use checksums |
786eca762f00225ac372021705a96ded3e65672bd6179e2f20577982d8cf2c11
|
|
BLAKE2b-256 checksum How to use checksums |
c557f97421691903133ae4b2f6b00eb49693bf996b1baaf801a16c1340b5c3c6
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
No |
| Uploaded via |
twine/7.0.0 CPython/3.13.7
|
Security for fastapi-payloadshield 1.2.4
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
No provenance found
None of the files in this release were uploaded with provenance metadata.
A note on package security
PyPI is a repository for distributing packages, not a service for auditing them. While third-party security researchers and our staff do their best to detect and remove malicious packages, PyPI cannot guarantee the quality, security or suitability of any given package.
To learn more about protecting yourself from malicious actors, visit our package security documentation.