VAULTEX — Full-Stack Design Document

Version: 1.0
Status: Architecture Draft
Target: Phase 1 — Desktop (Linux/Windows) → Phase 2 — Android/iOS


Table of Contents

  1. Project Overview
  2. Threat Model & Security Philosophy
  3. System Architecture
  4. Cryptographic Design
  5. Backend — Server Infrastructure
  6. Backend — API Design
  7. Database Schema
  8. Frontend — Desktop (Phase 1)
  9. Networking & Transport Layer
  10. Identity & Key Management
  11. Mobile Porting Strategy (Phase 2)
  12. Directory Structure
  13. Tech Stack Summary
  14. Development Roadmap
  15. Deployment Architecture
  16. Security Audit Checklist

1. Project Overview

VAULTEX is a zero-knowledge, end-to-end encrypted messaging application designed to address the fundamental weaknesses of existing secure messaging solutions:

Problem Existing Apps VAULTEX Approach
Server-side metadata Stored by provider Never collected
Endpoint vulnerability App-level only OS-level sandboxing
Key management Provider-assisted User-sovereign keys
Identity linkage Phone number / email Cryptographic identity only
Forward secrecy Optional / partial Mandatory, per-message
Mesh capability None Built-in relay mesh

Core Principles


2. Threat Model & Security Philosophy

Adversaries Considered

┌─────────────────────────────────────────────────────┐
│  THREAT LEVEL 1 — Commercial / Data Broker          │
│  Goal: Harvest metadata, contacts, behavior         │
│  Mitigation: No phone numbers, no social graph      │
└─────────────────────────────────────────────────────┘

┌─────────────────────────────────────────────────────┐
│  THREAT LEVEL 2 — Nation-State Passive              │
│  Goal: Mass surveillance, traffic analysis          │
│  Mitigation: Sealed sender, traffic obfuscation,   │
│              decoy traffic, Tor/I2P transport       │
└─────────────────────────────────────────────────────┘

┌─────────────────────────────────────────────────────┐
│  THREAT LEVEL 3 — Nation-State Active               │
│  Goal: Targeted intercept, server compromise        │
│  Mitigation: E2E crypto, zero server plaintext,     │
│              reproducible builds, canary tokens     │
└─────────────────────────────────────────────────────┘

┌─────────────────────────────────────────────────────┐
│  THREAT LEVEL 4 — Endpoint Compromise               │
│  Goal: Access device, extract keys/messages         │
│  Mitigation: Secure enclave storage, memory zeroing,│
│              duress PIN, remote wipe                │
└─────────────────────────────────────────────────────┘

What We Protect

What We Do NOT Protect Against


3. System Architecture

High-Level Overview

┌──────────────────────────────────────────────────────────────────┐
│                         CLIENT DEVICE                            │
│                                                                  │
│  ┌─────────────────┐    ┌────────────────┐   ┌───────────────┐  │
│  │   UI Layer      │    │  Crypto Engine │   │  Local Store  │  │
│  │  (Tauri/React)  │◄──►│  (Rust/libsodium)│◄►│  (SQLCipher) │  │
│  └────────┬────────┘    └────────────────┘   └───────────────┘  │
│           │                                                       │
│  ┌────────▼────────┐    ┌────────────────┐                       │
│  │  Network Layer  │    │  Key Manager   │                       │
│  │  (Tokio/TLS1.3) │    │  (OS Keychain) │                       │
│  └────────┬────────┘    └────────────────┘                       │
└───────────┼──────────────────────────────────────────────────────┘
            │ WSS / HTTPS (TLS 1.3 only)
            │ Optional: Tor / I2P transport
            ▼
┌───────────────────────────────────────────────────────────────┐
│                      SERVER CLUSTER                           │
│                                                               │
│  ┌─────────────┐  ┌─────────────┐  ┌────────────────────┐   │
│  │  API Gateway│  │  WebSocket  │  │  Message Queue     │   │
│  │  (Nginx)    │  │  Relay      │  │  (Redis Streams)   │   │
│  └──────┬──────┘  └──────┬──────┘  └────────┬───────────┘   │
│         │                │                   │               │
│  ┌──────▼────────────────▼───────────────────▼───────────┐   │
│  │              Application Server (Rust/Axum)           │   │
│  └───────────────────────────┬───────────────────────────┘   │
│                              │                               │
│  ┌───────────────────────────▼───────────────────────────┐   │
│  │        Database (PostgreSQL + Redis)                   │   │
│  │  ONLY stores: encrypted blobs, delivery receipts,     │   │
│  │  public keys, sealed-sender tokens                     │   │
│  └───────────────────────────────────────────────────────┘   │
└───────────────────────────────────────────────────────────────┘

Component Responsibilities

Component Technology Responsibility
UI Shell Tauri 2.x Native desktop window, OS integration
UI Frontend React 18 + TypeScript All user-facing screens
Crypto Engine Rust (libsodium bindings) All cryptographic operations
Local Database SQLCipher Encrypted local message store
Network Client Tokio + rustls WebSocket connections, HTTP
Key Store OS Keychain (libsecret / DPAPI) Master key storage
API Server Rust / Axum HTTP endpoints
Message Relay Rust / Tokio WebSockets Real-time message delivery
Queue Redis Streams Offline message buffering
Database PostgreSQL 16 Server-side metadata (minimal)
Proxy Layer Nginx TLS termination, rate limiting

