feat(android): implement private Rust storage runtime (#90)
Dependency security audit / rustsec (push) Failing after 23s

This commit is contained in:
hugo committed 2026-10-08 14:36:05 +02:00
1 parent 180851087b
commit 874c8139a4
36 files changed
+2845 -36

No files matched your search

Generated
+1
View File
@@ -5415,6 +5415,7 @@ dependencies = [
"reqwest 0.13.4", "reqwest 0.13.4",
"rqrr", "rqrr",
"russh", "russh",
"rustix 1.1.4",
"rustls", "rustls",
"secret-service", "secret-service",
"security-framework 3.7.0", "security-framework 3.7.0",
+1
View File
@@ -30,6 +30,7 @@ arboard = { version = "3.6", default-features = false, features = ["wayland-data
ashpd = { version = "0.13", default-features = false, features = ["file_chooser", "tokio"] } ashpd = { version = "0.13", default-features = false, features = ["file_chooser", "tokio"] }
cap-std = "4.0" cap-std = "4.0"
cap-tempfile = "4.0" cap-tempfile = "4.0"
rustix = { version = "1.1", features = ["fs"] }
clap = { version = "4.6", features = ["derive"] } clap = { version = "4.6", features = ["derive"] }
clap_complete = "4.6" clap_complete = "4.6"
crossterm = "0.29" crossterm = "0.29"
@@ -0,0 +1,42 @@
package de.rfc1437.ironstorage
import android.content.pm.ApplicationInfo
import androidx.test.platform.app.InstrumentationRegistry
import org.junit.Assert.assertEquals
import org.junit.Assert.assertFalse
import org.junit.Test
import org.xmlpull.v1.XmlPullParser
class BackupPolicyTest {
@Test
fun installedResourcesExcludeCredentialAndDeviceProtectedDomains() {
val context = InstrumentationRegistry.getInstrumentation().targetContext
assertEquals(0, context.applicationInfo.flags and ApplicationInfo.FLAG_ALLOW_BACKUP)
val domains = setOf(
"root", "file", "database", "sharedpref", "external",
"device_root", "device_file", "device_database", "device_sharedpref"
)
for ((resource, sections) in mapOf(
R.xml.backup_rules to setOf("full-backup-content"),
R.xml.data_extraction_rules to setOf("cloud-backup", "device-transfer")
)) {
context.resources.getXml(resource).use { xml ->
val excluded = mutableMapOf<String, MutableSet<String>>()
var section = ""
while (xml.eventType != XmlPullParser.END_DOCUMENT) {
if (xml.eventType == XmlPullParser.START_TAG) {
if (xml.name in sections) section = xml.name
assertFalse(xml.name == "include")
if (xml.name == "exclude") {
assertEquals(".", xml.getAttributeValue(null, "path"))
excluded.getOrPut(section) { mutableSetOf() }
.add(xml.getAttributeValue(null, "domain"))
}
}
xml.next()
}
assertEquals(sections.associateWith { domains }, excluded)
}
}
}
}
@@ -8,6 +8,7 @@ import dagger.hilt.android.qualifiers.ApplicationContext
import dagger.hilt.components.SingletonComponent import dagger.hilt.components.SingletonComponent
import de.rfc1437.ironstorage.core.NativeFixtureShellRepository import de.rfc1437.ironstorage.core.NativeFixtureShellRepository
import de.rfc1437.ironstorage.core.ShellRepository import de.rfc1437.ironstorage.core.ShellRepository
import de.rfc1437.ironstorage.platform.privateDirectories
import javax.inject.Singleton import javax.inject.Singleton
import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
@@ -21,6 +22,7 @@ object ShellModule {
fun shellRepository(@ApplicationContext context: Context): ShellRepository = fun shellRepository(@ApplicationContext context: Context): ShellRepository =
NativeFixtureShellRepository( NativeFixtureShellRepository(
context, context,
CoroutineScope(SupervisorJob() + Dispatchers.Main.immediate) CoroutineScope(SupervisorJob() + Dispatchers.Main.immediate),
{ privateDirectories(context) }
) )
} }
@@ -4,4 +4,8 @@
<exclude domain="database" path="." /> <exclude domain="database" path="." />
<exclude domain="sharedpref" path="." /> <exclude domain="sharedpref" path="." />
<exclude domain="external" path="." /> <exclude domain="external" path="." />
<exclude domain="device_root" path="." />
<exclude domain="device_file" path="." />
<exclude domain="device_database" path="." />
<exclude domain="device_sharedpref" path="." />
</full-backup-content> </full-backup-content>
@@ -5,6 +5,10 @@
<exclude domain="database" path="." /> <exclude domain="database" path="." />
<exclude domain="sharedpref" path="." /> <exclude domain="sharedpref" path="." />
<exclude domain="external" path="." /> <exclude domain="external" path="." />
<exclude domain="device_root" path="." />
<exclude domain="device_file" path="." />
<exclude domain="device_database" path="." />
<exclude domain="device_sharedpref" path="." />
</cloud-backup> </cloud-backup>
<device-transfer> <device-transfer>
<exclude domain="root" path="." /> <exclude domain="root" path="." />
@@ -12,5 +16,9 @@
<exclude domain="database" path="." /> <exclude domain="database" path="." />
<exclude domain="sharedpref" path="." /> <exclude domain="sharedpref" path="." />
<exclude domain="external" path="." /> <exclude domain="external" path="." />
<exclude domain="device_root" path="." />
<exclude domain="device_file" path="." />
<exclude domain="device_database" path="." />
<exclude domain="device_sharedpref" path="." />
</device-transfer> </device-transfer>
</data-extraction-rules> </data-extraction-rules>
@@ -5,8 +5,13 @@ import de.rfc1437.ironstorage.bridge.NativeBridgeException
import de.rfc1437.ironstorage.bridge.NativeFailureKind import de.rfc1437.ironstorage.bridge.NativeFailureKind
import de.rfc1437.ironstorage.bridge.NativeRuntime import de.rfc1437.ironstorage.bridge.NativeRuntime
import de.rfc1437.ironstorage.bridge.generated.MobileIcon import de.rfc1437.ironstorage.bridge.generated.MobileIcon
import de.rfc1437.ironstorage.bridge.generated.MobilePrivateDirectories
import de.rfc1437.ironstorage.bridge.generated.MobilePrivateRuntime
import de.rfc1437.ironstorage.bridge.generated.MobilePrivateStorageErrorKind
import de.rfc1437.ironstorage.bridge.generated.MobilePrivateStorageFfiException
import de.rfc1437.ironstorage.bridge.generated.MobileShellState import de.rfc1437.ironstorage.bridge.generated.MobileShellState
import de.rfc1437.ironstorage.bridge.generated.MobileTab import de.rfc1437.ironstorage.bridge.generated.MobileTab
import de.rfc1437.ironstorage.bridge.generated.mobilePrivateStorageFailure
import java.util.concurrent.atomic.AtomicLong import java.util.concurrent.atomic.AtomicLong
import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
@@ -17,12 +22,13 @@ import kotlinx.coroutines.flow.update
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext import kotlinx.coroutines.withContext
/** #92's native fixture integration; repository initialization is owned by #90. */ /** Rust-owned private initialization; debug controls can switch to explicit fixtures. */
class NativeFixtureShellRepository internal constructor( class NativeFixtureShellRepository internal constructor(
private val scope: CoroutineScope, private val scope: CoroutineScope,
private val load: suspend (ShellState) -> ShellSnapshot private val load: suspend (ShellState) -> ShellSnapshot
) : ShellRepository { ) : ShellRepository {
constructor(context: Context, scope: CoroutineScope) : this(scope, nativeLoader(context)) constructor(context: Context, scope: CoroutineScope, directories: () -> PrivateDirectories) :
this(scope, nativeLoader(context, directories))
private val mutableSnapshot = MutableStateFlow(ShellSnapshot(ShellState.Loading, emptyList())) private val mutableSnapshot = MutableStateFlow(ShellSnapshot(ShellState.Loading, emptyList()))
override val snapshot: StateFlow<ShellSnapshot> = mutableSnapshot override val snapshot: StateFlow<ShellSnapshot> = mutableSnapshot
private val revision = AtomicLong() private val revision = AtomicLong()
@@ -46,17 +52,52 @@ class NativeFixtureShellRepository internal constructor(
ShellSnapshot(ShellState.Error, emptyList(), nativeFailure = error.kind.toFacade()) ShellSnapshot(ShellState.Error, emptyList(), nativeFailure = error.kind.toFacade())
} }
if (revision.get() == operation) { if (revision.get() == operation) {
mutableSnapshot.update { result.copy(selectedTab = it.selectedTab) } mutableSnapshot.update {
result.copy(
selectedTab = if (it.pages.isEmpty()) result.selectedTab else it.selectedTab
)
}
} }
} }
} }
} }
private fun nativeLoader(context: Context): suspend (ShellState) -> ShellSnapshot { private fun nativeLoader(
context: Context,
directoryProvider: () -> PrivateDirectories
): suspend (ShellState) -> ShellSnapshot {
val runtime = NativeRuntime(context) val runtime = NativeRuntime(context)
var storage: MobilePrivateRuntime? = null
var firstLoad = true
return { state -> return { state ->
withContext(Dispatchers.IO) { withContext(Dispatchers.IO) {
val shell = runtime.fixture(state.toNative()) var recovery: StorageRecovery? = null
val shell = if (firstLoad) {
firstLoad = false
try {
val directories = directoryProvider()
val initialized = storage ?: runtime.privateStorage(
MobilePrivateDirectories(
directories.files,
directories.noBackup,
directories.cache
)
)
storage = initialized
initialized.activate()
recovery = initialized.status().recovery?.toFacade()
initialized.shell()
} catch (error: MobilePrivateStorageFfiException.Failed) {
recovery = error.kind.toFacade()
mobilePrivateStorageFailure(error.kind)
} catch (_: java.io.IOException) {
recovery = StorageRecovery.InvalidRoots
runtime.fixture(MobileShellState.ERROR)
mobilePrivateStorageFailure(MobilePrivateStorageErrorKind.INVALID_ROOTS)
}
} else {
runtime.fixture(state.toNative())
}
val pages = shell.pages.map { page -> val pages = shell.pages.map { page ->
ShellPage( ShellPage(
page.tab.toFacade(), page.tab.toFacade(),
@@ -68,11 +109,39 @@ private fun nativeLoader(context: Context): suspend (ShellState) -> ShellSnapsho
} }
val actual = shell.pages.firstOrNull()?.state val actual = shell.pages.firstOrNull()?.state
?: throw NativeBridgeException(NativeFailureKind.RustFailure) ?: throw NativeBridgeException(NativeFailureKind.RustFailure)
ShellSnapshot(actual.toFacade(), pages) ShellSnapshot(
actual.toFacade(),
pages,
selectedTab = shell.selectedTab.toFacade(),
storageRecovery = recovery
)
} }
} }
} }
private fun MobilePrivateStorageErrorKind.toFacade(): StorageRecovery = when (this) {
MobilePrivateStorageErrorKind.INVALID_ROOTS -> StorageRecovery.InvalidRoots
MobilePrivateStorageErrorKind.UNSAFE_PATH -> StorageRecovery.UnsafePath
MobilePrivateStorageErrorKind.MISSING_ROOT -> StorageRecovery.MissingRoot
MobilePrivateStorageErrorKind.ACCESS_DENIED -> StorageRecovery.AccessDenied
MobilePrivateStorageErrorKind.READ_ONLY -> StorageRecovery.ReadOnly
MobilePrivateStorageErrorKind.STORAGE_FULL -> StorageRecovery.StorageFull
MobilePrivateStorageErrorKind.BUSY -> StorageRecovery.Busy
MobilePrivateStorageErrorKind.IO -> StorageRecovery.Io
MobilePrivateStorageErrorKind.CORRUPT_LAYOUT -> StorageRecovery.CorruptLayout
MobilePrivateStorageErrorKind.MIGRATION_REQUIRED -> StorageRecovery.MigrationRequired
MobilePrivateStorageErrorKind.CORRUPT_CONFIGURATION -> StorageRecovery.CorruptConfiguration
MobilePrivateStorageErrorKind.CONFIGURATION_MISSING_WITH_DATA ->
StorageRecovery.ConfigurationMissingWithData
MobilePrivateStorageErrorKind.INTERRUPTED_SETUP -> StorageRecovery.InterruptedSetup
MobilePrivateStorageErrorKind.RESTORED_WITHOUT_KEYSTORE ->
StorageRecovery.RestoredWithoutKeystore
MobilePrivateStorageErrorKind.SECURE_STORAGE_LOCKED -> StorageRecovery.SecureStorageLocked
MobilePrivateStorageErrorKind.SECURE_STORAGE_UNAVAILABLE ->
StorageRecovery.SecureStorageUnavailable
MobilePrivateStorageErrorKind.INVALID_SECURE_BINDING -> StorageRecovery.InvalidSecureBinding
}
private fun MobileShellState.toFacade(): ShellState = when (this) { private fun MobileShellState.toFacade(): ShellState = when (this) {
MobileShellState.LOADING -> ShellState.Loading MobileShellState.LOADING -> ShellState.Loading
MobileShellState.EMPTY -> ShellState.Empty MobileShellState.EMPTY -> ShellState.Empty
@@ -0,0 +1,25 @@
package de.rfc1437.ironstorage.core
data class PrivateDirectories(val files: String, val noBackup: String, val cache: String) {
override fun toString(): String = "PrivateDirectories([REDACTED])"
}
enum class StorageRecovery {
InvalidRoots,
UnsafePath,
MissingRoot,
AccessDenied,
ReadOnly,
StorageFull,
Busy,
Io,
CorruptLayout,
MigrationRequired,
CorruptConfiguration,
ConfigurationMissingWithData,
InterruptedSetup,
RestoredWithoutKeystore,
SecureStorageLocked,
SecureStorageUnavailable,
InvalidSecureBinding
}
@@ -44,7 +44,8 @@ data class ShellSnapshot(
val state: ShellState, val state: ShellState,
val pages: List<ShellPage>, val pages: List<ShellPage>,
val selectedTab: ShellTab = ShellTab.Home, val selectedTab: ShellTab = ShellTab.Home,
val nativeFailure: NativeFailure? = null val nativeFailure: NativeFailure? = null,
val storageRecovery: StorageRecovery? = null
) )
enum class NativeFailure { enum class NativeFailure {
@@ -0,0 +1,11 @@
package de.rfc1437.ironstorage.platform
import android.content.Context
import de.rfc1437.ironstorage.core.PrivateDirectories
/** Only Context's OS directory facts; Rust owns all subordinate paths and files. */
fun privateDirectories(context: Context): PrivateDirectories = PrivateDirectories(
context.filesDir.canonicalPath,
context.noBackupFilesDir.canonicalPath,
context.cacheDir.canonicalPath
)
+1 -1
View File
@@ -31,7 +31,7 @@ kotlin {
} }
dependencies { dependencies {
implementation("${libs.jna.get().module}:${libs.versions.jna.get()}@aar") implementation(variantOf(libs.jna) { artifactType("aar") })
implementation(libs.annotation) implementation(libs.annotation)
testImplementation(libs.junit) testImplementation(libs.junit)
androidTestImplementation(libs.android.test.runner) androidTestImplementation(libs.android.test.runner)
@@ -0,0 +1,69 @@
package de.rfc1437.ironstorage.bridge
import androidx.test.platform.app.InstrumentationRegistry
import de.rfc1437.ironstorage.bridge.generated.MobileOnboardingFfiException
import de.rfc1437.ironstorage.bridge.generated.MobilePrivateDirectories
import de.rfc1437.ironstorage.bridge.generated.MobilePrivateStorageFfiException
import de.rfc1437.ironstorage.bridge.generated.MobileShellState
import de.rfc1437.ironstorage.bridge.generated.MobileTab
import de.rfc1437.ironstorage.bridge.generated.mobileLocalOnboardingOperation
import de.rfc1437.ironstorage.bridge.generated.setSelectedMobileTab
import org.junit.Assert.assertEquals
import org.junit.Assert.assertFalse
import org.junit.Assert.assertTrue
import org.junit.Test
class PrivateStorageTest {
@Test
fun canonicalSandboxInitializationAndRecreationKeepDurableLayout() {
val context = InstrumentationRegistry.getInstrumentation().targetContext
val directories = MobilePrivateDirectories(
context.filesDir.canonicalPath,
context.noBackupFilesDir.canonicalPath,
context.cacheDir.canonicalPath
)
val native = NativeRuntime(context)
val initialized = try {
native.privateStorage(directories)
} catch (error: MobilePrivateStorageFfiException.Failed) {
throw AssertionError("Private initialization failed: ${error.kind}", error)
}
initialized.use { first ->
assertEquals(1u, first.status().version)
assertEquals(null, first.status().recovery)
val configured = first.status().configured
assertEquals(
if (configured) MobileShellState.READY else MobileShellState.EMPTY,
first.shell().pages.first().state
)
val layout = first.layout()
assertEquals("${context.noBackupFilesDir.canonicalPath}/ironstorage", layout.data)
assertEquals("${layout.data}/totp-catalog.toml", layout.catalog)
assertEquals("${context.cacheDir.canonicalPath}/ironstorage", layout.cache)
first.activate()
if (!configured) {
mobileLocalOnboardingOperation().use { onboarding ->
try {
onboarding.generate(
"Sandbox fixture <fixture@example.test>",
"public-test-passphrase",
false
)
} catch (error: MobileOnboardingFfiException.Failed) {
throw AssertionError("Private onboarding failed: ${error.kind}", error)
}
}
}
assertTrue(first.status().configured)
setSelectedMobileTab(MobileTab.PREFERENCES)
assertEquals(MobileTab.PREFERENCES, first.shell().selectedTab)
assertEquals(MobileShellState.READY, first.shell().pages.first().state)
first.clearDisposableCache()
native.privateStorage(directories).use { recreated ->
assertEquals(layout, recreated.layout())
assertEquals(first.status(), recreated.status())
}
assertFalse(layout.toString().contains(context.noBackupFilesDir.path))
}
}
}
@@ -3,9 +3,13 @@ package de.rfc1437.ironstorage.bridge
import android.content.Context import android.content.Context
import android.os.Build import android.os.Build
import android.os.Looper import android.os.Looper
import de.rfc1437.ironstorage.bridge.generated.MobilePrivateDirectories
import de.rfc1437.ironstorage.bridge.generated.MobilePrivateRuntime
import de.rfc1437.ironstorage.bridge.generated.MobilePrivateStorageFfiException
import de.rfc1437.ironstorage.bridge.generated.MobileShell import de.rfc1437.ironstorage.bridge.generated.MobileShell
import de.rfc1437.ironstorage.bridge.generated.MobileShellState import de.rfc1437.ironstorage.bridge.generated.MobileShellState
import de.rfc1437.ironstorage.bridge.generated.mobileShellFixture import de.rfc1437.ironstorage.bridge.generated.mobileShellFixture
import de.rfc1437.ironstorage.bridge.generated.openMobilePrivateRuntime
import de.rfc1437.ironstorage.bridge.generated.uniffiEnsureInitialized import de.rfc1437.ironstorage.bridge.generated.uniffiEnsureInitialized
import java.io.File import java.io.File
import java.io.IOException import java.io.IOException
@@ -24,6 +28,11 @@ class NativeRuntime(context: Context) {
mobileShellFixture(state) mobileShellFixture(state)
} }
fun privateStorage(directories: MobilePrivateDirectories): MobilePrivateRuntime = call {
initialize()
openMobilePrivateRuntime(directories)
}
private fun initialize() = synchronized(lock) { private fun initialize() = synchronized(lock) {
initializationFailure?.let { throw NativeBridgeException(it) } initializationFailure?.let { throw NativeBridgeException(it) }
if (!initialized) { if (!initialized) {
@@ -51,6 +60,8 @@ class NativeRuntime(context: Context) {
fail(error.kind) fail(error.kind)
} catch (error: CancellationException) { } catch (error: CancellationException) {
throw error throw error
} catch (error: MobilePrivateStorageFfiException) {
throw error
} catch (error: NativeBindingMismatchException) { } catch (error: NativeBindingMismatchException) {
fail(error.kind) fail(error.kind)
} catch (_: IOException) { } catch (_: IOException) {
+727 -6
View File
@@ -2524,6 +2524,189 @@ public func FfiConverterTypeMobileOnboardingOperation_lower(_ value: MobileOnboa
public protocol MobilePrivateRuntimeProtocol: AnyObject, Sendable {
func activate() throws
func authentication(backend: MobileSecretBackend, clock: MobileClock) throws -> MobileAuthentication
func clearDisposableCache() throws
func layout() -> MobilePrivateStorageLayout
func shell() -> MobileShell
func status() -> MobilePrivateStorageStatus
func verifySecureStorage(backend: MobileSecretBackend) throws
}
open class MobilePrivateRuntime: MobilePrivateRuntimeProtocol, @unchecked Sendable {
fileprivate let handle: UInt64
/// Used to instantiate a [FFIObject] without an actual handle, for fakes in tests, mostly.
#if swift(>=5.8)
@_documentation(visibility: private)
#endif
public struct NoHandle {
public init() {}
}
// TODO: We'd like this to be `private` but for Swifty reasons,
// we can't implement `FfiConverter` without making this `required` and we can't
// make it `required` without making it `public`.
#if swift(>=5.8)
@_documentation(visibility: private)
#endif
required public init(unsafeFromHandle handle: UInt64) {
self.handle = handle
}
// This constructor can be used to instantiate a fake object.
// - Parameter noHandle: Placeholder value so we can have a constructor separate from the default empty one that may be implemented for classes extending [FFIObject].
//
// - Warning:
// Any object instantiated with this constructor cannot be passed to an actual Rust-backed object. Since there isn't a backing handle the FFI lower functions will crash.
#if swift(>=5.8)
@_documentation(visibility: private)
#endif
public init(noHandle: NoHandle) {
self.handle = 0
}
#if swift(>=5.8)
@_documentation(visibility: private)
#endif
public func uniffiCloneHandle() -> UInt64 {
return try! rustCall { uniffi_ironstorage_mobile_fn_clone_mobileprivateruntime(self.handle, $0) }
}
// No primary constructor declared for this class.
deinit {
if handle == 0 {
// Mock objects have handle=0 don't try to free them
return
}
try! rustCall { uniffi_ironstorage_mobile_fn_free_mobileprivateruntime(handle, $0) }
}
open func activate()throws {try rustCallWithError(FfiConverterTypeMobilePrivateStorageFfiError_lift) {
uniffiCallStatus in
uniffi_ironstorage_mobile_fn_method_mobileprivateruntime_activate(
self.uniffiCloneHandle(),uniffiCallStatus
)
}
}
open func authentication(backend: MobileSecretBackend, clock: MobileClock)throws -> MobileAuthentication {
return try FfiConverterTypeMobileAuthentication_lift(try rustCallWithError(FfiConverterTypeMobileAuthenticationFfiError_lift) {
uniffiCallStatus in
uniffi_ironstorage_mobile_fn_method_mobileprivateruntime_authentication(
self.uniffiCloneHandle(),
FfiConverterTypeMobileSecretBackend_lower(backend),
FfiConverterTypeMobileClock_lower(clock),uniffiCallStatus
)
})
}
open func clearDisposableCache()throws {try rustCallWithError(FfiConverterTypeMobilePrivateStorageFfiError_lift) {
uniffiCallStatus in
uniffi_ironstorage_mobile_fn_method_mobileprivateruntime_clear_disposable_cache(
self.uniffiCloneHandle(),uniffiCallStatus
)
}
}
open func layout() -> MobilePrivateStorageLayout {
return try! FfiConverterTypeMobilePrivateStorageLayout_lift(try! rustCall() {
uniffiCallStatus in
uniffi_ironstorage_mobile_fn_method_mobileprivateruntime_layout(
self.uniffiCloneHandle(),uniffiCallStatus
)
})
}
open func shell() -> MobileShell {
return try! FfiConverterTypeMobileShell_lift(try! rustCall() {
uniffiCallStatus in
uniffi_ironstorage_mobile_fn_method_mobileprivateruntime_shell(
self.uniffiCloneHandle(),uniffiCallStatus
)
})
}
open func status() -> MobilePrivateStorageStatus {
return try! FfiConverterTypeMobilePrivateStorageStatus_lift(try! rustCall() {
uniffiCallStatus in
uniffi_ironstorage_mobile_fn_method_mobileprivateruntime_status(
self.uniffiCloneHandle(),uniffiCallStatus
)
})
}
open func verifySecureStorage(backend: MobileSecretBackend)throws {try rustCallWithError(FfiConverterTypeMobilePrivateStorageFfiError_lift) {
uniffiCallStatus in
uniffi_ironstorage_mobile_fn_method_mobileprivateruntime_verify_secure_storage(
self.uniffiCloneHandle(),
FfiConverterTypeMobileSecretBackend_lower(backend),uniffiCallStatus
)
}
}
}
#if swift(>=5.8)
@_documentation(visibility: private)
#endif
public struct FfiConverterTypeMobilePrivateRuntime: FfiConverter {
typealias FfiType = UInt64
typealias SwiftType = MobilePrivateRuntime
public static func lift(_ handle: UInt64) throws -> MobilePrivateRuntime {
return MobilePrivateRuntime(unsafeFromHandle: handle)
}
public static func lower(_ value: MobilePrivateRuntime) -> UInt64 {
return value.uniffiCloneHandle()
}
public static func read(from buf: inout (data: Data, offset: Data.Index)) throws -> MobilePrivateRuntime {
let handle: UInt64 = try readInt(&buf)
return try lift(handle)
}
public static func write(_ value: MobilePrivateRuntime, into buf: inout [UInt8]) {
writeInt(&buf, lower(value))
}
}
#if swift(>=5.8)
@_documentation(visibility: private)
#endif
public func FfiConverterTypeMobilePrivateRuntime_lift(_ handle: UInt64) throws -> MobilePrivateRuntime {
return try FfiConverterTypeMobilePrivateRuntime.lift(handle)
}
#if swift(>=5.8)
@_documentation(visibility: private)
#endif
public func FfiConverterTypeMobilePrivateRuntime_lower(_ value: MobilePrivateRuntime) -> UInt64 {
return FfiConverterTypeMobilePrivateRuntime.lower(value)
}
/** /**
* Receives storage-encoded opaque bytes; cannot select accounts, leases or protection. * Receives storage-encoded opaque bytes; cannot select accounts, leases or protection.
*/ */
@@ -5519,6 +5702,208 @@ public func FfiConverterTypeMobilePreferences_lower(_ value: MobilePreferences)
} }
public struct MobilePrivateDirectories: Equatable, Hashable {
public var files: String
public var noBackup: String
public var cache: String
// Default memberwise initializers are never public by default, so we
// declare one manually.
public init(files: String, noBackup: String, cache: String) {
self.files = files
self.noBackup = noBackup
self.cache = cache
}
}
#if compiler(>=6)
extension MobilePrivateDirectories: Sendable {}
#endif
#if swift(>=5.8)
@_documentation(visibility: private)
#endif
public struct FfiConverterTypeMobilePrivateDirectories: FfiConverterRustBuffer {
public static func read(from buf: inout (data: Data, offset: Data.Index)) throws -> MobilePrivateDirectories {
return
try MobilePrivateDirectories(
files: FfiConverterString.read(from: &buf),
noBackup: FfiConverterString.read(from: &buf),
cache: FfiConverterString.read(from: &buf)
)
}
public static func write(_ value: MobilePrivateDirectories, into buf: inout [UInt8]) {
FfiConverterString.write(value.files, into: &buf)
FfiConverterString.write(value.noBackup, into: &buf)
FfiConverterString.write(value.cache, into: &buf)
}
}
#if swift(>=5.8)
@_documentation(visibility: private)
#endif
public func FfiConverterTypeMobilePrivateDirectories_lift(_ buf: RustBuffer) throws -> MobilePrivateDirectories {
return try FfiConverterTypeMobilePrivateDirectories.lift(buf)
}
#if swift(>=5.8)
@_documentation(visibility: private)
#endif
public func FfiConverterTypeMobilePrivateDirectories_lower(_ value: MobilePrivateDirectories) -> RustBuffer {
return FfiConverterTypeMobilePrivateDirectories.lower(value)
}
public struct MobilePrivateStorageLayout: Equatable, Hashable {
public var data: String
public var configuration: String
public var vault: String
public var keys: String
public var catalog: String
public var scratch: String
public var cache: String
// Default memberwise initializers are never public by default, so we
// declare one manually.
public init(data: String, configuration: String, vault: String, keys: String, catalog: String, scratch: String, cache: String) {
self.data = data
self.configuration = configuration
self.vault = vault
self.keys = keys
self.catalog = catalog
self.scratch = scratch
self.cache = cache
}
}
#if compiler(>=6)
extension MobilePrivateStorageLayout: Sendable {}
#endif
#if swift(>=5.8)
@_documentation(visibility: private)
#endif
public struct FfiConverterTypeMobilePrivateStorageLayout: FfiConverterRustBuffer {
public static func read(from buf: inout (data: Data, offset: Data.Index)) throws -> MobilePrivateStorageLayout {
return
try MobilePrivateStorageLayout(
data: FfiConverterString.read(from: &buf),
configuration: FfiConverterString.read(from: &buf),
vault: FfiConverterString.read(from: &buf),
keys: FfiConverterString.read(from: &buf),
catalog: FfiConverterString.read(from: &buf),
scratch: FfiConverterString.read(from: &buf),
cache: FfiConverterString.read(from: &buf)
)
}
public static func write(_ value: MobilePrivateStorageLayout, into buf: inout [UInt8]) {
FfiConverterString.write(value.data, into: &buf)
FfiConverterString.write(value.configuration, into: &buf)
FfiConverterString.write(value.vault, into: &buf)
FfiConverterString.write(value.keys, into: &buf)
FfiConverterString.write(value.catalog, into: &buf)
FfiConverterString.write(value.scratch, into: &buf)
FfiConverterString.write(value.cache, into: &buf)
}
}
#if swift(>=5.8)
@_documentation(visibility: private)
#endif
public func FfiConverterTypeMobilePrivateStorageLayout_lift(_ buf: RustBuffer) throws -> MobilePrivateStorageLayout {
return try FfiConverterTypeMobilePrivateStorageLayout.lift(buf)
}
#if swift(>=5.8)
@_documentation(visibility: private)
#endif
public func FfiConverterTypeMobilePrivateStorageLayout_lower(_ value: MobilePrivateStorageLayout) -> RustBuffer {
return FfiConverterTypeMobilePrivateStorageLayout.lower(value)
}
public struct MobilePrivateStorageStatus: Equatable, Hashable {
public var version: UInt32
public var configured: Bool
public var recoveredTemporaryFiles: Bool
public var recoveredCatalog: Bool
public var secureBindingPresent: Bool
public var recovery: MobilePrivateStorageErrorKind?
// Default memberwise initializers are never public by default, so we
// declare one manually.
public init(version: UInt32, configured: Bool, recoveredTemporaryFiles: Bool, recoveredCatalog: Bool, secureBindingPresent: Bool, recovery: MobilePrivateStorageErrorKind?) {
self.version = version
self.configured = configured
self.recoveredTemporaryFiles = recoveredTemporaryFiles
self.recoveredCatalog = recoveredCatalog
self.secureBindingPresent = secureBindingPresent
self.recovery = recovery
}
}
#if compiler(>=6)
extension MobilePrivateStorageStatus: Sendable {}
#endif
#if swift(>=5.8)
@_documentation(visibility: private)
#endif
public struct FfiConverterTypeMobilePrivateStorageStatus: FfiConverterRustBuffer {
public static func read(from buf: inout (data: Data, offset: Data.Index)) throws -> MobilePrivateStorageStatus {
return
try MobilePrivateStorageStatus(
version: FfiConverterUInt32.read(from: &buf),
configured: FfiConverterBool.read(from: &buf),
recoveredTemporaryFiles: FfiConverterBool.read(from: &buf),
recoveredCatalog: FfiConverterBool.read(from: &buf),
secureBindingPresent: FfiConverterBool.read(from: &buf),
recovery: FfiConverterOptionTypeMobilePrivateStorageErrorKind.read(from: &buf)
)
}
public static func write(_ value: MobilePrivateStorageStatus, into buf: inout [UInt8]) {
FfiConverterUInt32.write(value.version, into: &buf)
FfiConverterBool.write(value.configured, into: &buf)
FfiConverterBool.write(value.recoveredTemporaryFiles, into: &buf)
FfiConverterBool.write(value.recoveredCatalog, into: &buf)
FfiConverterBool.write(value.secureBindingPresent, into: &buf)
FfiConverterOptionTypeMobilePrivateStorageErrorKind.write(value.recovery, into: &buf)
}
}
#if swift(>=5.8)
@_documentation(visibility: private)
#endif
public func FfiConverterTypeMobilePrivateStorageStatus_lift(_ buf: RustBuffer) throws -> MobilePrivateStorageStatus {
return try FfiConverterTypeMobilePrivateStorageStatus.lift(buf)
}
#if swift(>=5.8)
@_documentation(visibility: private)
#endif
public func FfiConverterTypeMobilePrivateStorageStatus_lower(_ value: MobilePrivateStorageStatus) -> RustBuffer {
return FfiConverterTypeMobilePrivateStorageStatus.lower(value)
}
public struct MobileShell: Equatable, Hashable { public struct MobileShell: Equatable, Hashable {
public var selectedTab: MobileTab public var selectedTab: MobileTab
public var pages: [MobilePage] public var pages: [MobilePage]
@@ -8521,6 +8906,252 @@ public func FfiConverterTypeMobilePreferenceError_lower(_ value: MobilePreferenc
} }
public enum MobilePrivateStorageErrorKind: Equatable, Hashable {
case invalidRoots
case unsafePath
case missingRoot
case accessDenied
case readOnly
case storageFull
case busy
case io
case corruptLayout
case migrationRequired
case corruptConfiguration
case configurationMissingWithData
case interruptedSetup
case restoredWithoutKeystore
case secureStorageLocked
case secureStorageUnavailable
case invalidSecureBinding
}
#if compiler(>=6)
extension MobilePrivateStorageErrorKind: Sendable {}
#endif
#if swift(>=5.8)
@_documentation(visibility: private)
#endif
public struct FfiConverterTypeMobilePrivateStorageErrorKind: FfiConverterRustBuffer {
typealias SwiftType = MobilePrivateStorageErrorKind
public static func read(from buf: inout (data: Data, offset: Data.Index)) throws -> MobilePrivateStorageErrorKind {
let variant: Int32 = try readInt(&buf)
switch variant {
case 1: return .invalidRoots
case 2: return .unsafePath
case 3: return .missingRoot
case 4: return .accessDenied
case 5: return .readOnly
case 6: return .storageFull
case 7: return .busy
case 8: return .io
case 9: return .corruptLayout
case 10: return .migrationRequired
case 11: return .corruptConfiguration
case 12: return .configurationMissingWithData
case 13: return .interruptedSetup
case 14: return .restoredWithoutKeystore
case 15: return .secureStorageLocked
case 16: return .secureStorageUnavailable
case 17: return .invalidSecureBinding
default: throw UniffiInternalError.unexpectedEnumCase
}
}
public static func write(_ value: MobilePrivateStorageErrorKind, into buf: inout [UInt8]) {
switch value {
case .invalidRoots:
writeInt(&buf, Int32(1))
case .unsafePath:
writeInt(&buf, Int32(2))
case .missingRoot:
writeInt(&buf, Int32(3))
case .accessDenied:
writeInt(&buf, Int32(4))
case .readOnly:
writeInt(&buf, Int32(5))
case .storageFull:
writeInt(&buf, Int32(6))
case .busy:
writeInt(&buf, Int32(7))
case .io:
writeInt(&buf, Int32(8))
case .corruptLayout:
writeInt(&buf, Int32(9))
case .migrationRequired:
writeInt(&buf, Int32(10))
case .corruptConfiguration:
writeInt(&buf, Int32(11))
case .configurationMissingWithData:
writeInt(&buf, Int32(12))
case .interruptedSetup:
writeInt(&buf, Int32(13))
case .restoredWithoutKeystore:
writeInt(&buf, Int32(14))
case .secureStorageLocked:
writeInt(&buf, Int32(15))
case .secureStorageUnavailable:
writeInt(&buf, Int32(16))
case .invalidSecureBinding:
writeInt(&buf, Int32(17))
}
}
}
#if swift(>=5.8)
@_documentation(visibility: private)
#endif
public func FfiConverterTypeMobilePrivateStorageErrorKind_lift(_ buf: RustBuffer) throws -> MobilePrivateStorageErrorKind {
return try FfiConverterTypeMobilePrivateStorageErrorKind.lift(buf)
}
#if swift(>=5.8)
@_documentation(visibility: private)
#endif
public func FfiConverterTypeMobilePrivateStorageErrorKind_lower(_ value: MobilePrivateStorageErrorKind) -> RustBuffer {
return FfiConverterTypeMobilePrivateStorageErrorKind.lower(value)
}
public
enum MobilePrivateStorageFfiError: Swift.Error, Equatable, Hashable, Foundation.LocalizedError {
case Failed(kind: MobilePrivateStorageErrorKind
)
public var errorDescription: String? {
String(reflecting: self)
}
}
#if compiler(>=6)
extension MobilePrivateStorageFfiError: Sendable {}
#endif
#if swift(>=5.8)
@_documentation(visibility: private)
#endif
public struct FfiConverterTypeMobilePrivateStorageFfiError: FfiConverterRustBuffer {
typealias SwiftType = MobilePrivateStorageFfiError
public static func read(from buf: inout (data: Data, offset: Data.Index)) throws -> MobilePrivateStorageFfiError {
let variant: Int32 = try readInt(&buf)
switch variant {
case 1: return .Failed(
kind: try FfiConverterTypeMobilePrivateStorageErrorKind.read(from: &buf)
)
default: throw UniffiInternalError.unexpectedEnumCase
}
}
public static func write(_ value: MobilePrivateStorageFfiError, into buf: inout [UInt8]) {
switch value {
case let .Failed(kind):
writeInt(&buf, Int32(1))
FfiConverterTypeMobilePrivateStorageErrorKind.write(kind, into: &buf)
}
}
}
#if swift(>=5.8)
@_documentation(visibility: private)
#endif
public func FfiConverterTypeMobilePrivateStorageFfiError_lift(_ buf: RustBuffer) throws -> MobilePrivateStorageFfiError {
return try FfiConverterTypeMobilePrivateStorageFfiError.lift(buf)
}
#if swift(>=5.8)
@_documentation(visibility: private)
#endif
public func FfiConverterTypeMobilePrivateStorageFfiError_lower(_ value: MobilePrivateStorageFfiError) -> RustBuffer {
return FfiConverterTypeMobilePrivateStorageFfiError.lower(value)
}
public public
enum MobileSecretError: Swift.Error, Equatable, Hashable, Foundation.LocalizedError { enum MobileSecretError: Swift.Error, Equatable, Hashable, Foundation.LocalizedError {
@@ -8637,6 +9268,7 @@ public func FfiConverterTypeMobileSecretError_lower(_ value: MobileSecretError)
public enum MobileSecretLocator: Equatable, Hashable { public enum MobileSecretLocator: Equatable, Hashable {
case applicationBinding
case openPgpPassphrase(fingerprint: String case openPgpPassphrase(fingerprint: String
) )
case httpsGitCredential(serverId: String, applicationId: String case httpsGitCredential(serverId: String, applicationId: String
@@ -8664,13 +9296,15 @@ public struct FfiConverterTypeMobileSecretLocator: FfiConverterRustBuffer {
let variant: Int32 = try readInt(&buf) let variant: Int32 = try readInt(&buf)
switch variant { switch variant {
case 1: return .openPgpPassphrase(fingerprint: try FfiConverterString.read(from: &buf) case 1: return .applicationBinding
case 2: return .openPgpPassphrase(fingerprint: try FfiConverterString.read(from: &buf)
) )
case 2: return .httpsGitCredential(serverId: try FfiConverterString.read(from: &buf), applicationId: try FfiConverterString.read(from: &buf) case 3: return .httpsGitCredential(serverId: try FfiConverterString.read(from: &buf), applicationId: try FfiConverterString.read(from: &buf)
) )
case 3: return .sshKeyPassphrase(fingerprint: try FfiConverterString.read(from: &buf) case 4: return .sshKeyPassphrase(fingerprint: try FfiConverterString.read(from: &buf)
) )
default: throw UniffiInternalError.unexpectedEnumCase default: throw UniffiInternalError.unexpectedEnumCase
@@ -8681,19 +9315,23 @@ public struct FfiConverterTypeMobileSecretLocator: FfiConverterRustBuffer {
switch value { switch value {
case let .openPgpPassphrase(fingerprint): case .applicationBinding:
writeInt(&buf, Int32(1)) writeInt(&buf, Int32(1))
case let .openPgpPassphrase(fingerprint):
writeInt(&buf, Int32(2))
FfiConverterString.write(fingerprint, into: &buf) FfiConverterString.write(fingerprint, into: &buf)
case let .httpsGitCredential(serverId,applicationId): case let .httpsGitCredential(serverId,applicationId):
writeInt(&buf, Int32(2)) writeInt(&buf, Int32(3))
FfiConverterString.write(serverId, into: &buf) FfiConverterString.write(serverId, into: &buf)
FfiConverterString.write(applicationId, into: &buf) FfiConverterString.write(applicationId, into: &buf)
case let .sshKeyPassphrase(fingerprint): case let .sshKeyPassphrase(fingerprint):
writeInt(&buf, Int32(3)) writeInt(&buf, Int32(4))
FfiConverterString.write(fingerprint, into: &buf) FfiConverterString.write(fingerprint, into: &buf)
} }
@@ -9492,6 +10130,30 @@ fileprivate struct FfiConverterOptionTypeMobileHomeActionKind: FfiConverterRustB
} }
} }
#if swift(>=5.8)
@_documentation(visibility: private)
#endif
fileprivate struct FfiConverterOptionTypeMobilePrivateStorageErrorKind: FfiConverterRustBuffer {
typealias SwiftType = MobilePrivateStorageErrorKind?
public static func write(_ value: SwiftType, into buf: inout [UInt8]) {
guard let value = value else {
writeInt(&buf, Int8(0))
return
}
writeInt(&buf, Int8(1))
FfiConverterTypeMobilePrivateStorageErrorKind.write(value, into: &buf)
}
public static func read(from buf: inout (data: Data, offset: Data.Index)) throws -> SwiftType {
switch try readInt(&buf) as Int8 {
case 0: return nil
case 1: return try FfiConverterTypeMobilePrivateStorageErrorKind.read(from: &buf)
default: throw UniffiInternalError.unexpectedOptionalTag
}
}
}
#if swift(>=5.8) #if swift(>=5.8)
@_documentation(visibility: private) @_documentation(visibility: private)
#endif #endif
@@ -10016,6 +10678,22 @@ public func validateMobileCredentialRequest(request: MobileCredentialRequest)thr
) )
}) })
} }
public func mobilePrivateStorageFailure(error: MobilePrivateStorageErrorKind) -> MobileShell {
return try! FfiConverterTypeMobileShell_lift(try! rustCall() {
uniffiCallStatus in
uniffi_ironstorage_mobile_fn_func_mobile_private_storage_failure(
FfiConverterTypeMobilePrivateStorageErrorKind_lower(error),uniffiCallStatus
)
})
}
public func openMobilePrivateRuntime(directories: MobilePrivateDirectories)throws -> MobilePrivateRuntime {
return try FfiConverterTypeMobilePrivateRuntime_lift(try rustCallWithError(FfiConverterTypeMobilePrivateStorageFfiError_lift) {
uniffiCallStatus in
uniffi_ironstorage_mobile_fn_func_open_mobile_private_runtime(
FfiConverterTypeMobilePrivateDirectories_lower(directories),uniffiCallStatus
)
})
}
private enum InitializationResult { private enum InitializationResult {
case ok case ok
@@ -10089,6 +10767,12 @@ private let initializationResult: InitializationResult = {
if (uniffi_ironstorage_mobile_checksum_func_validate_mobile_credential_request() != 60738) { if (uniffi_ironstorage_mobile_checksum_func_validate_mobile_credential_request() != 60738) {
return InitializationResult.apiChecksumMismatch return InitializationResult.apiChecksumMismatch
} }
if (uniffi_ironstorage_mobile_checksum_func_mobile_private_storage_failure() != 38666) {
return InitializationResult.apiChecksumMismatch
}
if (uniffi_ironstorage_mobile_checksum_func_open_mobile_private_runtime() != 27207) {
return InitializationResult.apiChecksumMismatch
}
if (uniffi_ironstorage_mobile_checksum_method_mobileauthentication_acknowledge_watch_snapshot() != 21391) { if (uniffi_ironstorage_mobile_checksum_method_mobileauthentication_acknowledge_watch_snapshot() != 21391) {
return InitializationResult.apiChecksumMismatch return InitializationResult.apiChecksumMismatch
} }
@@ -10323,6 +11007,27 @@ private let initializationResult: InitializationResult = {
if (uniffi_ironstorage_mobile_checksum_method_mobilesecretbackend_unlock() != 2737) { if (uniffi_ironstorage_mobile_checksum_method_mobilesecretbackend_unlock() != 2737) {
return InitializationResult.apiChecksumMismatch return InitializationResult.apiChecksumMismatch
} }
if (uniffi_ironstorage_mobile_checksum_method_mobileprivateruntime_activate() != 64835) {
return InitializationResult.apiChecksumMismatch
}
if (uniffi_ironstorage_mobile_checksum_method_mobileprivateruntime_authentication() != 21237) {
return InitializationResult.apiChecksumMismatch
}
if (uniffi_ironstorage_mobile_checksum_method_mobileprivateruntime_clear_disposable_cache() != 35831) {
return InitializationResult.apiChecksumMismatch
}
if (uniffi_ironstorage_mobile_checksum_method_mobileprivateruntime_layout() != 33040) {
return InitializationResult.apiChecksumMismatch
}
if (uniffi_ironstorage_mobile_checksum_method_mobileprivateruntime_shell() != 42966) {
return InitializationResult.apiChecksumMismatch
}
if (uniffi_ironstorage_mobile_checksum_method_mobileprivateruntime_status() != 46293) {
return InitializationResult.apiChecksumMismatch
}
if (uniffi_ironstorage_mobile_checksum_method_mobileprivateruntime_verify_secure_storage() != 12909) {
return InitializationResult.apiChecksumMismatch
}
uniffiCallbackInitMobileClipboard() uniffiCallbackInitMobileClipboard()
uniffiCallbackInitMobileClock() uniffiCallbackInitMobileClock()
@@ -10529,6 +11234,22 @@ extension MobilePreferences: CustomStringConvertible, CustomDebugStringConvertib
public var description: String { "MobilePreferences([REDACTED])" } public var description: String { "MobilePreferences([REDACTED])" }
public var debugDescription: String { description } public var debugDescription: String { description }
} }
extension MobilePrivateDirectories: CustomStringConvertible, CustomDebugStringConvertible {
public var description: String { "MobilePrivateDirectories([REDACTED])" }
public var debugDescription: String { description }
}
extension MobilePrivateStorageFfiError: CustomStringConvertible, CustomDebugStringConvertible {
public var description: String { "MobilePrivateStorageFfiError([REDACTED])" }
public var debugDescription: String { description }
}
extension MobilePrivateStorageLayout: CustomStringConvertible, CustomDebugStringConvertible {
public var description: String { "MobilePrivateStorageLayout([REDACTED])" }
public var debugDescription: String { description }
}
extension MobilePrivateStorageStatus: CustomStringConvertible, CustomDebugStringConvertible {
public var description: String { "MobilePrivateStorageStatus([REDACTED])" }
public var debugDescription: String { description }
}
extension MobileSecretError: CustomStringConvertible, CustomDebugStringConvertible { extension MobileSecretError: CustomStringConvertible, CustomDebugStringConvertible {
public var description: String { "MobileSecretError([REDACTED])" } public var description: String { "MobileSecretError([REDACTED])" }
public var debugDescription: String { description } public var debugDescription: String { description }
+109
View File
@@ -860,6 +860,51 @@ void uniffi_ironstorage_mobile_fn_method_mobilesecretbackend_lock(uint64_t ptr,
void uniffi_ironstorage_mobile_fn_method_mobilesecretbackend_unlock(uint64_t ptr, RustCallStatus *_Nonnull out_status void uniffi_ironstorage_mobile_fn_method_mobilesecretbackend_unlock(uint64_t ptr, RustCallStatus *_Nonnull out_status
); );
#endif #endif
#ifndef UNIFFI_FFIDEF_UNIFFI_IRONSTORAGE_MOBILE_FN_CLONE_MOBILEPRIVATERUNTIME
#define UNIFFI_FFIDEF_UNIFFI_IRONSTORAGE_MOBILE_FN_CLONE_MOBILEPRIVATERUNTIME
uint64_t uniffi_ironstorage_mobile_fn_clone_mobileprivateruntime(uint64_t handle, RustCallStatus *_Nonnull out_status
);
#endif
#ifndef UNIFFI_FFIDEF_UNIFFI_IRONSTORAGE_MOBILE_FN_FREE_MOBILEPRIVATERUNTIME
#define UNIFFI_FFIDEF_UNIFFI_IRONSTORAGE_MOBILE_FN_FREE_MOBILEPRIVATERUNTIME
void uniffi_ironstorage_mobile_fn_free_mobileprivateruntime(uint64_t handle, RustCallStatus *_Nonnull out_status
);
#endif
#ifndef UNIFFI_FFIDEF_UNIFFI_IRONSTORAGE_MOBILE_FN_METHOD_MOBILEPRIVATERUNTIME_ACTIVATE
#define UNIFFI_FFIDEF_UNIFFI_IRONSTORAGE_MOBILE_FN_METHOD_MOBILEPRIVATERUNTIME_ACTIVATE
void uniffi_ironstorage_mobile_fn_method_mobileprivateruntime_activate(uint64_t ptr, RustCallStatus *_Nonnull out_status
);
#endif
#ifndef UNIFFI_FFIDEF_UNIFFI_IRONSTORAGE_MOBILE_FN_METHOD_MOBILEPRIVATERUNTIME_AUTHENTICATION
#define UNIFFI_FFIDEF_UNIFFI_IRONSTORAGE_MOBILE_FN_METHOD_MOBILEPRIVATERUNTIME_AUTHENTICATION
uint64_t uniffi_ironstorage_mobile_fn_method_mobileprivateruntime_authentication(uint64_t ptr, uint64_t backend, uint64_t clock, RustCallStatus *_Nonnull out_status
);
#endif
#ifndef UNIFFI_FFIDEF_UNIFFI_IRONSTORAGE_MOBILE_FN_METHOD_MOBILEPRIVATERUNTIME_CLEAR_DISPOSABLE_CACHE
#define UNIFFI_FFIDEF_UNIFFI_IRONSTORAGE_MOBILE_FN_METHOD_MOBILEPRIVATERUNTIME_CLEAR_DISPOSABLE_CACHE
void uniffi_ironstorage_mobile_fn_method_mobileprivateruntime_clear_disposable_cache(uint64_t ptr, RustCallStatus *_Nonnull out_status
);
#endif
#ifndef UNIFFI_FFIDEF_UNIFFI_IRONSTORAGE_MOBILE_FN_METHOD_MOBILEPRIVATERUNTIME_LAYOUT
#define UNIFFI_FFIDEF_UNIFFI_IRONSTORAGE_MOBILE_FN_METHOD_MOBILEPRIVATERUNTIME_LAYOUT
RustBuffer uniffi_ironstorage_mobile_fn_method_mobileprivateruntime_layout(uint64_t ptr, RustCallStatus *_Nonnull out_status
);
#endif
#ifndef UNIFFI_FFIDEF_UNIFFI_IRONSTORAGE_MOBILE_FN_METHOD_MOBILEPRIVATERUNTIME_SHELL
#define UNIFFI_FFIDEF_UNIFFI_IRONSTORAGE_MOBILE_FN_METHOD_MOBILEPRIVATERUNTIME_SHELL
RustBuffer uniffi_ironstorage_mobile_fn_method_mobileprivateruntime_shell(uint64_t ptr, RustCallStatus *_Nonnull out_status
);
#endif
#ifndef UNIFFI_FFIDEF_UNIFFI_IRONSTORAGE_MOBILE_FN_METHOD_MOBILEPRIVATERUNTIME_STATUS
#define UNIFFI_FFIDEF_UNIFFI_IRONSTORAGE_MOBILE_FN_METHOD_MOBILEPRIVATERUNTIME_STATUS
RustBuffer uniffi_ironstorage_mobile_fn_method_mobileprivateruntime_status(uint64_t ptr, RustCallStatus *_Nonnull out_status
);
#endif
#ifndef UNIFFI_FFIDEF_UNIFFI_IRONSTORAGE_MOBILE_FN_METHOD_MOBILEPRIVATERUNTIME_VERIFY_SECURE_STORAGE
#define UNIFFI_FFIDEF_UNIFFI_IRONSTORAGE_MOBILE_FN_METHOD_MOBILEPRIVATERUNTIME_VERIFY_SECURE_STORAGE
void uniffi_ironstorage_mobile_fn_method_mobileprivateruntime_verify_secure_storage(uint64_t ptr, uint64_t backend, RustCallStatus *_Nonnull out_status
);
#endif
#ifndef UNIFFI_FFIDEF_UNIFFI_IRONSTORAGE_MOBILE_FN_FUNC_MOBILE_AUTHENTICATION #ifndef UNIFFI_FFIDEF_UNIFFI_IRONSTORAGE_MOBILE_FN_FUNC_MOBILE_AUTHENTICATION
#define UNIFFI_FFIDEF_UNIFFI_IRONSTORAGE_MOBILE_FN_FUNC_MOBILE_AUTHENTICATION #define UNIFFI_FFIDEF_UNIFFI_IRONSTORAGE_MOBILE_FN_FUNC_MOBILE_AUTHENTICATION
uint64_t uniffi_ironstorage_mobile_fn_func_mobile_authentication(RustCallStatus *_Nonnull out_status uint64_t uniffi_ironstorage_mobile_fn_func_mobile_authentication(RustCallStatus *_Nonnull out_status
@@ -962,6 +1007,16 @@ RustBuffer uniffi_ironstorage_mobile_fn_func_mobile_platform_layout(RustBuffer p
RustBuffer uniffi_ironstorage_mobile_fn_func_validate_mobile_credential_request(RustBuffer request, RustCallStatus *_Nonnull out_status RustBuffer uniffi_ironstorage_mobile_fn_func_validate_mobile_credential_request(RustBuffer request, RustCallStatus *_Nonnull out_status
); );
#endif #endif
#ifndef UNIFFI_FFIDEF_UNIFFI_IRONSTORAGE_MOBILE_FN_FUNC_MOBILE_PRIVATE_STORAGE_FAILURE
#define UNIFFI_FFIDEF_UNIFFI_IRONSTORAGE_MOBILE_FN_FUNC_MOBILE_PRIVATE_STORAGE_FAILURE
RustBuffer uniffi_ironstorage_mobile_fn_func_mobile_private_storage_failure(RustBuffer error, RustCallStatus *_Nonnull out_status
);
#endif
#ifndef UNIFFI_FFIDEF_UNIFFI_IRONSTORAGE_MOBILE_FN_FUNC_OPEN_MOBILE_PRIVATE_RUNTIME
#define UNIFFI_FFIDEF_UNIFFI_IRONSTORAGE_MOBILE_FN_FUNC_OPEN_MOBILE_PRIVATE_RUNTIME
uint64_t uniffi_ironstorage_mobile_fn_func_open_mobile_private_runtime(RustBuffer directories, RustCallStatus *_Nonnull out_status
);
#endif
#ifndef UNIFFI_FFIDEF_FFI_IRONSTORAGE_MOBILE_RUSTBUFFER_ALLOC #ifndef UNIFFI_FFIDEF_FFI_IRONSTORAGE_MOBILE_RUSTBUFFER_ALLOC
#define UNIFFI_FFIDEF_FFI_IRONSTORAGE_MOBILE_RUSTBUFFER_ALLOC #define UNIFFI_FFIDEF_FFI_IRONSTORAGE_MOBILE_RUSTBUFFER_ALLOC
RustBuffer ffi_ironstorage_mobile_rustbuffer_alloc(uint64_t size, RustCallStatus *_Nonnull out_status RustBuffer ffi_ironstorage_mobile_rustbuffer_alloc(uint64_t size, RustCallStatus *_Nonnull out_status
@@ -1334,6 +1389,18 @@ uint16_t uniffi_ironstorage_mobile_checksum_func_mobile_platform_layout(void
#define UNIFFI_FFIDEF_UNIFFI_IRONSTORAGE_MOBILE_CHECKSUM_FUNC_VALIDATE_MOBILE_CREDENTIAL_REQUEST #define UNIFFI_FFIDEF_UNIFFI_IRONSTORAGE_MOBILE_CHECKSUM_FUNC_VALIDATE_MOBILE_CREDENTIAL_REQUEST
uint16_t uniffi_ironstorage_mobile_checksum_func_validate_mobile_credential_request(void uint16_t uniffi_ironstorage_mobile_checksum_func_validate_mobile_credential_request(void
);
#endif
#ifndef UNIFFI_FFIDEF_UNIFFI_IRONSTORAGE_MOBILE_CHECKSUM_FUNC_MOBILE_PRIVATE_STORAGE_FAILURE
#define UNIFFI_FFIDEF_UNIFFI_IRONSTORAGE_MOBILE_CHECKSUM_FUNC_MOBILE_PRIVATE_STORAGE_FAILURE
uint16_t uniffi_ironstorage_mobile_checksum_func_mobile_private_storage_failure(void
);
#endif
#ifndef UNIFFI_FFIDEF_UNIFFI_IRONSTORAGE_MOBILE_CHECKSUM_FUNC_OPEN_MOBILE_PRIVATE_RUNTIME
#define UNIFFI_FFIDEF_UNIFFI_IRONSTORAGE_MOBILE_CHECKSUM_FUNC_OPEN_MOBILE_PRIVATE_RUNTIME
uint16_t uniffi_ironstorage_mobile_checksum_func_open_mobile_private_runtime(void
); );
#endif #endif
#ifndef UNIFFI_FFIDEF_UNIFFI_IRONSTORAGE_MOBILE_CHECKSUM_METHOD_MOBILEAUTHENTICATION_ACKNOWLEDGE_WATCH_SNAPSHOT #ifndef UNIFFI_FFIDEF_UNIFFI_IRONSTORAGE_MOBILE_CHECKSUM_METHOD_MOBILEAUTHENTICATION_ACKNOWLEDGE_WATCH_SNAPSHOT
@@ -1802,6 +1869,48 @@ uint16_t uniffi_ironstorage_mobile_checksum_method_mobilesecretbackend_lock(void
#define UNIFFI_FFIDEF_UNIFFI_IRONSTORAGE_MOBILE_CHECKSUM_METHOD_MOBILESECRETBACKEND_UNLOCK #define UNIFFI_FFIDEF_UNIFFI_IRONSTORAGE_MOBILE_CHECKSUM_METHOD_MOBILESECRETBACKEND_UNLOCK
uint16_t uniffi_ironstorage_mobile_checksum_method_mobilesecretbackend_unlock(void uint16_t uniffi_ironstorage_mobile_checksum_method_mobilesecretbackend_unlock(void
);
#endif
#ifndef UNIFFI_FFIDEF_UNIFFI_IRONSTORAGE_MOBILE_CHECKSUM_METHOD_MOBILEPRIVATERUNTIME_ACTIVATE
#define UNIFFI_FFIDEF_UNIFFI_IRONSTORAGE_MOBILE_CHECKSUM_METHOD_MOBILEPRIVATERUNTIME_ACTIVATE
uint16_t uniffi_ironstorage_mobile_checksum_method_mobileprivateruntime_activate(void
);
#endif
#ifndef UNIFFI_FFIDEF_UNIFFI_IRONSTORAGE_MOBILE_CHECKSUM_METHOD_MOBILEPRIVATERUNTIME_AUTHENTICATION
#define UNIFFI_FFIDEF_UNIFFI_IRONSTORAGE_MOBILE_CHECKSUM_METHOD_MOBILEPRIVATERUNTIME_AUTHENTICATION
uint16_t uniffi_ironstorage_mobile_checksum_method_mobileprivateruntime_authentication(void
);
#endif
#ifndef UNIFFI_FFIDEF_UNIFFI_IRONSTORAGE_MOBILE_CHECKSUM_METHOD_MOBILEPRIVATERUNTIME_CLEAR_DISPOSABLE_CACHE
#define UNIFFI_FFIDEF_UNIFFI_IRONSTORAGE_MOBILE_CHECKSUM_METHOD_MOBILEPRIVATERUNTIME_CLEAR_DISPOSABLE_CACHE
uint16_t uniffi_ironstorage_mobile_checksum_method_mobileprivateruntime_clear_disposable_cache(void
);
#endif
#ifndef UNIFFI_FFIDEF_UNIFFI_IRONSTORAGE_MOBILE_CHECKSUM_METHOD_MOBILEPRIVATERUNTIME_LAYOUT
#define UNIFFI_FFIDEF_UNIFFI_IRONSTORAGE_MOBILE_CHECKSUM_METHOD_MOBILEPRIVATERUNTIME_LAYOUT
uint16_t uniffi_ironstorage_mobile_checksum_method_mobileprivateruntime_layout(void
);
#endif
#ifndef UNIFFI_FFIDEF_UNIFFI_IRONSTORAGE_MOBILE_CHECKSUM_METHOD_MOBILEPRIVATERUNTIME_SHELL
#define UNIFFI_FFIDEF_UNIFFI_IRONSTORAGE_MOBILE_CHECKSUM_METHOD_MOBILEPRIVATERUNTIME_SHELL
uint16_t uniffi_ironstorage_mobile_checksum_method_mobileprivateruntime_shell(void
);
#endif
#ifndef UNIFFI_FFIDEF_UNIFFI_IRONSTORAGE_MOBILE_CHECKSUM_METHOD_MOBILEPRIVATERUNTIME_STATUS
#define UNIFFI_FFIDEF_UNIFFI_IRONSTORAGE_MOBILE_CHECKSUM_METHOD_MOBILEPRIVATERUNTIME_STATUS
uint16_t uniffi_ironstorage_mobile_checksum_method_mobileprivateruntime_status(void
);
#endif
#ifndef UNIFFI_FFIDEF_UNIFFI_IRONSTORAGE_MOBILE_CHECKSUM_METHOD_MOBILEPRIVATERUNTIME_VERIFY_SECURE_STORAGE
#define UNIFFI_FFIDEF_UNIFFI_IRONSTORAGE_MOBILE_CHECKSUM_METHOD_MOBILEPRIVATERUNTIME_VERIFY_SECURE_STORAGE
uint16_t uniffi_ironstorage_mobile_checksum_method_mobileprivateruntime_verify_secure_storage(void
); );
#endif #endif
#ifndef UNIFFI_FFIDEF_FFI_IRONSTORAGE_MOBILE_UNIFFI_CONTRACT_VERSION #ifndef UNIFFI_FFIDEF_FFI_IRONSTORAGE_MOBILE_UNIFFI_CONTRACT_VERSION
+3
View File
@@ -3,6 +3,9 @@
//! Shared UniFFI contract for native phone presentation. Watch exports are opt-in. //! Shared UniFFI contract for native phone presentation. Watch exports are opt-in.
mod private_storage;
pub use private_storage::*;
use std::{ use std::{
error::Error, error::Error,
fmt, fmt,
+3 -1
View File
@@ -70,6 +70,7 @@ pub fn mobile_platform_layout(
#[derive(Clone, uniffi::Enum)] #[derive(Clone, uniffi::Enum)]
pub enum MobileSecretLocator { pub enum MobileSecretLocator {
ApplicationBinding,
OpenPgpPassphrase { OpenPgpPassphrase {
fingerprint: String, fingerprint: String,
}, },
@@ -85,6 +86,7 @@ pub enum MobileSecretLocator {
impl From<&SecretLocator> for MobileSecretLocator { impl From<&SecretLocator> for MobileSecretLocator {
fn from(locator: &SecretLocator) -> Self { fn from(locator: &SecretLocator) -> Self {
match locator { match locator {
SecretLocator::ApplicationBinding => Self::ApplicationBinding,
SecretLocator::OpenPgpPassphrase { fingerprint } => Self::OpenPgpPassphrase { SecretLocator::OpenPgpPassphrase { fingerprint } => Self::OpenPgpPassphrase {
fingerprint: fingerprint.clone(), fingerprint: fingerprint.clone(),
}, },
@@ -232,7 +234,7 @@ impl SecretStoreBackend for SecretBackendAdapter {
pub trait MobileClock: Send + Sync { pub trait MobileClock: Send + Sync {
fn monotonic_milliseconds(&self) -> u64; fn monotonic_milliseconds(&self) -> u64;
} }
struct ClockAdapter(Arc<dyn MobileClock>); pub(crate) struct ClockAdapter(pub Arc<dyn MobileClock>);
impl AuthenticationClock for ClockAdapter { impl AuthenticationClock for ClockAdapter {
fn now(&self) -> Duration { fn now(&self) -> Duration {
Duration::from_millis(self.0.monotonic_milliseconds()) Duration::from_millis(self.0.monotonic_milliseconds())
+221
View File
@@ -0,0 +1,221 @@
//! Mechanical exports for the storage-owned Android private runtime.
use crate::{
MobileAuthentication, MobileShell,
platform::{ClockAdapter, MobileClock, MobileSecretBackend, SecretBackendAdapter},
};
use ironstorage::mobile_private_storage::{
PrivateDirectories, PrivateStorage, PrivateStorageError,
};
use std::sync::Arc;
#[derive(Clone, uniffi::Record)]
pub struct MobilePrivateDirectories {
pub files: String,
pub no_backup: String,
pub cache: String,
}
#[derive(Clone, Copy, Debug, Eq, PartialEq, uniffi::Enum)]
pub enum MobilePrivateStorageErrorKind {
InvalidRoots,
UnsafePath,
MissingRoot,
AccessDenied,
ReadOnly,
StorageFull,
Busy,
Io,
CorruptLayout,
MigrationRequired,
CorruptConfiguration,
ConfigurationMissingWithData,
InterruptedSetup,
RestoredWithoutKeystore,
SecureStorageLocked,
SecureStorageUnavailable,
InvalidSecureBinding,
}
impl From<PrivateStorageError> for MobilePrivateStorageErrorKind {
fn from(value: PrivateStorageError) -> Self {
match value {
PrivateStorageError::InvalidRoots => Self::InvalidRoots,
PrivateStorageError::UnsafePath => Self::UnsafePath,
PrivateStorageError::MissingRoot => Self::MissingRoot,
PrivateStorageError::AccessDenied => Self::AccessDenied,
PrivateStorageError::ReadOnly => Self::ReadOnly,
PrivateStorageError::StorageFull => Self::StorageFull,
PrivateStorageError::Busy => Self::Busy,
PrivateStorageError::Io => Self::Io,
PrivateStorageError::CorruptLayout => Self::CorruptLayout,
PrivateStorageError::MigrationRequired => Self::MigrationRequired,
PrivateStorageError::CorruptConfiguration => Self::CorruptConfiguration,
PrivateStorageError::ConfigurationMissingWithData => Self::ConfigurationMissingWithData,
PrivateStorageError::InterruptedSetup => Self::InterruptedSetup,
PrivateStorageError::RestoredWithoutKeystore => Self::RestoredWithoutKeystore,
PrivateStorageError::SecureStorageLocked => Self::SecureStorageLocked,
PrivateStorageError::SecureStorageUnavailable => Self::SecureStorageUnavailable,
PrivateStorageError::InvalidSecureBinding => Self::InvalidSecureBinding,
}
}
}
#[derive(Debug, uniffi::Error)]
pub enum MobilePrivateStorageFfiError {
Failed { kind: MobilePrivateStorageErrorKind },
}
impl std::fmt::Display for MobilePrivateStorageFfiError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.write_str("Private storage operation failed")
}
}
impl std::error::Error for MobilePrivateStorageFfiError {}
impl From<PrivateStorageError> for MobilePrivateStorageFfiError {
fn from(value: PrivateStorageError) -> Self {
Self::Failed { kind: value.into() }
}
}
#[derive(Clone, uniffi::Record)]
pub struct MobilePrivateStorageStatus {
pub version: u32,
pub configured: bool,
pub recovered_temporary_files: bool,
pub recovered_catalog: bool,
pub secure_binding_present: bool,
pub recovery: Option<MobilePrivateStorageErrorKind>,
}
#[derive(Clone, uniffi::Record)]
pub struct MobilePrivateStorageLayout {
pub data: String,
pub configuration: String,
pub vault: String,
pub keys: String,
pub catalog: String,
pub scratch: String,
pub cache: String,
}
#[derive(uniffi::Object)]
pub struct MobilePrivateRuntime {
storage: PrivateStorage,
}
#[uniffi::export]
impl MobilePrivateRuntime {
pub fn activate(&self) -> Result<(), MobilePrivateStorageFfiError> {
self.storage.activate().map_err(Into::into)
}
pub fn shell(&self) -> MobileShell {
self.storage.shell().into()
}
pub fn status(&self) -> MobilePrivateStorageStatus {
let status = self.storage.status();
MobilePrivateStorageStatus {
version: status.version,
configured: status.configured,
recovered_temporary_files: status.recovered_temporary_files,
recovered_catalog: status.recovered_catalog,
secure_binding_present: status.secure_binding_present,
recovery: status.recovery.map(Into::into),
}
}
pub fn layout(&self) -> MobilePrivateStorageLayout {
let root = self.storage.root();
let path = |name| root.join(name).to_string_lossy().into_owned();
MobilePrivateStorageLayout {
data: root.to_string_lossy().into_owned(),
configuration: path("config.toml"),
vault: path("vault"),
keys: path("keys"),
catalog: path("totp-catalog.toml"),
scratch: path("scratch"),
cache: self.storage.cache_path().to_string_lossy().into_owned(),
}
}
pub fn verify_secure_storage(
&self,
backend: Arc<dyn MobileSecretBackend>,
) -> Result<(), MobilePrivateStorageFfiError> {
self.storage
.verify_secure_storage(&SecretBackendAdapter(backend))
.map_err(Into::into)
}
pub fn clear_disposable_cache(&self) -> Result<(), MobilePrivateStorageFfiError> {
self.storage.clear_disposable_cache().map_err(Into::into)
}
pub fn authentication(
&self,
backend: Arc<dyn MobileSecretBackend>,
clock: Arc<dyn MobileClock>,
) -> Result<Arc<MobileAuthentication>, crate::MobileAuthenticationFfiError> {
self.storage
.verify_secure_storage(&SecretBackendAdapter(Arc::clone(&backend)))
.map_err(private_auth_error)?;
let config = self
.storage
.configuration()
.map_err(private_auth_error)?
.ok_or_else(|| private_auth_error(PrivateStorageError::ConfigurationMissingWithData))?;
let authentication =
ironstorage::mobile_authentication::MobileAuthentication::with_platform(
config,
Arc::new(SecretBackendAdapter(backend)),
Arc::new(ClockAdapter(clock)),
)?;
Ok(Arc::new(MobileAuthentication {
authentication: Arc::new(authentication),
}))
}
}
fn private_auth_error(error: PrivateStorageError) -> crate::MobileAuthenticationFfiError {
crate::MobileAuthenticationFfiError::Failed {
kind: crate::MobileAuthenticationErrorKind::Configuration,
title: "Private Storage Recovery Required".into(),
detail: error.detail().into(),
}
}
#[uniffi::export]
pub fn open_mobile_private_runtime(
directories: MobilePrivateDirectories,
) -> Result<Arc<MobilePrivateRuntime>, MobilePrivateStorageFfiError> {
let storage = PrivateStorage::open(PrivateDirectories {
files: directories.files.into(),
no_backup: directories.no_backup.into(),
cache: directories.cache.into(),
})?;
Ok(Arc::new(MobilePrivateRuntime { storage }))
}
#[uniffi::export]
pub fn mobile_private_storage_failure(error: MobilePrivateStorageErrorKind) -> MobileShell {
let value = match error {
MobilePrivateStorageErrorKind::InvalidRoots => PrivateStorageError::InvalidRoots,
MobilePrivateStorageErrorKind::UnsafePath => PrivateStorageError::UnsafePath,
MobilePrivateStorageErrorKind::MissingRoot => PrivateStorageError::MissingRoot,
MobilePrivateStorageErrorKind::AccessDenied => PrivateStorageError::AccessDenied,
MobilePrivateStorageErrorKind::ReadOnly => PrivateStorageError::ReadOnly,
MobilePrivateStorageErrorKind::StorageFull => PrivateStorageError::StorageFull,
MobilePrivateStorageErrorKind::Busy => PrivateStorageError::Busy,
MobilePrivateStorageErrorKind::Io => PrivateStorageError::Io,
MobilePrivateStorageErrorKind::CorruptLayout => PrivateStorageError::CorruptLayout,
MobilePrivateStorageErrorKind::MigrationRequired => PrivateStorageError::MigrationRequired,
MobilePrivateStorageErrorKind::CorruptConfiguration => {
PrivateStorageError::CorruptConfiguration
}
MobilePrivateStorageErrorKind::ConfigurationMissingWithData => {
PrivateStorageError::ConfigurationMissingWithData
}
MobilePrivateStorageErrorKind::InterruptedSetup => PrivateStorageError::InterruptedSetup,
MobilePrivateStorageErrorKind::RestoredWithoutKeystore => {
PrivateStorageError::RestoredWithoutKeystore
}
MobilePrivateStorageErrorKind::SecureStorageLocked => {
PrivateStorageError::SecureStorageLocked
}
MobilePrivateStorageErrorKind::SecureStorageUnavailable => {
PrivateStorageError::SecureStorageUnavailable
}
MobilePrivateStorageErrorKind::InvalidSecureBinding => {
PrivateStorageError::InvalidSecureBinding
}
};
ironstorage::mobile::MobileShell::storage_failure(value.detail().into()).into()
}
+2
View File
@@ -30,6 +30,7 @@ full = [
"dep:regex", "dep:regex",
"dep:reqwest", "dep:reqwest",
"dep:rustls", "dep:rustls",
"dep:rustix",
"dep:rqrr", "dep:rqrr",
"dep:secret-service", "dep:secret-service",
"dep:security-framework", "dep:security-framework",
@@ -46,6 +47,7 @@ watch = []
[dependencies] [dependencies]
cap-std = { workspace = true, optional = true } cap-std = { workspace = true, optional = true }
cap-tempfile = { workspace = true, optional = true } cap-tempfile = { workspace = true, optional = true }
rustix = { workspace = true, optional = true }
clap = { workspace = true, optional = true } clap = { workspace = true, optional = true }
clap_complete = { workspace = true, optional = true } clap_complete = { workspace = true, optional = true }
data-encoding.workspace = true data-encoding.workspace = true
+109
View File
@@ -0,0 +1,109 @@
//! Android cannot link anonymous temporary files through /proc under app SELinux.
#[cfg(not(any(target_os = "android", test)))]
pub(crate) use cap_tempfile::TempFile;
#[cfg(any(target_os = "android", test))]
mod named {
#[cfg(unix)]
use cap_std::fs::OpenOptionsExt;
use cap_std::fs::{Dir, File, OpenOptions};
use std::{
ffi::OsStr,
io::{self, Write},
};
pub(crate) struct TempFile<'a> {
directory: &'a Dir,
file: File,
name: String,
}
impl<'a> TempFile<'a> {
pub(crate) fn new(directory: &'a Dir) -> io::Result<Self> {
#[cfg(unix)]
let _creation = crate::file_lock::directory(directory)?;
for _ in 0..128 {
let name = format!(".ironstorage-atomic-{:032x}", rand::random::<u128>());
let mut options = OpenOptions::new();
options.read(true).write(true).create_new(true);
#[cfg(unix)]
options.mode(0o600);
match directory.open_with(&name, &options) {
Ok(file) => {
let temporary = Self {
directory,
file,
name,
};
crate::file_lock::lock(&temporary.file.try_clone()?.into_std())?;
return Ok(temporary);
}
Err(error) if error.kind() == io::ErrorKind::AlreadyExists => continue,
Err(error) => return Err(error),
}
}
Err(io::ErrorKind::AlreadyExists.into())
}
pub(crate) fn as_file(&self) -> &File {
&self.file
}
pub(crate) fn replace(self, destination: impl AsRef<OsStr>) -> io::Result<()> {
self.directory
.rename(&self.name, self.directory, destination.as_ref())
}
}
impl Write for TempFile<'_> {
fn write(&mut self, bytes: &[u8]) -> io::Result<usize> {
self.file.write(bytes)
}
fn flush(&mut self) -> io::Result<()> {
self.file.flush()
}
}
impl Drop for TempFile<'_> {
fn drop(&mut self) {
let _ = self.directory.remove_file(&self.name);
}
}
}
#[cfg(any(target_os = "android", test))]
pub(crate) use named::TempFile;
pub(crate) fn is_temporary(name: &str) -> bool {
name.strip_prefix(".ironstorage-atomic-")
.is_some_and(|suffix| suffix.len() == 32 && suffix.bytes().all(|b| b.is_ascii_hexdigit()))
}
#[cfg(test)]
mod tests {
use super::*;
use cap_std::{ambient_authority, fs::Dir};
use std::io::Write;
#[test]
fn replacement_is_atomic_private_locked_and_failures_preserve_the_target() {
let root = tempfile::tempdir().unwrap();
let directory = Dir::open_ambient_dir(root.path(), ambient_authority()).unwrap();
directory.write("target", b"old").unwrap();
let mut temporary = TempFile::new(&directory).unwrap();
temporary.write_all(b"new").unwrap();
let name = directory
.entries()
.unwrap()
.map(|entry| entry.unwrap().file_name())
.find(|name| is_temporary(&name.to_string_lossy()))
.unwrap();
let other = directory.open(&name).unwrap().into_std();
assert_eq!(
crate::file_lock::try_lock(&other).unwrap_err().kind(),
std::io::ErrorKind::WouldBlock
);
temporary.as_file().sync_all().unwrap();
temporary.replace("target").unwrap();
assert_eq!(directory.read("target").unwrap(), b"new");
assert!(!directory.exists(&name));
let mut failed = TempFile::new(&directory).unwrap();
failed.write_all(b"partial").unwrap();
assert!(failed.replace("missing/target").is_err());
assert_eq!(directory.read("target").unwrap(), b"new");
assert_eq!(directory.entries().unwrap().count(), 1);
}
}
+54 -4
View File
@@ -8,11 +8,12 @@ use std::{
fs::OpenOptions, fs::OpenOptions,
io::{self, Write}, io::{self, Write},
path::{Component, Path, PathBuf}, path::{Component, Path, PathBuf},
sync::OnceLock,
time::Duration, time::Duration,
}; };
use crate::atomic_file::TempFile;
use cap_std::{ambient_authority, fs::Dir}; use cap_std::{ambient_authority, fs::Dir};
use cap_tempfile::TempFile;
#[cfg(not(any(target_os = "ios", target_os = "watchos")))] #[cfg(not(any(target_os = "ios", target_os = "watchos")))]
use gix::bstr::ByteSlice as _; use gix::bstr::ByteSlice as _;
use serde::Deserialize; use serde::Deserialize;
@@ -31,6 +32,7 @@ use crate::{
const APPLICATION_DIRECTORY: &str = "ironstorage"; const APPLICATION_DIRECTORY: &str = "ironstorage";
const CONFIG_FILE: &str = "config.toml"; const CONFIG_FILE: &str = "config.toml";
const MAX_CONFIG_BYTES: u64 = 1024 * 1024; const MAX_CONFIG_BYTES: u64 = 1024 * 1024;
static PRIVATE_CONFIG_CONTEXT: OnceLock<ConfigLoader> = OnceLock::new();
/// Validated application configuration. /// Validated application configuration.
#[derive(Clone, Debug, PartialEq)] #[derive(Clone, Debug, PartialEq)]
@@ -131,6 +133,10 @@ impl ConfigSettings {
impl Config { impl Config {
/// Load an explicit configuration file, or the native per-user default. /// Load an explicit configuration file, or the native per-user default.
pub fn load(explicit: Option<&Path>) -> Result<Self, ConfigError> { pub fn load(explicit: Option<&Path>) -> Result<Self, ConfigError> {
if let Some(path) = explicit.filter(|path| path.is_absolute()) {
let parent = path.parent().unwrap_or(Path::new("/"));
return ConfigLoader::new(parent.to_owned(), parent.to_owned()).load(Some(path));
}
ConfigLoader::system()?.load(explicit) ConfigLoader::system()?.load(explicit)
} }
@@ -713,6 +719,10 @@ impl Config {
let contents = toml::to_string_pretty(&self.document).map_err(|_| ConfigError::Write { let contents = toml::to_string_pretty(&self.document).map_err(|_| ConfigError::Write {
path: self.source.clone(), path: self.source.clone(),
})?; })?;
#[cfg(unix)]
let creation = crate::file_lock::directory(&directory).map_err(|_| ConfigError::Write {
path: self.source.clone(),
})?;
let temporary = (0..128_u8) let temporary = (0..128_u8)
.find_map(|attempt| { .find_map(|attempt| {
let path = parent.join(format!( let path = parent.join(format!(
@@ -739,10 +749,29 @@ impl Config {
path: self.source.clone(), path: self.source.clone(),
})?; })?;
let (temporary_path, mut temporary_file) = temporary; let (temporary_path, mut temporary_file) = temporary;
crate::file_lock::lock(&temporary_file).map_err(|_| ConfigError::Write {
path: self.source.clone(),
})?;
#[cfg(unix)]
drop(creation);
let installed = temporary_file let installed = temporary_file
.write_all(contents.as_bytes()) .write_all(contents.as_bytes())
.and_then(|()| temporary_file.sync_all()) .and_then(|()| temporary_file.sync_all())
.and_then(|()| fs::hard_link(&temporary_path, parent.join(name))); .and_then(|()| {
#[cfg(target_os = "android")]
return rustix::fs::renameat_with(
&directory,
temporary_path
.file_name()
.expect("temporary file has a name"),
&directory,
name,
rustix::fs::RenameFlags::NOREPLACE,
)
.map_err(Into::into);
#[cfg(not(target_os = "android"))]
fs::hard_link(&temporary_path, parent.join(name))
});
drop(temporary_file); drop(temporary_file);
let _ = fs::remove_file(&temporary_path); let _ = fs::remove_file(&temporary_path);
match installed { match installed {
@@ -829,11 +858,29 @@ pub struct ConfigLoader {
impl ConfigLoader { impl ConfigLoader {
pub fn system() -> Result<Self, ConfigError> { pub fn system() -> Result<Self, ConfigError> {
let current_directory = env::current_dir().map_err(|_| ConfigError::CurrentDirectory)?; if let Some(context) = PRIVATE_CONFIG_CONTEXT.get() {
return Ok(context.clone());
}
#[cfg(target_os = "android")]
return Err(ConfigError::NativeConfigDirectory);
#[cfg(not(target_os = "android"))]
{
let current_directory =
env::current_dir().map_err(|_| ConfigError::CurrentDirectory)?;
let native_config_directory = let native_config_directory =
native_config_directory().ok_or(ConfigError::NativeConfigDirectory)?; native_config_directory().ok_or(ConfigError::NativeConfigDirectory)?;
Ok(Self::new(current_directory, native_config_directory)) Ok(Self::new(current_directory, native_config_directory))
} }
}
pub(crate) fn activate_private_root(root: &Path) -> bool {
let Some(parent) = root.parent() else {
return false;
};
let context = Self::new(root.to_owned(), parent.to_owned());
let _ = PRIVATE_CONFIG_CONTEXT.set(context.clone());
PRIVATE_CONFIG_CONTEXT.get() == Some(&context)
}
pub fn new(current_directory: PathBuf, native_config_directory: PathBuf) -> Self { pub fn new(current_directory: PathBuf, native_config_directory: PathBuf) -> Self {
Self { Self {
@@ -2328,7 +2375,10 @@ fn native_config_directory() -> Option<PathBuf> {
.map(|home| home.join("Library/Application Support")) .map(|home| home.join("Library/Application Support"))
} }
#[cfg(all(unix, not(any(target_os = "ios", target_os = "macos"))))] #[cfg(all(
unix,
not(any(target_os = "ios", target_os = "macos", target_os = "android"))
))]
fn native_config_directory() -> Option<PathBuf> { fn native_config_directory() -> Option<PathBuf> {
match env::var_os("XDG_CONFIG_HOME") { match env::var_os("XDG_CONFIG_HOME") {
Some(path) if !path.is_empty() && Path::new(&path).is_absolute() => { Some(path) if !path.is_empty() && Path::new(&path).is_absolute() => {
+61
View File
@@ -0,0 +1,61 @@
//! Android's pinned std lacks file locks; retain identical advisory-lock semantics.
use std::{fs::File, io};
pub(crate) fn lock(file: &File) -> io::Result<()> {
#[cfg(target_os = "android")]
return rustix::fs::flock(file, rustix::fs::FlockOperation::LockExclusive).map_err(Into::into);
#[cfg(not(target_os = "android"))]
file.lock()
}
pub(crate) fn try_lock(file: &File) -> io::Result<()> {
#[cfg(target_os = "android")]
return rustix::fs::flock(file, rustix::fs::FlockOperation::NonBlockingLockExclusive)
.map_err(Into::into);
#[cfg(not(target_os = "android"))]
file.try_lock().map_err(|error| match error {
std::fs::TryLockError::WouldBlock => io::ErrorKind::WouldBlock.into(),
std::fs::TryLockError::Error(error) => error,
})
}
#[cfg(unix)]
pub(crate) fn directory(directory: &cap_std::fs::Dir) -> io::Result<File> {
let file = directory.open(".")?.into_std();
lock(&file)?;
Ok(file)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn independent_owners_contend_until_the_lock_is_dropped() {
let temp = tempfile::tempdir().unwrap();
let path = temp.path().join("owner.lock");
let first = File::create(&path).unwrap();
let second = File::open(&path).unwrap();
lock(&first).unwrap();
assert_eq!(
try_lock(&second).unwrap_err().kind(),
io::ErrorKind::WouldBlock
);
drop(first);
try_lock(&second).unwrap();
}
#[cfg(unix)]
#[test]
fn directory_gate_covers_file_creation_before_its_owner_lock() {
let root = tempfile::tempdir().unwrap();
let dir =
cap_std::fs::Dir::open_ambient_dir(root.path(), cap_std::ambient_authority()).unwrap();
let first = directory(&dir).unwrap();
let second = dir.open(".").unwrap().into_std();
assert_eq!(
try_lock(&second).unwrap_err().kind(),
io::ErrorKind::WouldBlock
);
drop(first);
try_lock(&second).unwrap();
}
}
+6
View File
@@ -5,6 +5,8 @@
//! //!
//! This crate is the sole owner of stored and derived password-store objects. //! This crate is the sole owner of stored and derived password-store objects.
#[cfg(feature = "full")]
mod atomic_file;
#[cfg(feature = "full")] #[cfg(feature = "full")]
pub mod authentication; pub mod authentication;
#[cfg(feature = "full")] #[cfg(feature = "full")]
@@ -18,6 +20,8 @@ pub mod desktop;
#[cfg(feature = "full")] #[cfg(feature = "full")]
pub mod document; pub mod document;
#[cfg(feature = "full")] #[cfg(feature = "full")]
mod file_lock;
#[cfg(feature = "full")]
pub mod generate; pub mod generate;
#[cfg(feature = "full")] #[cfg(feature = "full")]
pub mod git; pub mod git;
@@ -42,6 +46,8 @@ pub mod mobile_passwords;
#[cfg(feature = "full")] #[cfg(feature = "full")]
pub mod mobile_platform; pub mod mobile_platform;
#[cfg(feature = "full")] #[cfg(feature = "full")]
pub mod mobile_private_storage;
#[cfg(feature = "full")]
pub mod mobile_totp; pub mod mobile_totp;
pub mod mobile_watch; pub mod mobile_watch;
#[cfg(feature = "full")] #[cfg(feature = "full")]
+9 -1
View File
@@ -151,7 +151,11 @@ pub struct MobileShell {
impl MobileShell { impl MobileShell {
pub fn load() -> Self { pub fn load() -> Self {
match Config::load(None) { Self::from_configuration(Config::load(None))
}
pub fn from_configuration(configuration: Result<Config, ConfigError>) -> Self {
match configuration {
Ok(config) => match Repository::open(config.vault()) { Ok(config) => match Repository::open(config.vault()) {
Ok(_) => Self::new(config.mobile_tab(), MobileShellState::Ready, None), Ok(_) => Self::new(config.mobile_tab(), MobileShellState::Ready, None),
Err(RepositoryError::Io { Err(RepositoryError::Io {
@@ -179,6 +183,10 @@ impl MobileShell {
Self::new(MobileTab::Home, state, None) Self::new(MobileTab::Home, state, None)
} }
pub fn storage_failure(detail: String) -> Self {
Self::new(MobileTab::Home, MobileShellState::Error, Some(detail))
}
pub fn selected_tab(&self) -> MobileTab { pub fn selected_tab(&self) -> MobileTab {
self.selected_tab self.selected_tab
} }
+1 -1
View File
@@ -2,8 +2,8 @@
use std::{collections::BTreeMap, error::Error, fmt, fs, io::Write as _, path::Path}; use std::{collections::BTreeMap, error::Error, fmt, fs, io::Write as _, path::Path};
use crate::atomic_file::TempFile;
use cap_std::{ambient_authority, fs::Dir}; use cap_std::{ambient_authority, fs::Dir};
use cap_tempfile::TempFile;
use data_encoding::{BASE64URL_NOPAD, HEXLOWER}; use data_encoding::{BASE64URL_NOPAD, HEXLOWER};
use sha2::{Digest as _, Sha256}; use sha2::{Digest as _, Sha256};
File diff suppressed because it is too large. Load diff
+59 -3
View File
@@ -4,7 +4,7 @@ use std::{
collections::{BTreeMap, BTreeSet}, collections::{BTreeMap, BTreeSet},
error::Error, error::Error,
fmt, fs, fmt, fs,
io::Write as _, io::{Read as _, Write as _},
path::Path, path::Path,
sync::{ sync::{
Mutex, Mutex,
@@ -12,8 +12,8 @@ use std::{
}, },
}; };
use crate::atomic_file::TempFile;
use cap_std::{ambient_authority, fs::Dir}; use cap_std::{ambient_authority, fs::Dir};
use cap_tempfile::TempFile;
use data_encoding::HEXLOWER; use data_encoding::HEXLOWER;
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use sha2::{Digest as _, Sha256}; use sha2::{Digest as _, Sha256};
@@ -27,6 +27,7 @@ use crate::{
}; };
const CACHE_VERSION: u32 = 1; const CACHE_VERSION: u32 = 1;
const MAX_CATALOG_BYTES: u64 = 32 * 1024 * 1024;
// ponytail: cache writes are rare; use per-cache locks only if profiling shows contention. // ponytail: cache writes are rare; use per-cache locks only if profiling shows contention.
static CACHE_WRITE_LOCK: Mutex<()> = Mutex::new(()); static CACHE_WRITE_LOCK: Mutex<()> = Mutex::new(());
@@ -613,7 +614,9 @@ fn inspect_entry(
fn load_cache(path: &Path, store: &str) -> (Option<CachedCatalog>, Option<String>) { fn load_cache(path: &Path, store: &str) -> (Option<CachedCatalog>, Option<String>) {
if let Ok(metadata) = fs::symlink_metadata(path) if let Ok(metadata) = fs::symlink_metadata(path)
&& (metadata.file_type().is_symlink() || !metadata.is_file()) && (metadata.file_type().is_symlink()
|| !metadata.is_file()
|| metadata.len() > MAX_CATALOG_BYTES)
{ {
return ( return (
None, None,
@@ -642,6 +645,59 @@ fn load_cache(path: &Path, store: &str) -> (Option<CachedCatalog>, Option<String
} }
} }
pub(crate) fn validate_private_catalog(
root: &Path,
path: &Path,
) -> Result<bool, crate::mobile_private_storage::PrivateStorageError> {
use crate::mobile_private_storage::PrivateStorageError;
let _lock = CACHE_WRITE_LOCK
.lock()
.map_err(|_| PrivateStorageError::Io)?;
let metadata = match fs::symlink_metadata(path) {
Ok(metadata) => metadata,
Err(error) if error.kind() == std::io::ErrorKind::NotFound => return Ok(false),
Err(error) => return Err(error.into()),
};
if metadata.file_type().is_symlink() || !metadata.is_file() {
return Err(PrivateStorageError::UnsafePath);
}
let store = store_identity(root, path);
let parent = Dir::open_ambient_dir(
path.parent().ok_or(PrivateStorageError::UnsafePath)?,
ambient_authority(),
)?;
let name = path.file_name().ok_or(PrivateStorageError::UnsafePath)?;
let file = parent.open(name)?;
crate::mobile_private_storage::private_permissions(&file, false)?;
let mut bytes = Vec::new();
if metadata.len() <= MAX_CATALOG_BYTES {
file.take(MAX_CATALOG_BYTES + 1).read_to_end(&mut bytes)?;
}
let source = std::str::from_utf8(&bytes).ok();
let valid = metadata.len() <= MAX_CATALOG_BYTES
&& bytes.len() as u64 <= MAX_CATALOG_BYTES
&& source
.and_then(|source| toml::from_str::<CachedCatalog>(source).ok())
.is_some_and(|catalog| {
catalog.version == CACHE_VERSION
&& catalog.store == store
&& catalog.entries.iter().all(|(entry, record)| {
EntryPath::parse(entry).is_ok()
&& record.ciphertext_hash.len() == 64
&& record
.ciphertext_hash
.bytes()
.all(|byte| byte.is_ascii_hexdigit())
})
});
if valid {
return Ok(false);
}
parent.remove_file(name)?;
parent.open(".")?.sync_all()?;
Ok(true)
}
fn upsert_cache_record( fn upsert_cache_record(
path: &Path, path: &Path,
store: &str, store: &str,
+2 -2
View File
@@ -3,9 +3,9 @@
use std::{error::Error, fmt, path::Path}; use std::{error::Error, fmt, path::Path};
#[cfg(feature = "full")] #[cfg(feature = "full")]
use cap_std::{ambient_authority, fs::Dir}; use crate::atomic_file::TempFile;
#[cfg(feature = "full")] #[cfg(feature = "full")]
use cap_tempfile::TempFile; use cap_std::{ambient_authority, fs::Dir};
use data_encoding::HEXLOWER; use data_encoding::HEXLOWER;
#[cfg(feature = "full")] #[cfg(feature = "full")]
use data_encoding::HEXLOWER_PERMISSIVE; use data_encoding::HEXLOWER_PERMISSIVE;
+1 -1
View File
@@ -14,9 +14,9 @@ use std::collections::BTreeSet;
#[cfg(test)] #[cfg(test)]
use std::sync::atomic::{AtomicUsize, Ordering}; use std::sync::atomic::{AtomicUsize, Ordering};
use crate::atomic_file::TempFile;
pub use crate::secret::SecretBytes; pub use crate::secret::SecretBytes;
use cap_std::{ambient_authority, fs::Dir}; use cap_std::{ambient_authority, fs::Dir};
use cap_tempfile::TempFile;
const ENTRY_EXTENSION: &str = "gpg"; const ENTRY_EXTENSION: &str = "gpg";
const RECIPIENT_FILE: &str = ".gpg-id"; const RECIPIENT_FILE: &str = ".gpg-id";
+4
View File
@@ -149,6 +149,7 @@ impl fmt::Debug for SecretReference {
/// server/application pair can retrieve its account without TOML metadata. /// server/application pair can retrieve its account without TOML metadata.
#[derive(Clone, Eq, Ord, PartialEq, PartialOrd)] #[derive(Clone, Eq, Ord, PartialEq, PartialOrd)]
pub enum SecretLocator { pub enum SecretLocator {
ApplicationBinding,
OpenPgpPassphrase { OpenPgpPassphrase {
fingerprint: String, fingerprint: String,
}, },
@@ -164,6 +165,7 @@ pub enum SecretLocator {
impl fmt::Debug for SecretLocator { impl fmt::Debug for SecretLocator {
fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result { fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
match self { match self {
Self::ApplicationBinding => formatter.write_str("SecretLocator::ApplicationBinding"),
Self::OpenPgpPassphrase { .. } => { Self::OpenPgpPassphrase { .. } => {
formatter.write_str("SecretLocator::OpenPgpPassphrase([REDACTED])") formatter.write_str("SecretLocator::OpenPgpPassphrase([REDACTED])")
} }
@@ -180,6 +182,7 @@ impl fmt::Debug for SecretLocator {
impl SecretLocator { impl SecretLocator {
fn service_and_user(&self) -> (&'static str, String) { fn service_and_user(&self) -> (&'static str, String) {
match self { match self {
Self::ApplicationBinding => ("de.rfc1437.ironstorage.private-binding", "device".into()),
Self::OpenPgpPassphrase { fingerprint } => { Self::OpenPgpPassphrase { fingerprint } => {
(OPENPGP_PASSPHRASE_SERVICE, fingerprint.clone()) (OPENPGP_PASSPHRASE_SERVICE, fingerprint.clone())
} }
@@ -246,6 +249,7 @@ impl SecretProtectionPolicy {
SecretLocator::OpenPgpPassphrase { .. } => self.openpgp, SecretLocator::OpenPgpPassphrase { .. } => self.openpgp,
SecretLocator::HttpsGitCredential { .. } => self.git, SecretLocator::HttpsGitCredential { .. } => self.git,
SecretLocator::SshKeyPassphrase { .. } => self.git, SecretLocator::SshKeyPassphrase { .. } => self.git,
SecretLocator::ApplicationBinding => SecretProtection::DeviceUnlocked,
} }
} }
} }
+1 -1
View File
@@ -10,8 +10,8 @@ use std::{
time::Duration, time::Duration,
}; };
use crate::atomic_file::TempFile;
use cap_std::{ambient_authority, fs::Dir}; use cap_std::{ambient_authority, fs::Dir};
use cap_tempfile::TempFile;
use hmac::{Hmac, Mac as _}; use hmac::{Hmac, Mac as _};
use russh::{ use russh::{
ChannelMsg, Disconnect, Preferred, cipher, client, compression, kex, ChannelMsg, Disconnect, Preferred, cipher, client, compression, kex,
@@ -0,0 +1,58 @@
#![forbid(unsafe_code)]
use ironstorage::{
config::{Config, ConfigLoader},
mobile::{MobileShell, MobileShellState},
mobile_onboarding::MobileOnboardingOperation,
mobile_private_storage::{PrivateDirectories, PrivateStorage},
repository::SecretBytes,
};
use std::{error::Error, fs};
#[test]
fn activated_os_roots_drive_existing_local_setup_and_configuration_apis()
-> Result<(), Box<dyn Error>> {
let temp = tempfile::tempdir()?;
for name in ["files", "no-backup", "cache"] {
fs::create_dir(temp.path().join(name))?;
}
let storage = PrivateStorage::open(PrivateDirectories {
files: fs::canonicalize(temp.path().join("files"))?,
no_backup: fs::canonicalize(temp.path().join("no-backup"))?,
cache: fs::canonicalize(temp.path().join("cache"))?,
})?;
storage.activate()?;
assert_eq!(
ConfigLoader::system()?.default_path(),
storage.root().join("config.toml")
);
assert_eq!(
MobileShell::load().pages()[0].state(),
MobileShellState::Empty
);
MobileOnboardingOperation::default().setup_local_generated(
"Android private fixture <fixture@example.test>",
SecretBytes::new(b"public-test-passphrase".to_vec()),
false,
)?;
let config = Config::load(None)?;
assert!(storage.status().configured);
assert_eq!(config.vault(), storage.root().join("vault"));
assert_eq!(config.key_material(), storage.root().join("keys"));
assert_eq!(
MobileShell::load().pages()[0].state(),
MobileShellState::Ready
);
assert!(
config
.source()
.strip_prefix(temp.path().join("files"))
.is_err()
);
assert!(
config
.source()
.strip_prefix(temp.path().join("cache"))
.is_err()
);
Ok(())
}
+4 -2
View File
@@ -31,8 +31,10 @@ selected variants belong to the native UI. Actions already use typed enums.
- `MobilePlatformPaths` accepts absolute private data and cache roots, rejects - `MobilePlatformPaths` accepts absolute private data and cache roots, rejects
traversal and overlapping roots, and lets storage derive config/vault/key traversal and overlapping roots, and lets storage derive config/vault/key
paths. Validation does not create or migrate files. #90 owns initialization, paths. This lightweight validator does not create or migrate files. #90's
permissions, relocation, backup and cache policy. `MobilePrivateRuntime` opens the three canonical OS directory capabilities,
activates the shared configuration context, and owns initialization,
permissions, relocation, backup and cache policy; see [private storage](private-storage.md).
- `MobileSecretBackend` performs create/retrieve/replace/delete/lock/unlock on - `MobileSecretBackend` performs create/retrieve/replace/delete/lock/unlock on
a typed locator and protection supplied by storage. Values are opaque encoded a typed locator and protection supplied by storage. Values are opaque encoded
storage records. OS errors are enums. It cannot select accounts, protection, storage records. OS errors are enums. It cannot select accounts, protection,
+6 -4
View File
@@ -6,10 +6,12 @@ apple/Sources/AutoFill/CredentialProviderViewController.swift, crates/apple/src/
and crates/storage/src/mobile*.rs. See [architecture.md](architecture.md) for and crates/storage/src/mobile*.rs. See [architecture.md](architecture.md) for
support, dependency, security and update policy. support, dependency, security and update policy.
**Status:** all Android implementation rows below are pending. No Android app **Status:** #88 records the architecture baseline. The Android shell (#89),
exists at this decision. The table is a complete assignment of work, not a native packaging (#92), shared contract (#91), and private runtime (#90) are
claim of completed parity. #111 must attach screen/component/test evidence to implemented; feature presentation rows remain assigned below. Issue comments
each row. Every issue linked here belongs to milestone 23. record per-issue validation and outstanding test gaps. #111 must attach the
final screen/component/test evidence to each row before claiming full parity.
Every issue linked here belongs to milestone 23.
## Current iPhone and shared-mobile capabilities ## Current iPhone and shared-mobile capabilities
+87
View File
@@ -0,0 +1,87 @@
# Android private storage (#90)
The platform supplies canonical `Context.filesDir`, `noBackupFilesDir` and
`cacheDir` facts off the UI thread. `PrivateStorage` validates existing, distinct
roots and opens directory capabilities. It derives all subordinate paths;
Kotlin never lists, reads or mutates repository contents. `activate()` installs
one immutable process path context so existing Rust configuration/onboarding,
Git, key-transfer and TOTP APIs use these roots. Android calls made before
activation fail instead of consulting HOME, cwd, XDG or public storage.
| Object | Placement | Update / cleanup policy |
| --- | --- | --- |
| pass-format repository | `noBackupFilesDir/ironstorage/vault` | Retained; existing storage/Git atomic mutation rules |
| GPG key material | `noBackupFilesDir/ironstorage/keys` | Retained; Rust crypto import/export only |
| secret-free configuration | `noBackupFilesDir/ironstorage/config.toml` | Atomic Rust writes; relative vault/key paths survive relocation |
| TOTP discovery catalog | `noBackupFilesDir/ironstorage/totp-catalog.toml` | Valid catalogs survive updates and relocation; invalid/oversized/wrong-store catalogs are removed and rebuilt by discovery |
| version / device binding | `noBackupFilesDir/ironstorage/layout.toml` | Atomic version 1 marker; unknown versions require a compatible migration |
| transactional scratch | `noBackupFilesDir/ironstorage/scratch` | Per-owner file locks; startup removes orphaned transactions and skips live owners |
| disposable cache | `cacheDir/ironstorage` | Only this capability is cleared by `clearDisposableCache`; vault/key/catalog data is retained |
| protected binding and credentials | OS secure backend / no-backup ciphertext (#93) | No serialized token or passphrase in configuration or layout metadata |
| `filesDir` | OS root fact only | No durable password-store object is placed here |
Directory opens use safe Rust `rustix` handles with `NOFOLLOW`; names are single
components. Existing vault/key trees reject links and special files. Directories
are 0700; files are 0600, preserving an existing owner executable bit. These are
Unix permission checks inside the Android UID/SELinux sandbox, not promises
about physical media erasure or an untrusted OS. No application process or
handwritten unsafe FFI is introduced.
Configuration must refer to `vault` and key material below `keys`. A corrupt or
missing configuration with remaining data produces typed recovery, never a new
store or automatic repository deletion. Interrupted `.local-setup-*`
transactions are retained because they can contain the previous repository.
Owned orphan `.ironstorage-config-*` and `.ironstorage-atomic-*` files are removed only after taking their
file lock; active atomic writers are left alone. A directory gate protects the
creation-to-owner-lock interval from concurrent startup cleanup. Android atomic files use
private named capabilities and rename because SELinux denies anonymous-file
linking via `/proc`; safe `rustix` locks supply the semantics absent from the
pinned Rust standard library on Android. Missing layout metadata with existing
data is a recovery error, so a device binding cannot be silently discarded. Scratch/cache cleanup unlinks
links without following their targets and cannot descend into the vault.
`verifySecureStorage` uses the OS-mechanics callback from #91 and a storage-owned
`ApplicationBinding` locator. Rust creates an opaque 32-byte device record and
persists only its SHA-256 in layout metadata. A missing or mismatched protected
record for an already bound layout reports `RestoredWithoutKeystore` or
`InvalidSecureBinding`; it never creates a replacement that would conceal key
loss. Locked/unavailable storage has separate recovery kinds. #93 supplies the
real Android Keystore adapter; the contract already executes through the
injected backend and is tested with loss/mismatch fixtures.
Errors distinguish full storage, read-only storage, access denial, missing
roots, concurrent initialization, corrupt configuration/layout and unsupported
layout versions. Failed initialization or writes leave existing repository,
key and configuration data intact. Free space or repair the reported condition
and retry; destructive reset is not an update/recovery operation.
Normal APK/Play updates and process recreation retain no-backup app data.
Uninstall deletes the sandbox and Keystore entries; recovery then requires the
upstream Git repository and GPG key/passphrase or an explicit encrypted export.
Backup/transfer is not an alternative recovery path. The manifest disables
backup, and both legacy backup rules and cloud/device-transfer rules exclude
all nine credential- and device-protected domains. No-backup and cache roots
are intrinsically excluded by Android. See the [Android backup contract](https://developer.android.com/identity/data/autobackup).
Validation includes real GPG fixture byte preservation through updates and
directory relocation, valid/corrupt TOTP catalogs, interrupted writes/setup,
concurrent scratch ownership, link-safe cleanup, device-binding loss/mismatch,
and typed storage-pressure/read-only I/O faults. A separate integration test
activates isolated roots and uses existing local onboarding to create an actual
Git repository and protected GPG key without environment/path overrides.
Instrumented tests inspect installed backup XML/manifest resources and execute
private initialization/recreation/cache clearing on persistent 4 KB and strict
16 KB Android emulators. No physical-device testing is required.
Delivery verification on 2026-10-08: all four required workspace gates passed
(431 Rust tests), Android ktlint/unit/lint gates passed, and each persistent
API 37 emulator (4 KB and strict 16 KB) passed eight native tests and two app
tests covering all 25 shell state/tab combinations and installed backup policy.
The native fixture creates a real protected GPG key and Git repository through
Rust, persists a preference atomically, and survives subsequent APK/test-process
updates. Debug and release install-over/startup tests preserve the layout on both
emulators. All three ABI payloads pass the APK/AAB native audit. Swift private
runtime/redaction tests and 588 shared contract entries pass; iPhone, Watch and
AutoFill simulator builds pass. The persistent iPhone app also launches with its
existing store intact. Backup verification reads installed policy and package
flags; it does not export sandbox data to a transport.
@@ -61,6 +61,33 @@ enum MobileContractProbe {
} }
let request = try validateMobileCredentialRequest(request: MobileCredentialRequest(requestId: "opaque", package: "de.example.app", signingCertificateSha256: String(repeating: "A", count: 64), origins: ["https://EXAMPLE.test:443/"])) let request = try validateMobileCredentialRequest(request: MobileCredentialRequest(requestId: "opaque", package: "de.example.app", signingCertificateSha256: String(repeating: "A", count: 64), origins: ["https://EXAMPLE.test:443/"]))
precondition(request.origins == ["https://example.test"]) precondition(request.origins == ["https://example.test"])
print("Swift native contract: fixtures/icons, callbacks, large payload, redaction, cancellation and concurrency passed") do { try privateStorageProbe() }
catch MobilePrivateStorageFfiError.Failed(let kind) {
print("Swift private storage failed: \(kind)")
exit(1)
} }
print("Swift native contract: fixtures/icons, callbacks, large payload, redaction, cancellation, concurrency and private storage passed")
}
static func privateStorageProbe() throws {
let root = URL(fileURLWithPath: "/private/tmp").appendingPathComponent(UUID().uuidString)
defer { try? FileManager.default.removeItem(at: root) }
for name in ["files", "no-backup", "cache"] {
try FileManager.default.createDirectory(at: root.appendingPathComponent(name), withIntermediateDirectories: true)
}
let directories = MobilePrivateDirectories(
files: root.appendingPathComponent("files").path,
noBackup: root.appendingPathComponent("no-backup").path,
cache: root.appendingPathComponent("cache").path
)
precondition(!String(reflecting: directories).contains(root.path))
let storage = try openMobilePrivateRuntime(directories: directories)
precondition(storage.status().version == 1 && !storage.status().configured)
precondition(storage.shell().pages.first?.state == .empty)
precondition(!String(reflecting: storage.layout()).contains(root.path))
try storage.clearDisposableCache()
let recreated = try openMobilePrivateRuntime(directories: directories)
precondition(recreated.status() == storage.status())
precondition(recreated.layout() == storage.layout())
}
} }