Skip to content
Kura Vault
GitHub

Security model

What Kura Vault protects, and where it stops

Kura Vault is built so that the server, the database and the storage bucket can be fully compromised without anyone seeing a single photo. This page explains how, in plain language, including the limits.

Last updated 27 October 2026

The technical reference is docs/security.md and docs/crypto in the repository. To report a vulnerability, see SECURITY.md. Kura Vault has not had an independent audit yet.

The core guarantee

Photos, videos, file names, location data, album names and vault names are encrypted on your device. Every key is either random or derived from a passphrase that never leaves the device, and is stored only wrapped: under a passphrase, under another key, or sealed to a user’s public key. The API, the database and the storage bucket hold only ciphertext and wrapped keys.

Someone with full access to all three still cannot view anything without a user’s master passphrase.

Three secrets

SecretWhat it opensWho sees it
PasswordThe API: a session on the serverThe server, once per sign-in, to check a bcrypt hash
Master passphraseYour account’s private keysNobody. It is checked on your device
Vault passphraseA vault you created, on top of the master passphraseNobody

The apps refuse a master passphrase that equals your password, because the server sees passwords.

How the keys fit together

Every vault, album and item has its own random key. Each key is wrapped by the one above it, and the top of the chain is opened only by a passphrase on your device.

master passphrase ─Argon2id─► master key ─► your private keys
                                              (X25519 for sealing, Ed25519 for signing)
vault passphrase  ─Argon2id─► passphrase key   (vaults you created)

vault key   random, one per vault
  ├─ your copy        sealed to your public key, under your passphrase key
  ├─ each recipient   sealed to their public key, signed by you
  └─ wraps ─► album key   random, one per album
  └─ wraps ─► media key   random, one per item
                 └─ encrypts ─► thumbnail, preview, original, metadata
  • Key derivation: Argon2id v1.3, 3 iterations, 64 MiB of memory, a 16-byte random salt.
  • Encryption: AES-256-GCM with a fresh random 12-byte IV for every encryption.
  • Sealing to a person: an ephemeral X25519 key exchange, HKDF-SHA256, then AES-256-GCM.
  • Signatures: Ed25519.
  • Binding: every encryption authenticates a label naming what it is and the rows it belongs to, so the server cannot move a key or a file to another user, vault, album or item.

Whose public key is this?

The server hands out public keys, so in principle it could hand out its own. Two signed statements, checked on your device, stop that. The server owner’s client signs a certificate for every user’s keys, and your device only seals a key to someone whose certificate verifies. Every shared key also carries the sharer’s signature, so the server cannot plant a key it knows.

A new user’s keys are bound to their invite link. The link carries a 32-byte secret in its #fragment, which browsers never send to any server, and the owner’s client checks the binding before it certifies anyone.

Threats and mitigations

ThreatMitigation
Storage provider or bucket compromiseBlobs are AES-256-GCM ciphertext; keys are never in the bucket.
Database or API server compromiseOnly wrapped keys, salts, public keys and structural metadata are stored.
The server substitutes a public key to receive a shared keyKeys are sealed only to users whose keys the owner certified. A new user’s keys are bound to their invite link, which the server never sees.
The server plants a key it knows on a userA shared key is used only if its certified sender signed it.
The server moves a wrapped key or blob to another rowEvery ciphertext authenticates the ids it belongs to.
Someone finds the APINo sign-up: accounts come from the owner’s invite links. bcrypt passwords, lockout and rate limiting.
Someone finds a new server before its owner doesCreating the owner needs the setup key from the server’s log or environment.
Stolen access token15-minute lifetime. It grants ciphertext and short-lived URLs, not keys.
Stolen refresh tokenStored hashed, rotated on every use, revoked on logout; an httpOnly cookie in browsers on the server’s own address.
Leaked storage URLPre-signed, valid 60 seconds for downloads and 15 minutes for uploads. It yields ciphertext only.
Passphrase guessingArgon2id with a 64 MiB memory cost per guess.
Someone learns a vault passphraseIt opens nothing without that account’s private keys as well.
Someone looks at your unlocked appVaults you created are never listed; only the ones matching an entered passphrase open.
Lost or borrowed deviceKeys are held in memory only and destroyed on lock, after the chosen idle time (15 minutes at most), and when the app or tab closes. The exception is the app’s optional fingerprint or face unlock.
A user should no longer have accessUnsharing deletes their wrapped keys and is enforced on their next request.

What is not hidden

The server needs some information to sort, paginate and authorise. Anyone who compromises it can see:

  • Usernames, each account’s role and access level, and public keys.
  • How many vaults, albums and items exist, which items are in which, who created each vault, and who it is shared with.
  • Per item: photo or video, capture time, dimensions, duration, file size, and who uploaded it.
  • Access patterns: when an account signs in and what it fetches.

Limits, by design or not yet built

  • No passphrase recovery. A forgotten master passphrase makes everything only that account could open permanently unreadable. The owner cannot reset it.
  • Revoking access is a server cut-off, not re-encryption. Someone who kept a key and later obtains ciphertext from a breach could still decrypt it. Key rotation is not built yet.
  • Download links already issued stay valid for up to 60 seconds after access is removed.
  • Deleting cannot reach copies that already left, such as downloads, operator backups or versioned buckets.
  • Read versus write is enforced by the server. What stops a view-only user from uploading is the API, not the cryptography.
  • Whoever opens an invite link first becomes that user. Links work once, for 24 hours. Send them over a channel you trust.
  • The owner is trusted to say who the users are, not to read anything. A malicious owner could certify a key of their own under someone’s name.
  • Hidden vaults are hidden from people looking at your screen, not from the server operator.
  • A compromised device is out of scope. Malware on an unlocked device can read what you can.
  • A web client is code served by its host. A compromised host could serve a modified client. The strongest setup is the web client served by your own server.

The hosted web client

Greytimelabs hosts a web client at kura-app.greytimelabs.com that can connect to any Kura Vault server. It is convenient, and weaker than the client your own server serves, in three ways:

  • Refresh tokens are kept in browser storage, because a cookie cannot cross sites. A script injection bug could steal a session, which reaches encrypted blobs and metadata but no passphrase or key.
  • The host is trusted by every server’s users, because it serves the code that handles passphrases.
  • The content policy is wider: it allows connections to any HTTPS origin.

The Android app

  • Screenshots and screen recording are blocked, and the recent-apps card is blank.
  • Nothing is in a device backup. Sign-in tokens are in the platform keystore.
  • Time in the background counts toward the lock time: 1, 5 or 15 minutes.
  • Fingerprint or face unlock is optional and off until you turn it on. It keeps the account’s master key, encrypted by a hardware key that only a biometric match releases, never the device PIN. Vault passphrases are always typed.
  • The app never asks to read your photo library. It sees only what the system picker returns, and deletes its temporary plaintext copy once it is uploaded.

How a server is hardened

  • One published port, on loopback by default. The database and bundled storage sit on internal networks.
  • Every container has a read-only root filesystem, no Linux capabilities and no-new-privileges.
  • Passwords are bcrypt hashes; refresh and invite tokens are stored only as SHA-256 hashes.
  • Five failed logins lock an account for 15 minutes. Requests are rate limited.
  • The web image sends a strict Content-Security-Policy that limits connections to the server and its bucket.
  • The installer and upgrade script check every file against the release’s published checksums.

Check it yourself

The web client, the Android app and the import tool are three independent implementations of the same format. A shared file of test vectors holds fixed inputs and the exact bytes expected from them, and all three test suites check it. The access and tampering rules are exercised against a real server in the web client’s API tests. Everything is in the repository, under the MIT licence.