4. Cryptographic Design

Key Hierarchy

┌─────────────────────────────────────────────────────┐
│  MASTER IDENTITY KEY PAIR                           │
│  Algorithm: Ed25519 (signing)                       │
│  Generated: Once, on first launch                   │
│  Storage:   OS Keychain / Secure Enclave            │
│  Never leaves device                                │
└──────────────┬──────────────────────────────────────┘
               │ Derives
               ▼
┌─────────────────────────────────────────────────────┐
│  SIGNED PREKEY PAIR                                 │
│  Algorithm: X25519 (key agreement)                  │
│  Rotation: Every 7 days                             │
│  Published: Public key to server                    │
└──────────────┬──────────────────────────────────────┘
               │ Combined with
               ▼
┌─────────────────────────────────────────────────────┐
│  ONE-TIME PREKEYS (OPK)                             │
│  Algorithm: X25519                                  │
│  Quantity: 100 pre-generated, replenished           │
│  Purpose: Extended Triple Diffie-Hellman (X3DH)    │
└──────────────┬──────────────────────────────────────┘
               │ Produces
               ▼
┌─────────────────────────────────────────────────────┐
│  SESSION ROOT KEY                                   │
│  Algorithm: HKDF-SHA256 from X3DH output            │
│  Feeds into Double Ratchet                          │
└──────────────┬──────────────────────────────────────┘
               │ Drives
               ▼
┌─────────────────────────────────────────────────────┐
│  MESSAGE KEYS (per-message)                         │
│  Algorithm: Double Ratchet (Signal Protocol)        │
│  Encryption: AES-256-GCM                            │
│  Deleted after decryption (forward secrecy)         │
└─────────────────────────────────────────────────────┘

Protocol Stack

Layer 1 — Identity:       Ed25519 keypair (signing + verification)
Layer 2 — Key Agreement:  X3DH (Extended Triple Diffie-Hellman)
Layer 3 — Ratchet:        Double Ratchet Algorithm
Layer 4 — Encryption:     AES-256-GCM (AEAD)
Layer 5 — Transport:      TLS 1.3 (minimum) over WebSocket
Layer 6 — Optional:       Tor hidden service OR I2P tunnel

Sealed Sender

To hide who is sending messages (metadata protection):

