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 } // The vector files arrive by environment variable, and Gradle does not // track an environment variable as an input. So a vector could move under // a build directory that already had results in it and the task would // report UP-TO-DATE without running a thing. // // That is not hypothetical. Twenty vector tests once "passed" in CI for // weeks without ever executing, because the runner is a developer machine // whose build directory survives between runs, and a replayed local pass // looks exactly like a real one in the log. // // Declaring each file as an input is what makes a changed vector // invalidate the task. Proven by changing one key in one vector and // watching the suite go red without `--rerun-tasks`. listOf( "FLAMENET_VECTORS", "FLAMENET_PQ_VECTORS", "FLAMENET_ACCOUNT_VECTORS", "FLAMENET_BACKUP_VECTORS", "FLAMENET_BACKUP_PAYLOAD_VECTORS", "FLAMENET_CONTENT_VECTORS", ).forEach { name -> val path = System.getenv(name) if (path != null && file(path).exists()) { inputs .file(path) .withPropertyName("vector-$name") .withPathSensitivity(PathSensitivity.NONE) } } } // 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().configureEach { finalizedBy(tasks.named("jacocoTestReport")) } tasks.named("jacocoTestReport") { dependsOn(tasks.named("test")) reports { xml.required.set(true); html.required.set(true) } }