alterion-encrypt
v1.1.0
Published
X25519 ECDH key exchange, AES-256-GCM session encryption, and a MessagePack + Deflate request/response pipeline — framework-agnostic client-side counterpart to the alterion-encrypt Rust crate.
Readme
The JavaScript/TypeScript frontend/client-side counterpart to alterion-encrypt — X25519 ECDH key exchange, AES-256-GCM session encryption, and a MessagePack + Deflate request/response pipeline, all in a framework-agnostic package.
Frontend only. This package is intended for use in browser environments and client-side runtimes. The server-side implementation lives in the alterion-encrypt Rust crate. Do not use this package as a server-side encryption backend.
What it does
Each request to the server is packaged as a Request:
Client → Request { data: AES-256-GCM ciphertext, kx, client_pk: ephemeral X25519, key_id, ts }Request path (buildRequestPacket):
- JSON-serialise the payload and deflate-compress it.
- Generate a random 32-byte AES-256
enc_keyper request. - AES-256-GCM encrypt the payload with
enc_key. - Generate an ephemeral X25519 key pair, perform ECDH against the server's public key.
- Derive a
wrap_keyvia HKDF-SHA256, use it to AES-GCM wrapenc_key→kx. - Encode a
Request { data, kx, client_pk, key_id, ts }with MessagePack. - Store
enc_keyclient-side keyed by request ID — it is never sent in plaintext.
Response path (decodeResponsePacket):
- MessagePack-decode the
Response { payload, hmac }. - Derive the HMAC key from
enc_keyvia HKDF-SHA256 ("alterion-response-mac"info label). - Constant-time verify the HMAC — reject if invalid.
- AES-256-GCM decrypt
payloadwithenc_key. - MessagePack-decode → deflate-decompress → JSON-parse → typed result.
No second ECDH round-trip is needed for the response; the server re-uses the enc_key it unwrapped from the request. The server side is implemented in alterion-encrypt.
Package layout
alterion-encrypt (JS)
├── serializer buildRequestPacket / decodeResponsePacket — the main public API
├── crypt aesEncrypt / aesDecrypt — AES-256-GCM with prepended nonce
├── compress compress / decompress — deflate-raw via CompressionStream
└── keys deriveWrapKey / deriveResponseMacKey — HKDF-SHA256 key derivationQuick start
1. Add the dependency
npm install alterion-encrypt2. Fetch the server's public key
The server exposes a POST /api/ecdh/init endpoint (from the alterion-encrypt Rust crate) that returns a one-time ephemeral key pair:
const { handshake_id, public_key } = await fetch("/api/ecdh/init", { method: "POST" })
.then(r => r.json());
const serverPk = Uint8Array.from(atob(public_key), c => c.charCodeAt(0));
const keyId = handshake_id;3. Encrypt a request
import { buildRequestPacket } from "alterion-encrypt";
const { wireBytes, encKey } = await buildRequestPacket(
{ username: "alice", action: "login" },
serverPk,
keyId,
);
// Store encKey client-side, keyed by request ID, to decrypt the response.
// Send wireBytes as application/octet-stream body.4. Decrypt the response
import { decodeResponsePacket } from "alterion-encrypt";
const rawResponse = await fetch("/api/example", {
method: "POST",
body: wireBytes,
headers: { "Content-Type": "application/octet-stream" },
});
const bytes = new Uint8Array(await rawResponse.arrayBuffer());
const result = await decodeResponsePacket<{ token: string }>(bytes, encKey);
console.log(result.token);API
buildRequestPacket
function buildRequestPacket(
value: unknown,
serverPk: Uint8Array,
keyId: string,
): Promise<{ wireBytes: Uint8Array; encKey: Uint8Array }>Encrypts value and returns the wire bytes to send and the enc_key to hold client-side.
| Parameter | Description |
|---|---|
| value | Any JSON-serialisable payload |
| serverPk | Server's 32-byte X25519 public key (base64-decoded from the key endpoint) |
| keyId | Key identifier returned alongside the server's public key |
Returns { wireBytes, encKey }. Store encKey indexed by request ID and pass it to decodeResponsePacket when the response arrives.
decodeResponsePacket
function decodeResponsePacket<T = unknown>(
wireBytes: Uint8Array,
encKey: Uint8Array,
): Promise<T>Verifies and decodes a server response. Throws if the HMAC is invalid or decryption fails.
| Parameter | Description |
|---|---|
| wireBytes | Raw bytes from the server response body |
| encKey | The AES key returned by the matching buildRequestPacket call |
Lower-level exports
import { aesEncrypt, aesDecrypt } from "alterion-encrypt";
import { compress, decompress } from "alterion-encrypt";
import { deriveWrapKey, deriveResponseMacKey } from "alterion-encrypt";| Function | Description |
|---|---|
| aesEncrypt(plaintext, key) | AES-256-GCM encrypt — 12-byte nonce prepended to output |
| aesDecrypt(data, key) | AES-256-GCM decrypt — reads nonce from first 12 bytes |
| compress(data) | Deflate-raw compress via CompressionStream |
| decompress(data) | Deflate-raw decompress via DecompressionStream |
| deriveWrapKey(sharedSecret, clientPk, serverPk) | HKDF-SHA256 wrap key — salt = clientPk ‖ serverPk, info = "alterion-wrap" |
| deriveResponseMacKey(encKey) | HKDF-SHA256 HMAC key — info = "alterion-response-mac" |
Pipelines
Client request (buildRequestPacket)
Any JSON-serialisable value
│
▼
JSON.stringify → TextEncoder
│
▼
deflate-raw compress (CompressionStream)
│
▼
MessagePack encode ──→ Uint8Array
│
▼
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 ──→ sent to serverenc_key is returned to the caller and must be stored client-side (e.g. keyed by request ID).
kx lets the server recover enc_key via ECDH without it ever appearing in plaintext on the wire.
Server response (decodeResponsePacket)
wire bytes received from server
│
▼
MessagePack decode ──→ Response { payload, hmac }
│
▼
HKDF-SHA256(enc_key, info="alterion-response-mac") ──→ mac_key
│
▼
HMAC-SHA256 verify (mac_key, payload) ── reject if invalid
│
▼
AES-256-GCM decrypt payload (enc_key)
│
▼
MessagePack decode ──→ Uint8Array
│
▼
deflate-raw decompress (DecompressionStream)
│
▼
JSON.parse ──→ TCompatibility
Requires CompressionStream / DecompressionStream — available in all modern browsers and Node.js 18+.
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
