How Frendy protects your data
Each section pairs a technical description with a plain-language explanation. This is a faithful edition of the Frendy security whitepaper.
Document version 1.3 · Prepared from source-code analysis · Kept current as changes ship
§0Summary (for non-technical readers)
Frendy is a messenger with end-to-end encryption (E2EE). E2EE means a message is encrypted on the sender's device and can only be decrypted on the recipient's device — no intermediary, including the server, holds the key. In practice:
- Messages are encrypted on the sender's phone and decrypted only on the recipient's phone.
- The server cannot read your conversations — it stores and forwards only encrypted "gibberish" (ciphertext) plus routing metadata.
- Even if the server is breached or seized, message contents stay inaccessible, because the decryption keys are never on the server.
- On the phone, messages are also stored encrypted, and the keys sit in the phone's hardware secure module (like banking apps).
- With a PIN set, keys are additionally encrypted with a key derived from the PIN — so even a stolen, fully cracked-open phone can't decrypt the conversation without the PIN (the PIN is stored nowhere).
What E2EE does not hide: the fact and timing of communication and file sizes (metadata); and it can't help if the user's own phone is fully compromised by malware or physically broken into at the OS level.
§1Architecture and platforms
| Component | Technology |
|---|---|
| Android (primary client) | Kotlin, Jetpack Compose |
| Desktop (Windows) | Kotlin / Compose Multiplatform |
| iOS | Swift / SwiftUI |
| Server | Node.js / Express / MongoDB / Socket.IO |
| 1:1 calls | WebRTC (P2P), DTLS-SRTP encryption |
| Group calls / conferences | LiveKit with E2EE (shared key from ECDH) |
§2Cryptographic primitives
Only standard, time-tested algorithms are used — no home-made cryptography:
| Purpose | Algorithm | Parameters |
|---|---|---|
| Message & file encryption | AES-256-GCM | 256-bit key, 96-bit random IV, 128-bit auth tag |
| Key exchange between users | ECDH on secp256r1 (P-256) | dialog key = SHA-256(ECDH shared secret) |
| PIN protection | PBKDF2-HMAC-SHA256 | 65,536 iterations, 128-bit salt |
| On-device key storage | Android Keystore (AES-256-GCM) | master key in the hardware module (TEE) |
| 2FA | TOTP (RFC 6238) | secret in DB encrypted with AES-256-GCM |
| Message forward secrecy | Double Ratchet + X3DH (libsignal) | per-device sessions; each message key erased after decryption |
| Post-quantum session setup | PQXDH — X25519 + ML-KEM (Kyber-1024) | classic and post-quantum exchange combined into one secret |
| Randomness source | SecureRandom (CSPRNG) | for all keys, IVs, salts |
This is not "home-made" cryptography
| Our mechanism | Where the same thing is used |
|---|---|
| AES-256-GCM (messages, files, calls) | HTTPS (the browser padlock), Signal, WhatsApp, banking apps; NIST/FIPS standard, approved in the US for classified data |
| ECDH on P-256 (key exchange) | TLS 1.2/1.3 — the entire secure internet; Apple iMessage; NIST standard |
| DTLS-SRTP (calls) | Google Meet, Jitsi, Discord, WhatsApp calls — mandatory WebRTC mechanism |
| PBKDF2-HMAC-SHA256 (PIN, keys) | password managers (1Password, Bitwarden), Wi-Fi security (WPA2), disk encryption |
| Hardware storage (Keystore / TEE) | Google Pay, banking apps, contactless payment |
How impossible is a brute-force attack
Our data encryption is AES-256-GCM, and the recovery key is derived from a seed phrase (BIP-39, 128 bits) via PBKDF2-HMAC-SHA256 with 600,000 iterations.
Layer 1 — the cipher itself (AES-256-GCM): breaking it is physically impossible. Brute-forcing AES-256 = 2²⁵⁶ ≈ 1.2 × 10⁷⁷ keys. This isn't "a long time" — it's thermodynamically impossible: by the Landauer limit, merely iterating through 2²⁵⁶ states needs ~3 × 10⁵⁶ J of energy, while the Sun over its entire remaining life (~5 billion years) will emit ~6 × 10⁴³ J — so you'd need about 10¹³× more energy than the Sun will ever produce.
Layer 2 — the real target: the seed phrase. An attacker goes after the 128-bit seed phrase, not the cipher. The seed key is derived via PBKDF2-SHA-256, so classical brute-force speed is capped by SHA-256 throughput. Even Earth's largest SHA-256 hashing power — the entire Bitcoin network (~6 × 10²⁰ SHA-256/s) — aimed at a single account would take about 2.2 × 10¹⁶ years ≈ 1.6 million times the age of the Universe. Probability of a break within 100 years of running such a cluster ≈ 4 × 10⁻¹⁵ — practically zero.
Honest note: the real risk isn't brute force — it's key theft. Encrypted data is realistically opened only if the seed phrase itself is stolen (device malware, phishing, a carelessly recorded phrase). So protection reduces to protecting the seed phrase: hardware key storage, wrapping under a PIN/biometrics, and user care in storing the recovery phrase.
Protection against quantum computers
- The threat. A quantum computer running Shor's algorithm could break the asymmetric cryptography (ECDH/RSA) that key exchange rests on. Sufficiently powerful machines don't exist today (~10–15 year horizon), but you must defend now because of "harvest now, decrypt later": an adversary records encrypted traffic today and decrypts it years later.
- The solution. Session setup (CR-01) mixes in CRYSTALS-Kyber-1024 = NIST ML-KEM-1024 (FIPS 203) in parallel with the classic X25519 exchange. The final session secret depends on both. It's a hybrid: to break it you'd have to break both mechanisms; the system stays secure as long as at least one holds.
- Honesty boundary. Post-quantum protection covers the handshake (the part most vulnerable to a quantum attack). Symmetric AES-256 is already quantum-resistant enough (Grover's algorithm only halves effective strength to ~128 bits — still unbreakable), so it doesn't need changing. We do not claim "absolute invulnerability" — we remove a specific, industry-recognized risk.
- Context. Signal deployed PQXDH in 2023; we follow the same path. Most messengers don't yet implement post-quantum protection.
§3How messages travel (data flow)
- On the sender's device text is encrypted with the dialog key (AES-256-GCM) → ciphertext + IV.
- The ciphertext travels to the server over TLS (double protection: transport TLS + payload already E2EE-encrypted).
- The server stores and forwards only ciphertext. It cannot decrypt — it holds no key.
- On the recipient's device the ciphertext is decrypted with the dialog key.
Transport: all connections (HTTPS + WebSocket/Socket.IO) run over TLS. On Android certificate pinning is additionally enabled (pinned to ISRG root certificates) — protection against certificate substitution and corporate/state MITM proxies.
Call encryption
- 1:1 calls (WebRTC): the media stream is encrypted with WebRTC's mandatory DTLS-SRTP and flows directly between devices (P2P) or, if a direct connection is impossible, via a TURN relay — always encrypted; the relay never sees the content.
- Group calls / conferences (LiveKit): E2EE is on — media is encrypted with a shared key derived via ECDH among participants; the conference server (SFU) relays already-encrypted streams and can't hear you.
§4What and where things are stored
4.1 On the server
- Stored: message ciphertext, encrypted files, users' public keys, service metadata (who, to whom, when, sizes, types); optionally, if the user enables it, an E2E history archive (message text encrypted under an archiveKey — opaque ciphertext to the server).
- NOT stored / inaccessible: message plaintext, users' private keys, dialog keys, PIN codes, the archiveKey and the archive backup phrase.
- Conclusion: compromising or seizing the server does not reveal the conversation (the archive included — without the main key and backup phrase it's unreadable).
4.2 On the device (Android)
| What | How it's stored |
|---|---|
| Message text (local cache) | ciphertext only in the local DB (SQLite/SQLDelight) |
| Media files (cache) | encrypted bytes only on disk; decryption happens in RAM |
| Keys (dialog, EC-private, tokens, PIN hash, salts) | EncryptedSharedPreferences: values AES-256-GCM, master key in Android Keystore (TEE) |
| Main & EC-private keys (when a PIN is set) | additionally encrypted with AES-256-GCM under KEK = PBKDF2(PIN) on top of Keystore; plaintext only in RAM after unlock |
§5Key management
- Each user has an EC key pair (P-256). The public key is known to the server/contacts; the private key never leaves the device (except an encrypted backup for recovery).
- The dialog key is computed via ECDH between one party's private key and the other's public key — both independently derive the same secret without sending it over the network.
- Group chats: a shared group key is distributed to members, each encrypted individually under that member's EC key (ECDH wrapping).
- Forward secrecy: at the base level — manual EC-key rotation (at the user's discretion); old private-key versions live on the device for 30 days, then are deleted. On top of that, automatic per-message forward secrecy runs via Double Ratchet + X3DH/PQXDH (libsignal): each message key is erased after decryption.
- Recovery: the private key can be recovered from the seed phrase (an encrypted copy is stored on the server and decrypted only by the user's seed phrase).
- History sync to a new device (optional, OFF by default). Since forward secrecy means a new device cannot read old conversations, there's a separate E2E archive: message text is encrypted on-device under archiveKey = PBKDF2-HMAC-SHA256(seed-derived main key ‖ separate backup phrase, salt, 600,000 iterations) and placed on the server as opaque ciphertext. Opening the archive requires both factors — the main key (recovered from seed) and a separate backup phrase.
§6Protection on the device itself
| Mechanism | Description |
|---|---|
| App PIN | stored as PBKDF2-HMAC-SHA256 (65,536 iterations, salt), never in the clear |
| PIN key-wrapping | main & EC-private keys encrypted under KEK = PBKDF2(PIN, 120,000); decryption only after PIN/biometric entry |
| Biometrics | optional unlock by fingerprint/face |
| Duress PIN | a special PIN triggers a panic wipe of keys and exit |
| Auto-lock | by timeout (configurable) and on system screen lock |
| FLAG_SECURE | option: block screenshots/recording and hide the preview in "recents" |
allowBackup=false | blocks extraction of app data via system backup (adb backup) |
| Plaintext-temp cleanup | decrypted temporary files (video/audio) are deleted after use |
§7Threat model (what happens if…)
| Threat | Protection | Outcome |
|---|---|---|
| Traffic interception (Wi-Fi, ISP, MITM) | TLS + cert pinning + E2EE | Only ciphertext is visible |
| Server breach / seizure | No decryption keys on the server | Conversation inaccessible |
| Dishonest operator / insider | E2EE: the server physically cannot read | Content inaccessible |
| Contact-key substitution (key-level MITM) | Fingerprint (safety number) verification + key-change alerts | Detected by the user |
| App clones / emulators | Play Integrity (blocks illegitimate builds at login) | Clones are rejected |
| User switch on a shared device | Full wipe of local caches/decrypted data on login under another account; per-session push tokens | Previous user's data inaccessible |
| Phone theft — image dump (locked, no root) | Android FBE + key in TEE; messages = ciphertext | Cannot decrypt |
| Phone theft — full OS compromise (root on unlocked) | Keys encrypted under KEK = PBKDF2(PIN) on top of TEE | Cannot decrypt without the PIN |
§8Authentication, sessions, notifications
- JWT (access) + refresh token; renewal is transparent to the user.
- 2FA TOTP (RFC 6238), secret in DB encrypted with AES-256-GCM.
- Session management: device list, remote revocation, "active now" for the current session.
- Login alerts from a new device — instantly to all your devices and as a separate message from the @frendy system account (stays in history). The alert shows device, IP and country by IP (from a local geolocation DB, no third-party requests).
- Push (FCM): only a signal "new message" (sender/dialog) — no message text; delivery is addressed by per-session tokens (push doesn't go to other sessions on a shared device).
§9Results of the checks performed
| Check | Result |
|---|---|
| Static analysis Semgrep (Kotlin: app+shared, 121 files, OWASP/secrets/security-audit) | 0 findings |
| Static analysis Semgrep (Node.js backend, 41 files, 131 rules) | 5 findings, all false after manual review |
| Backend security audit (internal) | Closed: NoSQL injection, IDOR, session revocation, admin default password |
| Logging audit (Android) | No secrets/plaintext in logs; HTTP bodies logged only in debug builds; allowBackup=false; components not exported |
| Crypto invariants | Covered by unit tests (cross-platform key format, AES-GCM, fingerprint) |
§10Hardening
Implemented
- PIN key-wrapping — main & EC-private keys (all versions) additionally encrypted under KEK =
PBKDF2(PIN, 120,000)on top of Keystore. Result: a full device compromise without the PIN can't decrypt the conversation. - StrongBox for the Keystore master key — on devices with a dedicated security chip the master key is stored there (safe fallback to regular TEE).
- CryptoObject biometrics (Class 3) — on strong-biometric devices the KEK is released by a hardware Keystore key only after fingerprint auth; enrolling a new fingerprint invalidates the key.
- SQLCipher for the local DB (Android) — the local cache is AES-encrypted under a Keystore key; now metadata on disk is encrypted too.
- R8/obfuscation + Log stripping in release — the release Android client is obfuscated and
Logcalls are stripped at build time. - Data isolation between accounts on a shared device — on login as another user, the previous user's local caches and decrypted data are fully wiped.
Redundant
7. HKDF instead of SHA-256 for deriving the dialog key — possible, but not particularly meaningful. SHA-256(ECDH shared secret) is already a cryptographically sound way to obtain a key with no known weakness HKDF would close; switching would be a breaking change requiring versioned migration across all three platforms. The current encryption is already at a high level; a rewrite to HKDF is not planned.
§11Conclusion
Frendy implements end-to-end encryption on standard primitives (AES-256-GCM, ECDH P-256, PBKDF2), stores conversations and media on the device encrypted, and keeps keys in a hardware secure module. The server is architecturally incapable of reading conversations. Transport is protected by TLS with pinning. Static analysis of the client found no vulnerabilities; backend findings turned out to be false.
On phone theft the conversation stays inaccessible: on an image dump — thanks to E2EE and hardware key storage; on a full OS compromise — thanks to PIN key-wrapping (no decryption without the PIN, and the PIN is stored nowhere on the device).