12. Security & Threat Model
Written for Sam. Every claim here must be one we would defend in a public audit.
What Kontaks defends against
| Threat | Defense |
|---|---|
| Unsolicited contact / spam / robocalls | There is no address to acquire. Reaching a device requires a route the recipient issued and a key held in that sender's hardware |
| Contact-list resale or breach | We hold no contact lists, names, numbers, or emails. There is nothing to resell |
| A contact leaking your address to a third party | The routing handle is inert without the paired private key, and the key is non-extractable |
| Server reading messages or hearing calls | Double Ratchet for messages; DTLS-SRTP with pair-signed fingerprints for media. Keys never reach the server |
| Server MITM of a call | The DTLS fingerprint is signed with the pair key; a substituted fingerprint fails verification |
| Server MITM of a pairing | An 8-digit PIN the server never sees, over a slow-hashed lookup key and a deliberately expensive bundle KDF (CRYPTO §6.1, §6.3) — or an in-band DIK fingerprint carried in the QR. Holding the entire invite table does not let the server complete a pairing at any permitted TTL |
| Push provider learning who talks to whom by name | Payloads carry a route id and ciphertext only |
| Stolen or seized device (locked) | Local database is SQLCipher, keyed from the Device Root Key; the DRK is enclave-bound, non-extractable, and gated on unlock + biometry |
Device backup extraction (iCloud, iTunes, adb, forensic tooling) | The DRK is ThisDeviceOnly and non-extractable, so it is absent from every backup. A restored backup yields a device that can decrypt nothing and is not on the roster |
| Rogue device enrollment | A roster entry must chain to a key your contacts already trust, or be ARK-signed — which is visibly discontinuous, alerted on every surviving device, and delayed 24 h (§6.1.3) |
| SIM swap | Not applicable. Kontaks never uses a phone number for anything (§6.2) |
| Social engineering of support | Structurally impossible. Support is advisory, never operative (§5.9, §6.15) — there is no account action a support agent can be talked into taking, because none exists. Someone impersonating you to our support gets the same thing you would: an explanation |
| Database dump | Yields: device push tokens, opaque route ids, verify keys, ciphertext ≤ 7 days old, opaque roster blobs. No names, no numbers, no plaintext, and no way to link the two directions of a pair |
| Harassment by an accepted contact | Unilateral, instant, irreversible route destruction with no notification and no appeal |
12.2 What the addressing claim does and does not cover
What we can and cannot say about contacts
"We never know your contacts" is not a claim this product can make. The precise position:
| True | Not true |
|---|---|
| We do not store your contact list. No such field exists in any collection | |
| We never see names. The name you present travels only inside end-to-end encryption, to the one person or room you chose; renames never leave your device; no collection has a field for either | |
| We cannot read your messages or hear your calls | |
| We do not know who your 1:1 contacts are — routes are unlinked, and only timing correlates them |
The exception is groups, and it is structural. A group post is one batched request carrying an envelope for every member's every device. That batch is the membership list, delivered to us in a structured request, every time anyone posts. Sending 87 separate requests instead would cost battery and latency and they would still arrive on one connection within the same second — so group membership exposure is the price of groups working at all, not a defect we have left unfixed.
Kontaks removes the reusable address. It does not make you anonymous to the people you talk to. Inside a room, members necessarily see one another. The honest inventory:
| Exposed inside a room | Reusable outside it? |
|---|---|
| The name a member chose to present to that room | Not as an address — unresolvable, not a lookup key anywhere. But it is pre-filled from a local default (SPEC §6.3), so an unedited one repeats across that person's rooms and 1:1 relationships and is recognizable to anyone in two of them. Recognition, not reach — D23, D25 |
| Who else is in the room | No, but it is a real social-graph fragment revealed to every other member |
| Message content, and writing style with it | Unavoidable |
| Invite provenance — "Tyler · admitted by Jess" | A relationship assertion, deliberately visible (§6.8.b) |
| Voice, if they send voice messages or take calls | Biometric. Unavoidable in a voice product |
| An avatar, if they set one | Potentially their face |
None of it grants reachability, and all of it dies when they leave — but "no identity" would not be a defensible summary of that table, and this document should never make one.
12.2.a What the server can infer about who talks to whom
The schema stores no contact list: routes are one-way rows with no field linking the two directions of a relationship, and no field grouping one person's routes (§17). That is a true statement about storage and a much weaker one about practice.
| What it gives an observer with server access | |
|---|---|
| Group sends are batched — one request carrying an envelope for every member's every device | This is the significant one. A batch of 87 envelopes arriving together is the group. Not an inference from timing — a direct, structured observation delivered in a single request |
| Timing correlation between two routes | Strong evidence that they are the two directions of one relationship |
| Push tokens | Tie routes to devices, and devices to a person's hardware |
| Route creation in bursts | A group forming, and roughly who is in it |
| Device revocation bursts | The second significant one. Killing a device issues ~750 signed deletes in a short window (SPEC §6.1.1, which owns the figures; §6.1.2 is the user-facing flow). The revoke_pubkey field is not a correlator at rest — every value is distinct — but the arrival pattern groups those routes as one account. Rare and conditional, unlike group batching; and it happens at exactly the moment the user is under duress. Mitigated by prioritising recent contacts and spreading the tail over an hour, which is weak cover at small user counts |
| Load-balancer IP logs (30 days, §13) | Network-level correlation of devices and sessions |
On batching, specifically — this was an implicit choice and it should be an explicit one. The alternatives are real and none is free:
| Cost | |
|---|---|
| Batch (current) | The server observes group membership with near-certainty |
| Separate requests per envelope | 87 HTTPS requests per group message. Battery and latency on mobile |
| Batch with padding, shuffling and jitter | Partial mitigation, real complexity, and still correlatable |
We batch, and we say so. Pretending otherwise would be worse than the exposure. The honest claim is "we do not record a contact graph" — never "we cannot determine one."
12.2.b This document is not the cryptographic specification
CRYPTO.md is authoritative for protocol. This PRD is the product view.
The protocol is necessarily described across several sections here — key inventory in §6.1.a, pairing in §6.2, primitives and novelty in §12.3, the inventory below, and wire-level records in Appendix A. A specialist cannot review a protocol scattered through a product document, and an implementer working from these sections alone will fill gaps by guessing.
So: write CRYPTO.md before the crypto code (§12.4), make it self-contained, and treat it as the single reviewable artefact. Where this document and CRYPTO.md differ on protocol, CRYPTO.md wins and the PRD is the defect.
12.2.c What Kontaks does not defend against — stated because pretending otherwise is worse than the gap
- A contact behaving badly. They can screenshot, relay, or read your messages aloud. They are a legitimate recipient; cryptography has nothing to say about it.
- Recognition by the people you have paired with or share a room with. The name you presented, your voice, your avatar, your writing, the fact of your membership, and who vouched for you are all visible to the person or room you presented them to — and to a determined observer there, that may be enough to identify you in the ordinary sense. What they cannot do is turn any of it into a way to reach you, find you, or link you to another room (§12.2). Reusing one name and avatar everywhere creates that link, and the product pre-fills a default name, so reuse is the path of least resistance rather than a deliberate act (SPEC §6.3, D23, D25). The link is legible only to someone already in both relationships; it is still not a lookup key and still yields no route to you.
- Traffic-analysis metadata, and group membership in particular. The server sees which device is awake, envelope sizes, and timing per route, and group sends arrive batched, which reveals membership directly (§12.2.a). The two directions of a pair are stored as unrelated rows — we do not record the link — but we do not claim it cannot be determined.
- A compromised endpoint, or a device stolen while unlocked. Malware, a jailbreak, or a snatched unlocked phone reads everything on that device. The app lock and panic wipe (§6.1.2) shrink the window; nothing closes it. What we do bound is the blast radius: it is one device on your keyring, revocable in seconds, with no action required from any contact — unlike a SIM swap, which takes every relationship at once and cannot be undone.
- Your recovery secret. It is the one bearer credential in the system. We minimized what it can do (sign roster entries only — it cannot read, send, or decrypt history) and made using it slow and loud (§6.1.3), but someone who obtains your 12 words, or your Apple/Google account together with a device passcode, can enroll a device. Tier 4 (no recovery at all) exists for users who would rather lose their contacts than carry that risk.
- A pre-compromised push token. Apple and Google know a device received a push and when.
- Coerced disclosure of your own device. Rubber-hose applies as it always does. There is no duress mode; the panic wipe (§6.1.2) is a wipe, not a plausible-deniability mode.
- A malicious build. Reproducible builds and a published binary transparency log are worth doing and are not achievable for an iOS app today. Decided: we publish build provenance instead — signed GitHub Actions attestations and published artifact hashes for every release, so a build is tied to a commit even though it cannot be byte-reproduced. Sam will ask; that is the honest answer.
Cryptographic inventory
| Purpose | Primitive |
|---|---|
| Messaging session | libsignal — the Signal Protocol library itself (PQXDH + Double Ratchet; PQXDH is X3DH plus a Kyber-1024 KEM), not a reimplementation |
| Identity signing | Ed25519 (software) / ECDSA P-256 (Secure Enclave). Algorithm tagged on the wire |
| Route authorization | Ed25519 signature over the request. Not novel — a signature check |
| Device roster | Append-only log of signed entries. Same shape as a certificate chain |
| Pairing authentication | Argon2id + HKDF + AEAD over the full invite token (12-letter Part A + 8-digit Part B), with a slow-hashed lookup key (CRYPTO §6.1) |
| Media | DTLS-SRTP, fingerprint signed by the pair key |
| Local storage | SQLCipher (AES-256), key sealed under the Device Root Key, WhenUnlockedThisDeviceOnly |
| Device root key | Secure Enclave (P-256) / StrongBox / WebAuthn PRF extension on the web. Non-extractable; seals all other on-device key material |
| Recovery escrow | iCloud Keychain / Google Block Store (both E2EE, HSM-rate-limited by the platform), or BIP-39 phrase → Argon2id |
| Roster backup | XChaCha20-Poly1305; key in iCloud Keychain / Block Store, or from a BIP-39 phrase via Argon2id |
| Invite lookup | Argon2id(Part A), t=2, m=32 MiB (CRYPTO §6.1) — slow on purpose; Part A is never stored |
| TURN credentials | HMAC-SHA1 ephemeral (coturn use-auth-secret), 5-minute validity |
12.3 Novelty is deliberately minimal
The probability of being broken, and the cost of reviewing it, both scale with how much of the cryptography is ours. Almost none of it is.
| Component | What it is |
|---|---|
| 1:1 messaging session | libsignal — the Signal Protocol library itself, not a reimplementation. Groups are pairwise fan-out of 1:1 sessions, not Signal's group protocol. No sealed sender |
| Route authorization | An Ed25519 signature over the request, checked against a stored public key |
| Device roster | An append-only log of signed entries — the same shape as a certificate chain |
| Pairing | Argon2id + HKDF + AEAD over the full invite token, slow-hashed lookup key. No PAKE |
| Call media | DTLS-SRTP with fingerprint verification — the standard WebRTC pattern |
What remains to review is protocol logic, not cryptographic mathematics: whether the roster chain really stops a rogue device, whether the recovery flow has a hole, whether the invite entropy budget is right, whether the fingerprint binding really prevents a server MITM. A strong security engineer answers those in a few days.
For a first version, being boring buys more risk reduction than being audited does. An unaudited app built on libsignal with no hand-rolled ratchet and no PAKE is far less likely to be broken than an audited app full of original constructions.
12.4 External review — optional, staged, and not a launch gate
Nothing gates us. No store, regulator, or law requires a third-party cryptographic audit for a consumer messaging app. Signal, Telegram, Threema and Session all shipped before one. We can write CRYPTO.md, build, launch, reach 10k users and take revenue with zero review spend.
"Optional" means nobody will stop you, not that there is no downside. If the protocol is broken we find out when someone else finds it, and for a privacy product that failure is terminal rather than recoverable. The natural trigger is affordability, not principle: at ~1,000 subscribers the product takes ~$3,000/month and a review is two to three months of revenue — at which point skipping it is a choice, not a constraint.
| When | What | Cost |
|---|---|---|
| Before writing crypto code | Write CRYPTO.md. The highest-value action in the entire plan. Most protocol bugs are caught by the author while writing a precise spec, because you cannot hand-wave in a document that must say exactly which key signs what and what the recipient checks | $0 |
| Before writing crypto code | Publish it and invite critique (IETF CFRG list, the places Sam reads). Take free scoping calls with two or three firms — they walk the design to quote it, and will say if something is obviously wrong | $0 |
| Before writing crypto code (optional) | 2–3 days of one experienced security engineer on the spec. Not a firm engagement | $3–5k |
| MVP + private beta | Testers told plainly it is unreviewed | $0 |
| Public launch, anti-spam marketing | Still no outcome guarantees (§12.5) | $0 |
| Only when making security claims publicly, or at real scale | Full design + implementation review | $6–10k, from revenue |
Timing note: the crypto is not the first code written. App shell, pairing UX, call plumbing and push infrastructure come first — months of work. "Before you write the crypto code" is realistically month three or four, by which point there may already be beta signal on the thing that actually determines viability.
12.5 What we may and may not claim publicly
The core pitch — "no spam calls, because there is no number to leak" — is a claim about architecture, verifiable by anyone who opens the app. No cryptographer needs to certify it. The line is describe what you built; do not guarantee an outcome.
| Safe without a review | Requires care |
|---|---|
| "There is no phone number and no username." | "No one can ever read your messages." |
| "Nobody can contact you unless you added them." | "Unbreakable" / "bank-grade" / "military-grade" |
| "Delete someone and they can never reach you again." | "Independently audited" (when it is not) |
| "We do not store your contacts." | "More secure than Signal." |
| "Messages are deleted from our servers the moment they are delivered." | Any guarantee about attacker capability |
| "1:1 sessions use libsignal, the Signal Protocol implementation." | "Uses the Signal Protocol" unqualified — it implies Signal's whole audited system, including a group protocol and sealed sender we do not have |
| "You're anonymous." / "Nobody knows who you are" | |
| "Nothing you share here can be used to reach or find you anywhere else." | "No identity." / "Untraceable" |
The encryption line is fine unreviewed if it is true — deception requires a false statement, not an unverified one. Exposure comes from guaranteeing outcomes, not from describing an implementation.
Be precise about libsignal, because over-claiming here invites exactly the criticism we least want. We use libsignal for 1:1 sessions. Group messaging is pairwise fan-out of those 1:1 sessions — it is not Signal's group protocol, and there is no sealed sender. Saying "uses the Signal Protocol" without qualification reads as "audited like Signal," which is not a claim we can support.
And anonymity is not one of our claims. People in a room see each other's chosen names, voices and avatars (§12.2). Marketing may say that nothing shared in Kontaks can be used to reach or find someone elsewhere; it may never say that users are anonymous or untraceable, because inside a room they are neither.
Two disclosures we cannot opt out of: the Apple Privacy Nutrition Label / Play Data Safety forms (see §13 — "Data Not Collected" is very likely not the right answer; do not tick Play's independent-security-validation badge), and US encryption export paperwork, which is free but must be answered correctly or a submission is blocked.