Skip to content
Aarav edited this page Aug 17, 2026 · 1 revision

SylvaCrypt (formerly ShadowCrypt) is an open-source, end-to-end encrypted messaging web application built on a zero-knowledge relay. All cryptographic operations happen entirely in the browser — the server routes opaque ciphertext and never holds keys or plaintext.

Version: 8.5.0 · License: AGPL-3.0 · Repo: github.com/Forestritium/SylvaCrypt


1. Overview

SylvaCrypt is a zero-knowledge messaging platform. Its core design principle is that the relay (Supabase backend) is treated as fully untrusted — it only ever sees sealed, opaque ciphertext. No server-side secret is ever used to encrypt or decrypt user messages.

Key properties at a glance:

Property Value
Message encryption Double Ratchet (X25519 + HKDF-SHA256 + AES-256-GCM)
Forward secrecy Full — keys ratchet after every single message
Break-in recovery DH ratchet regenerates fresh key material on every reply
Post-quantum layer ML-KEM-768 (CRYSTALS-Kyber, FIPS 203), hybrid with X3DH
Key exchange Full X3DH (IK + SPK + OPK prekeys)
Vault KDF Argon2id (64 MB / 3 iterations)
Sender anonymity Sealed-sender box (ephemeral X25519 + AES-256-GCM)
Header privacy Envelope headers AES-256-GCM encrypted
At-rest encryption All DB columns vault-wrapped with AES-256-GCM
Password recovery BIP-39 12-word mnemonic (128-bit entropy); hash stored server-side only
Relay behaviour Messages deleted after delivery; 30-day hard cap via pg_cron

2. Feature Summary

Feature Details
End-to-End Encryption Double Ratchet — per-message unique keys, full forward secrecy
X3DH Key Exchange Full X3DH: IK + SPK + OPK; fresh shared secret per conversation
Post-Quantum Encryption ML-KEM-768 hybrid layer — classical + PQ shared secret via HKDF
Sealed Sender Relay cannot identify the sender of any message
Header Encryption Relay cannot read ratchet metadata (message number, chain counters)
Vault Encryption All local data encrypted with Argon2id-derived key + AES-256-GCM
Real-time Messaging Supabase Realtime relay — ciphertext only, auto-deleted after delivery
Image Sharing AES-256-GCM per-image key, private bucket, signed URLs; 10 images/day cap
Voice Messages AES-256-GCM encrypted Opus/WebM audio; key inside ratchet; 10 min/day cap
Reply & Quote Thread-aware reply with quoted message preview
Contact Requests Accept/decline incoming requests; full block list management
Multi-Device Linked devices with independent E2E sessions
Recovery Phrase BIP-39 12-word mnemonic for password reset without email or server trust
Safety Numbers 60-digit fingerprint to verify a contact's public key out-of-band
Push Notifications Anonymous — never reveals sender identity or message content
Themes Public user-contributed UI themes with ratings and comments
PWA Installable as a Progressive Web App on desktop and mobile
Dark / Light Mode System-aware with manual override

3. Security Model

3.1 Threat Model

What SylvaCrypt protects against:

Threat Mitigation
Server compromise Server stores only AES-256-GCM ciphertext; has no decryption keys
Database leak All message content and image/voice keys are vault-wrapped before DB write
MITM on relay Messages are E2E-encrypted before reaching the relay
Retrospective decryption Double Ratchet forward secrecy — one compromised key exposes nothing past or future
Ratchet header metadata Headers AES-256-GCM encrypted with NHK, rotated on every DH ratchet step
Weak passwords Argon2id with 64 MB memory — GPU/ASIC cracking is extremely expensive
Email-less recovery BIP-39 mnemonic hash only; plaintext phrase never sent to server
Encrypted image exposure Key travels inside ratchet ciphertext; private bucket with signed URLs only
Encrypted voice exposure Same model as images; plaintext audio never touches the server
Sender identification Sealed-sender box hides sender identity from relay on every message

What SylvaCrypt does NOT protect against:

