Implement Rust-owned Android local and HTTPS onboarding (#95)
This commit is contained in:
1 parent
66b1d4a4c4
commit
36e5f55283
46 files changed
+3002
-79
No files matched your search
@@ -36,6 +36,7 @@ kotlin {
|
||||
}
|
||||
|
||||
dependencies {
|
||||
implementation(libs.rustls.verifier)
|
||||
implementation(variantOf(libs.jna) { artifactType("aar") })
|
||||
implementation(libs.annotation)
|
||||
testImplementation(libs.junit)
|
||||
|
||||
@@ -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.** { *; }
|
||||
@@ -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>
|
||||
+160
@@ -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)
|
||||
}
|
||||
Reference in new issue
Block a user