A private Android messenger built around end-to-end encryption, local encrypted history, revocable group access, and metadata-resistant transport.
Status: experimental. Not independently audited. Not production-ready. Cipher has run on an Android emulator against a real relay and PostgreSQL. It has never been run on a physical device, and its Tor/Orbot integration has never been validated against the live Tor network. Read What Cipher does not claim before relying on anything here.
Cipher assumes the network and the server are hostile.
- Messages are encrypted on the sender's device and decrypted only on authorized recipient devices.
- The relay stores opaque ciphertext and holds no message-decryption keys.
- Network privacy is a separate layer from message encryption: if the privacy transport (or every node behind it) is malicious, message plaintext is still protected by the E2EE layer.
- Every security claim is tied to a named adversary in the threat model and, where possible, to an automated test (see security invariants).
flowchart TD
A["Android app (Kotlin / Compose)"] --> B["cipher-ffi (narrow UniFFI bridge)"]
B --> C["cipher-core: OpenMLS E2EE, vault, history keys"]
C --> D["Privacy transport: SOCKS5 route, fail closed"]
D --> E["Tor via local SOCKS5 (intended; NOT yet validated live)"]
E --> F["cipher-relay (untrusted, ciphertext only)"]
F --> G[("PostgreSQL")]
F -. "queued ciphertext" .-> H["Recipient device(s)"]
| Component | Role |
|---|---|
crates/cipher-core |
Security core: OpenMLS adapter, group policy, encrypted vault, per-epoch history keys, attachments, delivery capabilities, network profiles. |
crates/cipher-wire |
Shared wire types, size limits, request-signing canonicalisation. |
crates/cipher-relay |
The untrusted delivery relay (axum + PostgreSQL): signed requests, bounded queues, group commit sequencer, anonymous capability delivery. |
crates/cipher-ffi |
Narrow UniFFI boundary: no key or MLS state crosses it; every input validated; panics contained. |
android/ |
Kotlin/Compose app, Android Keystore wrapper, OkHttp-based SOCKS route, UI. |
| PostgreSQL | Relay persistence: public keys, queued ciphertext, counters. No plaintext column exists. |
| Privacy transport | A fail-closed SOCKS5 route from the app to the relay. Cipher implements no anonymity protocol of its own (ADR). |
More: ARCHITECTURE · SECURITY_ARCHITECTURE · CRYPTOGRAPHIC_DESIGN.
- OpenMLS (RFC 9420) for 1:1 and group conversations; roles (owner / admin / member) enforced by every receiver, not by the relay.
- Authenticated, padded message frames; chunked, encrypted attachments (images, files, PDFs, voice notes).
- Contacts by random Cipher ID or QR code (no phone number, no address-book upload); a changed identity key is never accepted silently.
When a compliant Cipher client observes that it has been removed from a group, it deletes that group's locally held history-key material. Retained ciphertext then becomes unavailable to that client, while remaining members keep their authorized history. Removal also rotates the group's secrets and routing capability. Design and tests: HISTORY_REVOCATION.
Limitation, stated plainly: Cipher cannot erase plaintext, screenshots, exports, or key material that was deliberately copied outside Cipher while the member was authorized, and it cannot retroactively revoke anything from a malicious, modified client that preserved keys. Revocation takes effect on a removed member's device only once that device observes the removal.
- Encrypted local history (record-level AEAD under a vault key wrapped by the Android Keystore); StrongBox/TEE preferred, software keystores refused in release builds.
- App lock, rollback detection of the local database,
FLAG_SECUREscreenshot / recording protection where Android permits. - Hardware validation on physical devices is not done (checklist).
- A privacy-transport seam: every relay request can go through a local SOCKS5 endpoint (intended: Tor via Orbot). The relay hostname is resolved by the proxy, not by the device.
- Fail closed, no silent direct fallback: if the route is down, messages stay queued (encrypted) and nothing is sent another way; a direct route exists only in debug builds.
- Delivery capabilities: contacts deliver with random, rotating, revocable capabilities instead of authenticated, device-addressed sends, so the relay sees no sender for those deliveries (DELIVERY_CAPABILITIES).
- Padding (1 KiB minimum frame class, Padmé for attachments), optional bounded cover traffic, and
STANDARD/ENHANCEDnetwork profiles. - Tor/Orbot has not been validated against the live Tor network. What was measured, and what was not: PRIVACY_TRANSPORT_REVIEW.
Cipher does not currently claim:
- perfect anonymity, or anonymity of accounts (account and device IDs are stable pseudonyms to the relay);
- resistance to a global passive adversary or to end-to-end timing correlation;
- that an ISP cannot detect use of a privacy network;
- production readiness;
- an independent security audit (none has been done; ST-005 stays open);
- protection from a compromised, unlocked endpoint (malware, rooted OS, hooking, a camera pointed at the screen);
- deletion of plaintext copied outside Cipher;
- complete traffic-analysis resistance.
Full model: THREAT_MODEL · network metadata: NETWORK_PRIVACY_THREAT_MODEL.
| Adversary | What Cipher aims to provide | Main residual |
|---|---|---|
| Compromised relay | No plaintext or keys; cannot forge receipts or restore removed members | Sees recipients (via capabilities), timing, size classes, group-commit authors |
| Database dump | Ciphertext only; no senders for capability deliveries; no raw IPs | Account-to-device directory, unexpired ciphertext by recipient |
| Object-storage compromise | Attachments are client-side encrypted, padded blobs | Blob size class and timing (blobs currently live in PostgreSQL) |
| Malicious network / MITM | TLS 1.3 only, system trust anchors, no downgrade (tested) | No certificate pinning shipped (operator decision) |
| ISP / mobile operator | With the privacy route: no direct connection to Cipher endpoints, no relay DNS/SNI | Privacy-network use and traffic cadence stay visible; live Tor untested |
| Stolen locked phone | Vault key wrapped by the Keystore; no plaintext API while locked | Emulator-only validation; no StrongBox/TEE device tests |
| Removed group member | No new content; Cipher-held history keys deleted on observing removal | What they already copied; a relay that withholds the removal delays it |
| Replay | Signed, nonce-bound requests; MLS replay protection; tombstones for old Welcomes | |
| Malicious privacy relay | Sees only TLS ciphertext; cannot decrypt Cipher messages | Colluding entry+relay can correlate by timing |
Endpoint compromise is out of scope for cryptographic protection. If the unlocked device runs hostile code, the attacker can read what the user can read. Cipher reduces the surface (no keys in the UI layer, hardened windows) but does not claim otherwise.
| Area | Status |
|---|---|
| Content confidentiality | Strong as tested; unaudited |
| Server blindness | Partial (content-blind; sees recipients, timing, sizes, committers) |
| Source-IP privacy | Partial; plumbing verified against a test double, live Tor not tested |
| Social-graph resistance | Partial |
| Traffic-analysis resistance | Partial (link-only observer); weak against an observer of both ends |
| Global-observer resistance | Not provided |
| Independent review | Not completed |
| Production readiness | No (development: yes; private beta: conditional; public beta / production: no) |
Details and evidence: FINAL_SECURITY_REVIEW. Open items: SECURITY_TODO. What an external reviewer should examine: EXTERNAL_REVIEW_SCOPE.
Counted from the repository at the time of the public release (see PUBLIC_RELEASE_AUDIT):
| Rust tests | 325 (324 run, 1 ignored measurement), including relay-against-PostgreSQL and FFI boundary tests |
| Android tests | 13 JVM unit tests, 45 instrumented tests (run on an API 34 emulator; two need live-relay arguments) |
| Security / privacy invariants | 65 (38 SEC, 10 REV, 17 PRIV) mapped to 263 tests; 18 fully automated, 46 partial, 1 manual: the gaps are listed per invariant |
| Fuzz targets | 12 libFuzzer targets; the runs so far were short smoke campaigns (about 5 minutes per target), not exhaustive fuzzing |
Passing tests does not prove Cipher secure.
Prerequisites: Rust (pinned by rust-toolchain.toml) with the Android targets and cargo-ndk, JDK 17, Android SDK (API 36) and NDK 27.2, Docker (PostgreSQL for the relay tests), Python 3, OpenSSL, and cargo-deny + cargo-audit for the supply-chain gates. . scripts/android-env.sh sets the Android environment (override the paths by exporting them first).
Rust: format, lint, test
docker run -d --name cipher-pg -e POSTGRES_PASSWORD=devonly-not-a-secret -p 55432:5432 postgres:17-alpine # public, throwaway test credential
cargo fmt --all -- --check
cargo clippy --workspace --all-targets --all-features --locked -- -D warnings
cargo test --workspace --all-features --locked # relay tests use CIPHER_TEST_DATABASE_URL (defaults to the container above)Security gates
python3 scripts/check_invariants.py && python3 scripts/gen_invariants_doc.py --check
bash scripts/check-relay-deps.sh
python3 scripts/secret_scan.py && python3 scripts/public_release_scan.py
cargo deny check && cargo auditRelay (development) with a throwaway test CA. CIPHER_RELAY_AUDIENCE is the relay's host[:port] as clients dial it (10.0.2.2 is how the emulator reaches the host). All settings: .env.example.
bash scripts/make-test-ca.sh # throwaway CA + relay certificate under android/build/test-ca/ (git-ignored)
CIPHER_RELAY_AUDIENCE=10.0.2.2:8443 \
CIPHER_RELAY_REGISTRATION_TOKEN="$(openssl rand -base64 36)" \
CIPHER_RELAY_DATABASE_URL=postgres://postgres:devonly-not-a-secret@127.0.0.1:55432/postgres \
CIPHER_RELAY_PEPPER="$(openssl rand -base64 36)" \
CIPHER_RELAY_LISTEN=0.0.0.0:8443 CIPHER_RELAY_TLS_CERT=android/build/test-ca/relay-chain.pem \
CIPHER_RELAY_TLS_KEY=android/build/test-ca/relay.key cargo run --release -p cipher-relayAndroid: build and test (the Gradle build cross-compiles the Rust core with cargo-ndk)
cd android
./gradlew :app:testDebugUnitTest :app:ktlintCheck :app:lintDebug :app:assembleDebug
./gradlew :app:assembleRelease # minified; refuses software Keystore; no relay baked in
./gradlew :app:connectedDebugAndroidTest # needs an emulator or device; see scripts/ci-emulator-tests.sh
cd .. && bash scripts/check-release-apk.sh && bash scripts/analyze-release-apk.shThe debug app trusts only the throwaway test CA; release builds trust system roots only. To exercise the privacy route without Tor use the development SOCKS5 test double scripts/dev-socks-proxy.py (it is not an anonymity system).
Start at the documentation index. Most useful first reads:
| FINAL_SECURITY_REVIEW | Findings, fixes, open risks, readiness verdicts |
| NETWORK_PRIVACY_THREAT_MODEL · PRIVACY_TRANSPORT_REVIEW | Network-metadata adversaries, measurements, collusion analysis |
| HISTORY_REVOCATION | How removed members lose Cipher-controlled access to history |
| METADATA_MODEL | Exactly what each party can observe |
| EXTERNAL_REVIEW_SCOPE · SECURITY_TODO | What still needs independent review; every open item |
Please do not open a public issue for an exploitable vulnerability. Use GitHub's private vulnerability reporting for this repository. See SECURITY.md.
See CONTRIBUTING.md and the Code of Conduct. Security-relevant changes must update the threat model and invariants; no custom cryptography.
No license has been selected yet. Until the maintainer chooses one, all rights are reserved by default: the source is publicly visible for review, but no permission to use, copy, modify or distribute it is granted. This is tracked as a release blocker for any reuse.