Short version: your direct messages and your 1:1 calls are end-to-end encrypted. Shard channels, Splinters, and file attachments are encrypted on our servers with keys we hold. Rift can't read your DMs. The limitations are on this page too.
What is and is not encrypted
End-to-end encrypted. Rift never has the keys.
- The text of 1:1 direct messages, including edits and formatting.
- Voice, camera, and screen share in 1:1 DM calls, when both clients support it.
Encrypted at rest with keys Rift holds. This protects against a database breach, not against Rift.
- Shard channel messages, using per-message keys wrapped by a master key.
- Splinter (group conversation) messages, the same way.
- Attachments in any conversation, including DMs. The file bytes, filename, size, and type are visible to our storage layer.
Transport encryption only. TLS between you and us, plaintext on our media server.
- Shard voice channels. The media server mixes the audio, so it hears it.
Threat model
The DM encryption is built to hold against:
- Anyone who gets a copy of our database, our backups, or our object storage.
- Rift itself. We can't produce DM plaintext, so we can't be made to.
- Anyone on the network between you and us.
It isn't built to hold against:
- A compromised device that's signed in to your account. Every signed-in device holds your full identity.
- Someone who learns your Rift Key. Every key below derives from it, and none of them can be rotated without a new account.
- A malicious Rift that serves you modified client code, or a false identity for the person you're talking to. See the trust root.
- Traffic analysis. We see who talks to whom, when, and how much.
Keys and where they come from
Everything derives from the Rift Key, the 12 words you get at signup. Nothing is generated per device. That's on purpose, and the tradeoffs are under adding a device.
- Account signing key (Ed25519). The 12 words are a standard BIP-39 mnemonic. The 128 bits of entropy are concatenated with the label "rift-identity-v1" and hashed with SHA-256. That hash is the Ed25519 seed. The public half is uploaded once at registration and never changes. It signs login challenges, account recovery, and every key below.
- DM identity key (X25519). SHA-256 over the signing private key concatenated with the label "rift-dm-e2ee-x25519-v1", used directly as the X25519 private scalar.
- Signed prekey (X25519). HKDF-SHA256 over the signing private key with salt "rift-dm-signed-prekey" and info "rift-dm-spk". There's no counter in that derivation, so the signed prekey stays the same for the life of the account. We re-upload it on a schedule, which doesn't change it.
- One-time prekeys (X25519). A base secret is SHA-256 over the signing private key concatenated with "rift-dm-otp-base". Each one-time key is HKDF-SHA256 over that base with the key's numeric id as salt and info "rift-dm-otp". Keys go up in batches with increasing ids and get topped up periodically. The server hands each one out at most once.
What the server holds about your keys
Only public halves, plus the signatures that tie them to your account signing key:
- Your account signing public key, written once at registration.
- Your DM identity public key and an Ed25519 signature over it by your account signing key.
- Your signed prekey public key and its signature.
- Your unused one-time prekey public keys.
Unsigned uploads are refused. So are signatures that don't verify against the key stored at registration. The server checks again every time it hands out a bundle, so a corrupted row never goes out as a valid key.
Starting a conversation
The first message in each direction runs X3DH. The sender fetches the recipient's bundle and checks the identity signature. No signature, no session. Then it generates a fresh ephemeral X25519 key and computes:
- DH1 = sender identity with recipient signed prekey
- DH2 = sender ephemeral with recipient identity
- DH3 = sender ephemeral with recipient signed prekey
- DH4 = sender ephemeral with recipient one-time prekey, when the server had one left
The shared secret is HKDF-SHA256 over DH1 to DH4 concatenated, with info "RiftX3DH", 32 bytes out. If the recipient's one-time prekeys have run out, the handshake runs with three DH terms instead of four. That's standard X3DH, and it's weaker against a replayed handshake.
Verification runs both ways. The recipient checks the incoming handshake against the sender's registered identity before it creates a session. A handshake the server doesn't vouch for is rejected and the message is never shown. A frame claiming some other identity can't kill an existing session.
Encrypting messages
After the handshake, messages use a Double Ratchet with these parameters:
- DH ratchet: X25519.
- Root key step: HKDF-SHA256 with the DH output as input keying material, the current root key as salt, and info "RiftRatchet", producing a new root key and chain key.
- Chain key step: HMAC-SHA256 over the chain key with a single byte, 0x01 for the message key and 0x02 for the next chain key.
- Cipher: AES-256-GCM with a random 12-byte nonce per message.
- Associated data: the message header, which is the sender's current ratchet public key, the previous chain length, and the message number, each as fixed-width bytes.
- Out-of-order delivery: up to 1000 skipped message keys are kept per session. A header claiming a larger gap is rejected before any key is derived.
What gets encrypted: the text, plus formatting when there's any. The body also carries the sender, the recipient, both identity keys, and the client message id, and the receiver checks all of them against the conversation it's in. Move a ciphertext to another conversation, or bounce it back at its author, and it fails.
On the wire, each message carries the version, the algorithm name, the ratchet header, the ciphertext, and, for the first message in a direction, the sender's ephemeral key and the id of the one-time prekey used.
The second copy every message carries
Ratchet state lives on one device. Your identity lives on every device. A message sealed only with the ratchet opens on the one device that holds the session and nowhere else: not your phone if you sent it from your desktop, not any device loading history later. So every DM also carries a second ciphertext, under a key any of your devices can derive. That's how multi-device and history work without handing Rift a key:
- Shared secret: X25519 between the sender's DM identity key and the recipient's DM identity key.
- Key: HKDF-SHA256 over that secret with a per-message salt and info "rift-dm-static-fallback-v3", followed by the sender's and recipient's identity keys in that order. The two directions of a conversation get different keys, so a reflected message fails to decrypt.
- Cipher: AES-256-GCM, with associated data binding the version, both identity keys, and the client message id.
Your other devices and history loads decrypt this copy. The live conversation on the sending device uses the ratchet.
The forward secrecy the Double Ratchet gives the live session doesn't extend to stored messages. The second copy sits under long-lived keys. Lose your Rift Key, or the DM identity key on any device, and every message the server still holds can be decrypted, both directions. The ratchet still protects the live session from a passive observer and limits what a captured session reveals. It doesn't protect history from a key compromise the way it would in a single-device design.
We picked this over per-device identities, which put a device list on the server and no history on a new phone, and over server-side key escrow. Your controls are deletion, which is a real delete, and disappearing messages.
Adding a device
Rift has one identity per account, not one per device. Signing in on a new device either re-derives everything from the Rift Key you type in, or gets a copy from a device you already trust:
- The new device generates an ephemeral X25519 key and shows a short pairing code.
- You confirm the code on a signed-in device. That device encrypts your whole vault, including the account signing private key and the DM identity private key, to the new device's ephemeral key: X25519 shared secret, HKDF-SHA256 with info "rift-pairing-bundle-v1", AES-256-GCM.
- The server relays the ciphertext. It never has the key to open it.
Every signed-in device holds the identity, so compromising one device compromises the identity. Signing a device out ends its session. It doesn't take back keys that device already copied. Recovery changes your password and ends every session, but the DM keys come from the Rift Key, so they stay the same. If the Rift Key itself gets out, the account is compromised. Make a new one.
Where keys live on your device
- Web and desktop: in IndexedDB, encrypted under a non-extractable WebCrypto key that's also stored in IndexedDB. Script can't export that key, but any script running on the Rift origin can use it. This raises the bar against a stolen browser profile, not against code running as you.
- Mobile: in encrypted app storage under a random wrap key held in the iOS Keychain or Android Keystore, available after the first unlock. A rooted or jailbroken device defeats this.
- The Rift Key itself isn't stored unless you turn on the optional cache. The cache encrypts the words under a key derived from your password with PBKDF2-HMAC-SHA512 at 600,000 iterations on desktop and web, or unlocks with a platform authenticator (Windows Hello, Touch ID) through the WebAuthn PRF extension where the browser supports it.
The trust root and its limits
Every signature check above runs against the account signing key the server gave your client. The server writes that key once at registration and has no code path to replace it. A peer identity that changes gets checked against that key again, and a frame that fails can't touch the existing session. But the first time you talk to someone, you're trusting the server to hand you the right key. That's trust on first use. Most messengers work that way.
Safety numbers close that gap. In a 1:1 DM, Verify encryption shows both of you the same 60-digit number: Signal's numeric fingerprint, SHA-512 run 5200 times over each account signing key and user id. Each client builds its own half from its own Rift Key, not from anything the server said, so the numbers match only if both sides have the real keys. Compare them in person or on a call, then mark the contact verified.
Clients also remember every contact's key the first time they see it. Those keys never change, so a different one later gets flagged in the conversation. For a contact you verified, the client won't open a new session under the new key until you review it. Your verifications follow your account, sealed with AES-256-GCM under a key from your Rift Key. The server stores that copy. It can't read it, forge a verification, or swap a remembered key.
Rift doesn't publish a log of key changes (key transparency). A contact you haven't verified is trusted on first use, so a compromised or compelled server could hand you the wrong key the first time you talk to them.
The web and desktop clients load their code from our servers. End-to-end encryption in a browser is only as strong as the code the browser was served that day, and no design can fix that from inside the page. The mobile apps and desktop installers are signed releases.
What the server can see
For an encrypted DM, the server stores and can read:
- Who the two participants are, and when the conversation was created.
- For each message: sender, timestamp, edit timestamp, an expiry if disappearing messages are on, which message it replies to, the client message id, the ratchet header (a public key and two counters), and the ciphertext.
- The length of each ciphertext. Messages aren't padded, so the server can estimate message length.
- Attachments in full, as described at the top of this page.
- Which devices are signed in, and push notification tokens. Push notifications for encrypted DMs can't include the text, because we don't have it.
The server doesn't generate link previews for encrypted DMs, because it can't see the links.
Encrypted calls
A 1:1 DM call encrypts media frames on the client before they reach our media server, so the server relays ciphertext it can't decode. The caller generates a random 256-bit call key, seals it to the callee's DM identity using the same X25519 shared secret as the message fallback with info "rift-dmcall-keywrap-v1", and seals a copy to itself for reload recovery. The callee acknowledges only after it unwraps the key. Encryption turns on only when both sides confirm, and the call UI shows which one you got. An older client on either end gets a plaintext call that says so. Never a quiet downgrade.
Encrypted calls send audio with discontinuous transmission off, so packet timing doesn't reveal who's speaking. Packet sizes still vary with the audio, which leaks some information about speech versus silence. We haven't closed that yet.
Deletion
Deleting a DM removes the row. There's no soft delete and no moderation retention copy for encrypted DMs, since the server couldn't read one anyway. The same goes for purging your side of a conversation. Attachments are removed from storage once no message references them.
Known limitations
The short list:
- Forward secrecy doesn't hold for stored messages, because of the second copy every message carries.
- First contact trusts the server unless you compare safety numbers. There's no public log of key changes.
- One identity per account. Any signed-in device holds it. Keys can't be rotated without a new account.
- The signed prekey is static for the life of the account.
- Ciphertexts aren't padded.
- Web and desktop trust the code they're served.
- Shard channels, Splinters, attachments, and Shard voice aren't end-to-end encrypted.
- When a recipient's one-time prekeys run out, the handshake drops to three DH terms.
How to check any of this
Rift's client code isn't public. Here's what you can check without it:
- The key bundle endpoints return only the public keys and signatures listed under what the server holds.
- Every encrypted DM on the wire has the shape described under messages and the second copy.
- The server refuses an unsigned identity upload.
- Your data export lists counts of your encrypted messages and none of their content.
If the code doesn't match this page, that's a security issue and we want to know: how to report.