Verification document

Phantom Chat — Marketing Claim Audit

Date 2026-06-19 (re-verified against the current build)
Prepared by Curtis Tunaley, Veilus Digital
Audience Journalists, podcast hosts, security researchers evaluating Phantom Chat.

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:182PQXDHHybrid.combine() merges classical X3DH with Kyber shared secret via HKDF
Phantom Chat/KeyStore.swift:329-352ensureKyberKey() 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:71AES.GCM.seal() for message encryption
Phantom Chat/SecureMediaManager.swift:41AES.GCM.seal(data, using: key) for media
Phantom Chat/ChatAPI.swift:978, 1338AES.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, 109SecureEnclave.P256.KeyAgreement.PrivateKey creation and persistence
Phantom Chat/KeyStore.swift:192 — biometric access control on SE key
Phantom Chat/KeyStore.swift:554-580createAndPersistSecureEnclaveKey() 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-475AppAttestService instantiates DCAppAttestService.shared
Phantom Chat/AuthService.swift:469service.generateKey() called and cached
Phantom Chat/AuthService.swift:452, 475-485attestKey(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.rulesfcmTokens / 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.tsgenerateInvitationCode + 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:458Stepper(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.

2.7 Metadata the Server Can See (full disclosure)

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():

On the server — deleteAccount (functions_index_consolidated.ts:212-740):

Recoverability — the honest answer:

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-102PanicShakeDetector (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

3.2 Lock Screen Widget

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:526AppPasscodeStore.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-826handleScreenshot() 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:

For deeper questions: support@veilusdigital.co.

— Curtis Tunaley Founder, Veilus Digital Western Australia