Commit Graph
11 Commits
Author SHA1 Message Date
Paul Hitt 77a7f3981c The instrumented suite could not sign up, and said nothing about it
🔴 **Sign-up requires a year of birth and no instrumented test filled it.**
`submit` is `enabled = … && (!registering || birthYear.length == 4)`, so
the button was DISABLED — and `performClick()` on a disabled Compose node
does not throw, it does nothing. Every sign-up in this suite has been
tapping a dead button since the age gate landed, failing two minutes later
with 'nav-contacts did not appear' and no error anywhere on screen.

Nothing was wrong with the app. The suite simply stopped being able to
create an account, and a failing instrumented suite reads as a flaky one.

Also fixes the skip counter in scripts/instrumented.sh: it grepped a bare
SKIPPED, which matches Gradle's own `Task … SKIPPED` lines, so a verdict
written to stop a skip being read as a pass was inventing skips instead.
2026-09-05 16:15:29 -04:00
Paul Hitt 1a7c25386d Allow the test fleet in the instrumented runner, refuse only production
relay1/relay2 are what the fleet is FOR, and the suites need https because
Android blocks cleartext — so a local http relay cannot serve them at all.
Refusing both left no way to run the suite as designed.

Production stays refused outright. The test fleet is allowed with a loud
line saying accounts will be created on it, because they will be.
2026-09-05 15:42:29 -04:00
Paul Hitt d8d48d8651 "Checking what this server uses…" was also the failure state
🔴 **The relay probe had no deadline and no way to say it failed.**
`discovered` is null both while the probe is in flight and after it has
come back with nothing, so `identityDescription` shows "Checking what
this server uses…" in both cases. A relay that never answers leaves the
sign-up form on that sentence permanently — no error, no retry, nothing
to act on.

Found by the instrumented suite on a clean emulator: nine of ten tests
failed with `nav-contacts did not appear within 120000ms`, and the screen
dump showed the form still checking.

⚠️ **The reason it never answered is worth writing down.** The app was
pointed at `http://10.0.2.2:18810`, and Android has blocked cleartext
HTTP by default since API 28 — there is no `usesCleartextTraffic` and no
network security config here. The request never left the device.

🔴 That refusal is CORRECT and is not what changed. `SiteConfig` already
says why: a bare hostname becoming `http://` would hand key material to
anyone on the path "and it would work, which is the worst outcome." What
was wrong is that it failed silently, so somebody self-hosting who omits
the scheme gets a dead screen and no idea why.

Now: bounded at fifteen seconds — far longer than a healthy relay needs —
and a failed probe is a distinct state that says
"Couldn't reach that server. Check the address — and note it has to be
https, not http."

Same defect as android#19 one screen earlier, which I fixed this morning
without noticing the probe had it too.

Also adds `scripts/instrumented.sh`, which closes two ways an on-device
run silently points at production: the suites DEFAULT to live hosts when
no relay is named, and a saved emulator snapshot restores whatever the
AVD was last doing — observed today as a snapshot signed in as
`fnreviewbeta` against relay.flamenet.io, under a run told to use a local
relay. The script has no default, refuses both live hosts by name, cold
boots with `-no-snapshot-load`, and exits 3 on a skip because a skip is
not a pass.
2026-09-05 15:41:49 -04:00
Paul Hitt ad7c63ff4f An APK no longer ships without a word about what changed in it
Publishing was three steps done by hand, and only two of them left a trace.
The manifest and the APK could go out with nothing anywhere saying what was in
the build — which is how 0.8 shipped with notes and 0.9 shipped with none.

The gate goes on the manifest rather than on a checklist, because the manifest
is the step that cannot be skipped: it is what the in-app updater reads, so an
APK published without it offers nobody an update. A checklist item is skipped
on exactly the busy evening the notes matter most.

🔴 It matches on the `versionName` read out of the APK, never a number typed
here — the same reason this script already reads the version rather than
accepting one. The two cannot disagree.

The check itself lives in the relay repository (Scripts/covers-android.py),
because that is what serves the /releases page and defines the format. Parsing
the JSON here would leave a second copy of the schema to drift out of step
with the first, and a drifted gate is a gate that passes everything.

⚠️ FN_SKIP_RELEASE_NOTES=1 still publishes, and says so loudly on stderr — a
skipped gate that printed nothing would be a gate nobody remembered turning
off. A missing relay checkout is a refusal, not a pass, and names FN_RELAY_DIR.

Proven on a real APK in all four states: covered (manifest written, valid
JSON), uncovered (refused, and stdout stays EMPTY so no half-written manifest
lands), skipped, and no checkout. Every message goes to stderr, because this
script's stdout is the manifest itself.
2026-09-05 11:38:31 -04:00
Paul HittandClaude Opus 4.8 141f3168a5 Stop publishing the build machine's paths with the F-Droid repo
`fdroid update` writes a status/ directory containing update.json and
running.json, which record the absolute paths of the machine that built the
repo — home directory, username, toolchain locations — and that signing
happened there. My publish script rsynced the whole repo directory, so those
went public and were being served unauthenticated.

That is targeting data for a supply-chain attack against the laptop holding the
Android release key, which is permanent and cannot be rotated without orphaning
every install. fdroid's own deploy command excludes status/; a raw rsync does
not, and I used a raw rsync.

Removed from the live repo and excluded here so it does not come back.

