Cryptography Reference

Concise reference for all cryptographic operations in Vauchi.

Algorithms

PurposeAlgorithmLibraryNotes
SigningEd25519ed25519-dalekIdentity, device registry, revocation
Key ExchangeX25519x25519-dalekX3DH with identity binding for in-person exchange
Symmetric EncryptionXChaCha20-Poly1305chacha20poly1305Primary cipher (192-bit nonce)
Forward SecrecyDouble Ratchethkdf + hmacHKDF-SHA256 + HMAC-SHA256, chain limit 2000
Key DerivationHKDF-SHA256hkdfRFC 5869, domain-separated
Password KDFArgon2idargon2m=64MB, t=3, p=4 (OWASP)
CSPRNGOsRngrandOS-provided entropy via rand::rngs::OsRng
TLSTLS 1.2/1.3rustls (aws-lc-rs backend)Relay connections only

Key Types

Identity Keys

KeyTypeSizePurpose
Master SeedSymmetric256-bitRoot of all keys
Signing KeyEd2551932+64 bytesIdentity, signatures
Exchange KeyX2551932 bytesKey agreement

Storage Keys (Shredding Hierarchy)

Master Seed (256-bit)
├── Identity Signing Key       — raw seed (Ed25519 requirement)
├── Exchange Key               — HKDF(seed, "Vauchi_Exchange_Seed_v2")
└── SMK (Shredding Master Key) — HKDF(seed, "Vauchi_Shred_Key_v2")
    ├── SEK (Storage Encryption Key) — HKDF(SMK, "Vauchi_Storage_Key_v2")
    │   └── encrypts all local SQLite data
    ├── FKEK (File Key Encryption Key) — HKDF(SMK, "Vauchi_FileKey_Key_v2")
    │   └── encrypts file key storage
    └── Per-Contact CEK — random 256-bit per contact
        └── encrypts individual contact's card data

HKDF Convention: Master seed as IKM, no salt, domain string as info. All derivations use HKDF::derive_key(None, &seed, info).

HKDF Context Strings:

ContextUsage
Vauchi_Exchange_Seed_v2Exchange key derivation from master seed
Vauchi_Shred_Key_v2SMK derivation from master seed
Vauchi_Storage_Key_v2SEK derivation from SMK
Vauchi_FileKey_Key_v2FKEK derivation from SMK
vauchi-x3dh-symmetric-v2X3DH transcript binding (4-key HKDF info)
vauchi-x3dh-key-v2X3DH key agreement derivation
Vauchi_Root_RatchetDH ratchet root key step
Vauchi_Message_KeySymmetric ratchet message key
Vauchi_Chain_KeySymmetric ratchet chain key advance
Vauchi_AnonymousSender_v2Anonymous sender ID derivation
Vauchi_Mailbox_v1Contact mailbox token (daily rotation, SP-33)
Vauchi_DeviceSync_v1Device sync self-token (daily rotation, SP-33)

Ratchet Keys

KeyTypeLifecycle
Root Key32 bytesUpdated on DH ratchet
Chain Key32 bytesAdvances with each message
Message Key32 bytesSingle-use, deleted after

Ciphertext Format

algorithm_tag (1 byte) || nonce || ciphertext || tag
TagAlgorithmNonceNotes
0x01AES-256-GCM12 bytesRemoved — no longer supported
0x02XChaCha20-Poly130524 bytesDefault since v0.1.2
0x03XChaCha20-Poly1305 + AD24 bytesDouble Ratchet (header-bound)

Tag 0x03 binds message header as AEAD associated data to prevent relay manipulation.

Message Padding

All messages padded to fixed buckets before encryption:

BucketSizeTypical Content
Small256 BACK, presence, revocation
Medium1 KBCard deltas, small updates
Large4 KBMedia references, large payloads

Messages > 4 KB: rounded to next 256-byte boundary.

Format: [4-byte BE length prefix] [plaintext] [random padding]

X3DH Key Agreement

Full X3DH with identity binding (no signed pre-keys):

QR / Mutual Exchange (Symmetric)

Both sides:
  ephemeral ← generate X25519 keypair
  shared_bytes ← DH(our_ephemeral_secret, their_ephemeral_public)

  // Transcript binding: all four public keys sorted lexicographically
  // and appended to info, preventing identity misbinding attacks
  info ← "vauchi-x3dh-symmetric-v2" || sort(id_lo, id_hi) || sort(eph_lo, eph_hi)
  shared ← HKDF(ikm=shared_bytes, salt=None, info=info)

NFC/BLE Exchange

Same as Mutual QR — fresh ephemeral keys on both sides, HKDF-derived shared secret.

Double Ratchet

