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,58 @@
package de.rfc1437.ironstorage.core
/** OS facts and cipher mechanics. Rust chooses policy, aliases and persistence. */
class KeystoreDeviceState(
val api: Int,
val locked: Boolean,
val secure: Boolean,
val strongBiometric: Boolean,
val strongbox: Boolean
)
enum class KeystoreAuthorization { None, StrongBiometric, UserPresence }
class KeystoreKeySpecification(
val alias: String,
val authorization: KeystoreAuthorization,
val unlockedDevice: Boolean,
val invalidateOnEnrollment: Boolean,
val strongbox: Boolean
)
class KeystoreCiphertext(val nonce: ByteArray, val bytes: ByteArray)
enum class KeystoreFailure {
MissingKey,
Invalidated,
Lockout,
Locked,
Denied,
Cancelled,
Corrupted,
UnsupportedProtection,
StrongboxUnavailable,
AlreadyExists,
Unavailable
}
class KeystoreException(val kind: KeystoreFailure) : Exception("Keystore operation failed")
interface AndroidKeystore {
fun deviceState(): KeystoreDeviceState
fun createKey(specification: KeystoreKeySpecification)
fun encrypt(
alias: String,
authorization: KeystoreAuthorization,
aad: ByteArray,
value: ByteArray
): KeystoreCiphertext
fun decrypt(
alias: String,
authorization: KeystoreAuthorization,
aad: ByteArray,
value: KeystoreCiphertext
): ByteArray
fun deleteKey(alias: String)
fun aliases(): List<String>
fun cancelPending()
}
@@ -0,0 +1,74 @@
package de.rfc1437.ironstorage.core
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
internal class KeystoreCallback(private val os: AndroidKeystore) : MobileAndroidKeystore {
override fun deviceState(): MobileAndroidDeviceState = call {
val s = os.deviceState()
MobileAndroidDeviceState(s.api.toUInt(), s.locked, s.secure, s.strongBiometric, s.strongbox)
}
override fun createKey(specification: MobileAndroidKeySpecification) = call {
os.createKey(
KeystoreKeySpecification(
specification.alias,
specification.authorization.toOs(),
specification.unlockedDevice,
specification.invalidateOnEnrollment,
specification.strongbox
)
)
}
override fun encrypt(
alias: String,
authorization: MobileAndroidAuthorization,
aad: ByteArray,
value: ByteArray
): MobileAndroidCiphertext = call {
try {
val result = os.encrypt(alias, authorization.toOs(), aad, value)
MobileAndroidCiphertext(result.nonce, result.bytes)
} finally {
value.fill(0)
}
}
override fun decrypt(
alias: String,
authorization: MobileAndroidAuthorization,
aad: ByteArray,
value: MobileAndroidCiphertext
): ByteArray = call {
os.decrypt(alias, authorization.toOs(), aad, KeystoreCiphertext(value.nonce, value.bytes))
}
override fun deleteKey(alias: String) = call { os.deleteKey(alias) }
override fun aliases(): List<String> = call { os.aliases() }
override fun cancelPending() = os.cancelPending()
}
private fun MobileAndroidAuthorization.toOs(): KeystoreAuthorization = when (this) {
MobileAndroidAuthorization.NONE -> KeystoreAuthorization.None
MobileAndroidAuthorization.STRONG_BIOMETRIC -> KeystoreAuthorization.StrongBiometric
MobileAndroidAuthorization.USER_PRESENCE -> KeystoreAuthorization.UserPresence
}
private inline fun <T> call(block: () -> T): T = try {
block()
} catch (error: KeystoreException) {
throw when (error.kind) {
KeystoreFailure.MissingKey -> MobileAndroidKeystoreException.MissingKey()
KeystoreFailure.Invalidated -> MobileAndroidKeystoreException.Invalidated()
KeystoreFailure.Lockout -> MobileAndroidKeystoreException.Lockout()
KeystoreFailure.Locked -> MobileAndroidKeystoreException.Locked()
KeystoreFailure.Denied -> MobileAndroidKeystoreException.Denied()
KeystoreFailure.Cancelled -> MobileAndroidKeystoreException.Cancelled()
KeystoreFailure.Corrupted -> MobileAndroidKeystoreException.Corrupted()
KeystoreFailure.UnsupportedProtection ->
MobileAndroidKeystoreException.UnsupportedProtection()
KeystoreFailure.StrongboxUnavailable ->
MobileAndroidKeystoreException.StrongboxUnavailable()
KeystoreFailure.AlreadyExists -> MobileAndroidKeystoreException.AlreadyExists()
KeystoreFailure.Unavailable -> MobileAndroidKeystoreException.Unavailable()
}
}
@@ -27,8 +27,13 @@ class NativeFixtureShellRepository internal constructor(
private val scope: CoroutineScope,
private val load: suspend (ShellState) -> ShellSnapshot
) : ShellRepository {
constructor(context: Context, scope: CoroutineScope, directories: () -> PrivateDirectories) :
this(scope, nativeLoader(context, directories))
constructor(
context: Context,
scope: CoroutineScope,
directories: () -> PrivateDirectories,
keystore: AndroidKeystore
) :
this(scope, nativeLoader(context, directories, keystore))
private val mutableSnapshot = MutableStateFlow(ShellSnapshot(ShellState.Loading, emptyList()))
override val snapshot: StateFlow<ShellSnapshot> = mutableSnapshot
private val revision = AtomicLong()
@@ -64,7 +69,8 @@ class NativeFixtureShellRepository internal constructor(
private fun nativeLoader(
context: Context,
directoryProvider: () -> PrivateDirectories
directoryProvider: () -> PrivateDirectories,
keystore: AndroidKeystore
): suspend (ShellState) -> ShellSnapshot {
val runtime = NativeRuntime(context)
var storage: MobilePrivateRuntime? = null
@@ -85,6 +91,8 @@ private fun nativeLoader(
)
storage = initialized
initialized.activate()
val secrets = initialized.androidSecretBackend(KeystoreCallback(keystore))
initialized.verifySecureStorage(secrets)
recovery = initialized.status().recovery?.toFacade()
initialized.shell()
} catch (error: MobilePrivateStorageFfiException.Failed) {