Found by an audit pass, not by anything going wrong — which is the argument for
running them.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-08-30 07:38:44 -04:00
Paul HittandClaude Opus 4.8 84b0af1c9a Add a publish script for the F-Droid repository
Our own repo, not a submission to f-droid.org — and the difference is not
cosmetic. f-droid.org rebuilds from source and signs with their key, so an app
already installed under our signature could not be upgraded by it; Android
refuses a differently-signed APK over an existing install. This repo indexes
the APK we already signed, so everybody who sideloaded from the website
upgrades cleanly and then gets updates automatically.

The working directory holds a signing keystore and its password and lives
outside every checkout, because all four repos are public. The script refuses
to run if the keystore is missing rather than re-initialising over a live repo
— replacing the index key makes every client that has added the repository
reject its updates.

Version comes out of the APK via aapt2 rather than being typed, same rule as
the update manifest.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-08-29 16:29:32 -04:00
Paul HittandClaude Opus 4.8 31551f60f2 Tell a sideloaded app when a newer build exists
Nothing updates this app on its own. It is downloaded as an APK from a web
page, so there is no store to notice a release and no background updater to
install one — a person can run a build from months ago indefinitely, through
every fix that came after it, and never find out. That is the part of
sideloading that is genuinely worse than a store, and this is the only thing
that closes it.

The background check asks the RELAY the install is signed in to, not a fixed
address of ours. Pointing every install at flamenetmessenger.com would mean an
app whose whole claim is that it does not phone home checking in with us on a
timer, from every device, including the self-hosted ones that deliberately
never contact us at all. A relay that publishes no manifest simply produces no
prompt.

"Check for updates" in Account is the other half, and the reason the fallback
to our site exists: pressing it is a question the user asked, so answering
"your relay does not publish updates" when we plainly could is unhelpful. It
tries the relay first and names whichever host answered, so it is never a
silent phone-home. It also distinguishes "no newer build" from "could not
check" — reporting silence as good news is how somebody sits on a build with a
fix in it believing otherwise.

The download URL is built from the host that answered and never read from the
manifest. A `url` field would let any relay send its users to an APK on a host
of its choosing; Android's signature check is a backstop, not a reason to build
the hole in front of it. A test asserts a hostile manifest cannot move it, and
fails if the guard is removed.

versionCode is bumped and `scripts/release-manifest.sh` reads it back out of
the built APK, so the manifest and the artifact cannot drift.

⚠️ Nobody already installed sees this: every build before this one has no
check in it, so the first prompt anybody gets is for the build after this one.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-08-29 15:02:59 -04:00
Paul HittandClaude Opus 4.8 96b43f4e43 The secret sweep could not see a new file
`git ls-files` lists only TRACKED files. A brand-new one — which is exactly what
a dropped credential looks like — was invisible to this gate, while `git add -A`
would stage it happily. A planted `sk_live_…` in a new file passed.

It now scans `--cached --others --exclude-standard`: the same set `git add -A`
would stage.

🔴 **My earlier "verified it still fails on a planted key" was testing the wrong
thing.** That probe was `git add -N`'d first, which made it tracked, so it
exercised the path that already worked and reported confidence in the path that
did not. A verification that quietly changes the conditions is worse than none —
it is why this survived being checked.

Also allowlists the two VoIP test fixtures (`alert-token`, `voip-token`) by
literal value. A gate that is always red is a gate everybody learns to ignore,
which is the failure this one was one commit away from.

Verified in all four repositories: an untracked planted key now fails the gate,
and all four are clean without it.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-08-29 14:12:16 -04:00
Paul HittandClaude Opus 4.8 bd2cdf0394 Add CONTRIBUTING, and point the vectors at the repository that owns them
The README said the engine is held to `engine/test/vectors/protocol-v1.json`,
a path from the pre-split subtree layout that does not exist here. The vectors
live in flamenet-e2e and are read from there rather than copied, because a
copied vector file drifts and a drifting vector file stops being a check.
PublishedVectorsTest already searches for it and fails loudly when it cannot
find it; the README now says so, and names FLAMENET_VECTORS.

CONTRIBUTING records what a JVM test cannot tell you: Conscrypt and the JVM's
JCA are different providers, so :engine:test passing proves nothing about a
handset, which is what interop/ is for. Also the exception escaping a WebRTC
callback that SIGABRTs rather than surfacing, the missing permission that
presented as a dead button, and `*/` inside a KDoc reporting an unclosed
comment hundreds of lines from its cause.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-08-29 09:45:04 -04:00
Paul HittandClaude Opus 4.8 6baaf1315a T4+S3: CI on the Mac, and a build a stranger can verify
The unit suites run on the project's own runner before main moves. And
scripts/verify-build.py proves what two clean builds already showed:
every byte of the APK's contents reproduces from this source — only the
signing block differs, because signatures are randomized. Rebuild,
compare contents, check the certificate: that is the whole trust chain.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-08-29 07:36:09 -04:00
Paul Hitt ac42e44919 The Kotlin engine
Nothing here is an app. This is the protocol implemented a third time, and
it is deliberately first: a third implementation is only worth writing if it
is written against the specification and the published vectors rather than
against a reading of the Swift and JavaScript that already agree.

So far: canonical bytes, safety numbers, and the RFC 6962 key log with its
inclusion and consistency proofs. Nine tests, all against the vectors the
other two engines are held to. X25519, Ed25519, X3DH, the Double Ratchet,
sealed sender and the post-quantum halves are not written yet.

The vectors are read from the protocol repository rather than copied here,
and the suite fails rather than skips when it cannot find them -- an engine
that quietly stops checking itself is worth less than no third engine.
2026-08-21 18:22:30 -04:00