Standard:  Server knows: Sender A → Recipient B
Sealed:    Server knows: Someone → Recipient B
           (sender identity encrypted to recipient's public key)

Implementation:
1. Encrypt sender's certificate under recipient's X25519 public key
2. Append to message envelope
3. Server routes by recipient handle only
4. Recipient decrypts envelope to learn sender identity

Message Envelope Format

MESSAGE ENVELOPE (on wire, binary serialized with MessagePack):

{
  version:          u8,           // Protocol version
  recipient_id:     [u8; 32],     // Recipient's public key hash
  sealed_sender:    Vec<u8>,      // Encrypted sender cert
  message_body:     Vec<u8>,      // AES-256-GCM ciphertext
  ratchet_key:      [u8; 32],     // Current DH ratchet public key
  prev_chain_len:   u32,          // Previous chain message count
  message_number:   u32,          // Position in current chain
  nonce:            [u8; 12],     // AES-GCM nonce (random)
  auth_tag:         [u8; 16],     // GCM authentication tag
  timestamp_range:  [u64; 2],     // Fuzzy timestamp (obfuscated)
}

Self-Destructing Messages

Self-destruct implemented client-side:
1. Message carries TTL field (seconds) in encrypted body
2. Recipient client sets a local timer on decrypt
3. On timer fire: zero-wipe from SQLCipher DB + UI removal
4. Sender also deletes on same TTL if desired
5. Server NEVER stores plaintext — automatic after delivery

5. Backend — Server Infrastructure

Server Stack

Language:   Rust (Axum framework)
Runtime:    Tokio async runtime
Database:   PostgreSQL 16 (primary) + Redis 7 (cache/queue)
Proxy:      Nginx (TLS termination + rate limiting)
Containers: Docker + Docker Compose (Phase 1)
Orchestration: Kubernetes (Phase 2+)

What the Server Stores (and Does NOT Store)

STORED ON SERVER:
✓ User public keys (Ed25519 identity, X25519 prekeys)
✓ Encrypted message blobs (can't decrypt — no keys)
✓ Delivery status tokens (opaque, no content info)
✓ Rate limiting counters (IP-keyed, not identity-keyed)
✓ Account handle → public key mapping

NEVER STORED ON SERVER:
✗ Private keys (never transmitted)
✗ Message plaintext
✗ Contact lists
✗ IP addresses (stripped by nginx before hitting app)
✗ Device fingerprints
✗ Read receipts (optional, user-controlled)
✗ Real names, phone numbers, emails

Server Modules

src/
├── main.rs                  # Entry point, server bootstrap
├── api/
│   ├── accounts.rs          # Account registration / prekey upload
│   ├── messages.rs          # Message send / receive endpoints
│   ├── keys.rs              # Prekey bundle retrieval
│   ├── delivery.rs          # Delivery receipt endpoints
│   └── admin.rs             # Admin endpoints (rate limits, bans)
├── websocket/
│   ├── handler.rs           # WebSocket upgrade + session mgmt
│   ├── relay.rs             # Message routing logic
│   └── presence.rs          # Online/offline status (anonymized)
├── crypto/
│   ├── verify.rs            # Signature verification
│   └── sealed_sender.rs     # Sealed sender validation
├── db/
│   ├── postgres.rs          # PostgreSQL connection pool
│   ├── redis.rs             # Redis connection pool
│   ├── accounts.rs          # Account queries
│   ├── messages.rs          # Message queue queries
│   └── keys.rs              # Prekey bundle queries
├── middleware/
│   ├── auth.rs              # Request authentication
│   ├── rate_limit.rs        # Rate limiting
│   └── strip_ip.rs          # IP stripping middleware
└── models/
    ├── account.rs
    ├── message.rs
    └── prekey.rs

6. Backend — API Design

REST Endpoints

BASE URL: https://relay.vaultex.local/api/v1

ACCOUNTS
────────
POST   /accounts/register
  Body: { identity_key: hex, signed_prekey: bundle, one_time_prekeys: [bundle] }
  Returns: { account_id: uuid }
  Note: No email/phone. account_id is a random UUID, not linked to identity.

POST   /accounts/prekeys
  Body: { signed_prekey: bundle, one_time_prekeys: [bundle] }
  Auth: Signature over request body using identity key
  Returns: 204 No Content

GET    /accounts/{recipient_id}/prekey_bundle
  Returns: { identity_key, signed_prekey, one_time_prekey }
  Note: Fetching a prekey is anonymous, no auth required

DELETE /accounts/self
  Auth: Signature proof of key ownership
  Purges all stored data

MESSAGES
────────
POST   /messages/send
  Body: { envelope: MessageEnvelope (binary/base64) }
  Auth: Sealed sender (server cannot identify sender)
  Returns: { delivery_token: uuid }

GET    /messages/inbox
  Auth: Challenge-response, proves key ownership
  Returns: [ { delivery_token, envelope } ] — encrypted blobs only
  Note: Server marks as delivered; client ACKs deletion

DELETE /messages/inbox/{delivery_token}
  Auth: Key ownership proof
  Returns: 204 No Content

KEYS
────
GET    /keys/prekey_count
  Auth: Key ownership proof
  Returns: { one_time_prekey_count: u32 }

DISCOVERY (opt-in, default off)
───────────────────────────────
POST   /users/me/discoverable
  Body: { enabled: bool, display_name?: string }
  Auth: Key ownership proof
  Returns: 204 No Content
  Note: Enabling stores the optional display name + timestamp; disabling clears
        them. Relaxes only metadata privacy (display name) for opted-in users;
        E2E encryption is unaffected. Default is off.

GET    /users/me/discoverable
  Auth: Key ownership proof
  Returns: { enabled: bool, display_name?: string }
  Note: Read-back so the client reflects the true server state.

GET    /users?q=<substring>
  Auth: Key ownership proof
  Returns: { users: [ { account_id, identity_key_hex, display_name? } ] }
  Note: Only opted-in, non-suspended users. Rate-limited per account, capped,
        caller excluded. The query is matched literally (LIKE wildcards escaped).
        Clients MUST show the fingerprint before adding (trust-on-first-use).

HEALTH
──────
GET    /health
  Returns: { status: "ok", version: "1.0.0" }
  Note: For ops monitoring.

GET    /ping
  Unauthenticated.
  Returns: { service: "vaultex", version, min_client_version, capabilities: [string] }
  Note: Client pre-flight — confirm a URL is a real VAULTEX server and
        feature-detect before the authenticated WebSocket handshake.

WebSocket Protocol

WS URL: wss://relay.vaultex.local/ws

AUTHENTICATION:
  Client sends: { type: "auth", challenge_response: hex, public_key: hex }
  Server sends: { type: "auth_ok" } or { type: "auth_fail" }

MESSAGE EVENTS:
  Server → Client: { type: "message", envelope: base64 }
  Client → Server: { type: "ack", delivery_token: uuid }
  Client → Server: { type: "typing_enc", recipient_id: hex }  // encrypted blob
  Server → Client: { type: "typing_enc", blob: base64 }

PRESENCE (anonymized):
  Client → Server: { type: "ping" }      // keepalive, proves online
  Server → Client: { type: "pong" }

DISCONNECT:
  Client → Server: { type: "goodbye" }   // server clears session

7. Database Schema

PostgreSQL Tables

-- Accounts: minimal, no PII
CREATE TABLE accounts (
    account_id          UUID PRIMARY KEY DEFAULT gen_random_uuid(),
    identity_key_hex    CHAR(64) UNIQUE NOT NULL,   -- Ed25519 public key
    created_at          TIMESTAMPTZ DEFAULT NOW(),
    last_active_bucket  SMALLINT,                   -- hour-of-week bucket (0-167), not exact time
    suspended           BOOLEAN DEFAULT FALSE
);

-- Signed Prekeys (one per account, rotated weekly)
CREATE TABLE signed_prekeys (
    account_id          UUID REFERENCES accounts(account_id) ON DELETE CASCADE,
    prekey_id           INTEGER NOT NULL,
    public_key_hex      CHAR(64) NOT NULL,
    signature_hex       CHAR(128) NOT NULL,         -- Signed by identity key
    created_at          TIMESTAMPTZ DEFAULT NOW(),
    PRIMARY KEY (account_id, prekey_id)
);

-- One-Time Prekeys (100 per account, consumed on use)
CREATE TABLE one_time_prekeys (
    id                  BIGSERIAL PRIMARY KEY,
    account_id          UUID REFERENCES accounts(account_id) ON DELETE CASCADE,
    prekey_id           INTEGER NOT NULL,
    public_key_hex      CHAR(64) NOT NULL,
    consumed            BOOLEAN DEFAULT FALSE,
    UNIQUE (account_id, prekey_id)
);

-- Message Queue (encrypted blobs waiting for offline recipients)
CREATE TABLE message_queue (
    delivery_token      UUID PRIMARY KEY DEFAULT gen_random_uuid(),
    recipient_id        UUID REFERENCES accounts(account_id) ON DELETE CASCADE,
    envelope_data       BYTEA NOT NULL,             -- Fully encrypted blob
    received_at         TIMESTAMPTZ DEFAULT NOW(),
    expires_at          TIMESTAMPTZ DEFAULT (NOW() + INTERVAL '30 days'),
    delivered           BOOLEAN DEFAULT FALSE
);
CREATE INDEX idx_queue_recipient ON message_queue(recipient_id, delivered);
CREATE INDEX idx_queue_expires ON message_queue(expires_at);

-- Rate Limiting (no identity linkage — IP hash only)
CREATE TABLE rate_limits (
    ip_hash             CHAR(64) PRIMARY KEY,       -- SHA-256 of IP, salted
    request_count       INTEGER DEFAULT 0,
    window_start        TIMESTAMPTZ DEFAULT NOW()
);

Redis Data Structures

# Active WebSocket Sessions
HSET session:{account_id}  socket_id  connection_id
EXPIRE session:{account_id} 86400

# Online Presence (anonymized bucket — not exact)
SADD online_users  {account_id}
# TTL set by keepalive pings

# Pending Delivery Notifications
LIST pending:{account_id}   → delivery_token UUIDs
# WebSocket relay reads from this list

# Rate Limiting (sliding window)
ZADD ratelimit:{ip_hash}  {timestamp}  {request_id}
EXPIRE ratelimit:{ip_hash} 3600

8. Frontend — Desktop (Phase 1)

Technology

Shell:         Tauri 2.x (Rust backend + WebView2/WebKitGTK)
UI Framework:  React 18 + TypeScript
Styling:       Tailwind CSS + custom CSS variables
State:         Zustand (lightweight, no Redux overhead)
Routing:       React Router v6 (in-app navigation)
Build:         Vite
Local DB:      better-sqlite3-sqlcipher (Node bindings)
Crypto:        @tauri-apps/api + Rust sidecar process

Why Tauri (not Electron)

Factor Electron Tauri
Binary size ~150MB ~10MB
Memory usage ~300MB ~30MB
Security Chromium sandbox OS WebView (more isolated)
Rust integration Via N-API Native
Code signing External Built-in

Screen Map

VAULTEX Desktop App — Screen Flow

┌──────────────────────────────────────────────┐
│  ONBOARDING FLOW (First launch only)         │
│                                              │
│  WelcomeScreen                               │
│    → KeygenScreen     (generate Ed25519 pair)│
│    → BackupScreen     (seed phrase export)   │
│    → PinSetupScreen   (app lock PIN)         │
│    → DuressSetupScreen (optional duress PIN) │
│    → MainApp                                 │
└──────────────────────────────────────────────┘

┌──────────────────────────────────────────────┐
│  MAIN APP LAYOUT                             │
│                                              │
│  ┌──────────┬──────────────────┬──────────┐  │
│  │ Sidebar  │  Chat Window     │ InfoPanel│  │
│  │          │                  │ (toggle) │  │
│  │ Contact  │  MessageList     │          │  │
│  │ List     │                  │ Key Info │  │
│  │          │  InputArea       │ Session  │  │
│  │ NavIcons │                  │ Stats    │  │
│  └──────────┴──────────────────┴──────────┘  │
└──────────────────────────────────────────────┘

┌──────────────────────────────────────────────┐
│  SETTINGS SCREENS                            │
│                                              │
│  Settings/
│    ├── ProfileScreen      (identity key info)│
│    ├── SecurityScreen     (PIN, duress, wipe)│
│    ├── PrivacyScreen      (read receipts,    │
│    │                       typing indicators)│
│    ├── NetworkScreen      (Tor, proxy, relay)│
│    ├── NotificationsScreen                   │
│    └── BackupScreen       (key export)       │
└──────────────────────────────────────────────┘

Frontend Directory Structure

src/
├── main.tsx                    # React entry point
├── App.tsx                     # Root + routing
├── store/
│   ├── authStore.ts            # Authentication state
│   ├── contactsStore.ts        # Contact list (encrypted local)
│   ├── messagesStore.ts        # Active conversation messages
│   ├── uiStore.ts              # UI state (panels, theme)
│   └── networkStore.ts         # Connection status
├── screens/
│   ├── onboarding/
│   │   ├── Welcome.tsx
│   │   ├── KeyGen.tsx
│   │   ├── Backup.tsx
│   │   └── PinSetup.tsx
│   ├── main/
│   │   ├── MainLayout.tsx
│   │   ├── Sidebar.tsx
│   │   ├── ChatWindow.tsx
│   │   ├── MessageList.tsx
│   │   ├── MessageBubble.tsx
│   │   ├── InputArea.tsx
│   │   └── InfoPanel.tsx
│   └── settings/
│       ├── ProfileSettings.tsx
│       ├── SecuritySettings.tsx
│       ├── NetworkSettings.tsx
│       └── PrivacySettings.tsx
├── components/
│   ├── Avatar.tsx
│   ├── ContactItem.tsx
│   ├── EncryptionBadge.tsx
│   ├── KeyFingerprint.tsx
│   ├── SelfDestructTimer.tsx
│   └── VerificationModal.tsx
├── crypto/                     # Calls into Tauri Rust commands
│   ├── keyManager.ts
│   ├── messageEncrypt.ts
│   ├── messageDecrypt.ts
│   ├── x3dh.ts
│   └── doubleRatchet.ts
├── db/
│   ├── localDb.ts              # SQLCipher wrapper
│   ├── migrations/
│   └── queries/
├── network/
│   ├── websocketClient.ts
│   ├── apiClient.ts
│   └── torTransport.ts         # Optional Tor integration
├── types/
│   ├── message.ts
│   ├── contact.ts
│   ├── session.ts
│   └── keys.ts
└── utils/
    ├── fingerprint.ts          # Key fingerprint display
    ├── timestamp.ts            # Fuzzy timestamp utils
    └── memoryZero.ts           # Secure memory wiping

Local SQLCipher Schema

-- Encrypted with user's PIN-derived key

CREATE TABLE contacts (
    id                  TEXT PRIMARY KEY,          -- Hex identity key
    nickname            TEXT NOT NULL,
    fingerprint         TEXT NOT NULL,
    verified            BOOLEAN DEFAULT FALSE,
    added_at            INTEGER,                   -- Unix timestamp
    last_message_at     INTEGER,
    archived            BOOLEAN DEFAULT FALSE,
    blocked             BOOLEAN DEFAULT FALSE
);

CREATE TABLE sessions (
    contact_id          TEXT PRIMARY KEY REFERENCES contacts(id),
    root_key_enc        BLOB NOT NULL,             -- Encrypted ratchet state
    send_chain_key_enc  BLOB NOT NULL,
    recv_chain_key_enc  BLOB NOT NULL,
    send_message_number INTEGER DEFAULT 0,
    recv_message_number INTEGER DEFAULT 0,
    ratchet_key_pub     BLOB NOT NULL,
    updated_at          INTEGER
);

CREATE TABLE messages (
    id                  TEXT PRIMARY KEY,          -- UUID
    contact_id          TEXT REFERENCES contacts(id),
    direction           TEXT CHECK(direction IN ('sent', 'received')),
    body_enc            BLOB NOT NULL,             -- Encrypted even locally
    media_type          TEXT DEFAULT 'text',
    sent_at             INTEGER,
    delivered_at        INTEGER,
    read_at             INTEGER,
    self_destruct_at    INTEGER,                   -- NULL = no self-destruct
    deleted             BOOLEAN DEFAULT FALSE
);
CREATE INDEX idx_messages_contact ON messages(contact_id, sent_at);

CREATE TABLE prekeys (
    prekey_id           INTEGER PRIMARY KEY,
    public_key          BLOB NOT NULL,
    private_key_enc     BLOB NOT NULL,             -- Encrypted with master key
    consumed            BOOLEAN DEFAULT FALSE
);

CREATE TABLE settings (
    key                 TEXT PRIMARY KEY,
    value               TEXT NOT NULL
);

9. Networking & Transport Layer

Connection Flow

1. Client resolves relay server (DNS over HTTPS to avoid DNS leakage)
2. TCP connect to nginx (port 443)
3. TLS 1.3 handshake (minimum version enforced, TLS 1.2 rejected)
   - Cert pinning: client pins server's public key hash on first connection
   - Subsequent connections reject cert changes (TOFU model)
4. HTTP Upgrade → WebSocket
5. WebSocket auth challenge-response (proves key ownership)
6. Encrypted message stream begins

Optional Tor path:
1. Connect to local Tor SOCKS5 proxy (127.0.0.1:9050)
2. Route all traffic through Tor hidden service (.onion address)
3. Server's .onion address distributed separately from clearnet address

Traffic Obfuscation

Problem: Even encrypted traffic reveals that you're using a secure
         messenger (traffic shape, timing, connection patterns)

Mitigations:
1. Random padding: All messages padded to one of 5 fixed sizes
   (256, 512, 1024, 2048, 4096 bytes) to prevent size analysis

2. Decoy traffic: Client sends encrypted noise packets when idle
   (configurable, off by default to save bandwidth)

3. Coalesced sends: Messages batched with random 0-500ms delay
   to prevent exact timing correlation

4. Multiplexed sessions: Multiple virtual conversations over
   single WebSocket connection (indistinguishable externally)

Offline Message Delivery

Sender is online, Recipient is offline:
1. Sender encrypts message normally
2. Sends encrypted envelope to server via REST POST /messages/send
3. Server stores encrypted blob in message_queue table
4. When recipient comes online:
   a. Authenticates to WebSocket
   b. Server sends queued envelopes via WebSocket
   c. Recipient client decrypts
   d. Client sends DELETE /messages/inbox/{token} for each
5. Queued messages expire after 30 days (configurable)

10. Identity & Key Management

User Identity

There are NO usernames, email addresses, or phone numbers.
A user is identified ONLY by their Ed25519 public key.

User ID = SHA-256(Ed25519_public_key)[0..16] displayed as hex
Example:  7F3A·C291·08BE·4D12

Contact Discovery:
  Out-of-band only (QR code, manual key exchange, link share)
  No central directory (no way to search for users)
  No address book access

Key Exchange Flow (New Conversation)

ALICE wants to message BOB for first time:

1. Alice fetches Bob's prekey bundle from server:
   { identity_key_B, signed_prekey_B, one_time_prekey_B }

2. Alice performs X3DH:
   DH1 = ECDH(Alice_identity, Bob_signed_prekey)
   DH2 = ECDH(Alice_ephemeral, Bob_identity)
   DH3 = ECDH(Alice_ephemeral, Bob_signed_prekey)
   DH4 = ECDH(Alice_ephemeral, Bob_one_time_prekey)
   master_secret = HKDF(DH1 || DH2 || DH3 || DH4)

3. Alice initializes Double Ratchet with master_secret

4. First message includes Alice's ephemeral key (so Bob can
   reconstruct the same master_secret)

5. Bob, on receiving first message:
   - Looks up the one-time prekey used (identified by prekey_id)
   - Reconstructs master_secret via same X3DH
   - Initializes Double Ratchet
   - All subsequent messages use the ratchet state

Safety Numbers / Verification

To prevent MITM attacks, users verify each other's identity out-of-band:

Safety Number = display_format(
    SHA-512(Alice_identity || Bob_identity)[0..30]
)

Displayed as 12 groups of 5 digits:
Example: 05413 33475 29277 71229 00962 13481
         41039 01413 78534 55288 21219 33741

Verification methods:
1. In-person: Both users read the number aloud and compare
2. QR code: Scan each other's QR (encodes identity key)
3. Secure channel: Share via another verified secure channel

Key Backup & Recovery

Seed phrase (BIP-39 compatible, 24 words) derived from master key:
- Generated on first launch
- User must write down and store securely
- Allows re-derivation of identity key on new device
- WITHOUT seed phrase: identity is lost (no recovery service)

Key rotation:
- Signed prekeys: Auto-rotated every 7 days
- One-time prekeys: Replenished when count drops below 20
- Identity key: Never rotated (would break all existing verifications)
  - Compromise scenario: notify contacts manually + create new identity

11. Mobile Porting Strategy (Phase 2)

Platform Strategy

Shared Code (Rust):          ~60% of codebase reused
  - All cryptographic logic
  - Protocol implementation (X3DH, Double Ratchet)
  - Message serialization
  - Database layer (SQLCipher)
  - Network client

Platform-Specific:           ~40% rewritten
  - UI (React Native for shared, or native per platform)
  - Key storage (Android Keystore / iOS Secure Enclave)
  - Push notifications
  - Background processing
  - OS-level sandboxing

Android Port

Framework:    React Native (reuses most UI logic from desktop)
  OR:         Kotlin + Jetpack Compose (for deeper OS integration)

Crypto Layer: Rust compiled to ARM64/x86_64 via cargo-ndk
              Called via JNI (Java Native Interface)

Key Storage:  Android Keystore System
              - Keys never leave secure hardware (if TEE available)
              - Biometric authentication integration
              - Strongbox for Pixel/certified devices

Push:         Firebase Cloud Messaging (FCM)
              - Push payload contains ONLY a wakeup signal
              - No message content in push payload
              - App fetches encrypted messages on wakeup
              - FCM can be replaced with self-hosted ntfy.sh

Local DB:     SQLCipher for Android (same schema)

Background:   WorkManager for periodic key replenishment
              Foreground Service for persistent WebSocket

Build:        Gradle + Android Studio
              Signed with hardware-backed key if available

iOS Port

Framework:    React Native (share with Android UI)
  OR:         Swift + SwiftUI (for deeper OS integration)

Crypto Layer: Rust compiled to ARM64 via cargo-lipo
              Called via C FFI + Swift bridging header

Key Storage:  iOS Secure Enclave
              - P-256 keys hardware-bound to device
              - Face ID / Touch ID gate on key access
              - Note: Secure Enclave uses P-256, so identity key
                derivation layer needed to bridge to Ed25519

Push:         Apple Push Notification Service (APNs)
              - Same approach: wakeup only, no payload content
              - Background refresh mode for key replenishment

Local DB:     SQLCipher for iOS (same schema)

Build:        Xcode + Swift Package Manager
              Notarization + App Store or TestFlight distribution

Special:      App Clip for quick key exchange without full install
              Widgets for unread count (no message preview)

Desktop → Mobile Migration Path

Phase 2 Steps:
1. Extract Rust crypto core into shared crate (no UI deps)
2. Create C FFI interface for mobile consumption
3. Android: Build AAR with JNI bindings
4. iOS: Build xcframework with Swift bridging
5. React Native: Reuse existing React components (with mobile adaptations)
6. Test on Android 10+ / iOS 15+
7. Self-sign APK for sideloading (Android)
8. TestFlight beta → App Store review (iOS)

Feature parity target: 90% of desktop features in mobile v1.0
Mobile-exclusive features:
  - Biometric unlock
  - Ephemeral "vanish mode" (screen recording detection)
  - Nearby key exchange (Bluetooth/NFC)

12. Directory Structure

Full Repository Layout

vaultex/
├── README.md
├── DESIGN.md                        # This file
├── Cargo.toml                       # Rust workspace
├── package.json                     # Node workspace root
│
├── crates/
│   ├── vaultex-crypto/              # Core crypto library
│   │   ├── src/
│   │   │   ├── lib.rs
│   │   │   ├── identity.rs          # Ed25519 key management
│   │   │   ├── x3dh.rs             # X3DH implementation
│   │   │   ├── double_ratchet.rs    # Double Ratchet impl
│   │   │   ├── sealed_sender.rs     # Sealed sender
│   │   │   ├── aes_gcm.rs          # AES-256-GCM wrapper
│   │   │   └── prekeys.rs          # Prekey management
│   │   └── Cargo.toml
│   │
│   ├── vaultex-server/              # Backend server
│   │   ├── src/
│   │   │   ├── main.rs
│   │   │   ├── api/
│   │   │   ├── websocket/
│   │   │   ├── db/
│   │   │   ├── middleware/
│   │   │   └── models/
│   │   ├── migrations/              # SQL migrations (sqlx)
│   │   └── Cargo.toml
│   │
│   └── vaultex-ffi/                 # C FFI for mobile
│       ├── src/
│       │   ├── lib.rs
│       │   └── bindings.rs
│       └── Cargo.toml
│
├── apps/
│   ├── desktop/                     # Tauri desktop app
│   │   ├── src-tauri/
│   │   │   ├── src/
│   │   │   │   ├── main.rs
│   │   │   │   ├── commands/        # Tauri IPC commands
│   │   │   │   └── db/
│   │   │   ├── Cargo.toml
│   │   │   └── tauri.conf.json
│   │   ├── src/                     # React frontend
│   │   │   ├── (see Frontend structure above)
│   │   ├── package.json
│   │   └── vite.config.ts
│   │
│   ├── android/                     # Android app (Phase 2)
│   │   ├── app/
│   │   ├── rust-bridge/
│   │   └── build.gradle
│   │
│   └── ios/                         # iOS app (Phase 2)
│       ├── VaultexApp/
│       ├── rust-bridge/
│       └── Podfile
│
├── infrastructure/
│   ├── docker-compose.yml           # Local dev stack
│   ├── docker-compose.prod.yml      # Production stack
│   ├── nginx/
│   │   └── vaultex.conf
│   ├── postgres/
│   │   └── init.sql
│   └── scripts/
│       ├── setup-dev.sh
│       ├── gen-certs.sh             # Self-signed certs for dev
│       └── db-migrate.sh
│
└── docs/
    ├── PROTOCOL.md                  # Crypto protocol spec
    ├── API.md                       # API reference
    ├── SECURITY.md                  # Security considerations
    └── BUILD.md                     # Build instructions

13. Tech Stack Summary

Phase 1 — Desktop

Layer Technology Version Reason
Desktop Shell Tauri 2.x Tiny binary, Rust native, secure
UI Framework React + TypeScript 18.x Reusable for mobile, large ecosystem
Styling Tailwind CSS 3.x Utility-first, no runtime
State Management Zustand 4.x Lightweight, TypeScript-first
Build Tool Vite 5.x Fast HMR, Tauri-optimized
Crypto Core Rust + libsodium latest Audited, production-grade
Local Database SQLCipher 4.x AES-256 encrypted SQLite
Server Language Rust + Axum 0.7 Memory-safe, high performance
Async Runtime Tokio 1.x De facto Rust async standard
Primary DB PostgreSQL 16 ACID, solid JSON support
Cache / Queue Redis 7.x Fast pub/sub, message buffering
Proxy Nginx 1.25 Battle-tested TLS termination
Containerization Docker Compose 2.x Dev and prod parity
Migration Tool sqlx-cli 0.7 Compile-time checked SQL

Phase 2 — Mobile (Additional)

Layer Technology Reason
Mobile UI React Native Shares desktop React components
Android Crypto Rust → JNI → Kotlin Full crypto reuse
iOS Crypto Rust → C FFI → Swift Full crypto reuse
Android KeyStore Android Keystore API Hardware key protection
iOS Key Store Secure Enclave Hardware key protection
Push (Android) FCM (content-free) Wakeup only, no payload
Push (iOS) APNs (content-free) Wakeup only, no payload

14. Development Roadmap

Phase 1a — Foundation (Weeks 1–4)

[ ] Server setup
    [ ] PostgreSQL + Redis + Nginx Docker Compose stack
    [ ] Rust/Axum server skeleton with health endpoint
    [ ] Database migrations (accounts, prekeys, message_queue)
    [ ] Basic REST API: register, prekey upload/fetch
    [ ] WebSocket server: connect, auth, relay

[ ] Crypto core (Rust crate)
    [ ] Ed25519 key generation and signing
    [ ] X25519 Diffie-Hellman
    [ ] X3DH implementation (unit tested against test vectors)
    [ ] Double Ratchet implementation (unit tested)
    [ ] AES-256-GCM encrypt/decrypt
    [ ] Sealed sender construction/parsing

[ ] Desktop app skeleton
    [ ] Tauri project with React + TypeScript
    [ ] SQLCipher local database setup
    [ ] Tauri IPC commands to crypto crate
    [ ] Basic screen routing

Phase 1b — Core Features (Weeks 5–8)

[ ] Onboarding flow
    [ ] Key generation screen
    [ ] Seed phrase display + confirmation
    [ ] PIN setup + SQLCipher key derivation
    [ ] Server registration

[ ] Core messaging
    [ ] Contact add (manual key input + QR scan)
    [ ] Session establishment (X3DH + Double Ratchet init)
    [ ] Send encrypted message
    [ ] Receive encrypted message (WebSocket)
    [ ] Offline message delivery

[ ] UI polish
    [ ] Full chat UI (matches VAULTEX design mockup)
    [ ] Contact list with status
    [ ] Message status (sent/delivered/read)
    [ ] Key fingerprint + safety number display

Phase 1c — Security Features (Weeks 9–12)

[ ] Self-destructing messages (client-side timer)
[ ] Sealed sender implementation
[ ] Duress PIN (opens decoy app/wipes real data)
[ ] Traffic padding (fixed-size message padding)
[ ] Optional Tor transport integration
[ ] App lock (timeout + PIN re-entry)
[ ] Key rotation (prekeys, signed prekeys)
[ ] Safety number verification flow
[ ] Reproducible build setup
[ ] Security audit prep (code review, fuzzing)

Phase 1d — Polish & Release (Weeks 13–16)

[ ] Media support (images, files — encrypted)
[ ] Group messaging (sender key protocol)
[ ] Search (local SQLCipher FTS5)
[ ] Notification system (OS integration)
[ ] Settings screens
[ ] Export/import (key backup/restore)
[ ] Installer packages (Linux AppImage/deb, Windows MSI)
[ ] Documentation
[ ] Internal security audit

Phase 2 — Mobile (Weeks 17–28)

[ ] Rust FFI interface finalization
[ ] Android: JNI bindings + Kotlin app
[ ] iOS: Swift bridging + SwiftUI app
[ ] React Native shared UI layer
[ ] Biometric authentication
[ ] Push notification integration
[ ] Beta testing (TestFlight + sideload APK)
[ ] App Store submission prep

15. Deployment Architecture

Development Environment

# Quick start:
git clone https://github.com/your-org/vaultex
cd vaultex/infrastructure
docker-compose up -d         # Starts postgres, redis, nginx
cd ../crates/vaultex-server
cargo run                    # Starts dev server on :8080
cd ../../apps/desktop
npm install && npm run tauri dev   # Starts desktop app

Production Server (Single Node, Phase 1)

Minimum spec:
  CPU: 2 cores
  RAM: 4GB
  Storage: 40GB SSD
  OS: Ubuntu 22.04 LTS
  Network: 100Mbps, 1TB/mo transfer

Stack:
  Nginx (443/80) → Axum server (:8080) → PostgreSQL (:5432) + Redis (:6379)

TLS:
  Let's Encrypt (certbot) for clearnet domain
  Self-hosted CA option for high-security deployments

Tor Hidden Service:
  tor daemon with HiddenServicePort 443 → localhost:443
  .onion address distributed to users directly

Hardening:
  UFW firewall: only 22, 80, 443 inbound
  fail2ban on SSH and nginx
  Automatic security updates (unattended-upgrades)
  PostgreSQL not exposed to network (localhost only)
  Redis bound to 127.0.0.1 only
  AppArmor profiles for server process

Server docker-compose.yml

version: '3.9'

services:
  postgres:
    image: postgres:16-alpine
    environment:
      POSTGRES_DB: vaultex
      POSTGRES_USER: vaultex
      POSTGRES_PASSWORD: ${POSTGRES_PASSWORD}
    volumes:
      - pgdata:/var/lib/postgresql/data
      - ./postgres/init.sql:/docker-entrypoint-initdb.d/init.sql
    restart: unless-stopped
    networks: [internal]

  redis:
    image: redis:7-alpine
    command: redis-server --requirepass ${REDIS_PASSWORD} --save 60 1
    volumes:
      - redisdata:/data
    restart: unless-stopped
    networks: [internal]

  server:
    build: ../crates/vaultex-server
    environment:
      DATABASE_URL: postgresql://vaultex:${POSTGRES_PASSWORD}@postgres:5432/vaultex
      REDIS_URL: redis://:${REDIS_PASSWORD}@redis:6379
      SERVER_PORT: 8080
      LOG_LEVEL: info
    depends_on: [postgres, redis]
    restart: unless-stopped
    networks: [internal, external]

  nginx:
    image: nginx:1.25-alpine
    ports:
      - "80:80"
      - "443:443"
    volumes:
      - ./nginx/vaultex.conf:/etc/nginx/conf.d/default.conf
      - /etc/letsencrypt:/etc/letsencrypt:ro
    depends_on: [server]
    restart: unless-stopped
    networks: [external]

volumes:
  pgdata:
  redisdata:

networks:
  internal:
    internal: true
  external:

16. Security Audit Checklist

Cryptography

Server

Client

Operational


Document generated for Claude Code. Begin with Phase 1a foundation tasks. Run cargo test in vaultex-crypto crate first to validate X3DH and Double Ratchet implementations before building any UI.