0d274b2c1c5e1eda0c15222632c30756f148a7ea
4
Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
5f983c1455 |
Android: the app, and the two things only a phone could tell us
There is now a Flamenet Messenger app that installs, launches, creates an account on a self-hosted relay, registers a device, publishes prekeys, and stores all of it in an Android Keystore-backed vault. Verified on an emulator through the actual screens, not by unit test. 92 JVM tests, 37 cross-engine, 9 on-device, 3 against live relays. Two findings, both of which the JVM suite was structurally incapable of producing. **Android has no ordinary Ed25519 provider.** `KeyPairGenerator.Ed25519` and `KeyFactory.Ed25519` come from `AndroidKeyStore` and from nowhere else -- probed on API 35 rather than assumed -- and Keystore only makes keys it keeps. Every Ed25519 key in this engine is one we must hold: a device identity in our own vault, an account key derived from a passphrase. So the JCA route could never have worked. It compiled, passed 81 tests on a laptop, and threw "Not initialized" on the first launch on a phone. Worse, it was LATENT: a wrong password failed before any key was minted, so the error path worked perfectly while the happy path was broken. Ed25519 now uses BouncyCastle's `math.ec.rfc8032` core directly -- the engine's one real dependency, present because the platform leaves a hole, and a strictly better answer than the non-constant-time keygen it replaces. ProviderProbeTest records the measurement so nobody guesses again. **The prekey field is `prekeys`, not `opks`.** I guessed. The relay accepted the registration, returned 200, and stored nothing -- so the device was published and could not be started with, a failure with no error anywhere. Caught by a live test against the throwaway fleet, which exists because the same path through the emulator takes four minutes to say "timed out" and nothing about where. Two testing traps, recorded where they bite: An indeterminate progress spinner animates for ever, so Compose's test clock never goes idle and `waitUntil` never evaluates its condition -- it fails on the full timeout, reading as "sign-up is broken" when sign-up works. Auto-advance is now disabled around the wait only; disabling it for the whole test is the next mistake, because the app opens on a spinner while it reads the vault. Wiping the Keystore in `@Before` deletes the key the already-launched activity is holding, and every later write fails against a key that no longer exists. The test signs out through the UI instead, which is both safe and what a person does. Failure messages now include what the screen was showing. A bare timeout sends you to the network, the relay and the crypto in turn while the app has been displaying the reason the whole time. Also corrects a note that said this Mac had no Android SDK. It does. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> |
||
|
|
b1ba9cdc58 |
Kotlin: the relay transport
The engine can hold a conversation but had no way to reach a relay. This is that: the §8 routes, both authentication modes, and the transparency log. 13 tests against a real HTTP server rather than a hand-written fake. That distinction is the point -- a fake written alongside the client agrees with it about everything including the field name it got wrong, which is exactly how this protocol's one shipped bug survived until a live round trip. Two more tests run against the deployed relay, opt-in via FN_LIVE_RELAY and silent when unset. A test that reaches the network cannot sit in a hermetic suite; it fails on an aeroplane and a suite that is red for unrelated reasons stops being read. What they check is the thing MockWebServer structurally cannot: that the URL this client builds is one relay.flamenet.io actually serves. It does, on both mounts, and the Kotlin engine verified an inclusion proof from the production log against a head fetched separately -- not against the root the proof response carried, because a server that supplies both halves of a comparison has proven nothing. Details that are load-bearing: The per-request signature covers the LOGICAL section-8 path, not the URL's. The relay mounts these routes twice and signs against the former, so signing the REST-prefixed path yields a valid signature over the wrong string and a refusal with no hint as to why. Tested by asserting the other one does NOT verify. The nonce is not decoration. Two inbox polls in the same second with the same empty body would otherwise produce identical signatures, and the relay's replay cache cannot tell an honest repeat from a captured one -- so it refuses the honest client and the app quietly stops receiving mail. Token minting is lazy, and a deployment with no token route is asked exactly once. Laziness cost an hour here: the tests hung, because takeRequest() blocks forever and I was waiting for a mint that had not happened yet. They now use a bounded read, so that mistake fails instead of looking like a hung build. Application passwords are stripped of spaces. The account service prints them in readable groups and users paste what they were shown; sending the spaces is a 401 nobody can diagnose, because the password on screen looks right. Kotlin nests block comments, so `/e2e/` followed by a star inside a doc comment opens one that never closes and the compiler blames the last line of the file. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> |
||
|
|
fc93df24e6 |
Kotlin: Ed25519, and cross-engine interop against the web engine
The seventeen Kotlin tests that landed with X3DH and the ratchet prove the engine agrees with itself. That is not the property anyone needs. Two engines written by the same person agree about the same misreadings of the spec, and every one of those shows up months later as "the message will not decrypt" on someone else's phone. So this wires the Kotlin engine up against the web engine, which is normative, in both directions. 23 assertions, all passing: v1 X3DH both ways, identical associated data, alternating ratchet turns past the handshake, out-of-order and permanently lost messages, a drained one-time prekey pool, tamper rejection, safety numbers, and the canonical transparency-log leaf. Three things worth recording. Ed25519 was missing, and the engine could derive an account address but never sign a claim for one. The JCA offers no route from a private EdDSA key to its public half, so the RFC 8032 keygen is done here -- and verified against the RFC 8032 test vectors, not against a round-trip through our own code. A round-trip proves only that the file agrees with itself, and every wrong clamp, endianness and sign convention agrees with itself perfectly. The scalar multiplication uses BigInteger and is therefore not constant-time; it runs once at sign-in, in the user's own process, and signing deliberately stays with the platform provider. The harness reports a build failure as a failure, never a skip. The Swift harness beside it once printed "skipped" -- which reads as green -- while silently building a different checkout. Only a missing JDK skips here, and the resolved path is printed either way. Each section is wrapped so a divergence is a named failure rather than a crash. That was not theoretical: breaking the Kotlin message-key derivation on purpose crashed the process with a stack trace and skipped every later section. It now reports four failing sections and finishes the run. The negative control is how this was checked at all -- a suite that passes on its first run and has never been seen to fail is not evidence of anything. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> |
||
|
|
06b97ca69b |
Start Android with the engine, held to the published vectors
Android does not exist, and the first thing it needs is not a screen. It needs the protocol implemented a third time — which is only worth anything if the third implementation is checked against the spec and the published vectors rather than against a reading of our own Swift. So `android/engine` is a plain Kotlin/JVM library with no Android in it: no SDK, no emulator, no device, and therefore gateable in CI on Linux like everything else. It reads engine/test/vectors/protocol-v1.json from its one location rather than carrying a copy, because a vector transcribed into the test that checks it is a vector that agrees with itself. Covered so far: safety numbers (§7) and key transparency verification (§19) — RFC 6962 leaf and node hashing, the unbalanced split, and bottom-up audit paths. Nine tests, all green against the same file the JavaScript and Swift engines answer to. The §19 tests are the ones worth having. A top-down reading of a bottom-up audit path yields the correct root for every left-spine index, so it passes any suite that only checks index 0 — the vectors pin a right-descending index, and the tests assert both that the published path verifies and that the same path reversed does not. android/README.md lists what is missing — X25519, X3DH, the ratchet, sealed sender, both post-quantum halves, the relay client, the app — as a table rather than as an omission. Android stays honestly marked "not started" on the site until that list is shorter. Bytecode targets 17 without demanding a JDK 17 be installed: a jvmToolchain(17) fails on any newer JDK, which is a poor trade for a library with no JDK 17-specific behaviour. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> |