Skip to main content

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 exports
    • config.py - PayloadShieldEnc key configuration
    • decorators.py - PayloadShield decorators
    • crypto.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.py demo app (routes for every handler, auto-generates PEM keys, prints Postman request examples on startup)
  • tests/ - pytest suite for handlers, config, and decorators
  • document/ - Additional guides (see document/)

License

Apache-2.0 - See LICENSE for details.


📞 Support

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)

Source distribution for fastapi-payloadshield 1.2.4
File Size Uploaded
fastapi_payloadshield-1.2.4.tar.gz 24.9 kB Details

Built distribution (wheel)

Table of built distributions (wheels) for fastapi-payloadshield 1.2.4
File Interpreter ABI Platform
fastapi_payloadshield-1.2.4-py3-none-any.whl Python 3 none any Details

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

Release history Release notifications | RSS feed

This release

1.2.4 This release

2 release files

1.2.3

2 release files

1.2.1

2 release files

1.2.0

2 release files

1.1.0

2 release files

1.0.0

2 release files

Anthropic, PBC Visionary sponsor Bloomberg Visionary sponsor Hudson River Trading Visionary sponsor Meta Visionary sponsor NVIDIA Visionary sponsor Microsoft Sustainability sponsor Depot Continuous Integration AWS Cloud computing and Security Sponsor Datadog Monitoring Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page