Threat Explanation
Compromised device / malware Attacker can read decrypted messages from memory
Session hijacking Stolen JWT allows receiving future ciphertext but not decrypting it
Malicious browser extensions Extensions with page access can intercept plaintext before encryption
Physical access Vault key held in sessionStorage during active session
Traffic metadata Who is talking to whom and how often is not hidden
OS-level screen capture No web app can block OS screenshots; deterrence only
Denial of service No DDoS mitigations at the application level

3.2 Cryptographic Stack

Primitive Algorithm Usage
Key exchange X25519 (via @noble/curves) X3DH, Double Ratchet DH steps
Post-quantum KEM ML-KEM-768 / CRYSTALS-Kyber (via @noble/post-quantum) Hybrid X3DH setup
Symmetric encryption AES-256-GCM (Web Crypto API) Messages, images, voice, vault, headers
Key derivation (messages) HKDF-SHA256 (Web Crypto API) Root key, chain keys, message keys
Key derivation (vault) Argon2id (via hash-wasm WASM) Password → vault key (v1, current)
Key derivation (legacy) PBKDF2-SHA256 @ 310k iterations Vault key for v0 accounts (migrated on login)
Password recovery BIP-39 12-word mnemonic (via @scure/bip39) Account recovery without email
Password strength zxcvbn (via @zxcvbn-ts/core) Registration UX
Identity verification Ed25519 signatures Prekey bundle signing

3.3 Known Limitations

  1. sessionStorage vault key — The vault key is written to sessionStorage for tab-reload usability. Users on shared machines should log out and close the tab.

  2. Stored messages share one vault key — Forward secrecy applies to the relay, not to IndexedDB. All locally stored messages are protected by a single vault key; vault key compromise exposes the full local history.

  3. ML-KEM-768 secret key in JavaScript memory — The X25519 identity key is a non-extractable Web Crypto CryptoKey. The ML-KEM-768 secret key (2400 bytes) has no Web Crypto native support, so it lives as a base64 string in vault-encrypted IndexedDB. The effective security difference from the attacker's perspective is minimal (arbitrary JS execution compromises both), but the asymmetry is noted. Future mitigation: WASM memory isolation when the W3C WebCryptoAPI adds native ML-KEM support.

  4. Daily rate limits are not security boundaries — The 10-image/day and 10-min/day voice caps are abuse mitigations, not cryptographic guarantees.

  5. Self-hosted deployments — Self-hosters are responsible for securing their Supabase project, applying all migrations, and keeping dependencies updated.


4. Architecture

4.1 High-Level Diagram

┌─────────────────────────────────────────────────────────────────┐
│                        Browser (Client)                          │
│                                                                   │
│   React + TypeScript + Vite                                       │
│                                                                   │
│   AuthContext ──► session.ts ──► localStore.ts (IndexedDB)       │
│        │               │                                          │
│        ▼               ▼                                          │
│   AuthPage.tsx    crypto.ts  (Web Crypto API + hash-wasm)         │
│   ChatPage.tsx         │                                          │
│   SettingsPage.tsx     ▼                                          │
│                  doubleRatchet.ts ──► relay.ts                    │
│                                          │                        │
└──────────────────────────────────────────┼────────────────────────┘
                                           │ HTTPS / WSS
                                           │ (ciphertext only)
                           ┌──────────────▼──────────────┐
                           │          Supabase            │
                           │                              │
                           │  PostgreSQL (DB)             │
                           │  Realtime (change feed)      │
                           │  Storage (private buckets)   │
                           │  Edge Functions              │
                           └──────────────────────────────┘

4.2 Frontend

The frontend is a React 18 + TypeScript single-page application built with Vite.

Layer responsibilities:

Layer Path Responsibility
Pages src/pages/ Route-level views: Auth, Chat, Settings, Docs, Safety Number, etc.
Contexts src/contexts/ Global state: AuthContext (auth/session/vault), ThemeContext
Components src/components/ Chat panels, dialogs, emoji pickers, file bubbles, shadcn/ui primitives
Lib src/lib/ Pure logic: crypto.ts, x3dh.ts, doubleRatchet.ts, relay.ts, localStore.ts, session.ts, mnemonic.ts
DB src/db/ Supabase client singleton
Hooks src/hooks/ Reusable React hooks (PWA install, etc.)
Types src/types/ Shared TypeScript interfaces

