DSVPN’s cryptographic layer is powered by Charm, a standalone AEAD stream cipher library with formally verified implementations. All encryption, authentication, hashing, and random number generation go through Charm’s six public functions — no OpenSSL, no libsodium, no external dependencies of any kind.Documentation Index
Fetch the complete documentation index at: https://mintlify.com/jedisct1/dsvpn/llms.txt
Use this file to discover all available pages before exploring further.
The Charm Library
Charm is an AEAD (Authenticated Encryption with Associated Data) stream cipher built on the Xoodoo permutation — a 12-round construction operating on a 48-byte state represented as 12uint32_t words. Xoodoo applies a sequence of mixing (θ), plane-shifting (ρ_west), round-constant injection (ι), nonlinear (χ), and rotation (ρ_east) steps per round, with SIMD-accelerated paths for SSSE3 and NEON. The library is embedded directly in the DSVPN source tree.
Key properties:
- No heap allocations: the entire 48-byte state lives on the stack
- SIMD-accelerated: SSSE3 (x86) and NEON (ARM) paths are compiled in automatically
- Formally verified: implementations of the underlying permutation have been verified
- Self-contained: compiles with a single
#include "charm.h"
| Function | Purpose |
|---|---|
uc_state_init | Absorb a 32-byte key and 16-byte IV into a fresh 48-byte state |
uc_encrypt | In-place encrypt and produce a 16-byte authentication tag |
uc_decrypt | In-place decrypt and verify the authentication tag |
uc_hash | Sponge-style hash of arbitrary data into 32 bytes |
uc_memzero | Volatile-barrier zeroing of sensitive buffers |
uc_randombytes_buf | OS-backed random bytes (getrandom on Linux, arc4random_buf elsewhere) |
Key Exchange Protocol
Every new TCP connection begins with a mutual authentication handshake. The raw 32-byte key file is loaded once at startup into a key-exchange state, and new per-connection session keys are derived from that shared state.Load key file → initialize KX state
On startup, The raw key bytes are zeroed immediately after absorption. From this point on, only the derived KX state is retained in memory.
load_key_file reads the 32-byte key and calls:Client sends challenge packet
The client generates a 72-byte packet and sends it to the server:The timestamp is the current Unix time encoded as a big-endian
uint64_t.Server verifies and replies
The server copies the shared KX state locally, recomputes the hash over the first 40 bytes, and confirms it matches the trailing 32 bytes. It then checks that the client’s timestamp falls within
TS_TOLERANCE = 7200 seconds (±2 hours) of its own clock.On success the server sends a 64-byte reply:Client verifies server reply
The client recomputes the hash over the 32-byte server nonce and confirms it matches the trailing 32 bytes of the server’s reply.
Both sides derive session key and initialize streams
After successful mutual authentication, both sides call:The session key Because the server sets
k is then used to initialize two directional stream states:iv[0] = 1 for TX and the client sets iv[0] = 0 for TX, the server’s TX stream is identical to the client’s RX stream, and vice versa — the two endpoints are perfectly synchronized without any additional negotiation.The raw 32-byte key loaded from disk is never used directly for packet encryption. It authenticates the handshake only. Each new TCP connection derives a fresh session key via
uc_hash, so the on-wire session keys are independent of the stored key file beyond the initial authentication.Packet Wire Format
Every encrypted packet sent over the TCP connection uses the following layout:| Field | Size | Description |
|---|---|---|
length | 2 bytes | Big-endian uint16_t — length of the plaintext/ciphertext payload |
auth tag | 6 bytes | First TAG_LEN = 6 bytes of the 16-byte Charm authentication tag |
ciphertext | N bytes | In-place encrypted payload (same length as plaintext) |
event_loop in vpn.c):
"Corrupted stream" and immediately reconnect. There is no partial-trust fallback.
TAG_LEN is 6 bytes, truncated from the full 16-byte Charm tag. This is a deliberate trade-off between bandwidth efficiency and authentication strength, providing 48-bit authentication tags. The full 128-bit tag is generated internally; only the first 6 bytes are transmitted.Replay and Timing Attacks
DSVPN addresses replay at two layers: Handshake replay: the 8-byte big-endian timestamp in the client’s challenge packet is verified by the server. If the clock difference between client and server exceedsTS_TOLERANCE = 7200 seconds (2 hours), the handshake is rejected. A captured handshake packet cannot be replayed after the tolerance window expires.
Packet replay: the Charm stream cipher state advances monotonically with every packet processed. There is no mechanism to rewind or replay the state, so a replayed ciphertext will be decrypted incorrectly and its authentication tag will fail verification, triggering a reconnect.
Clocks on both endpoints must be reasonably synchronized (within 2 hours). Use NTP or equivalent to ensure this in production.
Memory Security
DSVPN takes explicit steps to minimize the lifetime of sensitive key material in memory:uc_memzerouses avolatilepointer loop that cannot be optimized away by the compiler, ensuring the key buffer is actually zeroed rather than elided as dead code.- The 32-byte key loaded from disk is zeroed via
uc_memzeroimmediately afteruc_state_initabsorbs it into the KX state. - Session key
k(32 bytes, stack-allocated) goes out of scope after stream initialization and is not explicitly carried forward. - No heap allocations:
Bufstructs and theContextstruct are allocated on the stack inmain(). There are nomalloc/freecycles that could leave key material in freed heap pages. - On connection teardown,
client_disconnectcallsmemset(context->uc_st, 0, sizeof context->uc_st)to clear both stream states.