Skip to content

Latest commit

 

History

History
227 lines (169 loc) · 7.99 KB

File metadata and controls

227 lines (169 loc) · 7.99 KB

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