Share the platform-neutral Rust mobile contract (#91)
Dependency security audit / rustsec (push) Failing after 22s

This commit is contained in:
hugo committed 2026-10-08 13:25:09 +02:00
1 parent fcb36a0684
commit 180851087b
52 files changed
+8932 -4263

No files matched your search

+15 -11
View File
@@ -46,6 +46,9 @@ val cargoExecutable = providers.environmentVariable("CARGO").orElse(
}
)
val rustVersion = libs.versions.rust.get()
val rustcExecutable = providers.exec {
commandLine("rustup", "which", "--toolchain", rustVersion, "rustc")
}.standardOutput.asText.map { it.trim() }
val ndkDirectory = androidComponents.sdkComponents.ndkDirectory
val sdkDirectory = androidComponents.sdkComponents.sdkDirectory
val rustInputs = files(
@@ -79,6 +82,8 @@ androidComponents.onVariants { variant ->
inputs.property("ndkVersion", libs.versions.ndk.get())
inputs.property("minSdk", libs.versions.minSdk.get())
outputs.dir(rawNative)
environment("RUSTUP_TOOLCHAIN", rustVersion)
environment("RUSTC", rustcExecutable.get())
environment("ANDROID_NDK_HOME", ndkDirectory.get().asFile.absolutePath)
environment("ANDROID_HOME", sdkDirectory.get().asFile.absolutePath)
environment(
@@ -128,16 +133,10 @@ androidComponents.onVariants { variant ->
outputs.dir(rawBindings)
commandLine(
cargoExecutable.get(), "+$rustVersion", "run", "--locked",
"--package", "ironstorage-android-build", "--bin", "ironstorage-uniffi-bindgen",
"--", "generate", "--library",
nativeOutput.get().file(
"arm64-v8a/libironstorage_android.so"
).asFile.absolutePath,
"--language", "kotlin", "--config",
rootProject.file(
"uniffi.toml"
).absolutePath,
"--out-dir", rawBindings.get().asFile.absolutePath, "--no-format"
"--package", "ironstorage-android-build", "--", "contract-bindings",
nativeOutput.get().file("arm64-v8a/libironstorage_android.so").asFile.absolutePath,
rootProject.file("uniffi.toml").absolutePath,
rawBindings.get().asFile.absolutePath, "phone"
)
}
val prepare = tasks.register<Exec>("prepare${title}RustBindings") {
@@ -153,7 +152,7 @@ androidComponents.onVariants { variant ->
commandLine(
cargoExecutable.get(), "+$rustVersion", "run", "--locked",
"--package", "ironstorage-android-build", "--", "prepare-bindings",
rawBindings.get().asFile.absolutePath, bindings.get().asFile.absolutePath
rawBindings.get().dir("kotlin").asFile.absolutePath, bindings.get().asFile.absolutePath
)
}
variant.sources.kotlin!!.addStaticSourceDirectory(bindings.get().asFile.absolutePath)
@@ -162,3 +161,8 @@ androidComponents.onVariants { variant ->
it.name == "pre${title}Build" || it.name == "compile${title}Kotlin"
}.configureEach { dependsOn(prepare) }
}
// Instrumentation suites share persistent emulators and must not compete for input focus.
tasks.matching { it.name == "connectedDebugAndroidTest" }.configureEach {
mustRunAfter(":app:connectedDebugAndroidTest")
}
@@ -0,0 +1,138 @@
package de.rfc1437.ironstorage.bridge
import androidx.test.platform.app.InstrumentationRegistry
import de.rfc1437.ironstorage.bridge.generated.MobileCameraFrame
import de.rfc1437.ironstorage.bridge.generated.MobileClipboard
import de.rfc1437.ironstorage.bridge.generated.MobileClipboardContent
import de.rfc1437.ironstorage.bridge.generated.MobileClipboardDisposition
import de.rfc1437.ironstorage.bridge.generated.MobileClipboardWait
import de.rfc1437.ironstorage.bridge.generated.MobileCredentialRequest
import de.rfc1437.ironstorage.bridge.generated.MobileEntryCopy
import de.rfc1437.ironstorage.bridge.generated.MobilePlatformFfiException
import de.rfc1437.ironstorage.bridge.generated.MobilePlatformPaths
import de.rfc1437.ironstorage.bridge.generated.MobileSecretLocator
import de.rfc1437.ironstorage.bridge.generated.MobileShellState
import de.rfc1437.ironstorage.bridge.generated.MobileTotpDiscoveryPhase
import de.rfc1437.ironstorage.bridge.generated.copyMobileEntry
import de.rfc1437.ironstorage.bridge.generated.decodeMobileCameraFrame
import de.rfc1437.ironstorage.bridge.generated.mobilePlatformLayout
import de.rfc1437.ironstorage.bridge.generated.mobileTotpOperation
import de.rfc1437.ironstorage.bridge.generated.validateMobileCredentialRequest
import java.util.concurrent.Executors
import org.junit.Assert.assertEquals
import org.junit.Assert.assertFalse
import org.junit.Assert.assertThrows
import org.junit.Test
class MobileContractTest {
private val context = InstrumentationRegistry.getInstrumentation().targetContext
private fun initialize() {
NativeRuntime(context).fixture(MobileShellState.EMPTY)
}
@Test
fun privatePathsAndProviderFactsAreValidatedInRust() {
initialize()
val layout = mobilePlatformLayout(
MobilePlatformPaths(context.noBackupFilesDir.path, context.cacheDir.path)
)
assertEquals("${context.noBackupFilesDir.path}/config.toml", layout.configuration)
assertThrows(MobilePlatformFfiException.InvalidPaths::class.java) {
mobilePlatformLayout(MobilePlatformPaths("relative", context.cacheDir.path))
}
val request = validateMobileCredentialRequest(
MobileCredentialRequest(
"opaque-request",
"de.example.app",
"A".repeat(64),
listOf("https://EXAMPLE.test:443/")
)
)
assertEquals(listOf("https://example.test"), request.origins)
assertThrows(MobilePlatformFfiException.InvalidCredentialRequest::class.java) {
validateMobileCredentialRequest(request.copy(origins = listOf("file:///tmp")))
}
assertFalse(request.toString().contains("opaque-request"))
assertFalse(
MobileSecretLocator.OpenPgpPassphrase("SecretReferenceMarker")
.toString().contains("SecretReferenceMarker")
)
}
@Test
fun foreignClipboardCallbackTransfersLargeSecretsAndPreservesNewerContent() {
initialize()
val secret = "binding-payload-".repeat(70_000)
val clipboard = Clipboard()
assertEquals(
MobileClipboardDisposition.RESTORED_PREVIOUS,
copyMobileEntry(MobileEntryCopy(secret, 45uL), clipboard)
)
assertEquals("previous", clipboard.text)
assertEquals(secret.length, clipboard.writtenBytes)
assertEquals(45_000uL, clipboard.timeout)
clipboard.newer = "newer content"
assertEquals(
MobileClipboardDisposition.PRESERVED_NEWER,
copyMobileEntry(MobileEntryCopy(secret, 45uL), clipboard)
)
assertEquals("newer content", clipboard.text)
clipboard.newer = null
clipboard.cancelled = true
assertThrows(MobilePlatformFfiException.Cancelled::class.java) {
copyMobileEntry(MobileEntryCopy(secret, 45uL), clipboard)
}
assertEquals("newer content", clipboard.text)
assertFalse(MobileClipboardContent.Text(secret.toByteArray()).toString().contains(secret))
}
@Test
fun cameraPlaneBoundsAndConcurrentOperationCancellationSurviveNativeOwnership() {
initialize()
assertThrows(MobilePlatformFfiException.InvalidCameraFrame::class.java) {
decodeMobileCameraFrame(MobileCameraFrame(UInt.MAX_VALUE, 1u, 1u, 1u, byteArrayOf(0)))
}
assertThrows(MobilePlatformFfiException.QrNotFound::class.java) {
decodeMobileCameraFrame(
MobileCameraFrame(1024u, 1024u, 2048u, 1u, ByteArray(2 * 1024 * 1024) { -1 })
)
}
val executor = Executors.newFixedThreadPool(4)
try {
val operations = List(16) { mobileTotpOperation() }
val tasks = operations.map { operation ->
executor.submit {
operation.cancel()
assertEquals(MobileTotpDiscoveryPhase.CANCELLED, operation.progress().phase)
}
}
tasks.forEach { it.get() }
operations.forEach { it.close() }
} finally {
executor.shutdownNow()
}
}
private class Clipboard : MobileClipboard {
var text = "previous"
var writtenBytes = 0
var timeout = 0uL
var newer: String? = null
var cancelled = false
override fun read(): MobileClipboardContent =
MobileClipboardContent.Text(text.toByteArray())
override fun write(value: ByteArray) {
writtenBytes = maxOf(writtenBytes, value.size)
text = value.decodeToString()
}
override fun clear() {
text = ""
}
override fun waitForTimeout(milliseconds: ULong): MobileClipboardWait {
timeout = milliseconds
newer?.let { text = it }
return if (cancelled) MobileClipboardWait.CANCELLED else MobileClipboardWait.ELAPSED
}
}
}
@@ -55,9 +55,9 @@ class NativeRuntimeTest {
fun wrongNativeContractAndChecksumFailWithTypedRedactedErrors() {
NativeRuntime(context).fixture(MobileShellState.EMPTY)
val version = IntegrityCheckingUniffiLib
.ffi_ironstorage_apple_uniffi_contract_version().toLong()
.ffi_ironstorage_mobile_uniffi_contract_version().toLong()
val checksum = IntegrityCheckingUniffiLib
.uniffi_ironstorage_apple_checksum_func_mobile_shell_fixture().toLong() and 0xffffL
.uniffi_ironstorage_mobile_checksum_func_mobile_shell_fixture().toLong() and 0xffffL
NativeBindingChecks.requireMatch(-27278, 38258, NativeFailureKind.ChecksumMismatch)
for ((actual, kind) in listOf(
version to NativeFailureKind.ContractMismatch,