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>
61 lines
2.4 KiB
Kotlin
61 lines
2.4 KiB
Kotlin
plugins {
|
|
kotlin("jvm")
|
|
}
|
|
|
|
repositories { mavenCentral() }
|
|
|
|
dependencies {
|
|
testImplementation(kotlin("test"))
|
|
// `org.json` is part of the Android framework, so on a phone this costs
|
|
// zero shipped bytes and is `compileOnly` for that reason -- bundling a
|
|
// second copy would shadow the platform's and is a classic duplicate-class
|
|
// build failure. On the JVM (tests, the interop driver) it has to be a real
|
|
// dependency.
|
|
//
|
|
// Safe here only because nothing security-relevant depends on key order:
|
|
// Android's implementation preserves insertion order and the reference one
|
|
// does not, and the two bytes-must-match encodings in this engine -- the
|
|
// ratchet header and the canonical log leaf -- are both built by hand.
|
|
compileOnly("org.json:json:20240303")
|
|
testImplementation("org.json:json:20240303")
|
|
|
|
// The engine's ONE real dependency, and it is here because the platform
|
|
// leaves a hole rather than because it was convenient.
|
|
//
|
|
// Android serves Ed25519 key generation from `AndroidKeyStore` and nowhere
|
|
// else -- probed on API 35, see ProviderProbeTest -- and Keystore only ever
|
|
// makes keys it keeps. This engine needs Ed25519 keys it holds: a device
|
|
// identity that lives in our own vault, and an account key derived from a
|
|
// passphrase. Neither can be a Keystore key by definition.
|
|
//
|
|
// That left implementing RFC 8032 by hand, which for SIGNING means
|
|
// non-constant-time arithmetic over a secret scalar, or taking a vetted
|
|
// constant-time one. Only the low-level `math.ec.rfc8032.Ed25519` class is
|
|
// used -- not the JCA provider, which is awkward to register on Android --
|
|
// so R8 shrinks nearly all of it away.
|
|
implementation("org.bouncycastle:bcprov-jdk18on:1.79")
|
|
}
|
|
|
|
// Bytecode level 17, which is what Android's toolchain consumes, produced by
|
|
// whatever JDK happens to be installed. A `jvmToolchain(17)` here would demand
|
|
// a JDK 17 on every machine that builds this and fail on anything newer, which
|
|
// is a poor trade for a library with no JDK 17-specific behaviour.
|
|
kotlin {
|
|
compilerOptions {
|
|
jvmTarget.set(org.jetbrains.kotlin.gradle.dsl.JvmTarget.JVM_17)
|
|
}
|
|
}
|
|
|
|
java {
|
|
sourceCompatibility = JavaVersion.VERSION_17
|
|
targetCompatibility = JavaVersion.VERSION_17
|
|
}
|
|
|
|
tasks.test {
|
|
useJUnitPlatform()
|
|
testLogging {
|
|
events("passed", "failed", "skipped")
|
|
showStandardStreams = true
|
|
}
|
|
}
|