Phantom Chat — Marketing Claim Audit
This document maps every technical claim made on veilusdigital.co and inside the iOS app to the specific Swift source file that implements it. Each row reflects what the code actually does — not what marketing copy aspires to. Where a claim is partial or unimplemented, that is noted explicitly. The goal is to make this product easy to fact-check before you write or record about it.
On line numbers: the line references below were accurate when re-verified on the date above. Line numbers drift as code evolves — the file name + symbol/function name is the durable reference. If a line has moved, search for the named symbol in that file. Happy to share the exact commit on request.
If anything here is unclear, contact support@veilusdigital.co and I'll walk through the relevant code or schedule a screen-share.
1. Cryptography
1.1 Post-Quantum Key Exchange (PQXDH / Hybrid Curve25519 + ML-KEM-768)
| Claim |
Every new conversation uses NIST FIPS 203 ML-KEM-768 (Kyber) hybridised with classical Curve25519 — if either survives the next 20 years, messages stay safe. |
| Status |
✅ Verified, executes on every new conversation. Not dead code. |
| Code path |
Phantom Chat/X3DHProtocol.swift:64-80 (Kyber-768 keypair generation, integrated into bundle)
Phantom Chat/X3DHProtocol.swift:161-191 (PQXDH hybrid is engaged for both initiator and responder)
Phantom Chat/X3DHProtocol.swift:182 — PQXDHHybrid.combine() merges classical X3DH with Kyber shared secret via HKDF
Phantom Chat/KeyStore.swift:329-352 — ensureKyberKey() generates and persists Kyber-768 keypairs (2400 bytes) |
| Note |
Implementation is custom Swift, not libsignal. The algorithms (X3DH, Double Ratchet, ML-KEM-768) are standard and publicly documented; the Swift code re-implements them rather than linking the Rust libsignal library. |
1.2 Double Ratchet (Signal Protocol)
| Claim |
Every message uses a fresh derived key, rotated automatically per message with a Diffie-Hellman ratchet step on conversation-direction change. |
| Status |
✅ Verified — full implementation of all 5 algorithm components. |
| Code path |
Phantom Chat/DoubleRatchet.swift:20-33 (root key + send/receive chain keys + DH ratchet keys)
Phantom Chat/DoubleRatchet.swift:45-46 (skipped-message-keys dictionary for out-of-order delivery)
Phantom Chat/DoubleRatchet.swift:66-88 (encryptMessage() derives the next message key, advances send chain)
Phantom Chat/DoubleRatchet.swift:112-114 (performDHRatchet() step)
Phantom Chat/DoubleRatchet.swift:117-128 (skipped-keys path) |
1.3 AES-256-GCM
| Claim |
Messages and media are encrypted with AES-256-GCM. |
| Status |
✅ Verified — used everywhere. |
| Code path |
Phantom Chat/DoubleRatchet.swift:71 — AES.GCM.seal() for message encryption
Phantom Chat/SecureMediaManager.swift:41 — AES.GCM.seal(data, using: key) for media
Phantom Chat/ChatAPI.swift:978, 1338 — AES.GCM.SealedBox for decrypt
Phantom Chat/VerificationStore.swift:388 — AES-GCM for stored fingerprint encryption |
1.4 Hardware-Bound Identity (Secure Enclave)
| Claim |
Long-term identity key generated inside the iPhone's Secure Enclave; private key never leaves hardware. |
| Status |
✅ Verified — SE preferred on first launch. |
| Code path |
Phantom Chat/KeyStore.swift:40, 60, 78, 109 — SecureEnclave.P256.KeyAgreement.PrivateKey creation and persistence
Phantom Chat/KeyStore.swift:192 — biometric access control on SE key
Phantom Chat/KeyStore.swift:554-580 — createAndPersistSecureEnclaveKey() and loadSecureEnclaveKey() via SE blob |
| Caveat |
Falls back to software key only if the device does not have a Secure Enclave (older simulators, jailbroken devices missing SEP). Software fallback preserved for backward compatibility. |
1.5 App Attest (Device Attestation)
| Claim |
Server verifies that the connecting device is a real iPhone running unmodified Phantom Chat code. |
| Status |
✅ Verified — wired end-to-end (client + server). |
| Code path |
Phantom Chat/AuthService.swift:329, 410-475 — AppAttestService instantiates DCAppAttestService.shared
Phantom Chat/AuthService.swift:469 — service.generateKey() called and cached
Phantom Chat/AuthService.swift:452, 475-485 — attestKey(keyID, clientDataHash:) + POST to verifyAppAttestKey Cloud Function
Phantom Chat/functions_index_consolidated.ts:1390-1410 — server-side attestation verification |
2. Privacy & Anonymity
2.1 Anonymous Sign-up
| Claim |
No phone number, email, real name, or personal information collected at sign-up. Username only. |
| Status |
✅ Verified. |
| Code path |
Phantom Chat/AuthView.swift:6 — @State private var username: String = "" (the only signup field)
Phantom Chat/AuthView.swift:239-241 — signup collects only username, trims whitespace, calls viewModel.signIn(username: name) |
2.2 What the Server Stores
| Claim |
The server (Firebase/Firestore) holds only: username, public key bundle (keyBundle), push/FCM token, subscription status, and encrypted message ciphertext while it is in transit. It cannot read message content, contacts, or who you talk to. |
| Status |
✅ Verified, with the two clarifications below. |
| Code path |
Phantom Chat/ChatAPI.swift:2820, 2835 — user doc: username + keyBundle written
Phantom Chat/EncryptedProfileStore.swift:82-92 — profile write
Phantom Chat/SignalSessionInitiationView.swift:281-282 — public keyBundle upload
Phantom Chat/IAPService.swift:263 — subscription status
Phantom Chat/firestore.rules — fcmTokens / fcm / tokens subcollections (push token) |
| Clarification 1 — the username is plaintext |
The username is stored as a plaintext field, because it is the routing address other people redeem an invite against. It is not encrypted. No phone number, email, or real name is stored anywhere (ChatAPI.swift:2820). |
| Clarification 2 — diagnostic probes are transient |
During connection setup the app writes a tiny probe doc to users/{uid}/diagProbes/ to measure write latency, then immediately deletes it in the same call (ChatAPI.swift:316-327). It contains only a server timestamp + the string "diag" — never user content. |
2.2b Message Retention — How Long the Server Keeps Ciphertext
| Claim |
Encrypted message ciphertext is removed from the server when its disappearing-message timer expires, or when the account / conversation is deleted. |
| Status |
✅ Verified — and stated honestly: messages are not wiped the instant they're delivered. |
| What actually happens |
Ciphertext stays on the server until (a) the per-message expireAt timer fires, or (b) the user deletes their account or conversation. This is deliberate: Firestore re-delivers the message backlog to a device that was offline, which is exactly why the client keeps a persisted seenRemoteIDs dedup set (ChatAPI.swift:508, 608). Throughout, the payload stays end-to-end encrypted and unreadable by the server. |
| Code path |
expireAt set on send: Phantom Chat/ChatAPI.swift:1696, 1895, 2168-2170, 2231-2233 expiry honoured on read: Phantom Chat/ChatAPI.swift:1359-1365 account deletion: Phantom Chat/ChatAPI.swift:4395 (deleteAccount()) |
| What we do NOT claim |
We deliberately avoid saying "deleted from our servers immediately after delivery" or "messages live only on your device." The honest framing is: the server holds only ciphertext it cannot read, removed on timer expiry or account/conversation deletion. |
2.3 Push Notifications Carry No Content
| Claim |
Push notifications never include message content — only an opaque wake signal and metadata so the app can fetch and decrypt locally. |
| Status |
✅ Verified. |
| Code path |
Phantom Chat/MessagePreviewExtention/NotificationService.swift:30-43 — security comment explicitly forbids sending message content; payload shows only "New Message" + sender pseudonym
Phantom Chat/functions_notifyGroupInvitation.js:107 — push body is "${inviterUsername} invited you to join ${groupName}" (metadata only) |
2.4 No In-App User Search / No Public Directory
| Claim |
There is no feature in the app to search for or browse other users by name. Contact discovery is invite-code / QR only. |
| Status |
✅ Verified for the app. ⚠️ See the honest backend note below. |
| Code path |
Client-side username-search code removed entirely (commits 2026-05-27).
RedeemInviteView / GenerateInviteView are the only discovery paths.
Phantom Chat/firestore.rules:78-79 — legacy usernameIndex/{hash} read locked to allow read: if false, so the leftover index documents on existing accounts cannot be enumerated. |
| Honest backend note |
The users/{uid} profile documents are readable by any authenticated app user (firestore.rules:66-68, allow read: if isSignedIn()). In normal use the app fetches a single profile by a known UID (a get). Because Firestore's read permission also grants list, a determined authenticated client could in principle enumerate the users collection — which would expose usernames and public key bundles only. It would never expose a phone number, email, real name (none are stored) or any message content (end-to-end encrypted). There is no in-app feature that performs this enumeration. |
2.5 Invitation Code Discovery (1-on-1)
| Claim |
One-shot invite codes (XXXX-XXXX-XXXXXX) or QR codes are the only contact-discovery method. |
| Status |
✅ Verified. |
| Code path |
Phantom Chat/ChatListView.swift:1341+ — RedeemInviteView + GenerateInviteView
Phantom Chat/functions_index_consolidated.ts — generateInvitationCode + redeemInvitationCode cloud functions |
2.6 Group Invitation Codes (1–500 redemptions per code)
| Claim |
Each group invite code can be redeemed up to 500 times; expiry from 1 day to never; creator can issue multiple codes. |
| Status |
✅ Verified. |
| Code path |
Phantom Chat/GroupInviteCodeManagerView.swift:458 — Stepper(value: $maxJoins, in: 1...500)
Phantom Chat/GroupInviteCodeManagerView.swift:729-746 — expiry options including "Never" |
| Note |
There is no per-group total cap in the code; the 500 is per-code. Multiple codes pointing at the same group are supported. The website wording reflects this honestly. |
| Claim |
Message content is end-to-end encrypted and unreadable by us. We are honest that some metadata exists server-side, like every messenger. |
| Status |
✅ Content encrypted. ⚠️ Metadata disclosed honestly below — we do not claim "zero metadata" or "we can't see who talks to whom." |
| What the server CAN see (plaintext) |
• 1-on-1 pairing: pairs/{id} stores a plaintext participants:[uidA,uidB] (ChatAPI.swift ~1668-1674). • Group membership + title: groups/{id} stores plaintext name, participants, createdBy (ChatAPI.swift ~3373-3380). • Per-message envelope: plaintext senderId, createdAt, expireAt (the body/ciphertext is encrypted). • Account: username (plaintext), public key bundle, push token, subscription status. |
| What the server CANNOT see |
Message text, media, and any conversation content — all AES-256-GCM under the Double-Ratchet/PQXDH session keys, which never leave the devices. |
| Honest framing |
"We can't read your messages — there's no content for us to disclose. The minimal account metadata we hold (username, which accounts share a conversation, timestamps, push token, subscription) we minimise and resist requests for." This is the Signal trust model. Reducing metadata further (sealed sender) is a roadmap item. |
2.8 Emergency Destruct — exactly what is deleted, and what is recoverable
| Claim |
Emergency Reset / account destruct removes your account, conversations, and media from our servers, and destroys the encryption keys on your device so any residual data is undecryptable. |
| Status |
✅ Accurate with the honest scope/recoverability notes below. We do not claim we can forcibly overwrite data on Google's physical servers (no cloud service can). |
On the device — SecureDataDestruction.killSwitch():
- Calls the server
deleteAccount cloud function.
- Deletes the user's Firebase Storage media (personal / pair / group).
- Keychain wipe —
SecItemDelete across all key classes (SecureDataDestruction.swift:158-176). This destroys the encryption keys = crypto-erase.
- 7-pass DoD 5220.22-M overwrite of every file in Documents/Caches/App Support/tmp, then delete (
SecureDataDestruction.swift:222-254).
- Clears UserDefaults, Firebase cache, overwrites heap memory.
On the server — deleteAccount (functions_index_consolidated.ts:212-740):
- 1-on-1
pairs: deleted entirely — both halves, all messages, all media.
- Account: user doc, all sub-collections, username index, push tokens, invites, invite codes deleted; then Firebase Auth account deleted (
auth.deleteUser).
- Groups: the user is removed (sender key + receipts deleted). The group is fully wiped only if no participants remain; otherwise the group persists and messages the user already sent into it remain (encrypted) for the other members (
functions_index_consolidated.ts:438-494).
Recoverability — the honest answer:
- Device: effectively permanent. The Keychain key-destruction is a cryptographic erase — any residual flash data is ciphertext with no key, mathematically unrecoverable. (On iOS flash/APFS a multi-pass overwrite isn't guaranteed to hit the original physical cells; the crypto-erase is the real guarantee, the overwrite is secondary.)
- Server: the app issues a logical delete — data is removed from the live Firestore database and Storage and is gone from the app, queries, and console. We cannot securely overwrite data on Google's infrastructure; after deletion it follows Google Cloud's deletion process. If Point-in-Time Recovery or scheduled backups are enabled on the project, deleted data is restorable by the project owner for that window (PITR ≤ 7 days). Mitigation: the server only ever held encrypted ciphertext, so any residual copy is undecryptable once the device keys are crypto-erased.
- Not in scope of destruct: the recipient's device still holds its own decrypted copy of any messages they received (no messenger can reach into another user's phone), and group messages the user contributed remain with groups that still have members.
Honest one-liner: "Destruct deletes your account, 1-on-1 conversations, and media from our servers and crypto-erases your keys on-device, so any encrypted copy that briefly persists in our provider's backups can never be read. We don't claim to physically overwrite Google's disks — no service can."
3. Emergency Account Reset (Three Independent Paths)
3.1 Shake-to-Wipe
| Claim |
Shaking the phone four times in roughly one second surfaces the destruction confirmation. |
| Status |
✅ Verified — uses CoreMotion accelerometer thresholds, not motionEnded. |
| Code path |
Phantom Chat/RootView.swift:16-102 — PanicShakeDetector (4+ reversal peaks within a 1.2s window)
Phantom Chat/RootView.swift:91-92 — peak-count threshold
Phantom Chat/RootView.swift:436-440 — callback sets showPanicConfirm = true |
| Claim |
A Lock Screen widget that, on tap (Face ID required by iOS), opens the app to the destruction confirmation. |
| Status |
✅ Verified — iOS enforces Face ID on Lock Screen widget delivery. |
| Code path |
PhantomPanicWidget/PhantomPanicWidget.swift:51 — .widgetURL(URL(string: "phantomchat://panic"))
Phantom Chat/RootView.swift:374-397 — .onOpenURL catches the panic URL, triggers confirmation alert
Phantom Chat/PhantomPanicWidget.swift:8-10 — comment notes Face ID is system-enforced for Lock Screen variant |
3.3 Duress Passcode
| Claim |
A separate "duress" PIN entered at App Lock looks identical to the real PIN but silently destroys all data while presenting the decoy persona pack to whoever is holding the phone. |
| Status |
✅ Verified. |
| Code path |
Phantom Chat/RootView.swift:526 — AppPasscodeStore.verify(entered) returns .duress verdict
Phantom Chat/RootView.swift:535-553 — duress branch triggers silent wipe via deleteAccount() while showing decoy UI
Phantom Chat/SettingsView.swift:536-548 — "Set Duress Passcode" UI |
3.4 Multi-Pass Secure Deletion (DoD 5220.22-M, 7 passes)
| Claim |
Emergency Destruction overwrites local files 7 times before deletion. |
| Status |
✅ Verified — the code literally performs DoD 5220.22-M (3 random + 3 complement + 1 zero passes). |
| Code path |
Phantom Chat/SecureDataDestruction.swift:227-250 — exact 7-pass implementation |
| Caveat (technical) |
iOS uses APFS on flash storage with copy-on-write semantics. Multi-pass overwrites of file contents don't necessarily hit the original physical NAND blocks — that is a known property of flash storage, not specific to this app. The cryptographic-erase approach (destroying the data-protection key) is the primary defense; the multi-pass overwrite is a secondary belt-and-braces layer. We do both. The bare minimum guarantee — that the data is no longer cryptographically accessible — is satisfied either way. |
4. UI & State Privacy
4.1 App Switcher Blackout (Privacy Overlay)
| Claim |
When the app moves to the background or App Switcher, a solid black overlay covers any sensitive content before iOS takes its snapshot. |
| Status |
✅ Verified, covers both .background and .inactive scene phases. |
| Code path |
Phantom Chat/RootView.swift:354-355 — .onChange(of: scenePhase)
Phantom Chat/RootView.swift:630-690 — three explicit handlers: • .background → sets isScreenObscured = true immediately • .inactive → debounced obscure via 400ms delay (suppresses transient push flickers) • .active → cancels any pending obscure
Phantom Chat/RootView.swift:238-243 — black overlay renders when appState.isScreenObscured && !appState.isLocked |
4.2 Screenshot Detection
| Claim |
If someone screenshots a 1-on-1 chat, the other party is notified with a system message. |
| Status |
✅ Verified. |
| Code path |
Phantom Chat/ConversationScreen.swift:363 — listens for UIApplication.userDidTakeScreenshotNotification
Phantom Chat/ConversationScreen.swift:801-826 — handleScreenshot() sends "📸 Screenshot taken" as an actual encrypted message to the peer |
4.3 Disappearing Messages
| Claim |
Per-conversation timers that auto-delete messages from both devices after a chosen interval. |
| Status |
⚠️ Range verified. The minimum is 10 seconds, not 1 second. Maximum is ~30 days (1 month = 2.59 million seconds). The website was updated 2026-05-27 to reflect this. |
| Code path |
Phantom Chat/DisappearingMessagesView.swift:11-32 — timer options: 10s, 30s, 1m, 2m, 5m, 10m, 15m, 30m, 1h, 2h, 3h, 6h, 12h, 1d, 2d, 3d, 1w, 2w, 1mo |
5. Customisation
5.1 76 Alternate App Icons
| Claim |
76+ alternate app icons available in Settings → App Icon. |
| Status |
✅ Verified — exactly 76 unique icons in the bundle. |
| Code path |
Phantom Chat/Phantom Chat/AlternateIcons/*@2x.png — 76 unique base names (each icon has @2x and @3x variants for 152 files total) |
6. Coming Soon — Honestly Marked, Not Yet Built
6.1 Tor Relay Routing (Anonymous Tier)
| Marketing label |
"Coming Soon" |
| Status |
❌ Not implemented in shipping build. |
| Code path |
Phantom Chat/ChatAPI.swift:29-31 — Tor integration explicitly disabled with comment "DISABLED: Tor integration (types not available)"
Phantom Chat/CURRENT_ISSUES_SUMMARY.md:10-19 — open test failure Code=-1004 |
| Note |
Framework scaffolding exists in the build but is not functional. "Coming Soon" label on the website is accurate. |
6.2 Offline Bluetooth / P2P Mesh (Anonymous Tier)
| Marketing label |
"Coming Soon" |
| Status |
❌ Not implemented. Zero scaffolding. |
| Code path |
No MultipeerConnectivity, CoreBluetooth, or mesh implementation in the Swift code. Pure roadmap item. |
| Note |
"Coming Soon" label is honest — not even a stub exists yet. |
7. Things Removed From Marketing After Code Audit
These claims previously appeared on the website or in the in-app onboarding tour and have been removed because the code does not back them:
| Claim removed |
Reason |
| "Sealed Sender envelopes" |
No Sealed Sender implementation exists. (Signal-style "sealed sender" is a specific protocol feature that requires server-side blinding — Phantom Chat does not implement it.) |
| "iMessage doesn't have post-quantum encryption" |
Apple shipped PQ3 on iMessage in iOS 17.4 (March 2024). The comparison table was corrected. |
| "Air-gapped servers" |
The backend runs on Firebase (Google Cloud). "Air-gapped" — meaning no network connection at all — is provably false for any cloud-hosted service. Replaced with "encrypted-at-rest infrastructure". |
| "No metadata stored" (in hero) |
The Privacy Policy enumerates what IS stored (username, push token, subscription). "No metadata" was an overclaim. Replaced with "No phone. No email. No tracking." |
8. How to Verify This Yourself
The TestFlight beta is open: https://testflight.apple.com/join/rmSrMch9
For a deeper look:
- Open Wireshark or a similar tool on a Wi-Fi network with TestFlight running — confirm only encrypted Firestore + APNs traffic.
- Read the on-device logs — enable Settings → Logs & Support → File Logging, do anything, then tap "Send Logs to Veilus" — the log file shows every cryptographic operation step-by-step.
- Request the cloud functions source — happy to share
functions_index_consolidated.ts (~1700 lines) for review of the server-side claims.
For deeper questions: support@veilusdigital.co.
— Curtis Tunaley Founder, Veilus Digital Western Australia