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.
JavaScript/TypeScript client? See alterion-encrypt-js — the framework-agnostic JS counterpart implementing the same wire protocol.
What it does
Each request from the client is packaged as a Request:
Client → Request { data: AES-256-GCM ciphertext, kx, client_pk: ephemeral X25519, key_id, ts }
The Interceptor middleware:
- Decrypts the request — performs X25519 ECDH with the client's ephemeral key, derives a
wrap_keyvia HKDF-SHA256, uses it to unwrap the client's randomly-generatedenc_keyfromkx, then AES-GCM-decrypts the payload. The raw bytes are injected into request extensions asDecryptedBodyfor your handlers to read. - Encrypts the response — re-encrypts the JSON response body with the same
enc_keythe client generated. A separate HMAC key is derived fromenc_keyvia HKDF and used to sign the ciphertext. The response isResponse { payload, hmac }— no second ECDH round-trip is needed.
build_request_packet and decode_response_packet in tools::serializer implement the matching client-side pipeline so Rust clients can participate in the same exchange without re-implementing the protocol.
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.3"
= "0.3"
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;
// ── Server side ──────────────────────────────────────────────────────────────
// Decode an incoming request payload (msgpack → deflate decompress → JSON)
let payload: Request = decode_request_payload?;
// Build an AES-encrypted, HMAC-signed response from any Serialize type
let bytes = build_signed_response?;
// ── Client side ──────────────────────────────────────────────────────────────
// Build an encrypted request packet (JSON → compress → msgpack → AES-256-GCM → Request).
// Returns wire bytes and the AES key — hold enc_key to decrypt the server's response.
let =
build_request_packet?;
// Decode and verify a server Response using the AES key from build_request_packet.
let decoded: MyResponse =
decode_response_packet?;
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?;
Pipelines
Client request (build_request_packet)
Any Serialize value
│
▼
serde_json::to_vec
│
▼
Deflate compress
│
▼
MessagePack encode ──→ ByteBuf
│
▼
AES-256-GCM encrypt (random enc_key — stored client-side by request ID)
│
▼
Ephemeral X25519 keygen ──→ ECDH(client_sk, server_pk) ──→ HKDF-SHA256 ──→ wrap_key
│
▼
AES-256-GCM wrap enc_key (wrap_key) ──→ kx
│
▼
Request { data, kx, client_pk, key_id, ts }
│
▼
MessagePack encode ──→ wire bytes
enc_key is returned to the caller and must be stored client-side (e.g. keyed by request ID).
The kx lets the server recover enc_key via ECDH without it ever appearing in plain
text on the wire. AES-GCM authentication tags on both data and kx ensure integrity.
Server response (build_signed_response)
Handler returns JSON bytes
│
▼
Deflate compress
│
▼
MessagePack encode ──→ ByteBuf
│
▼
AES-256-GCM encrypt (enc_key — same key the client generated for the request)
│
▼
HMAC-SHA256 sign (mac_key derived from enc_key via HKDF, over the ciphertext)
│
▼
Response { payload: ByteBuf, hmac: ByteBuf }
│
▼
MessagePack encode ──→ sent to client
The client uses enc_key (retrieved by request ID) to verify the HMAC and decrypt via
decode_response_packet. No second ECDH round-trip is needed. If HMAC 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