State management: No external state library. State lives in React Context, component useState/useReducer, and in-memory singletons in localStore.ts.

Routing (React Router v7):

Path Component Guard
/auth AuthPage Public
/home, /chat ChatPage Authenticated
/add-contact AddContactPage Authenticated
/settings SettingsPage Authenticated
/linked-devices LinkedDevicesPage Authenticated
/settings/notifications NotificationSettingsPage Authenticated
/settings/themes ThemesPage Authenticated
/settings/waveform WaveformSettingsPage Authenticated
/safety-number/:contactId SafetyNumberPage Authenticated
/docs DocsHomePage Public
/docs/security-whitepaper SecurityWhitepaperPage Public
/docs/troubleshooting TroubleshootingPage Public
/privacy PrivacyPolicyPage Public

RouteGuard redirects unauthenticated users to /auth.

4.3 Backend (Supabase)

Supabase is used as a zero-knowledge relay: it provides auth, a PostgreSQL database, Realtime subscriptions, private object storage, and Edge Functions. Supabase is never given decryption keys; a full Supabase compromise reveals only ciphertext.

Row-Level Security (RLS) is enforced on all tables. Key rules:

  • profiles — readable only by the user themselves and their contacts.
  • messages / relay_messages — only the addressed recipient can read; only the sender can insert.
  • contacts — users read and modify only their own contact list.
  • contact_requests — sender and recipient can each read their own side.

4.4 Database Schema

Table Purpose
profiles Username, public key, bio, avatar URL, password version, mnemonic hash, KDF version, vault backup
user_signed_prekeys Public prekeys with Ed25519 signatures for X3DH
user_one_time_prekeys Single-use OPKs for X3DH — consumed per conversation
user_devices Linked device endpoints for multi-device sync
contacts Owner → contact mapping; denormalised username + public key for offline access
messages Internal relay storage (temporary)
relay_messages Sealed-sender ciphertext routed between users; auto-deleted after 30 days
voice_send_durations Per-user per-day voice seconds counter (10 min/day cap)
contact_requests Pending / accepted / declined add-contact requests
blocked_users Bidirectional block records
conversation_pins Saved conversation pointers
personal_pins Personal notes and pointers
push_subscriptions Web Push subscription endpoints
public_themes Globally shared UI themes
theme_comments Discussion on public themes

Migrations are tracked as ordered SQL files in supabase/migrations/ (00000 → 00119 at time of writing), applied via supabase db push.

4.5 Edge Functions

Function Trigger Description
delete-account Authenticated client call Verifies JWT, uses service-role key to call admin.deleteUser(). FK constraints cascade-delete all user data.
reset-password Unauthenticated client call Verifies BIP-39 mnemonic hash, uses service-role key to call admin.updateUserById() with new password.

5. Cryptographic Protocols

5.1 Vault Key Derivation

User password
      │
      ▼
Argon2id (mem=64 MB, iter=3, par=1)   ← KDF v1 (all new accounts)
      │
      ▼
32-byte raw key
      │
      ▼
AES-256-GCM CryptoKey (non-extractable during session)

All data in IndexedDB is stored as base64(IV[12] + AES-256-GCM-ciphertext). The vault key is kept in memory during the session and also written to sessionStorage (raw bytes) so page reloads don't require the user to re-enter their password.

Legacy v0 accounts used PBKDF2-SHA256 (310,000 iterations) and are migrated to Argon2id v1 on first login.

5.2 X3DH Key Exchange

Before the first message in any conversation, the sender runs the full Extended Triple Diffie-Hellman protocol:

  1. Fetch recipient's prekey bundle from user_signed_prekeys and user_one_time_prekeys (IK, SPK, OPK, KEM public key).
  2. Perform four classical DH operations: IK–SPK, Ephemeral–IK, Ephemeral–SPK, Ephemeral–OPK.
  3. ML-KEM-768 Encapsulate against the recipient's KEM public key.
  4. HKDF-combine all DH outputs + KEM shared secret → session secret → initial root key (RK) and sending chain key (CKs).

