Complete Android adaptive and accessibility hardening (#105)
This commit is contained in:
1 parent
f4a6edbc68
commit
45d78aa5b3
33 files changed
+4596
-1376
No files matched your search
+9
@@ -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())
|
||||
|
||||
+8
-8
@@ -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() }
|
||||
|
||||
+126
@@ -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) }
|
||||
)
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user