Files
flamenet-messenger-android/engine/build.gradle.kts
T
Paul HittandClaude Opus 4.8 a40cc2d2d4 Measure Android coverage, and round-trip the vault
JaCoCo was not configured on either JVM module, so the Android numbers
were unknown rather than good. :engine and :client are plain Kotlin/JVM
precisely so they can be measured without an emulator; now they are.
Baseline: engine 84.9% lines, client 47.3%.

Vault was the outlier in engine at 53%, and its uncovered half was almost
entirely save/load pairs — credentials, trust pins, the delivery key, the
key-log pins, the roster, avatars, per-line deletion.

That is not an arbitrary gap. It is the exact shape of a bug that has
already shipped in this app twice. `requireKeyLog` was computed, held in
memory and never written down, so a security control the UI offered
silently reverted on every relaunch. `Trust.changed` says the same thing
in its own doc comment: returned by `record()`, persisted by nothing, so
`state()` could never answer CHANGED, the "Accept new keys" button could
never render, and a contact who reinstalled became permanently
unmessageable while the header insisted nothing had changed.

Neither was hard to write. Both were invisible because a save and a load
that disagree still compile and only differ after a restart, which no
test performed. Every test here writes with one Vault and reads with a
second built over the same bytes, because that is the only arrangement in
which the two halves are forced to agree.

Includes the AAD binding the class doc promises: a trust record copied
into another contact's slot must not open, or someone with file access
could attribute a stranger's keys to someone you trust.

Vault 53.0% -> 84.9% lines, engine 84.9% -> 92.3%.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-08-30 09:13:26 -04:00

71 lines
2.9 KiB
Kotlin

plugins {
kotlin("jvm")
jacoco
}
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
}
}
// Coverage for this module. `:engine` and `:client` are plain JVM libraries
// precisely so they can be measured and tested without an emulator, so there is
// no reason for their numbers to be a guess.
tasks.withType<Test>().configureEach { finalizedBy(tasks.named("jacocoTestReport")) }
tasks.named<JacocoReport>("jacocoTestReport") {
dependsOn(tasks.named("test"))
reports { xml.required.set(true); html.required.set(true) }
}