Updated: May 24, 2026
This page includes technical disclosure details on how Pigeon encrypts messages, what the server stores, and what Pigeon structurally cannot produce in response to a legal order. The intended audience is privacy- or security-conscious users, security researchers, journalists, and practitioners.
Every message in Pigeon is encrypted with AES-256-GCM under a per-chat key that never leaves the sender's or recipient's device. The server stores only opaque ciphertext it cannot decrypt. Sender identity, chat association, group membership, display names, and encryption keys are not server-side fields as they exist only inside the encrypted payload. A subpoena to Pigeon or its infrastructure provider yields account email address, ciphertext messages and/or files, device public keys (public by definition), push tokens, subscription plan, and storage usage counters. Nothing else is structurally available to produce.
When a subpoena arrives, the following tables define what Pigeon hands over and what the architecture structurally prevents regardless of legal compulsion.
The following table shows what data we store on our server for each message sent. We don't store the sender of any message or any chat details so we only know your account received an indecipherable (to us) message at a certain time but not who sent it or what chat it was sent from.
This structure applies the same for files sent, with the addition of file size to track your usage limits against your plan. All messages and files are stored in encrypted form and you hold the keys. We can't decrypt your content records stored on the server.
| Field | Value |
|---|---|
| Message/File ID | Client-generated UUID |
| Recipient | Receiving account ID (routing only) |
| Ciphertext | Opaque AES-256-GCM blob, server holds no decryption key |
| Timestamp | Server-set at time of receipt, not client-supplied |
| Expiry | Set by chat retention setting |
| File size | Used to calculate usage limits based on your subscription plan |
The following table shows what data we cannot access either by direct query from our team or in response to legal requests for information. While we know if you have an account and receive messages or files using our service, we don't know what you send, who you talk to, what group chats you are a part of, or what you call yourself in each of those chats.
| Request | Why |
|---|---|
| Message content | AES-256-GCM; key never transmitted to server |
| Who sent a message | Sender identity is inside the ciphertext only — not a server-side field. |
| Which chat a message belongs to | Chat association is inside the ciphertext only — not a server-side field |
| Group membership | Messages are routed individually per device. The server sees individual deliveries, not group composition. No group membership table exists. Note: recipient account IDs are stored for routing (see above), so sophisticated traffic correlation across a large delivery window is a theoretical possibility, though no explicit group structure is available to produce. |
| Chat names | Never stored as plaintext — transmitted only inside encrypted envelopes |
| Display names | Never stored as plaintext — transmitted only inside encrypted envelopes |
| Encryption keys | Private keys never leave user's device Keychain; chat keys are delivered only as AES-256-GCM ciphertext |
The following table shows what data we store related to messages. These are needed to provide the service to you and to securely store your backups to allow you to manage connected devices and restore your preferences when signing in on a new device.
Pigeon was built using zero-knowledge architecture to protect your data as much as possible while still providing a great user experience. This prevents us from reading your messages, providing your social graph and message/file content in legal queries, running artificial intelligence on what you say to glean "insights", or "sharing" your social circles with advertisers for benefit or profit.
| Data | Purpose |
|---|---|
| Email address | Account authentication |
| Device public key | Encrypting key deliveries to that device (public by definition) |
| Push token | Delivering message notifications |
| Storage usage | Quota enforcement |
| Subscription status & payment processor reference ID | To know what features or limits the user has access to. Billing and payment data is managed exclusively by our third-party payment processor(s) and is not stored on Pigeon servers. |
The server holds encrypted file blobs. The key that decrypts each file is wrapped with the per-chat key and stored only in the local on-device database — the storage provider never holds a key that can decrypt the blobs it stores.
Master Secret (32 random bytes)
│ iOS Keychain — device-only, biometric-protected (iOS only; Android app not yet available)
│ Never leaves device. Generated once on first install.
│
├── Identity Key Pair (X25519)
│ Private key: derived on demand from master secret, never stored separately
│ Public key: registered with server for encrypted key delivery
│ Used for: end-to-end key delivery between devices
│
├── SQLCipher DB Key (HKDF-SHA256, 32 bytes)
│ Derived fresh on every app open — never stored anywhere
│ Encrypts the entire local message database at rest
│
└── Per-Chat Key (AES-256, 32 random bytes)
iOS Keychain — device-only, available while device is unlocked
Fresh key per chat. Compromise of one chat key exposes no other chats.
│
└── Per-File Key (AES-256, 32 random bytes)
Wrapped with the chat key before storage — never stored raw.
File blobs are encrypted before upload.
The local database key is derived via HKDF-SHA256, not PBKDF2. The master secret is 32 bytes of CSPRNG output (256-bit entropy) — PBKDF2 iteration stretching provides no security benefit when the input already has full entropy. HKDF is synchronous and completes in microseconds.
// Derives the local database encryption key from the master secret.
// Runs on every app open — the derived key is never persisted.
// RFC 5869 §2.2: omitting salt is safe when IKM already has full entropy (256-bit CSPRNG output).
const derived = hkdf(sha256, masterSecret, /*salt=*/undefined, context, /*length=*/32);
Chat keys and file keys are randomBytes(32) — no derivation; fresh entropy per key.
The ECIES shared secret used for key delivery between devices is hashed through HKDF-SHA256 before use, ensuring the raw Diffie-Hellman output is never used directly as an encryption key.
Every message has two encryption layers. The server sees only the outer blob.
{ iv: base64(12 bytes), data: base64(ciphertext || authTag), key_version: uint }
iv 12 bytes, random per message, never reused
data AES-256-GCM ciphertext concatenated with the 16-byte GCM auth tag
key_version which chat key version encrypted this message
(supports access revocation via re-key on member removal)
ephemeral public key (32 bytes) | IV (12 bytes) | AES-256-GCM ciphertext+tag
Entire blob is base64-encoded for transport.
Decrypting the outer layer requires the recipient's identity private key, which never leaves their device Keychain. Decrypting the inner layer requires the chat key, which is delivered only inside an outer ECIES envelope — the server never holds it.
The web app uses the same AES-256-GCM / X25519 primitives and the same server protocol. The differences are in key storage as browsers have no hardware-backed Keychain.
| iOS | Web | |
|---|---|---|
| Identity private key | iOS Keychain, biometric-protected | Browser storage — no OS-level biometric protection |
| Chat keys at rest | Device Keychain | Memory only — not persisted; cleared when the tab closes |
| Message database | Encrypted on-device SQLite (SQLCipher) | No local database — messages live in memory for the session |
| Crypto engine | Noble cryptography libraries (Hermes-compatible) | Web Crypto API (crypto.subtle) |
The web app's key storage is weaker than iOS in a compromised browser environment (malicious extension, XSS). This is a known limitation shared by all browser-based E2EE apps, including Signal Desktop and WhatsApp Web. The server-side guarantees of no plaintext, no sender, and no group membership are identical across platforms.
These checks run in the app, not just in tests.
The encrypt/decrypt functions for both message content (AES-256-GCM) and key delivery (ECIES X25519) are covered by automated tests that verify: correct decryption with the right key, and a thrown error on a wrong key or any byte-level tampering of the ciphertext. GCM's authentication tag guarantees tampering is always detected and the tests confirm this property is enforced by the library in use.
Every key delivery packet is validated at the point of production before it reaches the network. The output is checked against the minimum expected byte length for the wire format (ephemeral public key + IV + ciphertext with auth tag). A malformed packet throws rather than transmitting silently truncated data.
On every app open, before the local database is unlocked, the derived database key is verified to be the correct length and non-degenerate. A failed derivation throws immediately rather than opening the database with a bad key, which would silently corrupt reads.
May 24, 2026 - original publish date.
© 2026 Pigeon. All rights reserved. Built by Merandian.