Most apps that store your messages also store them in a way that lets the server read them. Your chat history is in a database column somewhere, in plaintext. The company running the app can read it. Their infrastructure providers can. Anyone who gets unauthorised access to that database can.
For a group travel app, this matters more than it might seem. You’re sharing your real-time GPS location, your hotel booking details, your itinerary — timing and location data that has real-world implications. TripKit encrypts that data before it leaves your device.
Here’s how it actually works.
The Basics: What Gets Encrypted
TripKit encrypts two things per trip when E2EE is enabled (the default for new trips):
- Group chat messages — every message body is encrypted before it’s sent to the server. The server stores ciphertext; only trip members with the trip key can read it.
- GPS coordinates — when you share your live location, the coordinates are encrypted on your device before upload. The server stores an opaque payload; only trip members decrypt it.
Everything else — itinerary items, expenses, checklists, documents — is not currently encrypted at the message level, though it’s stored on a properly secured Postgres database with row-level security.
How the Keys Work
Per-user keypair
When you first sign in to TripKit, the app generates an ECDH P-256 keypair in your browser using the Web Crypto API. The public key is stored on the server (it needs to be shared with other members). The private key is wrapped with a PBKDF2-derived AES-GCM key, locked behind a “vault password” you choose, and stored as ciphertext. The server never sees your raw private key.
When you open the app, you unlock your vault once per browser session. The private key is decrypted into memory and re-wrapped with a per-session random key in sessionStorage so you don’t have to re-enter the vault password on every page.
Per-trip symmetric key
Each E2EE trip has one AES-256-GCM symmetric key — the “trip key”. When the first member creates a trip, they generate this key. When a new member joins, any existing member holding the key can distribute an encrypted copy: the trip key is wrapped using ECDH key agreement between the sender’s private key and the new member’s public key. The server only ever stores the wrapped (encrypted) version.
This means:
- New members can only get the trip key if an existing member distributes it
- The server cannot decrypt the trip key (it doesn’t hold any private keys)
- Every member gets their own encrypted copy of the same underlying key
Encrypting a Message
When you send a message in an E2EE trip:
- Your browser retrieves the trip key from memory (or prompts vault unlock if needed)
- A random 12-byte IV is generated
- The message body is encrypted with AES-256-GCM:
ciphertext = AES-GCM(key, iv, plaintext) - The result is base64-encoded and prefixed with
enc:v1:so the app can distinguish ciphertext from legacy plaintext - Only the ciphertext is sent to the server — the plaintext never leaves your browser
Receiving works in reverse: the app checks for the enc:v1: prefix, decrypts using the trip key, and displays the plaintext to you. Messages without the prefix are displayed as-is (for trips that had E2EE disabled or retroactively enabled).
What This Means for the AI Bot
The @tripkit AI bot reads messages to reply in context. When E2EE is enabled, the server only sees ciphertext — it can’t pass that to the AI. So the AI bot is automatically disabled on E2EE-enabled trips.
This is an honest trade-off. If you want the AI assistant, you can disable E2EE in trip Settings. If you want full encryption, the AI won’t work. The choice is yours per trip.
What We Use (and What We Don’t)
All cryptography in TripKit uses the browser’s built-in Web Crypto API (SubtleCrypto). Zero npm crypto packages. This matters because npm packages can introduce supply-chain vulnerabilities — a compromised package could exfiltrate keys silently. With Web Crypto, the cryptographic operations happen in a sandboxed browser API that no JavaScript package can intercept.
| Operation | Algorithm |
|---|---|
| User keypair | ECDH P-256 |
| Key derivation (vault password) | PBKDF2-SHA-256, 600,000 iterations |
| Trip key | AES-256-GCM, 256-bit random |
| Key wrapping | ECDH → HKDF-SHA-256 → AES-256-GCM |
| Message/GPS encryption | AES-256-GCM, 12-byte random IV per message |
600,000 PBKDF2 iterations is the 2025 OWASP recommendation for PBKDF2-SHA-256. This makes brute-forcing vault passwords from the stored key data computationally expensive.
Current Limitations
Being honest about what this doesn’t protect:
- Metadata — the server knows who sent a message and when, just not what it said. Metadata analysis could reveal patterns even without plaintext.
- Itinerary, expenses, checklists — these aren’t encrypted at the content level. They’re protected by authentication and row-level security, but the server can read them.
- Push notification bodies — for E2EE trips, push notifications say “Open TripKit to read” rather than showing message content, but the delivery mechanism reveals that a message was sent.
These are real limitations. We’re not claiming to be Signal. We’re a group travel app that takes privacy seriously and does meaningful end-to-end encryption where it counts the most: your conversations and your location.
If you have questions about the implementation, reach out.