Implement Rust-owned Android local and HTTPS onboarding (#95)
Android ARM verification / rust (push) Failing after 46m9s
Android ARM verification / android (push) Failing after 8s
Dependency security audit / rustsec (push) Successful in 21s

This commit is contained in:
hugo committed 2026-10-09 11:31:50 +02:00
1 parent 66b1d4a4c4
commit 36e5f55283
46 files changed
+3002 -79

No files matched your search

+1
View File
@@ -36,6 +36,7 @@ kotlin {
}
dependencies {
implementation(libs.rustls.verifier)
implementation(variantOf(libs.jna) { artifactType("aar") })
implementation(libs.annotation)
testImplementation(libs.junit)
+3
View File
@@ -5,3 +5,6 @@
-dontwarn java.awt.GraphicsEnvironment
-dontwarn java.awt.HeadlessException
-dontwarn java.awt.Window
-keep class de.rfc1437.ironstorage.bridge.NativeTls { *; }
-keep, includedescriptorclasses class org.rustls.platformverifier.** { *; }
+1
View File
@@ -152,4 +152,5 @@ org.ow2.asm:asm-commons:9.9=androidLintTool
org.ow2.asm:asm-tree:9.9=androidLintTool
org.ow2.asm:asm:9.9=androidLintTool
org.slf4j:slf4j-api:2.0.7=ktlint
rustls:rustls-platform-verifier:0.1.1=debugAndroidTestCompileClasspath,debugAndroidTestLintChecksClasspath,debugAndroidTestRuntimeClasspath,debugCompileClasspath,debugLintChecksClasspath,debugRuntimeClasspath,debugUnitTestCompileClasspath,debugUnitTestLintChecksClasspath,debugUnitTestRuntimeClasspath,releaseCompileClasspath,releaseLintChecksClasspath,releaseRuntimeClasspath
empty=androidApis,androidJdkImage,androidTestUtil,coreLibraryDesugaring,debugAndroidTestAnnotationProcessorClasspath,debugAndroidTestImplementationDependenciesMetadata,debugAnnotationProcessorClasspath,debugImplementationDependenciesMetadata,debugUnitTestAnnotationProcessorClasspath,debugUnitTestImplementationDependenciesMetadata,kotlinCompilerPluginClasspath,kotlinCompilerPluginClasspathDebug,kotlinCompilerPluginClasspathDebugAndroidTest,kotlinCompilerPluginClasspathDebugUnitTest,kotlinCompilerPluginClasspathRelease,lintChecks,lintPublish,releaseAnnotationProcessorClasspath,releaseImplementationDependenciesMetadata
@@ -0,0 +1,4 @@
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
<uses-permission android:name="android.permission.INTERNET" />
</manifest>
@@ -0,0 +1,160 @@
package de.rfc1437.ironstorage.bridge
import androidx.test.platform.app.InstrumentationRegistry
import de.rfc1437.ironstorage.bridge.generated.MobileOnboardingErrorKind
import de.rfc1437.ironstorage.bridge.generated.MobileOnboardingFfiException
import de.rfc1437.ironstorage.bridge.generated.MobilePrivateDirectories
import de.rfc1437.ironstorage.bridge.generated.MobileSecretBackend
import de.rfc1437.ironstorage.bridge.generated.MobileSecretException
import de.rfc1437.ironstorage.bridge.generated.MobileSecretLocator
import de.rfc1437.ironstorage.bridge.generated.MobileSecretProtection
import java.io.File
import java.util.UUID
import org.junit.Assert.assertEquals
import org.junit.Assert.assertFalse
import org.junit.Assert.assertThrows
import org.junit.Assert.assertTrue
import org.junit.Assume.assumeTrue
import org.junit.Test
class AndroidOnboardingTest {
@Test fun realPublicHttpsDiscoveryAndConnectionUseThePlatformVerifier() {
assumeTrue(
InstrumentationRegistry.getArguments().getString("ironstorage_https_integration") ==
"true"
)
val context = InstrumentationRegistry.getInstrumentation().targetContext
val id = "https-onboarding-${UUID.randomUUID()}"
val directories = MobilePrivateDirectories(
File(context.filesDir, id).apply { mkdirs() }.canonicalPath,
File(context.noBackupFilesDir, id).apply { mkdirs() }.canonicalPath,
File(context.cacheDir, id).apply { mkdirs() }.canonicalPath
)
val backend = MemoryBackend()
NativeRuntime(context).privateStorage(directories).use { runtime ->
runtime.androidOnboarding(backend).use { setup ->
setup.remote(
"https://github.com",
"public-fixture",
"octocat/Hello-World",
"public-test-token"
).use { remote ->
assertTrue(remote.discover().branches.isNotEmpty())
}
assertFalse(setup.status().configured)
val key =
requireNotNull(
setup.generate(
"HTTPS fixture <fixture@example.test>",
"public-test-passphrase"
).selectedKey
)
setup.setupLocal(key, "public-test-passphrase")
setup.remote(
"https://github.com",
"public-fixture",
"octocat/Hello-World",
"public-test-token"
).use {
it.connect()
}
assertTrue(setup.status().connected)
setup.replaceToken("public-fixture", "public-replacement-token")
assertEquals(2, backend.records.size)
val config = File(runtime.layout().configuration).readText()
assertFalse(config.contains("public-test-token"))
assertFalse(config.contains("public-replacement-token"))
}
}
}
@Test fun realRustSetupSurvivesRecreationAndKeepsTokensOutOfConfiguration() {
val context = InstrumentationRegistry.getInstrumentation().targetContext
val id = "onboarding-${UUID.randomUUID()}"
val directories = MobilePrivateDirectories(
File(context.filesDir, id).apply { mkdirs() }.canonicalPath,
File(context.noBackupFilesDir, id).apply { mkdirs() }.canonicalPath,
File(context.cacheDir, id).apply { mkdirs() }.canonicalPath
)
val native = NativeRuntime(context)
val backend = MemoryBackend()
native.privateStorage(directories).use { runtime ->
runtime.androidOnboarding(backend).use { setup ->
assertFalse(setup.status().configured)
val prepared = setup.generate(
"Emulator fixture <fixture@example.test>",
"public-test-passphrase"
)
assertEquals(1, prepared.keys.size)
assertFalse(prepared.configured)
}
}
native.privateStorage(directories).use { runtime ->
runtime.androidOnboarding(backend).use { setup ->
val key = requireNotNull(setup.status().selectedKey)
setup.setupLocal(key, "public-test-passphrase")
assertTrue(setup.status().configured)
assertFalse(setup.status().connected)
val unsupported = assertThrows(MobileOnboardingFfiException.Failed::class.java) {
setup.remote(
"ssh://example.test",
"alice",
"team/passwords",
"public-test-token"
)
}
assertEquals(MobileOnboardingErrorKind.UNSUPPORTED_REMOTE, unsupported.kind)
setup.remote(
"https://example.test",
"alice",
"team/passwords",
"public-test-token"
).use {
assertThrows(MobileOnboardingFfiException.Failed::class.java) { it.connect() }
}
assertFalse(setup.status().connected)
val config = File(runtime.layout().configuration).readText()
assertFalse(config.contains("public-test-passphrase"))
assertFalse(config.contains("public-test-token"))
assertFalse(config.contains("public-replacement-token"))
assertTrue(File(runtime.layout().vault, ".gpg-id").readText().contains(key))
assertEquals(1, backend.records.size)
}
}
native.privateStorage(directories).use { runtime ->
runtime.androidOnboarding(backend).use { assertTrue(it.status().configured) }
}
}
private class MemoryBackend : MobileSecretBackend {
val records = mutableMapOf<MobileSecretLocator, ByteArray>()
override fun create(
locator: MobileSecretLocator,
protection: MobileSecretProtection,
value: ByteArray
) {
if (records.containsKey(locator)) throw MobileSecretException.AlreadyExists()
records[locator] = value.copyOf()
value.fill(0)
}
override fun retrieve(
locator: MobileSecretLocator,
protection: MobileSecretProtection
): ByteArray = records[locator]?.copyOf() ?: throw MobileSecretException.Missing()
override fun replace(
locator: MobileSecretLocator,
protection: MobileSecretProtection,
value: ByteArray
) {
if (!records.containsKey(locator)) throw MobileSecretException.Missing()
records[locator]?.fill(0)
records[locator] = value.copyOf()
value.fill(0)
}
override fun delete(locator: MobileSecretLocator, protection: MobileSecretProtection) {
(records.remove(locator) ?: throw MobileSecretException.Missing()).fill(0)
}
override fun lock() = Unit
override fun unlock() = Unit
}
}
@@ -18,7 +18,8 @@ import java.util.zip.ZipFile
/** OS library loading only. Callers schedule all native calls off the main thread. */
class NativeRuntime(context: Context) {
private val applicationInfo = context.applicationContext.applicationInfo
private val applicationContext = context.applicationContext
private val applicationInfo = applicationContext.applicationInfo
private val lock = Any()
private var initialized = false
private var initializationFailure: NativeFailureKind? = null
@@ -45,6 +46,7 @@ class NativeRuntime(context: Context) {
}
if (!packaged) throw NativeBridgeException(NativeFailureKind.MissingLibrary)
System.loadLibrary(LIBRARY)
NativeTls.initialize(applicationContext)
uniffiEnsureInitialized()
initialized = true
}
@@ -0,0 +1,6 @@
package de.rfc1437.ironstorage.bridge
/** Pass the OS Application to the native verifier; no trust decisions live here. */
internal object NativeTls {
external fun initialize(context: Any)
}