How KUVRT works: encryption and security design
KUVRT is an end-to-end encrypted messenger that runs in the browser. Keys are made on the device from a 24-word recovery phrase, and no phone number or email address is needed. Messages are encrypted per device with a hybrid of X25519 and ML-KEM-768 key agreement, a Double Ratchet and AES-256-GCM, and padded to fixed size classes. The server stores only sealed envelopes, deletes each one when it is delivered or after 45 days at the latest, and cannot read message contents. A public, append-only key log lets every app notice a swapped key, and every release is signed and recorded in that log before the app installs it. On the device, data is encrypted at rest, and with an app lock it opens only with a password stretched by Argon2id, a passkey or the recovery phrase. Connections use TLS 1.3 only.
Identity and keys
Your KUVRT is a set of keys made on your device. The server never receives a phone number, an email address or a name.
- The recovery phrase is 24 English words that encode 256 random bits, with a checksum, in the BIP-39 format.
- The phrase gives a 64-byte seed through PBKDF2-HMAC-SHA512 with 2,048 iterations (RFC 8018), from which HKDF-SHA256 (RFC 5869) derives a root secret.
- The root secret derives the root signing key, the key of the encrypted identity card and the key of phrase-protected exports.
- The root signing key is an Ed25519 key (RFC 8032) and is not kept on any device after setup.
- The identity key is a random Ed25519 key, certified by the root key and held on every linked device. It signs the device list.
- Each device makes its own Ed25519 signing key, X25519 agreement key (RFC 7748) and ML-KEM-768 last-resort prekey (FIPS 203). They never leave the device.
- Each device key is certified by the identity key for 400 days.
- A device id is the first 16 bytes of a SHA-256 hash of the identity id and the device keys, so one identity cannot present another identity's device.
- Every derivation and signature uses a dated purpose label such as kuvrt_20260902_root, so no key serves two purposes.
- One person can link up to five devices.
- Removing a device rotates the identity key, and the relay accepts only one rotation per old key. The root key from the recovery phrase can override any rotation.
Messages
Every message is encrypted on the sending device for each device of the recipient and for the sender's own other devices, each pair of devices with its own session.
- The session handshake combines X25519 with ML-KEM-768 (FIPS 203), so the initial secret is protected against a future quantum computer.
- The handshake runs three or four X25519 exchanges and one ML-KEM-768 encapsulation against prekeys the recipient device published and signed with its device key, and joins them with HKDF-SHA256.
- The prekeys chain to the identity key, so the relay can neither substitute a device nor strip the ML-KEM-768 part.
- Each message then advances a Double Ratchet: X25519 ratchet steps, HMAC-SHA256 chain steps (RFC 2104) and HKDF-SHA256.
- Each session re-keys with ML-KEM-768 after 50 messages or 7 days, whichever comes first, and mixes the new shared secret into the root key.
- The ratchet steps between those re-keys are X25519.
- ML-KEM-768 is used only inside a hybrid with X25519, so it never lowers the classical protection.
- A session cannot drop the ML-KEM-768 part silently: a re-key left unanswered for a second ratchet step resets the session.
- Each message is encrypted with its own AES-256-GCM key and nonce (NIST SP 800-38D), and the message key is deleted as soon as it is used.
- The ratchet gives forward secrecy: a ratchet key taken from a device today does not decrypt earlier messages recorded from the network.
- Content is padded before encryption to one of nine size classes: 512 bytes, 1, 2, 4, 8, 16, 32, 64 or 128 KiB.
- Each envelope is a sealed box to the recipient device, made with a fresh X25519 key, HKDF-SHA256 and AES-256-GCM.
- The sealed box hides from the relay which device sent the envelope, the session and the message counters.
- Every message carries a random 16-byte id and a sequence number, and a gap that lasts 60 seconds makes the recipient ask for the missing message again.
- A message that cannot be decrypted makes the sender rebuild the session and send it again; a message that cannot be delivered is shown to the sender as not delivered.
- Out-of-order messages are accepted for up to 45 days, with at most 2,000 skipped keys stored per chain.
- A device whose storage came back from an older copy stops sending until its sessions are new, so no AES-GCM key and nonce is used twice.
What the server stores
The server is a store-and-forward relay. It has no key that opens a message, a photo, a file or the identity card.
- The server deletes each envelope when it is delivered, and after 45 days at the latest.
- Envelope status records - an id, a state, times and a size - are kept 45 days after delivery or expiry.
- A conversation is a pair of random queues, one per direction and recipient device.
- Posting to a queue proves a per-queue Ed25519 sender key, not who is posting.
- In the database, a queue names no identity, no device and no time. It keeps the public halves of a fetch key and an owner key that the device derives for that queue alone.
- A queue's revocation tag is computed with HMAC-SHA256 under a key that lives only in the server's environment, never in the database or a backup.
- A copy of the database or a backup therefore names no owner for any keyed queue.
- A device reads its queues through a fetch session that exists only in the server's memory.
- Push addresses are not in the relay's database. Each device seals its push registration to a separate push service, which stores it sealed in its own database that is never backed up.
- Every push carries a payload padded to exactly 512 bytes and encrypted per RFC 8291, which names no sender and holds no content.
- The identity card - contacts, settings, name and picture - is stored encrypted with AES-256-GCM under a key from the recovery phrase, padded to size classes from 8 KiB.
- Every authenticated request and every posted envelope is signed with Ed25519 over its content, a timestamp and a 16-byte nonce, and each nonce is accepted once.
- Logs keep a salted hash of the address, the path, the status and the timing, for at most seven days.
Key transparency
Every identity's public keys are recorded in a public, append-only key log, and every app checks that log by itself, so the apps notice a swapped key.
- The key log is a Merkle tree in the structure of RFC 6962, with leaf hashes SHA-256(0x00 || entry) and node hashes SHA-256(0x01 || left || right).
- Apps verify inclusion proofs and consistency proofs, so the log cannot be rewritten without the apps noticing.
- An entry holds a tag derived from the identity id, the device list version and hash, and the public keys. It holds no name.
- The relay appends a leaf in the same database transaction as the identity change and signs a new tree head with an Ed25519 log key that the apps carry.
- After each of its own publications, an app checks that the log holds exactly what it published.
- Every 24 hours, an app checks its own entry and scans the log's leaves for any version of itself it does not know.
- An app checks a contact's keys when a request is accepted, when their device list changes, and every 24 hours while the chat is opened.
- A first message to a contact waits until their keys have been checked against the log.
- Apps pass each other their latest verified tree head inside conversations, so two people shown different logs notice.
- A key that does not match the log stops sending in that chat and shows a warning, with a safety code to compare in person.
- A log that contradicts itself - a fork - pauses sending in the affected chat and is shown at the top of the chat list.
Signed releases and updates
Every release of the web app is signed and recorded in the public key log, and the app checks both before it installs an update.
- The build writes a release manifest that lists the SHA-256 of every file.
- The manifest is signed with an Ed25519 release key that the build pipeline holds and the server never sees.
- The relay publishes every signed manifest in the public key log.
- The service worker installs a release only when the signature verifies, the key log holds the manifest with a valid inclusion proof, and every file matches its hash.
- The service worker refuses a release older than the one it holds.
- Once a browser holds a release, every page and file is answered from that verified release, and a script or style the release does not list is refused.
- Every page carries a content security policy that allows scripts from KUVRT's own origin only and enforces Trusted Types, and every script and stylesheet carries an integrity hash.
- The app loads no code, fonts or images from any other origin.
- Every version of KUVRT works for 90 days after its release, after which it asks to be updated.
- A browser version its maker no longer patches gets 28 days of grace before KUVRT asks for an update.
Encryption on the device
Everything KUVRT stores in the browser is encrypted. With an app lock, the stored data opens only with the password, a passkey or the recovery phrase.
- Every record is one AES-256-GCM blob under a random 256-bit data key.
- The keys records are stored under are scrambled with HMAC-SHA256, so they name no contact, chat or file.
- Without an app lock, the data key is kept on the device so KUVRT opens at once, which protects against nothing for someone who copies the device's storage.
- With an app lock, no key on the device opens the data by itself.
- The password is stretched with Argon2id (RFC 9106) using 64 MiB of memory, 3 passes and 1 lane, and the result wraps the data key.
- A passkey wraps the data key through the WebAuthn PRF extension, where the browser supports it.
- The recovery phrase opens the data through a key derived from it with HKDF-SHA256.
- While the app is locked, new messages arrive sealed to the vault's X25519 public key, and a notification says only "New message".
- Opened photos and videos are shown from memory and are never handed to the browser's blob storage.
- Deleting everything destroys the keys first, in one transaction, before the stored records are deleted.
Transport
Every connection to KUVRT uses TLS 1.3, and older versions are refused.
- The server accepts TLS 1.3 only (RFC 8446).
- kuvrt.com negotiates the hybrid post-quantum key exchange X25519MLKEM768 for the connection with browsers that offer it.
- This post-quantum key exchange protects the connection, the transport layer. Message contents are protected separately, end to end, by the hybrid encryption above.
- Browsers are told with HSTS (RFC 6797) to use HTTPS for two years.
- The app connects only to its own server and, when notifications are on, to the browser's push service.