Core Principle
LeafLock implements zero-knowledge end-to-end encryption. All note content is encrypted client-side before transmission. The server never has access to plaintext data or encryption keys.
Core Principle
LeafLock implements zero-knowledge end-to-end encryption. All note content is encrypted client-side before transmission. The server never has access to plaintext data or encryption keys.
Client-Side
XChaCha20-Poly1305
Passwords
Argon2id
Server-Side
ChaCha20-Poly1305
golang.org/x/cryptoSERVER_ENCRYPTION_KEYImplementation: frontend/src/App.tsx - CryptoService class
// Generate nonce (24 bytes for XChaCha20)const nonce = new Uint8Array(sodium.crypto_secretbox_NONCEBYTES) // 24 bytescrypto.getRandomValues(nonce) // Web Crypto API (not libsodium)
// Encrypt with XChaCha20-Poly1305const messageBytes = sodium.from_string(plaintext)const ciphertext = sodium.crypto_secretbox_easy(messageBytes, nonce, masterKey)
// Combine nonce + ciphertextconst combined = new Uint8Array(nonce.length + ciphertext.length)combined.set(nonce)combined.set(ciphertext, nonce.length)
// Base64 encode for transmissionconst encryptedData = sodium.to_base64(combined, sodium.base64_variants.ORIGINAL)// Decode base64const combined = sodium.from_base64(encryptedData, sodium.base64_variants.ORIGINAL)
// Extract nonce (first 24 bytes) and ciphertextconst nonce = combined.slice(0, sodium.crypto_secretbox_NONCEBYTES)const ciphertext = combined.slice(sodium.crypto_secretbox_NONCEBYTES)
// Decrypt with XChaCha20-Poly1305const decrypted = sodium.crypto_secretbox_open_easy(ciphertext, nonce, masterKey)
// Convert bytes to stringreturn sodium.to_string(decrypted)Backend Implementation: backend/crypto/password.go
// Hash password with Argon2idhash := argon2.IDKey( []byte(password), salt, // 16 bytes 3, // iterations (time cost) 64*1024, // memory cost (64MB = 65536 KB) 4, // parallelism (4 threads) 32, // hash length (32 bytes))
// Encoded format// $argon2id$v=19$m=65536,t=3,p=4$<base64-salt>$<base64-hash>
// Verification uses crypto/subtle.ConstantTimeCompare (timing attack prevention)| Data Type | Encrypted Where | Key Used | Algorithm |
|---|---|---|---|
| Note title | Client | Master key | XChaCha20-Poly1305 |
| Note content | Client | Master key | XChaCha20-Poly1305 |
| Folder names | Client | Master key | XChaCha20-Poly1305 |
| Tag names | Client | Master key | XChaCha20-Poly1305 |
| Attachments | Client | Master key | XChaCha20-Poly1305 |
| Email address | Server | Server key | ChaCha20-Poly1305 |
| Session data | Server | Server key | ChaCha20-Poly1305 |
| Audit log IPs | Server | Server key | ChaCha20-Poly1305 |
CREATE TABLE users ( id UUID PRIMARY KEY, email_hash BYTEA UNIQUE NOT NULL, -- SHA-256 hash email_encrypted BYTEA NOT NULL, -- ChaCha20-Poly1305 email_search_hash BYTEA UNIQUE, -- For login lookups password_hash TEXT NOT NULL, -- Argon2id salt BYTEA NOT NULL, -- 16 bytes mfa_secret_encrypted BYTEA, -- TOTP secret ...);CREATE TABLE notes ( id UUID PRIMARY KEY, workspace_id UUID, title_encrypted BYTEA NOT NULL, -- XChaCha20-Poly1305 (client) content_encrypted BYTEA NOT NULL, -- XChaCha20-Poly1305 (client) content_hash BYTEA NOT NULL, -- Integrity verification ...);See Database Schema for complete schema.
Token Storage
Validation
JWT_SECRETSession Key Format: session:<sha256-token-hash>
Implementation: backend/middleware/jwt.go
| Component | File Path | Key Functions |
|---|---|---|
| Client encryption | frontend/src/App.tsx | CryptoService.encryptData(), CryptoService.decryptData() |
| Password hashing | backend/crypto/password.go | HashPassword(), VerifyPassword() |
| Server encryption | backend/crypto/crypto.go | Encrypt(), Decrypt() (ChaCha20-Poly1305) |
| Database schema | backend/database/schema.go | Encrypted column definitions |
| Auth handlers | backend/handlers/auth.go | JWT generation, session management |
# Required for encryptionSERVER_ENCRYPTION_KEY=<32-character-base64-key> # openssl rand -base64 32JWT_SECRET=<64-character-base64-secret> # openssl rand -base64 64See Environment Variables for complete reference.