Skip to content

Encryption Architecture

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

  • Via libsodium-wrappers
  • 32-byte key (256-bit)
  • 24-byte nonce (192-bit)
  • AEAD with Poly1305 MAC

Passwords

Argon2id

  • Memory: 64MB
  • Iterations: 3
  • Parallelism: 4
  • Salt: 16 bytes
  • Output: 32 bytes

Server-Side

ChaCha20-Poly1305

  • Via golang.org/x/crypto
  • Encrypts metadata only
  • Uses SERVER_ENCRYPTION_KEY
  • Not zero-knowledge

Implementation: frontend/src/App.tsx - CryptoService class

// Generate nonce (24 bytes for XChaCha20)
const nonce = new Uint8Array(sodium.crypto_secretbox_NONCEBYTES) // 24 bytes
crypto.getRandomValues(nonce) // Web Crypto API (not libsodium)
// Encrypt with XChaCha20-Poly1305
const messageBytes = sodium.from_string(plaintext)
const ciphertext = sodium.crypto_secretbox_easy(messageBytes, nonce, masterKey)
// Combine nonce + ciphertext
const combined = new Uint8Array(nonce.length + ciphertext.length)
combined.set(nonce)
combined.set(ciphertext, nonce.length)
// Base64 encode for transmission
const encryptedData = sodium.to_base64(combined, sodium.base64_variants.ORIGINAL)
// Decode base64
const combined = sodium.from_base64(encryptedData, sodium.base64_variants.ORIGINAL)
// Extract nonce (first 24 bytes) and ciphertext
const nonce = combined.slice(0, sodium.crypto_secretbox_NONCEBYTES)
const ciphertext = combined.slice(sodium.crypto_secretbox_NONCEBYTES)
// Decrypt with XChaCha20-Poly1305
const decrypted = sodium.crypto_secretbox_open_easy(ciphertext, nonce, masterKey)
// Convert bytes to string
return sodium.to_string(decrypted)

Backend Implementation: backend/crypto/password.go

// Hash password with Argon2id
hash := 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 TypeEncrypted WhereKey UsedAlgorithm
Note titleClientMaster keyXChaCha20-Poly1305
Note contentClientMaster keyXChaCha20-Poly1305
Folder namesClientMaster keyXChaCha20-Poly1305
Tag namesClientMaster keyXChaCha20-Poly1305
AttachmentsClientMaster keyXChaCha20-Poly1305
Email addressServerServer keyChaCha20-Poly1305
Session dataServerServer keyChaCha20-Poly1305
Audit log IPsServerServer keyChaCha20-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

  • Access Token: Memory only (React state)
  • Refresh Token: Not yet implemented
  • Never stored in localStorage

Validation

  1. JWT signature verified with JWT_SECRET
  2. Redis session checked for existence
  3. Session data validated (encrypted IP/user agent)
  4. Expiration checked (default: 24 hours)

Session Key Format: session:<sha256-token-hash>

Implementation: backend/middleware/jwt.go

  • Server compromise: Attacker can’t read notes without user passwords
  • Database dump: All content encrypted, useless without keys
  • Man-in-the-middle: Encryption before TLS (defense in depth)
  • Timing attacks: Password verification uses constant-time comparison
  • Client-side attacks: XSS, malicious extensions can steal keys from memory
  • Weak passwords: Argon2id can’t protect simple passwords (12-char minimum enforced)
  • Phishing: Users entering passwords on fake sites
  • Compromised endpoints: Malicious JavaScript breaks encryption
ComponentFile PathKey Functions
Client encryptionfrontend/src/App.tsxCryptoService.encryptData(), CryptoService.decryptData()
Password hashingbackend/crypto/password.goHashPassword(), VerifyPassword()
Server encryptionbackend/crypto/crypto.goEncrypt(), Decrypt() (ChaCha20-Poly1305)
Database schemabackend/database/schema.goEncrypted column definitions
Auth handlersbackend/handlers/auth.goJWT generation, session management
# Required for encryption
SERVER_ENCRYPTION_KEY=<32-character-base64-key> # openssl rand -base64 32
JWT_SECRET=<64-character-base64-secret> # openssl rand -base64 64

See Environment Variables for complete reference.