Skip to content

Introduction

ursalock provides end-to-end encrypted document storage using passkey-derived keys.

You’re building a web app and want to:

  • Store user data securely
  • Sync data across devices
  • Keep user data truly private (zero-knowledge)

Traditional solutions either:

  • Store data in plaintext (Firebase, Supabase)
  • Require complex key management (PGP, manual encryption)
  • Vendor lock-in with proprietary E2EE (1Password, Bitwarden)

ursalock provides:

Passkey-Based E2EE

  • Your passkey derives the encryption key via WebAuthn PRF
  • No recovery key to store — your passkey IS the key
  • Same passkey = same data on any device

Document-Level Storage

  • Store encrypted documents in collections
  • Each document independently encrypted
  • Efficient syncing (only changed documents)

Zero-Knowledge Architecture

  • Server stores only encrypted ciphertext
  • Server never sees your plaintext
  • All crypto happens client-side

Self-Hostable

  • Single Docker image
  • SQLite storage (no external DB)
  • Your server, your data
┌──────────────────────────────────────────────────────┐
│ CLIENT │
│ │
│ Passkey → PRF → cipherJwk → deriveVaultKeys() │
│ ↓ │
│ encryptionKey + hmacKey │
│ ↓ │
│ Document → AES-256-GCM → Ciphertext │
│ │
└────────────────────────┬─────────────────────────────┘
│ HTTPS (encrypted documents)
┌──────────────────────────────────────────────────────┐
│ SERVER │
│ │
│ Receives encrypted documents → Stores in SQLite │
│ Server CANNOT read your data │
│ Server only knows document metadata (uid, version) │
│ │
└──────────────────────────────────────────────────────┘
  1. User authenticates with their passkey
  2. WebAuthn PRF derives a cipherJwk (master key)
  3. Vault-specific keys derived via HKDF
  4. Documents encrypted/decrypted with vault keys
  5. Server stores and syncs encrypted documents

The server never sees your encryption key or plaintext data.

  • Your biometric or security key IS the encryption key
  • Synced by your password manager (iCloud, Google, Proton Pass)
  • No separate secret to manage or lose
  • One tap to authenticate — no password to type

One tradeoff: the cipherJwk lives only in memory. After a page refresh:

  • JWT (auth token) persists ✓
  • cipherJwk is gone ✗

Solution: prompt for passkey on refresh. It’s a quick tap — no password to type.