Files
Paul HittandClaude Opus 5 ec214ba875 The review notes name accounts that exist
This file handed Apple flamenetreview1 and flamenetreview2. Neither is an
account on the relay, and the users table says the only review accounts are
fnreviewalpha and fnreviewbeta, which are also the pair verified end to end
against the live accounts service on 2026-09-04. A reviewer typing a handle
that does not exist cannot get past the sign-in screen, which is the same
failure that had 0.4.0 rejected under Guideline 2.1.

The walkthrough was wrong in a second way. It sent the reviewer to a Community
tab to add the other account. There is no Community tab, and has not been: the
tabs are Home, Messages, Calls, Stories, Files, Wallet and Account, and adding
somebody happens from the field at the top of Home. The two accounts are
already contacts, so the step is not needed at all and the walkthrough is four
steps now rather than five.

Two checklist lines added. One for the Sign-In Information field, which is the
one thing a reviewer cannot work around. One for re-running generate.sh after a
version bump, because the version lives in project.yml and the Xcode project is
generated from it, so a bump alone archives the old number.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-15 17:39:37 -04:00

14 KiB

TestFlight submission, every field, with paste-ready copy

Why you can't find "the description": TestFlight doesn't have one. The App Store Description field lives under the Distribution / App Store tab and is only needed for a public release. TestFlight's equivalent is Beta App Description, under TestFlight → Test Information.

Only external testing needs Beta App Review. Internal testers (up to 100 App Store Connect users on your team) get builds with no review and no copy at all, worth using for the first pass.


1. TestFlight → Test Information

Beta App Description

Flamenet Messenger is an end-to-end encrypted messenger you can host yourself. Sign up with a username, no phone number and no email required. One-to-one chats and group chats are encrypted on your device using the X3DH and Double Ratchet protocols, so the relay carries ciphertext it cannot read. Photos, voice notes and stories are encrypted the same way, with a per-file key.

Every conversation has a safety number you can compare with the other person to confirm nobody is in the middle. If someone's keys change, a reinstall, a new device, or an interception, the app tells you and holds the message until you decide.

This is an early beta. It is not audited, and it is built by one person in the open.

Feedback Email

<a mailbox you actually read>

Privacy Policy URL

https://flamenetmessenger.com/privacy

Marketing URL (optional)

https://flamenetmessenger.com

2. TestFlight → Beta App Review Information

Demo account

Username:  fnreviewalpha
Password:  <in the password store, NOT in this repository>

Second account (named in the Notes below):
Username:  fnreviewbeta
Password:  <the same passphrase>

The passphrase is deliberately not written here. This repository is public and these accounts are live; committing the value hands them to anyone. The tests read it from FN_DEMO_PASSPHRASE.

These two are the pair that exists. This file used to say flamenetreview1 and flamenetreview2, which are not accounts on the relay and have not been for some time: a reviewer typing those cannot get past the sign-in screen, which is the same failure that had 0.4.0 rejected under Guideline 2.1. Checked against the users table on 2026-09-15, fnreviewalpha and fnreviewbeta are the only review accounts there. fnreviewalpha / fnreviewbeta were also the pair verified end to end against the live accounts service on 2026-09-04, recorded on ios#12.

Whatever is in the Sign-In Information field in App Store Connect has to match these two. That field is the one thing a reviewer cannot work around.

The previous pair was burned and these replace them. The old passphrase was committed and later removed from the tree, but removing a secret from the tree does not remove it from history, and it remained readable in three commits on origin/main of this public repository. Because an fnid is derived from the passphrase, that value was not a password anyone could reset: it was those accounts, on every relay, permanently.

So the accounts were replaced rather than rotated. Rotating changes the passphrase and therefore the fnid, which leaves the pair unable to recognise each other and showing "identity changed", the state that has already made a contact permanently unmessageable on Android once.

The exposure is closed. The burned passphrase opened exactly two accounts, appreview1demo and appreview2demo, and both were deleted through the app's own DELETE /messenger/account route, so the device rows, prekeys, envelopes, group memberships and buddy edges went with them. Signing in with the old value now returns 401.