5.3 Double Ratchet Encryption

Every message advances the ratchet:

kdfCK(CKs) → (CKs', MK)          ← symmetric ratchet step
AES-256-GCM(MK, plaintext)        ← message encryption
AES-256-GCM(NHK, header)          ← header encryption

After each reply, the DH ratchet also advances: a new X25519 ephemeral pair is generated, HKDF-combined with the root key to produce fresh chain keys. This provides break-in recovery — compromise of one message key exposes nothing past or future.

5.4 Post-Quantum Layer (ML-KEM-768)

ML-KEM-768 (CRYSTALS-Kyber, FIPS 203) is combined with the classical X3DH output via HKDF-SHA256 to produce the initial root key. This means breaking the key exchange requires simultaneously defeating both:

  • Classical X25519 ECDH (vulnerable to future quantum computers), and
  • ML-KEM-768 (believed post-quantum secure).

5.5 Sealed Sender

Every outgoing message envelope is wrapped in a sealed-sender box:

ephemeral X25519 key pair generated per-message
ECDH(ephemeral_priv, recipient_pub) → shared secret
AES-256-GCM(shared_secret, envelope)

The relay receives only an opaque blob it cannot link to a sender identity. The recipient derives the shared secret using their private key and the ephemeral public key included in the box.

5.6 Header Encryption

Ratchet envelope headers (sender's DH public key, message number, previous chain length) are encrypted with a Next Header Key (NHK) derived via HKDF("SylvaCrypt-InitV2") from the initial shared secret. The NHK rotates on every DH ratchet step, giving full forward secrecy for metadata.

5.7 Image & Voice Encryption

Images:

  • A random 256-bit AES key + 12-byte IV are generated in the browser.
  • IV || ciphertext is uploaded to the private chat-images bucket.
  • The AES key is JSON-encoded into the ratchet plaintext and encrypted by the Double Ratchet before transit.
  • Recipient decrypts the ratchet ciphertext, extracts the key, fetches a 1-hour signed URL, and decrypts the image blob client-side.
  • Daily cap: 10 images per user per UTC day.

Voice:

  • Audio recorded as Opus CVBR (32 kbps ceiling) in a WebM container.
  • Same encryption scheme as images; stored in the private chat-voices bucket.
  • Plaintext audio never leaves the browser.
  • Daily cap: 10 minutes per user per UTC day (enforced via voice_send_durations table).

5.8 Recovery Phrase

Registration:
  generateMnemonic()     → 12-word BIP-39 phrase (128-bit entropy)
  Argon2id(phrase)       → mnemonic_hash   (stored in profiles table)
  AES-256-GCM(phrase)    → encrypted blob  (stored in vault IndexedDB)

Password reset:
  User provides phrase
  Edge Function: Argon2id(input) == profiles.mnemonic_hash?
    Yes → admin.updateUserById(userId, newPassword)
    No  → 400 error

The plaintext mnemonic is never transmitted to the server. After a password reset, the local vault is cleared (message history lost).


6. Data Flows

6.1 Sending a Text Message

1. User types message → ChatArea
2. relay.sendEncryptedMessage(conversationId, plaintext)
3.   localStore.getRatchetSession(conversationId) → session
4.   doubleRatchet.ratchetEncrypt(session, plaintext)
5.     kdfCK(CKs) → (CKs', MK)
6.     AES-256-GCM(MK, UTF8(plaintext)) → ciphertext + IV
7.     Build EncryptedEnvelope { senderDHPub, Ns, PN, IV, ciphertext }
8.   localStore.saveRatchetSession(conversationId, updatedSession)
9.   supabase.from('messages').insert(envelope)       ← relay
10.  dbStore.saveMessageToDBFull(localMessage)         ← local vault
11. Supabase Realtime pushes row to recipient
12. Recipient: relay.receiveAndDecryptMessage(envelope)
13.   doubleRatchet.ratchetDecrypt(session, envelope)
14.   Returns plaintext → stored in local vault + rendered in UI

6.2 Sending a Voice Message

Sender:
1. User taps mic → MediaRecorder starts (Opus CVBR / WebM, 250 ms chunks)
2. User taps stop → MediaRecorder.stop() → Blob (audio/webm)
3. relay.uploadVoiceMessage(userId, blob, duration, mimeType)
4.   Rate-limit check: get_voice_send_duration(userId) + duration ≤ 600s?
5.   encryptFileAESGCM(blob) → { ciphertextBlob, keyBase64 }
6.   supabase.storage('chat-voices').upload(path, ciphertextBlob)
7.   increment_voice_send_duration(userId, duration)
8. relay.sendEncryptedMessage(..., voiceAttachment)
   - ratchetPlaintext = { v:3, t:'', voiceStoragePath, voiceKey, duration }
   - Double Ratchet encrypts → voiceKey travels securely in ciphertext
9. dbStore.saveMessageToDBFull(localMessage) — key vault-wrapped before write

Recipient:
10. Receive ratchet-decrypted { v:3, voiceStoragePath, voiceKey, duration }
11. VoiceMessageBubble: on first play, fetchAndDecryptVoiceMessage(path, key)
12.   createSignedUrl(path, 3600) → 1-hour signed URL
13.   fetch(signedUrl) → ciphertextBlob
14.   decryptBlobAESGCM(ciphertextBlob, key) → ArrayBuffer (plaintext audio)
15.   URL.createObjectURL(new Blob([plainbuf], { type:'audio/webm' }))
16.   <audio> element plays — plaintext audio never leaves the browser

7. Repository Structure

SylvaCrypt/
├── src/
│   ├── App.tsx                      # Root: router, providers, PWA banner
│   ├── components/
│   │   ├── chat/                    # ChatArea, Sidebar, dialogs, bubbles, emoji picker
│   │   ├── common/                  # RouteGuard, PageMeta
│   │   └── ui/                      # shadcn/ui primitives
│   ├── contexts/
│   │   ├── AuthContext.tsx           # Auth state, vault unlock, mnemonic management
│   │   └── ThemeContext.tsx          # Dark/light theme
│   ├── lib/
│   │   ├── crypto.ts                 # AES-GCM, X25519, HKDF, PBKDF2, Argon2id
│   │   ├── x3dh.ts                   # X3DH key agreement
│   │   ├── doubleRatchet.ts          # Double Ratchet + NHK header key rotation
│   │   ├── relay.ts                  # [Supabase](https://supabase.com/) messaging relay & server sync
│   │   ├── localStore.ts             # Encrypted IndexedDB vault
│   │   ├── deviceStore.ts            # Multi-device session manager
│   │   ├── dbStore.ts                # Supabase contacts & messages store
│   │   ├── session.ts                # Session lifecycle, key derivation
│   │   ├── mnemonic.ts               # BIP-39 utilities
│   │   └── zxcvbn.ts                 # Password strength estimation
│   ├── pages/
│   │   ├── AuthPage.tsx              # Login, register, migrate, forgot password
│   │   ├── ChatPage.tsx              # Main chat interface
│   │   ├── SettingsPage.tsx          # Profile, bio, avatar, recovery phrase
│   │   ├── LinkedDevicesPage.tsx     # Multi-device management
│   │   ├── SafetyNumberPage.tsx      # Key fingerprint verification
│   │   ├── ThemesPage.tsx            # Public themes browser
│   │   ├── DocsHomePage.tsx          # In-app documentation hub
│   │   ├── SecurityWhitepaperPage.tsx
│   │   ├── TroubleshootingPage.tsx
│   │   └── PrivacyPolicyPage.tsx
│   └── types/types.ts                # Shared TypeScript interfaces
├── supabase/
│   ├── functions/
│   │   ├── delete-account/           # Admin account deletion
│   │   └── reset-password/           # Mnemonic-verified password reset
│   └── migrations/                   # 119 ordered SQL migration files
├── public/                           # Static assets, service worker, PWA icons
├── .env.example                      # Environment variable template
├── Dockerfile                        # Container build
├── nginx.conf                        # Nginx config for self-hosted deployments
├── package.json                      # Dependencies and scripts
├── vite.config.ts                    # Vite build configuration
├── tailwind.config.js                # Tailwind CSS configuration
├── biome.json                        # Biome linter configuration
├── ARCHITECTURE.md                   # Technical architecture (detailed)
├── SECURITY.md                       # Threat model and responsible disclosure
├── USAGE.md                          # End-user guide
├── CONTRIBUTING.md                   # Contribution guidelines
├── SELF_HOSTING.md                   # Self-hosting instructions
└── FAQ.md                            # Frequently asked questions

8. Tech Stack

Layer Technology
Frontend framework React 18, TypeScript 5, Vite (rolldown-vite)
Styling Tailwind CSS 3, shadcn/ui (Radix UI primitives)
Routing React Router v7 / v8
Cryptography Web Crypto API, hash-wasm (Argon2id WASM), @noble/curves (X25519), @noble/post-quantum (ML-KEM-768), @scure/bip39
Backend Supabase (PostgreSQL, Auth, Realtime, Storage, Edge Functions)
Local storage IndexedDB via idb, encrypted vault in localStore.ts
Password strength @zxcvbn-ts/core
Linting Biome 2.x
Build / test Vite, Vitest
Containerisation Docker + Nginx

9. Getting Started

9.1 Prerequisites

  • Node.js ≥ 18
  • pnpm ≥ 8
  • A Supabase project (cloud or self-hosted)
  • Supabase CLI (for running migrations and deploying functions)

9.2 Local Development

# Clone
git clone https://github.com/Forestritium/SylvaCrypt.git
cd SylvaCrypt

# Install dependencies
pnpm install

# Start dev server (http://localhost:5173)
pnpm dev

Linting (must pass before any PR):

pnpm run lint
# Runs: tsgo type-check → biome lint → Tailwind CSS syntax check → Vite production build

Tests:

pnpm test
# Runs Vitest in run mode

9.3 Environment Variables

Copy the template and fill in your Supabase project credentials:

cp .env.example .env
VITE_SUPABASE_URL=https://your-project-ref.supabase.co
VITE_SUPABASE_ANON_KEY=your-supabase-anon-key

9.4 Database Migrations

# Apply all migrations to your Supabase project
supabase db push

# Or apply manually, in order, via the Supabase SQL Editor
# Files: supabase/migrations/00000_*.sql → 00119_*.sql

9.5 Edge Functions

supabase functions deploy delete-account
supabase functions deploy reset-password

10. Self-Hosting

SylvaCrypt is fully self-hostable. A Dockerfile and nginx.conf are included.

  1. Create a Supabase project (cloud or self-hosted Supabase instance).
  2. Apply all migrations from supabase/migrations/ in sequential order.
  3. Enable Email/Password authentication in Supabase Auth settings.
  4. Create private Storage buckets named chat-images and chat-voices.
  5. Set environment variables and build:
pnpm build
# Output: dist/
  1. Serve the dist/ folder over HTTPS. The Web Crypto API requires a secure context (HTTPS or localhost).

Important: Self-hosters are responsible for securing their Supabase project, managing RLS policies, applying future migrations promptly, and keeping all dependencies up to date.


11. User Guide

11.1 Creating an Account

  1. Open SylvaCrypt in your browser and click Create Account.
  2. Enter a username (3–32 characters; letters, numbers, underscores only) and click Continue.
  3. Set a password meeting all requirements: 6–255 characters, at least one uppercase, lowercase, number, and special character. Aim for Strong or Very Strong on the strength meter.
  4. Click Create Account.
  5. Save your 12-word recovery phrase immediately — it appears once. Write it down or download the text file. It is the only way to recover your account if you forget your password.

11.2 Logging In

Enter your username and password, then click Sign In. If your account was created before the current password system, you will be prompted for a one-time password migration on first login.

11.3 Sending Messages

  1. Select a contact from the sidebar.
  2. Type in the message input and press Enter or click Send.
  3. To reply to a specific message, hover over it and click the Reply icon (or long-press on mobile). A preview appears above the input. Click × to cancel.

All messages are encrypted locally before transmission.

11.4 Images and Voice Messages

Images:

  1. Click the image icon in the chat input area.
  2. Select a file (max 10 MB), preview it, and click Send.
  3. Daily limit: 10 images per user, resetting at midnight UTC.

Voice messages:

  1. Click the microphone icon to start recording. A red indicator with an elapsed timer appears.
  2. Click the stop button to send, or the cancel button to discard.
  3. Daily limit: 10 minutes of voice per user, resetting at midnight UTC.

11.5 Contacts & Requests

Adding a contact:

  1. Click the + button in the sidebar.
  2. Enter the username and click Send Request.
  3. The other person must accept before messaging is possible.

Incoming requests: A badge appears in the sidebar. Click Accept or Decline in the Requests panel.

Blocking a user: Open a conversation → ⋮ menu → Block User.

Unblocking: Go to Settings → Blocked Users and click Unblock.

11.6 Settings & Profile

Access via the gear icon or your avatar in the sidebar.

Setting Description
Avatar Upload a profile picture (max 2 MB). Toggle avatar privacy to hide from non-contacts.
Bio Short bio visible to contacts (max 160 characters).
Username Changeable once every 30 days.
Recovery Phrase View or regenerate your BIP-39 mnemonic.
Blocked Users Manage your block list.
Linked Devices Add or remove linked devices.
Notifications Configure anonymous push notifications.
Themes Browse and apply public themes.
Waveform Customise voice message waveform display.
Delete Account Permanently delete account and all data.

11.7 Password & Recovery

Password requirements: 6–255 characters with uppercase, lowercase, number, and special character. No email is used for authentication.

Forgot password (with recovery phrase):

  1. Click Forgot Password on the login screen.
  2. Enter your username and your 12-word phrase (all lowercase, space-separated).
  3. Set a new password and click Reset Password.

After a password reset, your local vault is cleared — on-device message history is lost. Contacts are re-synced from the server.

Viewing or regenerating your phrase:

Go to Settings → Recovery Phrase and enter your current password to reveal it. You can regenerate it (old phrase immediately invalidated). Store the new phrase securely.

11.8 Account Deletion

Go to Settings → Delete Account, enter your password, and confirm. This permanently removes your Supabase Auth account, profile, contacts, relay messages, and local vault. This action cannot be undone.

11.9 Safety Numbers

A safety number is a 60-digit fingerprint derived from your public key and your contact's public key. To verify you are not subject to a man-in-the-middle attack, compare your safety number with your contact over a separate channel (phone call, in person). Navigate to a conversation → contact name → Safety Number.

11.10 Privacy Tips

  • Use a strong, unique password. Argon2id makes brute force very expensive, but a weak password undermines it.
  • Save your recovery phrase offline (paper or password manager). Do not store it as plain text on the same device.
  • Log out and close the tab on shared or untrusted machines.
  • Verify contact safety numbers out-of-band before sharing sensitive information.
  • Keep your browser updated — SylvaCrypt depends on modern Web Crypto APIs.
  • Notifications are anonymous — they reveal neither the sender's identity nor message content.

12. Contributing

12.1 Development Setup

# Fork the repo, then:
git clone https://github.com/YOUR_USERNAME/SylvaCrypt.git
cd SylvaCrypt
pnpm install
git checkout -b feat/my-feature

pnpm dev        # Start dev server
pnpm run lint   # Must pass before opening a PR
pnpm test       # Run tests

12.2 Code Style

  • 2-space indentation throughout.
  • No default exports for utility functions — use named exports.
  • TypeScript strict mode — no any or unchecked casts without an explanatory comment.
  • No raw Tailwind colour values (e.g. bg-blue-500) — use semantic tokens (bg-primary, text-muted-foreground).
  • All crypto operations must go through src/lib/crypto.ts. Do not introduce third-party crypto libraries without a security review.
  • Never write user message content, keys, or passwords to localStorage or unencrypted DB columns.
  • Comments explain why, not what. Remove dead code instead of commenting it out.

12.3 Commit Conventions

Follow [Conventional Commits](https://www.conventionalcommits.org):

<type>(<scope>): <short summary>
Type When to use
feat New user-visible feature
fix Bug fix
chore Maintenance, dependency updates, tooling
docs Documentation only
refactor Code restructuring without behaviour change
perf Performance improvements
style Formatting / whitespace (no logic changes)
test Adding or updating tests
build Build system or config changes
ci CI/CD pipeline changes

Examples:

feat(auth): add BIP-39 recovery phrase on registration
fix(relay): prevent duplicate message delivery on reconnect
chore: remove unused embla-carousel dependency
docs: add SECURITY.md and threat model

12.4 Pull Request Process

  1. Rebase your branch onto main before opening the PR.
  2. Open the PR against main on Forestritium/SylvaCrypt.
  3. Fill in the PR template completely. Reference related issues (Closes #123).
  4. Ensure pnpm run lint passes with zero errors (type-check + biome + Tailwind + build).
  5. A maintainer will review within 7 days. Address any requested changes promptly.
  6. Once approved and CI passes, a maintainer merges.

Reporting bugs: Search existing [issues](https://github.com/Forestritium/SylvaCrypt/issues) first; include steps to reproduce, expected vs. actual behaviour, browser/OS details, and any console errors.

Suggesting features: Open a [GitHub Discussion](https://github.com/Forestritium/SylvaCrypt/discussions) with the problem, proposed solution, alternatives considered, and whether you plan to implement it.


13. Reporting Vulnerabilities

Do NOT open a public GitHub issue for security vulnerabilities.

  1. Email the maintainer at the contact listed on the GitHub profile of [A-Solo-Engineer](https://github.com/A-Solo-Engineer).
  2. Include: vulnerability description, affected components/versions, reproduction steps, impact assessment, and any suggested mitigations.
  3. If the report is sensitive, request a PGP key before sending.

Response timeline:

Stage Target
Acknowledgement Within 72 hours
Initial assessment Within 7 days
Fix or mitigation ≤ 30 days (critical), ≤ 90 days (moderate)
Public disclosure Coordinated with reporter after fix is deployed

Reporters who follow responsible disclosure will be credited unless they prefer anonymity.


14. FAQ

What is SylvaCrypt? SylvaCrypt (formerly ShadowCrypt) is an open-source, end-to-end encrypted messaging web app with a zero-knowledge relay. The server never holds decryption keys or plaintext.

Is my data really private? Yes. All messages are encrypted locally on your device before transmission. Supabase only ever routes opaque ciphertext; a full server compromise exposes only encrypted blobs.

What encryption does SylvaCrypt use? Double Ratchet (X25519 + HKDF-SHA256 + AES-256-GCM) combined with an ML-KEM-768 post-quantum layer for the initial key exchange. AES-256-GCM for symmetric encryption; Argon2id for vault key derivation.

What is a Safety Number? A 60-digit fingerprint derived from both your and your contact's public keys. Verifying it out-of-band (e.g. over a phone call) confirms your conversation has not been intercepted.

How often do keys rotate? After every single message. The symmetric ratchet advances with each send/receive; the DH ratchet advances on every reply.

Can I use multiple devices? Yes. Each device maintains independent E2E sessions with your contacts via user_devices.

What if I lose my device? Log in from a new device using your credentials and recovery phrase. New messages will arrive; past message history is not recoverable unless you have a local backup (messages are deleted from the relay after delivery).

Why did my session reset? A Double Ratchet state mismatch (for example, after a long offline period where too many messages were missed) can trigger an unrecoverable state error. The app detects downgrade attacks and errors out. Re-initiate the conversation to establish a fresh session.

Is email required? No. SylvaCrypt uses username + password authentication. Password recovery uses the BIP-39 mnemonic, not email.


15. License

SylvaCrypt is licensed under the GNU Affero General Public License v3.0 (AGPL-3.0).

Under the AGPL, if you modify SylvaCrypt and run it as a network service, you must release your modifications under the same license.

See [LICENSE](https://github.com/Forestritium/SylvaCrypt/blob/main/LICENSE) for the full text.