feat(android): implement private Rust storage runtime (#90)
Dependency security audit / rustsec (push) Failing after 23s
Dependency security audit / rustsec (push) Failing after 23s
This commit is contained in:
1 parent
180851087b
commit
874c8139a4
36 files changed
+2845
-36
No files matched your search
Generated
+1
@@ -5415,6 +5415,7 @@ dependencies = [
|
||||
"reqwest 0.13.4",
|
||||
"rqrr",
|
||||
"russh",
|
||||
"rustix 1.1.4",
|
||||
"rustls",
|
||||
"secret-service",
|
||||
"security-framework 3.7.0",
|
||||
|
||||
@@ -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"] }
|
||||
cap-std = "4.0"
|
||||
cap-tempfile = "4.0"
|
||||
rustix = { version = "1.1", features = ["fs"] }
|
||||
clap = { version = "4.6", features = ["derive"] }
|
||||
clap_complete = "4.6"
|
||||
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 de.rfc1437.ironstorage.core.NativeFixtureShellRepository
|
||||
import de.rfc1437.ironstorage.core.ShellRepository
|
||||
import de.rfc1437.ironstorage.platform.privateDirectories
|
||||
import javax.inject.Singleton
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
@@ -21,6 +22,7 @@ object ShellModule {
|
||||
fun shellRepository(@ApplicationContext context: Context): ShellRepository =
|
||||
NativeFixtureShellRepository(
|
||||
context,
|
||||
CoroutineScope(SupervisorJob() + Dispatchers.Main.immediate)
|
||||
CoroutineScope(SupervisorJob() + Dispatchers.Main.immediate),
|
||||
{ privateDirectories(context) }
|
||||
)
|
||||
}
|
||||
@@ -4,4 +4,8 @@
|
||||
<exclude domain="database" path="." />
|
||||
<exclude domain="sharedpref" 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>
|
||||
@@ -5,6 +5,10 @@
|
||||
<exclude domain="database" path="." />
|
||||
<exclude domain="sharedpref" 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>
|
||||
<device-transfer>
|
||||
<exclude domain="root" path="." />
|
||||
@@ -12,5 +16,9 @@
|
||||
<exclude domain="database" path="." />
|
||||
<exclude domain="sharedpref" 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>
|
||||
</data-extraction-rules>
|
||||
+75
-6
@@ -5,8 +5,13 @@ import de.rfc1437.ironstorage.bridge.NativeBridgeException
|
||||
import de.rfc1437.ironstorage.bridge.NativeFailureKind
|
||||
import de.rfc1437.ironstorage.bridge.NativeRuntime
|
||||
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.MobileTab
|
||||
import de.rfc1437.ironstorage.bridge.generated.mobilePrivateStorageFailure
|
||||
import java.util.concurrent.atomic.AtomicLong
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
@@ -17,12 +22,13 @@ import kotlinx.coroutines.flow.update
|
||||
import kotlinx.coroutines.launch
|
||||
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(
|
||||
private val scope: CoroutineScope,
|
||||
private val load: suspend (ShellState) -> ShellSnapshot
|
||||
) : 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()))
|
||||
override val snapshot: StateFlow<ShellSnapshot> = mutableSnapshot
|
||||
private val revision = AtomicLong()
|
||||
@@ -46,17 +52,52 @@ class NativeFixtureShellRepository internal constructor(
|
||||
ShellSnapshot(ShellState.Error, emptyList(), nativeFailure = error.kind.toFacade())
|
||||
}
|
||||
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)
|
||||
var storage: MobilePrivateRuntime? = null
|
||||
var firstLoad = true
|
||||
return { state ->
|
||||
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 ->
|
||||
ShellPage(
|
||||
page.tab.toFacade(),
|
||||
@@ -68,11 +109,39 @@ private fun nativeLoader(context: Context): suspend (ShellState) -> ShellSnapsho
|
||||
}
|
||||
val actual = shell.pages.firstOrNull()?.state
|
||||
?: 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) {
|
||||
MobileShellState.LOADING -> ShellState.Loading
|
||||
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 pages: List<ShellPage>,
|
||||
val selectedTab: ShellTab = ShellTab.Home,
|
||||
val nativeFailure: NativeFailure? = null
|
||||
val nativeFailure: NativeFailure? = null,
|
||||
val storageRecovery: StorageRecovery? = null
|
||||
)
|
||||
|
||||
enum class NativeFailure {
|
||||
|
||||
+11
@@ -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
|
||||
)
|
||||
@@ -31,7 +31,7 @@ kotlin {
|
||||
}
|
||||
|
||||
dependencies {
|
||||
implementation("${libs.jna.get().module}:${libs.versions.jna.get()}@aar")
|
||||
implementation(variantOf(libs.jna) { artifactType("aar") })
|
||||
implementation(libs.annotation)
|
||||
testImplementation(libs.junit)
|
||||
androidTestImplementation(libs.android.test.runner)
|
||||
|
||||
+69
@@ -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.os.Build
|
||||
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.MobileShellState
|
||||
import de.rfc1437.ironstorage.bridge.generated.mobileShellFixture
|
||||
import de.rfc1437.ironstorage.bridge.generated.openMobilePrivateRuntime
|
||||
import de.rfc1437.ironstorage.bridge.generated.uniffiEnsureInitialized
|
||||
import java.io.File
|
||||
import java.io.IOException
|
||||
@@ -24,6 +28,11 @@ class NativeRuntime(context: Context) {
|
||||
mobileShellFixture(state)
|
||||
}
|
||||
|
||||
fun privateStorage(directories: MobilePrivateDirectories): MobilePrivateRuntime = call {
|
||||
initialize()
|
||||
openMobilePrivateRuntime(directories)
|
||||
}
|
||||
|
||||
private fun initialize() = synchronized(lock) {
|
||||
initializationFailure?.let { throw NativeBridgeException(it) }
|
||||
if (!initialized) {
|
||||
@@ -51,6 +60,8 @@ class NativeRuntime(context: Context) {
|
||||
fail(error.kind)
|
||||
} catch (error: CancellationException) {
|
||||
throw error
|
||||
} catch (error: MobilePrivateStorageFfiException) {
|
||||
throw error
|
||||
} catch (error: NativeBindingMismatchException) {
|
||||
fail(error.kind)
|
||||
} catch (_: IOException) {
|
||||
|
||||
@@ -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.
|
||||
*/
|
||||
@@ -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 var selectedTab: MobileTab
|
||||
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
|
||||
enum MobileSecretError: Swift.Error, Equatable, Hashable, Foundation.LocalizedError {
|
||||
|
||||
@@ -8637,6 +9268,7 @@ public func FfiConverterTypeMobileSecretError_lower(_ value: MobileSecretError)
|
||||
|
||||
public enum MobileSecretLocator: Equatable, Hashable {
|
||||
|
||||
case applicationBinding
|
||||
case openPgpPassphrase(fingerprint: String
|
||||
)
|
||||
case httpsGitCredential(serverId: String, applicationId: String
|
||||
@@ -8664,13 +9296,15 @@ public struct FfiConverterTypeMobileSecretLocator: FfiConverterRustBuffer {
|
||||
let variant: Int32 = try readInt(&buf)
|
||||
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
|
||||
@@ -8681,19 +9315,23 @@ public struct FfiConverterTypeMobileSecretLocator: FfiConverterRustBuffer {
|
||||
switch value {
|
||||
|
||||
|
||||
case let .openPgpPassphrase(fingerprint):
|
||||
case .applicationBinding:
|
||||
writeInt(&buf, Int32(1))
|
||||
|
||||
|
||||
case let .openPgpPassphrase(fingerprint):
|
||||
writeInt(&buf, Int32(2))
|
||||
FfiConverterString.write(fingerprint, into: &buf)
|
||||
|
||||
|
||||
case let .httpsGitCredential(serverId,applicationId):
|
||||
writeInt(&buf, Int32(2))
|
||||
writeInt(&buf, Int32(3))
|
||||
FfiConverterString.write(serverId, into: &buf)
|
||||
FfiConverterString.write(applicationId, into: &buf)
|
||||
|
||||
|
||||
case let .sshKeyPassphrase(fingerprint):
|
||||
writeInt(&buf, Int32(3))
|
||||
writeInt(&buf, Int32(4))
|
||||
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)
|
||||
@_documentation(visibility: private)
|
||||
#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 {
|
||||
case ok
|
||||
@@ -10089,6 +10767,12 @@ private let initializationResult: InitializationResult = {
|
||||
if (uniffi_ironstorage_mobile_checksum_func_validate_mobile_credential_request() != 60738) {
|
||||
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) {
|
||||
return InitializationResult.apiChecksumMismatch
|
||||
}
|
||||
@@ -10323,6 +11007,27 @@ private let initializationResult: InitializationResult = {
|
||||
if (uniffi_ironstorage_mobile_checksum_method_mobilesecretbackend_unlock() != 2737) {
|
||||
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()
|
||||
uniffiCallbackInitMobileClock()
|
||||
@@ -10529,6 +11234,22 @@ extension MobilePreferences: CustomStringConvertible, CustomDebugStringConvertib
|
||||
public var description: String { "MobilePreferences([REDACTED])" }
|
||||
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 {
|
||||
public var description: String { "MobileSecretError([REDACTED])" }
|
||||
public var debugDescription: String { description }
|
||||
|
||||
@@ -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
|
||||
);
|
||||
#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
|
||||
#define UNIFFI_FFIDEF_UNIFFI_IRONSTORAGE_MOBILE_FN_FUNC_MOBILE_AUTHENTICATION
|
||||
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
|
||||
);
|
||||
#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
|
||||
#define UNIFFI_FFIDEF_FFI_IRONSTORAGE_MOBILE_RUSTBUFFER_ALLOC
|
||||
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
|
||||
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
|
||||
#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
|
||||
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
|
||||
#ifndef UNIFFI_FFIDEF_FFI_IRONSTORAGE_MOBILE_UNIFFI_CONTRACT_VERSION
|
||||
|
||||
@@ -3,6 +3,9 @@
|
||||
|
||||
//! Shared UniFFI contract for native phone presentation. Watch exports are opt-in.
|
||||
|
||||
mod private_storage;
|
||||
pub use private_storage::*;
|
||||
|
||||
use std::{
|
||||
error::Error,
|
||||
fmt,
|
||||
|
||||
@@ -70,6 +70,7 @@ pub fn mobile_platform_layout(
|
||||
|
||||
#[derive(Clone, uniffi::Enum)]
|
||||
pub enum MobileSecretLocator {
|
||||
ApplicationBinding,
|
||||
OpenPgpPassphrase {
|
||||
fingerprint: String,
|
||||
},
|
||||
@@ -85,6 +86,7 @@ pub enum MobileSecretLocator {
|
||||
impl From<&SecretLocator> for MobileSecretLocator {
|
||||
fn from(locator: &SecretLocator) -> Self {
|
||||
match locator {
|
||||
SecretLocator::ApplicationBinding => Self::ApplicationBinding,
|
||||
SecretLocator::OpenPgpPassphrase { fingerprint } => Self::OpenPgpPassphrase {
|
||||
fingerprint: fingerprint.clone(),
|
||||
},
|
||||
@@ -232,7 +234,7 @@ impl SecretStoreBackend for SecretBackendAdapter {
|
||||
pub trait MobileClock: Send + Sync {
|
||||
fn monotonic_milliseconds(&self) -> u64;
|
||||
}
|
||||
struct ClockAdapter(Arc<dyn MobileClock>);
|
||||
pub(crate) struct ClockAdapter(pub Arc<dyn MobileClock>);
|
||||
impl AuthenticationClock for ClockAdapter {
|
||||
fn now(&self) -> Duration {
|
||||
Duration::from_millis(self.0.monotonic_milliseconds())
|
||||
|
||||
@@ -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()
|
||||
}
|
||||
@@ -30,6 +30,7 @@ full = [
|
||||
"dep:regex",
|
||||
"dep:reqwest",
|
||||
"dep:rustls",
|
||||
"dep:rustix",
|
||||
"dep:rqrr",
|
||||
"dep:secret-service",
|
||||
"dep:security-framework",
|
||||
@@ -46,6 +47,7 @@ watch = []
|
||||
[dependencies]
|
||||
cap-std = { workspace = true, optional = true }
|
||||
cap-tempfile = { workspace = true, optional = true }
|
||||
rustix = { workspace = true, optional = true }
|
||||
clap = { workspace = true, optional = true }
|
||||
clap_complete = { workspace = true, optional = true }
|
||||
data-encoding.workspace = true
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -8,11 +8,12 @@ use std::{
|
||||
fs::OpenOptions,
|
||||
io::{self, Write},
|
||||
path::{Component, Path, PathBuf},
|
||||
sync::OnceLock,
|
||||
time::Duration,
|
||||
};
|
||||
|
||||
use crate::atomic_file::TempFile;
|
||||
use cap_std::{ambient_authority, fs::Dir};
|
||||
use cap_tempfile::TempFile;
|
||||
#[cfg(not(any(target_os = "ios", target_os = "watchos")))]
|
||||
use gix::bstr::ByteSlice as _;
|
||||
use serde::Deserialize;
|
||||
@@ -31,6 +32,7 @@ use crate::{
|
||||
const APPLICATION_DIRECTORY: &str = "ironstorage";
|
||||
const CONFIG_FILE: &str = "config.toml";
|
||||
const MAX_CONFIG_BYTES: u64 = 1024 * 1024;
|
||||
static PRIVATE_CONFIG_CONTEXT: OnceLock<ConfigLoader> = OnceLock::new();
|
||||
|
||||
/// Validated application configuration.
|
||||
#[derive(Clone, Debug, PartialEq)]
|
||||
@@ -131,6 +133,10 @@ impl ConfigSettings {
|
||||
impl Config {
|
||||
/// Load an explicit configuration file, or the native per-user default.
|
||||
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)
|
||||
}
|
||||
|
||||
@@ -713,6 +719,10 @@ impl Config {
|
||||
let contents = toml::to_string_pretty(&self.document).map_err(|_| ConfigError::Write {
|
||||
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)
|
||||
.find_map(|attempt| {
|
||||
let path = parent.join(format!(
|
||||
@@ -739,10 +749,29 @@ impl Config {
|
||||
path: self.source.clone(),
|
||||
})?;
|
||||
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
|
||||
.write_all(contents.as_bytes())
|
||||
.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);
|
||||
let _ = fs::remove_file(&temporary_path);
|
||||
match installed {
|
||||
@@ -829,11 +858,29 @@ pub struct ConfigLoader {
|
||||
|
||||
impl ConfigLoader {
|
||||
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 =
|
||||
native_config_directory().ok_or(ConfigError::NativeConfigDirectory)?;
|
||||
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 {
|
||||
Self {
|
||||
@@ -2328,7 +2375,10 @@ fn native_config_directory() -> Option<PathBuf> {
|
||||
.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> {
|
||||
match env::var_os("XDG_CONFIG_HOME") {
|
||||
Some(path) if !path.is_empty() && Path::new(&path).is_absolute() => {
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
@@ -5,6 +5,8 @@
|
||||
//!
|
||||
//! This crate is the sole owner of stored and derived password-store objects.
|
||||
|
||||
#[cfg(feature = "full")]
|
||||
mod atomic_file;
|
||||
#[cfg(feature = "full")]
|
||||
pub mod authentication;
|
||||
#[cfg(feature = "full")]
|
||||
@@ -18,6 +20,8 @@ pub mod desktop;
|
||||
#[cfg(feature = "full")]
|
||||
pub mod document;
|
||||
#[cfg(feature = "full")]
|
||||
mod file_lock;
|
||||
#[cfg(feature = "full")]
|
||||
pub mod generate;
|
||||
#[cfg(feature = "full")]
|
||||
pub mod git;
|
||||
@@ -42,6 +46,8 @@ pub mod mobile_passwords;
|
||||
#[cfg(feature = "full")]
|
||||
pub mod mobile_platform;
|
||||
#[cfg(feature = "full")]
|
||||
pub mod mobile_private_storage;
|
||||
#[cfg(feature = "full")]
|
||||
pub mod mobile_totp;
|
||||
pub mod mobile_watch;
|
||||
#[cfg(feature = "full")]
|
||||
|
||||
@@ -151,7 +151,11 @@ pub struct MobileShell {
|
||||
|
||||
impl MobileShell {
|
||||
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(_) => Self::new(config.mobile_tab(), MobileShellState::Ready, None),
|
||||
Err(RepositoryError::Io {
|
||||
@@ -179,6 +183,10 @@ impl MobileShell {
|
||||
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 {
|
||||
self.selected_tab
|
||||
}
|
||||
|
||||
@@ -2,8 +2,8 @@
|
||||
|
||||
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_tempfile::TempFile;
|
||||
use data_encoding::{BASE64URL_NOPAD, HEXLOWER};
|
||||
use sha2::{Digest as _, Sha256};
|
||||
|
||||
|
||||
File diff suppressed because it is too large.
Load diff
@@ -4,7 +4,7 @@ use std::{
|
||||
collections::{BTreeMap, BTreeSet},
|
||||
error::Error,
|
||||
fmt, fs,
|
||||
io::Write as _,
|
||||
io::{Read as _, Write as _},
|
||||
path::Path,
|
||||
sync::{
|
||||
Mutex,
|
||||
@@ -12,8 +12,8 @@ use std::{
|
||||
},
|
||||
};
|
||||
|
||||
use crate::atomic_file::TempFile;
|
||||
use cap_std::{ambient_authority, fs::Dir};
|
||||
use cap_tempfile::TempFile;
|
||||
use data_encoding::HEXLOWER;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use sha2::{Digest as _, Sha256};
|
||||
@@ -27,6 +27,7 @@ use crate::{
|
||||
};
|
||||
|
||||
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.
|
||||
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>) {
|
||||
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 (
|
||||
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(
|
||||
path: &Path,
|
||||
store: &str,
|
||||
|
||||
@@ -3,9 +3,9 @@
|
||||
use std::{error::Error, fmt, path::Path};
|
||||
|
||||
#[cfg(feature = "full")]
|
||||
use cap_std::{ambient_authority, fs::Dir};
|
||||
use crate::atomic_file::TempFile;
|
||||
#[cfg(feature = "full")]
|
||||
use cap_tempfile::TempFile;
|
||||
use cap_std::{ambient_authority, fs::Dir};
|
||||
use data_encoding::HEXLOWER;
|
||||
#[cfg(feature = "full")]
|
||||
use data_encoding::HEXLOWER_PERMISSIVE;
|
||||
|
||||
@@ -14,9 +14,9 @@ use std::collections::BTreeSet;
|
||||
#[cfg(test)]
|
||||
use std::sync::atomic::{AtomicUsize, Ordering};
|
||||
|
||||
use crate::atomic_file::TempFile;
|
||||
pub use crate::secret::SecretBytes;
|
||||
use cap_std::{ambient_authority, fs::Dir};
|
||||
use cap_tempfile::TempFile;
|
||||
|
||||
const ENTRY_EXTENSION: &str = "gpg";
|
||||
const RECIPIENT_FILE: &str = ".gpg-id";
|
||||
|
||||
@@ -149,6 +149,7 @@ impl fmt::Debug for SecretReference {
|
||||
/// server/application pair can retrieve its account without TOML metadata.
|
||||
#[derive(Clone, Eq, Ord, PartialEq, PartialOrd)]
|
||||
pub enum SecretLocator {
|
||||
ApplicationBinding,
|
||||
OpenPgpPassphrase {
|
||||
fingerprint: String,
|
||||
},
|
||||
@@ -164,6 +165,7 @@ pub enum SecretLocator {
|
||||
impl fmt::Debug for SecretLocator {
|
||||
fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
match self {
|
||||
Self::ApplicationBinding => formatter.write_str("SecretLocator::ApplicationBinding"),
|
||||
Self::OpenPgpPassphrase { .. } => {
|
||||
formatter.write_str("SecretLocator::OpenPgpPassphrase([REDACTED])")
|
||||
}
|
||||
@@ -180,6 +182,7 @@ impl fmt::Debug for SecretLocator {
|
||||
impl SecretLocator {
|
||||
fn service_and_user(&self) -> (&'static str, String) {
|
||||
match self {
|
||||
Self::ApplicationBinding => ("de.rfc1437.ironstorage.private-binding", "device".into()),
|
||||
Self::OpenPgpPassphrase { fingerprint } => {
|
||||
(OPENPGP_PASSPHRASE_SERVICE, fingerprint.clone())
|
||||
}
|
||||
@@ -246,6 +249,7 @@ impl SecretProtectionPolicy {
|
||||
SecretLocator::OpenPgpPassphrase { .. } => self.openpgp,
|
||||
SecretLocator::HttpsGitCredential { .. } => self.git,
|
||||
SecretLocator::SshKeyPassphrase { .. } => self.git,
|
||||
SecretLocator::ApplicationBinding => SecretProtection::DeviceUnlocked,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -10,8 +10,8 @@ use std::{
|
||||
time::Duration,
|
||||
};
|
||||
|
||||
use crate::atomic_file::TempFile;
|
||||
use cap_std::{ambient_authority, fs::Dir};
|
||||
use cap_tempfile::TempFile;
|
||||
use hmac::{Hmac, Mac as _};
|
||||
use russh::{
|
||||
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(())
|
||||
}
|
||||
@@ -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
|
||||
traversal and overlapping roots, and lets storage derive config/vault/key
|
||||
paths. Validation does not create or migrate files. #90 owns initialization,
|
||||
permissions, relocation, backup and cache policy.
|
||||
paths. This lightweight validator does not create or migrate files. #90's
|
||||
`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
|
||||
a typed locator and protection supplied by storage. Values are opaque encoded
|
||||
storage records. OS errors are enums. It cannot select accounts, protection,
|
||||
|
||||
@@ -6,10 +6,12 @@ apple/Sources/AutoFill/CredentialProviderViewController.swift, crates/apple/src/
|
||||
and crates/storage/src/mobile*.rs. See [architecture.md](architecture.md) for
|
||||
support, dependency, security and update policy.
|
||||
|
||||
**Status:** all Android implementation rows below are pending. No Android app
|
||||
exists at this decision. The table is a complete assignment of work, not a
|
||||
claim of completed parity. #111 must attach screen/component/test evidence to
|
||||
each row. Every issue linked here belongs to milestone 23.
|
||||
**Status:** #88 records the architecture baseline. The Android shell (#89),
|
||||
native packaging (#92), shared contract (#91), and private runtime (#90) are
|
||||
implemented; feature presentation rows remain assigned below. Issue comments
|
||||
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
|
||||
|
||||
|
||||
@@ -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/"]))
|
||||
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())
|
||||
}
|
||||
|
||||
}
|
||||
Reference in new issue
Block a user