It was two, not six. The fndemo* and fnreview* pairs use a different passphrase and were never reachable with the leaked one, worth recording, because the first count of the blast radius was wrong in the alarming direction, and an overstated finding costs credibility on the next one.

Deleted through the route rather than by hand in SQL. The route walks a graph, devices before prekeys so a crash cannot leave a device advertising prekeys that no longer exist, then envelopes both ways, groups owned and joined, and the buddy edges pointing back at the account. A DELETE by hand gets some of that and looks finished.

Still cruft, though not exposed: fndemo1, fndemo2, fnreview1demo, fnreview2demo are three superseded generations of review account. Worth tidying, not urgent.

How this pair was verified. Registration and sign-in round-tripped against the live account service with an independently implemented KDF (byte-compared against the app's); the app's own client stack signed in via a unit-test probe with the passphrase supplied as bytes; and both accounts reached the signed-in shell through the app's real screens in consecutive UI runs. DemoAccountSetupUITests re-drives the screens whenever:

Handles burned along the way, all with unknown or stale secrets, none holding anything: flamenetdemo1/2 (created against the relay instead of the account service, the mistake the test header warns about, made a third time), flamenetrv1/2/3 (UI-provisioned while simulator typing was dropping characters). They are cruft on accounts.flamenet.io, not exposure.

xcodebuild test -project FlamenetMessenger.xcodeproj -scheme FlamenetMessenger \
  -destination 'platform=iOS Simulator,id=<udid>' \
  -only-testing:FlamenetMessengerUITests/DemoAccountSetupUITests

Never "verify" these with curl against relay.flamenet.io. That host is pinned, so the app does not use the relay's own sign-in, it authenticates against the shared FlameNet account service (accounts.flamenet.io) with a handle. An account made on the relay answers 200 to curl and "handle not found" to a person holding a phone.

Dead ends, for the record: appreview1demo / appreview2demo exist only on the relay. fnreviewalpha / fnreviewbeta were made through the shared-account path and carry no password at all.

Notes ← the field that decides whether this passes

Please read, the first account will look empty, and that is by design.

This is an end-to-end encrypted messenger. Messages are encrypted to a specific device, not to an account. When you sign in on your device you create a new device that did not exist when any earlier message was sent, so it cannot decrypt them. We cannot pre-populate a conversation for you; if we could, the encryption would not be working.

The order matters, please start with the second account.

  1. Sign in as fnreviewbeta (password above). Wait for the Home tab to load, then sign out from the Account tab. This registers this device for that account so it can receive encrypted messages; without it there is no key to encrypt to and the send in step 3 will fail.
  2. Sign in as fnreviewalpha. The two accounts are already contacts, so fnreviewbeta is on the Home tab and in Messages with no adding to do.
  3. Open the Messages tab, tap the conversation with fnreviewbeta, and send a message.
  4. Sign out, then sign in again as fnreviewbeta. The message is there.

To add somebody by hand instead, the field at the top of the Home tab says "Search or add by username". There is no separate contacts screen.

You may see a "Security keys changed" banner. That is the app working, it appears when a contact signs in on a new device, which is exactly what step 4 does. Accept it to continue.

Voice and video calls need two live devices; with one test device the call screen will open and wait. Pushes do arrive on a locked phone, so a call rings on the lock screen, but only against a relay set to the matching APNs environment. See FN_APNS_ENV below.


2b. Guideline 1.2, user-generated content

Paste this into App Review Notes on the App Store submission. It answers the four things 1.2 asks for, and three of them are features. The fourth is a position, and leaving it unsaid is what makes it look like an oversight.

Reporting. Press and hold a message and choose Report; open a story and Report is beside the reactions; a profile can be reported from the conversation menu. Six reasons, a free-text note, and the person reporting chooses whether to attach a copy of what they are reporting. The report reaches the operator of the relay the account is on, by email and in a table they can read again later. The person reported is not told.

Blocking. Enforced by the relay rather than hidden in the app, so a blocked account cannot reach the person who blocked them by reinstalling. Blocking works whether or not a report is also sent.

Published contact. flamenetmessenger.com/safety says how to report without an account and names a mailbox read by people. flamenetmessenger.com /support and /contact carry the same addresses.

Filtering. This is where we differ from an app that can scan, and we would rather state it than have it look like something we forgot. The relay holds ciphertext. It has no key to any message, attachment or story, so it cannot inspect content and no server-side filter is possible without breaking the encryption the app exists to provide. What replaces it: a report carries whatever the reporting user chose to attach, which is the only way the operator sees anything; strangers cannot reach anybody silently, the first message from an unknown account raises a keep-or-block prompt; there is no discovery of strangers by browsing, only by exact handle; and an account that draws a pattern of reports can be suspended on the relay, which stops it signing in, sending or reading anything.

⚠️ One number is not settled and it is the owner's to set. 1.2 asks for "timely responses". flamenetmessenger.com/safety currently says plainly that reports are read and that no reply is sent, which is honest and is not a timeframe. Decide what the operator will actually meet before quoting one here, and put the same words on the safety page. A stated time that is missed is worse than the current silence.


3. Per build, "What to Test"

First external build. Worth exercising:

  • Sign up with a username (no phone number, no email)
  • One-to-one messages, replies, reactions, unsend
  • Group chats, create one, add someone, send
  • Photos, voice notes and stories
  • The safety number on a contact's profile
  • Dark mode, and text size at accessibility settings

Known gaps: no push notifications (local only, and only while the app is open), and calls need two real devices.


4. Export compliance, answered per build

  • Does your app use encryption?, Yes
  • Does it qualify for an exemption?, read the options and pick the publicly available source code one if it is offered. This changed with EXPORT-COMPLIANCE.md v0.2: the app now goes under EAR 740.13(e), and all four repositories are public, so that answer is the accurate one. The No that used to be here was right under the mass-market route and is wrong now.
  • Proprietary or non-standard algorithms?, No

Paste the crypto description from EXPORT-COMPLIANCE.md when asked.

Do not set ITSAppUsesNonExemptEncryption in the plist on its own, the upload is rejected with "Invalid Export Compliance Code". Answer the questions in App Store Connect for the first build; ASC then issues a code for project.yml.

No recurring report. The annual BIS self-classification report belongs to the mass-market route this project no longer takes, and the 740.13(e) notification was sent once, on 2026-09-15.


5. Before you hit submit

  • Both demo accounts verified signing in through the app UI.
  • Walk the four steps above once on a real iPhone. Sign-in is proven; the full send-and-read round trip on one physical device is not.
  • sudo xcode-select -s /Applications/Xcode.app, you are pointed at Xcode 27.0 beta in iCloud Drive, and ASC rejects beta-Xcode builds.
  • Mint an Apple Distribution certificate for team M5923Y84P5. The only identity in the keychain is an Apple Development cert for the free personal team 2W7DA3A8W2: wrong type and wrong team.
  • No provisioning profiles are installed.
  • Privacy nutrition label under App Privacy, must agree with Resources/PrivacyInfo.xcprivacy.
  • Age rating set.
  • Sign-In Information in App Store Connect says fnreviewalpha and the passphrase those accounts actually take. A handle that does not exist is what had 0.4.0 rejected.
  • CURRENT_PROJECT_VERSION bumped since the last upload, and Scripts/generate.sh re-run afterwards. The version lives in project.yml and the Xcode project is generated from it, so a bump without a regenerate archives the old number and Xcode goes on showing it.
  • FN_APNS_ENV on the relay matches where the build is going. production for TestFlight and the App Store, sandbox for a build installed from Xcode. It is per-relay, not per-token, so one relay cannot serve both at once, and the wrong one fails as BadDeviceToken, which from the outside looks like calls not ringing rather than like a configuration mistake. It is production on the box today.

What NOT to write anywhere in this submission

  • "more secure than Signal" / "better than iMessage", not supportable
  • "we can't read your messages", false for the web client, and disprovable from page source. True of this iOS app; the sentence is not.
  • "audited", "military-grade", "unbreakable"
  • any post-quantum claim. PQ needs iOS 26+ and this app deploys to iOS 16
  • Safe: "end-to-end encrypted", "X3DH and Double Ratchet", "no phone number required", "self-hosted", "open published spec", "encrypted by default, unlike Telegram"