Skip to content

End-to-End Encryption

Private vaults use client-side end-to-end encryption. The server never sees plaintext file contents, raw encryption keys, or your passphrase.

Tusky uses a 3-layer key hierarchy:

Passphrase (user-provided)
└─ PBKDF2 (600,000 iterations, SHA-256)
└─ Wrapping Key (256-bit)
├─ Verifier (HMAC-SHA-256) — stored on server
└─ Decrypts → Master Key (256-bit, random)
└─ Wraps → Per-file Key (256-bit, random)
└─ Encrypts → File Content (AES-256-GCM)
KeyPurposeWhere it lives
Master KeyWraps/unwraps per-file keysServer (encrypted with wrapping key)
Wrapping KeyEncrypts master key, verifies passphraseDerived at runtime, never stored
Recovery KeyBackup wrap of master keyUser’s responsibility (shown once)
Per-file KeyEncrypts one fileServer (wrapped with master key)
ParameterValue
Symmetric cipherAES-256-GCM
Key derivationPBKDF2, SHA-256, 600,000 iterations
Salt128-bit (16 bytes)
AES key / IV / tag256-bit / 96-bit / 128-bit

Before using private vaults, set up encryption with a passphrase:

// Using the CLI
// $ tusky encryption setup
// Using the SDK — the web app handles this via UI
// Setup sends: salt, verifier, encryptedMasterKey, masterKeyWrappedBackup

The setup process:

  1. Generates a random 256-bit master key
  2. Derives a wrapping key from your passphrase via PBKDF2
  3. Computes a verifier (HMAC-SHA-256) for passphrase validation
  4. Encrypts the master key with the wrapping key
  5. Generates a recovery key and wraps the master key with it as backup
  6. Sends encrypted data to the server — the server never sees the raw keys

For private vaults, the client encrypts before uploading:

  1. Generate a random 256-bit per-file key and 96-bit IV
  2. Encrypt the file with AES-256-GCM using the per-file key
  3. Wrap the per-file key with the master key (AES-256-GCM)
  4. Compute SHA-256 checksum of the plaintext
  5. Upload the ciphertext via the standard upload flow
  6. Include encryption metadata: wrappedKey, encryptionIv, plaintextSizeBytes, plaintextChecksumSha256

The SDK and CLI handle this automatically when uploading to a private vault with an active encryption session.

  1. Get the download URL and encryption metadata from the API
  2. Download the encrypted bytes
  3. Unwrap the per-file key using the master key
  4. Decrypt the file with AES-256-GCM
  5. Verify the SHA-256 checksum matches
const { downloadUrl, encryption } = await tusky.files.getDownloadUrl(file.id);
const response = await fetch(downloadUrl);
const encrypted = new Uint8Array(await response.arrayBuffer());
// Decrypt client-side using encryption.wrappedKey and encryption.iv
// The CLI and web app handle this automatically

Changing your passphrase re-wraps the master key with a new wrapping key. The master key itself doesn’t change, so all existing encrypted files remain accessible:

Terminal window
tusky encryption change-passphrase

A new recovery key is generated — save it securely.

If you forget your passphrase, use the recovery key:

Terminal window
tusky encryption recover
# Enter recovery key
# Set a new passphrase

The server never sees:

  • Plaintext file contents (private vaults)
  • Raw master key, wrapping key, recovery key, or per-file keys
  • Your passphrase

Integrity guarantees:

  • AES-GCM authenticated encryption detects any tampering
  • SHA-256 checksum provides additional integrity verification
  • Constant-time comparison prevents timing attacks on verifier checks

Threat model:

  • A compromised server only obtains encrypted master keys — useless without the passphrase
  • PBKDF2 with 600,000 iterations provides brute-force resistance
  • If both server and recovery key are compromised, the master key can be recovered