Complete Android adaptive and accessibility hardening (#105)
Android ARM verification / rust (push) Failing after 50m4s
Android ARM verification / android (push) Failing after 9s
Dependency security audit / rustsec (push) Failing after 22s

This commit is contained in:
hugo committed 2026-10-15 11:44:31 +02:00
1 parent f4a6edbc68
commit 45d78aa5b3
33 files changed
+4596 -1376

No files matched your search

@@ -1,5 +1,7 @@
package de.rfc1437.ironstorage.core
import android.os.Bundle
import android.os.SystemClock
import androidx.test.platform.app.InstrumentationRegistry
import de.rfc1437.ironstorage.bridge.generated.MobileEntryEditorFieldKind
import de.rfc1437.ironstorage.bridge.generated.MobileEntryEditorInput
@@ -109,6 +111,7 @@ class NativeCredentialProviderTest {
emptyList()
)
}
val fillStart = SystemClock.elapsedRealtime()
val value = credentials.fill(
request.token,
caller,
@@ -117,6 +120,12 @@ class NativeCredentialProviderTest {
passphrase,
emptyList()
)
val fillMs = SystemClock.elapsedRealtime() - fillStart
assertTrue("Authenticated provider fill must finish within 5 s", fillMs < 5000)
InstrumentationRegistry.getInstrumentation().sendStatus(
0,
Bundle().apply { putLong("ironstorage_provider_authenticated_fill_ms", fillMs) }
)
assertEquals("alice", value.username)
assertEquals("fixture-password", value.password)
assertEquals("CredentialValue([REDACTED])", value.toString())
@@ -270,24 +270,24 @@ class NativeEntryViewerTest {
repeat(50) { directory ->
val folder = File(vault, "load/slot-${directory.toString().padStart(3, '0')}")
assertTrue(folder.mkdirs())
repeat(20) { entry -> File(folder, "entry-$entry.gpg").writeBytes(ciphertext) }
repeat(200) { entry -> File(folder, "entry-$entry.gpg").writeBytes(ciphertext) }
}
val storage = session.storage()
val navigateStart = android.os.SystemClock.elapsedRealtime()
assertEquals(50, storage.passwordNavigation("load", null, null, false).rows.size)
assertEquals(
20,
200,
storage.passwordNavigation("load/slot-000", null, null, false).rows.size
)
val navigateMs = android.os.SystemClock.elapsedRealtime() - navigateStart
assertTrue("1000-entry navigation must finish in 3 s", navigateMs < 3000)
assertTrue("10000-entry navigation must finish in 3 s", navigateMs < 3000)
val searchStart = android.os.SystemClock.elapsedRealtime()
assertEquals(1000, storage.passwordNameSearch("load").size)
assertEquals(20, storage.passwordNameSearch("load/slot-019").size)
assertEquals(10000, storage.passwordNameSearch("load").size)
assertEquals(200, storage.passwordNameSearch("load/slot-019").size)
val searchMs = android.os.SystemClock.elapsedRealtime() - searchStart
assertTrue("1000-entry name search must finish in 5 s", searchMs < 5000)
performance("tree_1000_navigation_ms", navigateMs)
performance("tree_1000_search_ms", searchMs)
assertTrue("10000-entry name search must finish in 5 s", searchMs < 5000)
performance("tree_10000_navigation_ms", navigateMs)
performance("tree_10000_search_ms", searchMs)
}
} finally {
withContext(Dispatchers.Main.immediate) { repository.lock() }
@@ -0,0 +1,126 @@
package de.rfc1437.ironstorage.core
import android.os.Bundle
import android.os.SystemClock
import androidx.test.platform.app.InstrumentationRegistry
import de.rfc1437.ironstorage.bridge.generated.MobileEntryEditorFieldKind
import de.rfc1437.ironstorage.bridge.generated.MobileEntryEditorInput
import de.rfc1437.ironstorage.bridge.generated.MobileTotpDiscoveryPhase
import de.rfc1437.ironstorage.bridge.generated.mobileTotpOperation
import java.io.File
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.SupervisorJob
import kotlinx.coroutines.cancel
import kotlinx.coroutines.runBlocking
import kotlinx.coroutines.withContext
import org.junit.Assert.assertEquals
import org.junit.Assert.assertFalse
import org.junit.Assert.assertTrue
import org.junit.Test
class NativeTotpPerformanceTest {
@Test fun cachedCatalogAndProviderStayResponsiveWhileLocked(): Unit = runBlocking {
val context = InstrumentationRegistry.getInstrumentation().targetContext
val roots = listOf(context.filesDir, context.noBackupFilesDir, context.cacheDir).map {
File(it, NativeEntryViewerTest.rootName).apply { mkdirs() }
}
val session = NativePrivateSession(context, {
PrivateDirectories(
roots[0].canonicalPath,
roots[1].canonicalPath,
roots[2].canonicalPath
)
}, NativeEntryViewerTest.MemoryKeystore())
val scope = CoroutineScope(SupervisorJob() + Dispatchers.Main.immediate)
val auth = NativeAuthenticationRepository(session, scope, Dispatchers.IO)
val passphrase = "public-totp-performance-fixture"
try {
withContext(Dispatchers.IO) {
session.initialize()
val setup = session.storage().androidOnboarding(session.backend())
val key =
requireNotNull(setup.generate("Performance fixture", passphrase).selectedKey)
setup.setupLocal(key, passphrase)
}
auth.authenticated { native ->
val editor = native.beginCreateEntry("", "seed", passphrase)
val page = native.addEntryEditorField(
editor.id,
MobileEntryEditorFieldKind.OTP_URI,
null,
"otpauth://totp/Public:seed?secret=GEZDGNBVGY3TQOJQGEZDGNBVGY3TQOJQ"
)
native.saveEntryEditor(
editor.id,
page.fields.map {
MobileEntryEditorInput(it.id, it.name, it.value.orEmpty())
}
)
}
withContext(Dispatchers.IO) {
// Replicate only Rust-encrypted public fixture bytes in this isolated test root.
val vault = File(roots[1], "ironstorage/vault")
val ciphertext = File(vault, "seed.gpg").readBytes()
repeat(99) { File(vault, "seed-$it.gpg").writeBytes(ciphertext) }
}
auth.authenticated { native ->
mobileTotpOperation().use { operation ->
assertEquals(100u, native.refreshTotpCatalog(operation, null).progress.matches)
}
}
withContext(Dispatchers.Main.immediate) { auth.lock() }
measure("totp_cached_open_p95_us", 1_000_000) {
auth.metadata { native ->
mobileTotpOperation().use { operation ->
assertEquals(100, native.searchCachedTotpNavigation("", operation)!!.size)
}
}
assertFalse(auth.state.value.unlocked)
}
measure("totp_warm_refresh_p95_us", 3_000_000) {
auth.metadata { native ->
mobileTotpOperation().use { operation ->
val progress = native.refreshTotpCatalog(operation, null).progress
assertEquals(MobileTotpDiscoveryPhase.COMPLETE, progress.phase)
assertEquals(100u, progress.cacheHits)
assertEquals(100u, progress.matches)
}
}
assertFalse(auth.state.value.unlocked)
}
val credentials = NativeCredentialRepository(auth)
val caller =
CredentialCaller("public.performance.client", listOf("AA".repeat(32)), null)
measure("provider_prepare_p95_us", 1_000_000) {
val request = credentials.begin(caller)
try {
assertEquals(100, request.candidates.size)
assertFalse(auth.state.value.unlocked)
} finally {
credentials.cancel(request.token)
}
}
} finally {
withContext(Dispatchers.Main.immediate) { scope.cancel() }
withContext(Dispatchers.IO) { roots.forEach { it.deleteRecursively() } }
}
}
private suspend fun measure(name: String, budgetUs: Long, action: suspend () -> Unit) {
repeat(10) { action() }
val samples = LongArray(30)
repeat(samples.size) { index ->
val start = SystemClock.elapsedRealtimeNanos()
action()
samples[index] = (SystemClock.elapsedRealtimeNanos() - start) / 1000
}
samples.sort()
val p95 = samples[28]
assertTrue("$name must finish within $budgetUs microseconds", p95 < budgetUs)
InstrumentationRegistry.getInstrumentation().sendStatus(
0,
Bundle().apply { putLong("ironstorage_$name", p95) }
)
}
}