Implement Rust-owned Android Keystore secure storage (#93)
Dependency security audit / rustsec (push) Failing after 23s

This commit is contained in:
hugo committed 2026-10-08 16:22:57 +02:00
1 parent 874c8139a4
commit 1abd75282e
36 files changed
+4380 -37

No files matched your search

@@ -0,0 +1,119 @@
package de.rfc1437.ironstorage.bridge
import androidx.test.platform.app.InstrumentationRegistry
import de.rfc1437.ironstorage.bridge.generated.MobileAndroidAuthorization
import de.rfc1437.ironstorage.bridge.generated.MobileAndroidCiphertext
import de.rfc1437.ironstorage.bridge.generated.MobileAndroidDeviceState
import de.rfc1437.ironstorage.bridge.generated.MobileAndroidKeySpecification
import de.rfc1437.ironstorage.bridge.generated.MobileAndroidKeystore
import de.rfc1437.ironstorage.bridge.generated.MobileAndroidKeystoreException
import de.rfc1437.ironstorage.bridge.generated.MobilePrivateDirectories
import de.rfc1437.ironstorage.bridge.generated.MobilePrivateStorageErrorKind
import de.rfc1437.ironstorage.bridge.generated.MobilePrivateStorageFfiException
import de.rfc1437.ironstorage.bridge.generated.MobileSecretException
import de.rfc1437.ironstorage.bridge.generated.MobileSecretLocator
import de.rfc1437.ironstorage.bridge.generated.MobileSecretProtection
import org.junit.Assert.assertArrayEquals
import org.junit.Assert.assertEquals
import org.junit.Assert.assertThrows
import org.junit.Assert.assertTrue
import org.junit.Test
class KeystoreContractTest {
@Test fun rustOwnsRecordsPoliciesAndForeignSecretLifetime() {
val context = InstrumentationRegistry.getInstrumentation().targetContext
NativeRuntime(context).privateStorage(
MobilePrivateDirectories(
context.filesDir.canonicalPath,
context.noBackupFilesDir.canonicalPath,
context.cacheDir.canonicalPath
)
).use { runtime ->
val os = FixtureKeystore()
val backend = runtime.androidSecretBackend(os)
val locator = MobileSecretLocator.HttpsGitCredential("issue93-server", "fixture")
val protection = MobileSecretProtection.DEVICE_UNLOCKED
backend.unlock()
try {
backend.create(locator, protection, "public-token".toByteArray())
assertThrows(MobileSecretException.AlreadyExists::class.java) {
backend.create(locator, protection, "duplicate".toByteArray())
}
backend.replace(locator, protection, "replacement".toByteArray())
val clear = backend.retrieve(locator, protection)
try {
assertArrayEquals("replacement".toByteArray(), clear)
assertTrue(os.returned.all { it == 0.toByte() })
} finally {
clear.fill(0)
}
os.lockout = true
assertThrows(MobileSecretException.Lockout::class.java) {
backend.retrieve(locator, protection)
}
os.lockout = false
backend.lock()
assertThrows(MobileSecretException.Locked::class.java) {
backend.retrieve(locator, protection)
}
backend.unlock()
assertThrows(MobileSecretException.UnsupportedProtection::class.java) {
backend.create(
MobileSecretLocator.OpenPgpPassphrase("A".repeat(40)),
protection,
"public-passphrase".toByteArray()
)
}
} finally {
backend.delete(locator, protection)
}
assertTrue(os.keys.isEmpty())
os.corruptAliases = true
val error = assertThrows(MobilePrivateStorageFfiException.Failed::class.java) {
runtime.androidSecretBackend(os)
}
assertEquals(MobilePrivateStorageErrorKind.INVALID_SECURE_BINDING, error.kind)
}
}
// Public fixture cipher only: production encryption is tested in the platform test APK.
private class FixtureKeystore : MobileAndroidKeystore {
val keys = mutableSetOf<String>()
var returned = byteArrayOf()
var lockout = false
var corruptAliases = false
override fun deviceState() = MobileAndroidDeviceState(37u, false, true, true, false)
override fun createKey(specification: MobileAndroidKeySpecification) {
assertEquals(MobileAndroidAuthorization.NONE, specification.authorization)
keys.add(specification.alias)
}
override fun encrypt(
alias: String,
authorization: MobileAndroidAuthorization,
aad: ByteArray,
value: ByteArray
): MobileAndroidCiphertext = try {
MobileAndroidCiphertext(ByteArray(12), value.copyOf(value.size + 16))
} finally {
value.fill(0)
}
override fun decrypt(
alias: String,
authorization: MobileAndroidAuthorization,
aad: ByteArray,
value: MobileAndroidCiphertext
): ByteArray {
if (lockout) throw MobileAndroidKeystoreException.Lockout()
returned = value.bytes.copyOf(value.bytes.size - 16)
return returned
}
override fun deleteKey(alias: String) {
keys.remove(alias)
}
override fun aliases(): List<String> {
if (corruptAliases) throw MobileAndroidKeystoreException.Corrupted()
return keys.toList()
}
override fun cancelPending() = Unit
}
}