A full end-to-end encryption pipeline for Actix-web — X25519 ECDH key exchange, AES-256-GCM session encryption, Argon2id password hashing, and a MessagePack + Deflate request/response pipeline, all behind a single middleware.
What it does
Each request from the client is packaged as a WrappedPacket:
Client → [AES-256-GCM encrypted body] + [ephemeral X25519 public key] + [key_id] + [timestamp] + [HMAC]
The Interceptor middleware:
- Decrypts the request — performs X25519 ECDH with the client's ephemeral key, derives
enc_keyandmac_keyvia HKDF-SHA256, verifies the packet MAC, then AES-decrypts the payload. The raw bytes are injected into request extensions asDecryptedBodyfor your handlers to read. - Encrypts the response — Takes the JSON response body and pipes it through:
Deflate → MessagePack → AES-256-GCM → HMAC-SHA256, returning a signedSignedResponsepacket.
The X25519 key pair rotates automatically on a configurable interval with a 300-second grace window so in-flight requests using the previous key still succeed.
Crate layout
alterion_encrypt
├── interceptor Actix-web middleware — the main public API
└── tools
├── crypt AES-256-GCM encrypt/decrypt, Argon2id password hashing, Argon2id KDF
├── serializer MessagePack serialisation, Deflate compression, signed-response builder
└── helper
├── hmac HMAC-SHA256 sign / constant-time verify
├── sha2 SHA-256 — raw bytes, hex string, file hash
└── pstore Versioned pepper store via OS native keyring (cross-platform)
Quick start
1. Add the dependency
[]
= "1.1"
= "0.1"
2. Initialise the key store and mount the interceptor
use Interceptor;
use ;
use ;
async
3. Read the decrypted body in a handler
use ;
use DecryptedBody;
async
4. Expose the current public key to clients
use ;
use ;
use Arc;
use RwLock;
async
The public_key is a base64-encoded 32-byte X25519 public key for the client to use in ECDH.
Key store
| Function | Description |
|---|---|
init_key_store(interval_secs) |
Generates the initial X25519 key pair and wraps it in an Arc<RwLock<KeyStore>> |
start_rotation(store, interval_secs) |
Spawns a background task that rotates the key every interval_secs seconds |
get_current_public_key(store) |
Returns (key_id, base64_public_key) for the active key |
ecdh(store, key_id, client_pk) |
Performs X25519 ECDH, returns (shared_secret, server_pk_bytes) |
The grace window is fixed at 300 seconds. The previous key remains valid for 5 minutes after rotation so any request encrypted just before a rotation still decrypts successfully.
Frontend note: Pre-fetch a new public key at
rotation_interval − 300seconds so the cached key is never stale when a rotation occurs.
Tools
tools::crypt
use crypt;
// AES-256-GCM (nonce prepended to output)
let ct = aes_encrypt?;
let pt = aes_decrypt?;
// Argon2id password hashing with HMAC-pepper
let = hash_password?;
let valid = verify_password?;
// Argon2id KDF — encrypt/decrypt a secret string with a password
let blob = key_encrypt?;
let secret = key_decrypt?;
tools::serializer
use serializer;
// Build an AES+HMAC signed response from any Serialize type
let bytes = build_signed_response?;
// Decode an incoming request payload (msgpack → deflate → JSON)
let payload: MyStruct = decode_request_payload?;
tools::helper
use ;
// SHA-256
let hex = hash_hex;
let file = hash_file?;
// HMAC-SHA256 (constant-time verify)
let sig = sign;
let valid = verify;
// OS keyring pepper store (Secret Service / Keychain / Credential Manager)
let = get_current_pepper?;
let new_version = rotate_pepper?;
Response pipeline
Handler returns JSON bytes
│
▼
Deflate compress
│
▼
MessagePack encode ──→ ByteBuf
│
▼
AES-256-GCM encrypt (enc_key, nonce prepended)
│
▼
HMAC-SHA256 sign (mac_key, over the ciphertext)
│
▼
SignedResponse { payload: ByteBuf, hmac: ByteBuf }
│
▼
MessagePack encode ──→ sent to client
The client verifies the HMAC before decrypting. If verification fails the response must be discarded.
Contributing
See CONTRIBUTING.md. Open an issue before writing any code.
License
GNU General Public License v3.0 — see LICENSE.
Made with ❤️ by the Alterion Software team