┌─────────────────────────────────────────────────────────────────┐
│                         DOUBLE RATCHET                          │
├─────────────────────────────────────────────────────────────────┤
│                                                                 │
│  ┌───────────────────────────────────────────────────────────┐  │
│  │                        DH RATCHET                         │  │
│  │                                                           │  │
│  │    our_dh_secret × their_dh_public                        │  │
│  │              ↓                                            │  │
│  │    HKDF(root_key, shared_secret, "Vauchi_Root_Ratchet")   │  │
│  │              ↓                                            │  │
│  │    [new_root_key, new_chain_key]                          │  │
│  │                                                           │  │
│  └───────────────────────────────────────────────────────────┘  │
│                              ↓                                  │
│  ┌───────────────────────────────────────────────────────────┐  │
│  │                   SYMMETRIC RATCHET                       │  │
│  │                                                           │  │
│  │    chain_key                                              │  │
│  │        ↓                                                  │  │
│  │    HKDF(chain_key, "Vauchi_Message_Key")                  │  │
│  │        → message_key (single use)                         │  │
│  │        ↓                                                  │  │
│  │    HKDF(chain_key, "Vauchi_Chain_Key")                    │  │
│  │        → next_chain_key                                   │  │
│  │                                                           │  │
│  └───────────────────────────────────────────────────────────┘  │
│                                                                 │
│  Limits:                                                        │
│    • Max chain generations: 2000                                │
│    • Max skipped keys stored: 1000                              │
│    • Message key deleted immediately after use                  │
│                                                                 │
└─────────────────────────────────────────────────────────────────┘

Ratchet Message (Authenticated, Not Encrypted Header)

#![allow(unused)]
fn main() {
RatchetMessage {
    dh_public: [u8; 32],      // Current DH public key
    dh_generation: u32,       // DH ratchet step counter
    message_index: u32,       // Message index in current chain
    previous_chain_length: u32, // Messages sent in previous chain
    ciphertext: Vec<u8>,      // Encrypted payload
}
}

Header (44 bytes) bound as AEAD associated data (tag 0x03).

Backup Format

v2 (Current)

[0x02] || salt(16) || ciphertext
  • Key derivation: Argon2id (m=64MB, t=3, p=4)
  • Cipher: XChaCha20-Poly1305
  • Plaintext: display_name_len(4) || display_name || master_seed(32) || device_index(4) || device_name_len(4) || device_name

v1 (Removed)

salt(16) || nonce(12) || ciphertext || tag(16)
  • Key derivation: PBKDF2-HMAC-SHA256
  • Cipher: AES-256-GCM
  • Status: Removed from codebase. Documented for format reference only.

Transport Encryption (Noise NK)

Client-to-relay communication uses a Noise NK inner transport layer as defense-in-depth inside TLS.

Pattern

Noise_NK_25519_ChaChaPoly_BLAKE2s

NK means the relay's static public key is known to the client before the handshake (distributed via the /info HTTP endpoint as base64url). The client does not authenticate to the relay (anonymous initiator).

Handshake

Pre-message:  <- s   (relay's static public key, known to client)
Message 1:    -> e, es   (client sends ephemeral, DH with relay static)
Message 2:    <- e, ee   (relay sends ephemeral, DH between ephemerals)

After Message 2, both sides derive symmetric keys for bidirectional encryption.

v2 Framing

v2 (Noise-encrypted) connections are identified by a 3-byte magic prefix:

0x00 'V' '2' || 48-byte NK handshake message

All connections use the 3-byte 0x00 V 2 prefix followed by the NK handshake. After the handshake completes, all subsequent WebSocket frames are Noise-encrypted.

Why NK?

PropertyBenefit
No client authenticationPreserves anonymity — relay cannot link connections to identities
Forward secrecyEphemeral DH keys ensure past sessions can't be decrypted
Relay authenticationClient verifies the relay's identity via its static key
Defense-in-depthIf TLS is compromised, routing metadata (recipient IDs, message types) stays encrypted

Configuration

VariableDefaultDescription
RELAY_REQUIRE_NOISEfalseRemoved in v0.1 — Noise NK is always mandatory

The relay's Noise keypair is auto-generated on first start and persisted to {data_dir}/relay_noise_key.bin.

Security Properties

PropertyMechanism
ConfidentialityXChaCha20-Poly1305 encryption
IntegrityAEAD authentication tag
AuthenticityEd25519 signatures
Forward SecrecyDouble Ratchet, message keys deleted
Break-in RecoveryDH ratchet with ephemeral keys
No Nonce ReuseRandom 24-byte nonces
Memory Safetyzeroize on drop for all keys
Traffic Analysis PreventionFixed-size message padding
Replay PreventionDouble Ratchet counters
Transport EncryptionNoise NK inside TLS (defense-in-depth)

Source Files

ModulePath
Key Derivationcore/vauchi-core/src/crypto/kdf.rs
Signingcore/vauchi-core/src/crypto/signing.rs
Encryptioncore/vauchi-core/src/crypto/encryption.rs
Double Ratchetcore/vauchi-core/src/crypto/ratchet.rs
Chain Keycore/vauchi-core/src/crypto/chain.rs
CEKcore/vauchi-core/src/crypto/cek.rs
Shreddingcore/vauchi-core/src/crypto/shredding.rs
Password KDFcore/vauchi-core/src/crypto/password_kdf.rs
X3DHcore/vauchi-core/src/exchange/x3dh.rs
X3DH Session (Symmetric)core/vauchi-core/src/exchange/session.rs
Paddingcore/vauchi-core/src/crypto/padding.rs