Prepare Apple App Store distribution (#59)

This commit is contained in:
2026-08-16 17:55:59 +02:00
parent b50e47ab73
commit f894d43eb6
46 changed files with 20694 additions and 166 deletions

View File

@@ -0,0 +1,69 @@
# Apple App Store distribution
Use `distribution/` only as ignored local staging. Never store Apple credentials,
signing material, review credentials, GPG material, application tokens, or
private diagnostics in the repository or release logs.
## Release identity
- iPhone application: `de.rfc1437.ironstorage`
- AutoFill extension: `de.rfc1437.ironstorage.autofill`
- Apple Watch companion: `de.rfc1437.ironstorage.watch`
- Apple Developer team: `MU22FMRGK8`
The submitted archive must embed the Watch application beneath the iPhone
application. An iPhone-only archive is not a releasable IronStorage build.
## Prepare and submit
1. Generate the project and release attribution, then run the release verifier:
```sh
cargo metadata --format-version 1 --filter-platform aarch64-apple-ios \
> /private/tmp/ironstorage-ios-metadata.json
cargo run -p ironstorage-apple-release -- licenses \
/private/tmp/ironstorage-ios-metadata.json \
apple/Resources/App/ThirdPartyLicenses.txt
cargo run -p ironstorage-apple-release -- verify .
```
2. Run the repository gates and build both simulator applications. Validate the
local-first flow with synthetic data: generate or import a GPG key, create
folders and encrypted entries, read TOTP codes, select Watch entries, and
verify the Watch list and selected-code progress display.
3. Create the production archive with normal App Store distribution signing:
```sh
cd apple
xcodegen generate
xcodebuild -project IronStorage.xcodeproj -scheme IronStorage \
-configuration Release -destination 'generic/platform=iOS' \
-archivePath /private/tmp/IronStorage.xcarchive archive
```
4. Inspect the archive before upload. The iPhone, AutoFill, and Watch bundle
identifiers and versions must match the metadata; the Watch app must be
embedded; privacy manifests and license resources must be present; the
archive must use distribution signing and must not contain `get-task-allow`,
simulator artifacts, synthetic demo data, private endpoints, or secrets.
5. Export and upload with the checked-in App Store Connect export options:
```sh
xcodebuild -exportArchive \
-archivePath /private/tmp/IronStorage.xcarchive \
-exportPath /private/tmp/IronStorage-AppStore \
-exportOptionsPlist AppStore/ExportOptions.plist
```
6. In App Store Connect, complete the listing from `metadata.toml`, upload the
reviewed screenshots, answer privacy and encryption questions, provide
local-first review instructions, select the processed build, and submit it
for review. Do not provide a production password store or real credentials.
7. After approval, install the public release from the App Store on the paired
iPhone and Apple Watch. Verify local onboarding, key overwrite confirmation,
local Git history, folders, encrypted entries, Face ID, TOTP, optional HTTPS
pull and push, Watch snapshot delivery, the Watch entry-only list, and the
larger selected code with its validity progress indicator.
8. Verify an App Store update in place. Verify the documented uninstall and
reinstall Keychain lifecycle only with disposable test data; never erase the
paired physical device or its unrelated IronStorage data.

View File

@@ -0,0 +1,20 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>destination</key>
<string>upload</string>
<key>manageAppVersionAndBuildNumber</key>
<false/>
<key>method</key>
<string>app-store-connect</string>
<key>signingStyle</key>
<string>automatic</string>
<key>stripSwiftSymbols</key>
<true/>
<key>teamID</key>
<string>MU22FMRGK8</string>
<key>uploadSymbols</key>
<true/>
</dict>
</plist>

View File

@@ -0,0 +1,52 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width,initial-scale=1">
<meta name="color-scheme" content="light dark">
<title>rfc1437 Apps</title>
<link rel="stylesheet" href="/assets/pico.pumpkin.min.css">
<link rel="stylesheet" href="/assets/bds.css">
<style>.app-header{display:flex;align-items:center;gap:1rem}.app-header img{width:5rem;height:5rem;border-radius:1.1rem}.gallery{display:grid;grid-template-columns:repeat(auto-fit,minmax(14rem,1fr));gap:1rem}figure{margin:0}figure img{display:block;width:100%;height:auto;border-radius:1.5rem}figcaption{margin-top:.5rem;color:var(--pico-muted-color)}</style>
</head>
<body><main>
<header><h1 class="archive-heading">rfc1437 Apps</h1><p>Small, focused apps published by <a href="https://rfc1437.de/">rfc1437</a>.</p></header>
<nav class="blog-menu" aria-label="Site navigation"><ul class="blog-menu-list"><li class="blog-menu-item"><a class="blog-menu-link" href="https://rfc1437.de/">Home</a></li><li class="blog-menu-item"><span class="blog-menu-link" aria-current="page">Apps</span></li></ul></nav>
<article class="post">
<h2>Install</h2>
<p>Gotcha is available through the rfc1437 AltStore PAL source at <code>https://rfc1437.de/apps/source.json</code>.</p>
<p>IronStorage is being prepared for the Apple App Store so its iPhone app and Apple Watch companion install together through the supported App Store path.</p>
</article>
<section><h2>Apps</h2>
<article class="post">
<header class="app-header"><img src="gotcha/icon.png" alt="Gotcha app icon"><div><h3>Gotcha</h3><p>Native Gitea and Forgejo on iPhone.</p></div></header>
<p>Follow repositories, issues, pull requests, commits, diffs, and milestones on servers you control.</p>
<nav><a href="gotcha/privacy/">Privacy</a> · <a href="https://git.rfc1437.de/hugo/Gotcha">Source</a> · <a href="https://git.rfc1437.de/hugo/Gotcha/issues">Support</a></nav>
<div class="gallery">
<figure><img src="gotcha/screenshots/01-home-activity.png" alt="Gotcha activity"><figcaption>Activity</figcaption></figure>
<figure><img src="gotcha/screenshots/02-issues.png" alt="Gotcha issues"><figcaption>Issues</figcaption></figure>
<figure><img src="gotcha/screenshots/03-commit-history.png" alt="Gotcha commits"><figcaption>Commits</figcaption></figure>
<figure><img src="gotcha/screenshots/04-diff.png" alt="Gotcha diff"><figcaption>Diffs</figcaption></figure>
<figure><img src="gotcha/screenshots/05-pull-request.png" alt="Gotcha pull request"><figcaption>Pull requests</figcaption></figure>
<figure><img src="gotcha/screenshots/06-milestones.png" alt="Gotcha milestones"><figcaption>Milestones</figcaption></figure>
</div>
</article>
<article class="post">
<header class="app-header"><img src="ironstorage/icon.png" alt="IronStorage app icon"><div><h3>IronStorage</h3><p>Native pass and pass-otp on iPhone and Apple Watch.</p></div></header>
<p>Generate or import a protected GPG key, keep local Git history, create encrypted entries and TOTP codes, and add optional HTTPS synchronization later.</p>
<p><strong>Apple App Store release in preparation.</strong></p>
<nav><a href="ironstorage/privacy/">Privacy</a> · <a href="https://git.rfc1437.de/hugo/IronStorage">Source</a> · <a href="https://git.rfc1437.de/hugo/IronStorage/issues">Support</a></nav>
<div class="gallery">
<figure><img src="ironstorage/screenshots/01-home.png" alt="IronStorage local Git home"><figcaption>Local Git status</figcaption></figure>
<figure><img src="ironstorage/screenshots/02-passwords.png" alt="IronStorage password folders"><figcaption>Passwords and folders</figcaption></figure>
<figure><img src="ironstorage/screenshots/03-totp.png" alt="IronStorage TOTP list"><figcaption>TOTP and Watch selection</figcaption></figure>
<figure><img src="ironstorage/screenshots/04-totp-detail.png" alt="IronStorage TOTP detail"><figcaption>Live TOTP detail</figcaption></figure>
<figure><img src="ironstorage/screenshots/05-preferences.png" alt="IronStorage preferences"><figcaption>Local-first preferences</figcaption></figure>
<figure><img src="ironstorage/screenshots/06-search.png" alt="IronStorage search"><figcaption>Password search</figcaption></figure>
<figure><img src="ironstorage/screenshots/watch-01-list.png" alt="IronStorage Watch entry list"><figcaption>Watch entry list</figcaption></figure>
<figure><img src="ironstorage/screenshots/watch-02-detail.png" alt="IronStorage Watch code detail"><figcaption>Watch code and progress</figcaption></figure>
</div>
</article>
</section>
<footer><small>Both apps are free and open-source software. Each listing identifies its supported distribution channel.</small></footer>
</main></body></html>

26
apple/AppStore/index.html Normal file
View File

@@ -0,0 +1,26 @@
<!doctype html>
<html lang="en">
<meta charset="utf-8">
<meta name="viewport" content="width=device-width,initial-scale=1">
<meta name="color-scheme" content="light dark">
<title>IronStorage</title>
<style>body{font:17px system-ui;line-height:1.55;max-width:64rem;margin:3rem auto;padding:0 1rem;color:#1d1d1f}header{display:flex;align-items:center;gap:1rem}header img{width:6rem;border-radius:1.3rem}.gallery{display:grid;grid-template-columns:repeat(auto-fit,minmax(14rem,1fr));gap:1rem}.gallery img{width:100%;border-radius:1.5rem}nav{margin:1.5rem 0}</style>
<header><img src="icon.png" alt="IronStorage app icon"><div><h1>IronStorage</h1><p>Native pass and pass-otp for iPhone and Apple Watch.</p></div></header>
<p>Start locally with a protected GPG key and versioned password store. Create folders and encrypted entries on your iPhone, generate TOTP codes, and optionally connect an HTTPS Git remote later.</p>
<p>The Apple Watch companion receives only the TOTP entries you explicitly share.</p>
<p><strong>Apple App Store release in preparation.</strong></p>
<nav><a href="privacy/">Privacy</a> · <a href="https://git.rfc1437.de/hugo/IronStorage">Source code</a> · <a href="https://git.rfc1437.de/hugo/IronStorage/issues">Support</a></nav>
<div class="gallery">
<img src="screenshots/01-home.png" alt="Local Git status">
<img src="screenshots/02-passwords.png" alt="Password entries and folders">
<img src="screenshots/03-totp.png" alt="TOTP entries selected for Apple Watch">
<img src="screenshots/04-totp-detail.png" alt="Live TOTP detail">
<img src="screenshots/05-preferences.png" alt="Local-first preferences">
<img src="screenshots/06-search.png" alt="Password search">
</div>
<h2>Apple Watch</h2>
<div class="gallery">
<img src="screenshots/watch-01-list.png" alt="Apple Watch TOTP entry list">
<img src="screenshots/watch-02-detail.png" alt="Apple Watch selected code and progress bar">
</div>
</html>

View File

@@ -0,0 +1,74 @@
[app]
name = "IronStorage"
bundle_identifier = "de.rfc1437.ironstorage"
developer_name = "rfc1437"
subtitle = "Native pass on iPhone"
description = "IronStorage is a native iPhone and Apple Watch client for pass and pass-otp password stores. Start entirely on-device with a protected GPG key and local Git history, then add optional HTTPS Git synchronization whenever you want it."
keywords = "password,gpg,pass,totp,git,security,watch"
icon_file = "apple/Assets.xcassets/AppIcon.appiconset/AppIcon.png"
category = "utilities"
privacy_url = "https://rfc1437.de/apps/ironstorage/privacy/"
support_url = "https://git.rfc1437.de/hugo/IronStorage/issues"
marketing_url = "https://rfc1437.de/apps/ironstorage/"
age_rating = "4+"
[release]
version = "0.1.0"
build = "1"
minimum_ios = "17.0"
notes = "Initial iPhone and Apple Watch release with local GPG key generation, pass-compatible local Git history, optional HTTPS synchronization, password folders, and explicitly shared TOTP codes on Apple Watch."
[[screenshots]]
file = "apple/AppStore/screenshots/home.png"
caption = "Local Git history works before a sync remote is configured"
width = 1206
height = 2622
[[screenshots]]
file = "apple/AppStore/screenshots/passwords.png"
caption = "Password entries and folders stay organized on device"
width = 1206
height = 2622
[[screenshots]]
file = "apple/AppStore/screenshots/totp.png"
caption = "TOTP entries are shared with Apple Watch only when selected"
width = 1206
height = 2622
[[screenshots]]
file = "apple/AppStore/screenshots/totp-detail.png"
caption = "Live TOTP codes include an exact validity indicator"
width = 1206
height = 2622
[[screenshots]]
file = "apple/AppStore/screenshots/preferences.png"
caption = "Generate or import a GPG key and add optional HTTPS sync later"
width = 1206
height = 2622
[[screenshots]]
file = "apple/AppStore/screenshots/search.png"
caption = "Search encrypted password entries by name or folder"
width = 1206
height = 2622
[[watch_screenshots]]
file = "apple/AppStore/screenshots/watch-list.png"
caption = "The Watch list shows only explicitly shared TOTP entries"
width = 416
height = 496
[[watch_screenshots]]
file = "apple/AppStore/screenshots/watch-detail.png"
caption = "The selected Watch code is large with a validity progress bar"
width = 416
height = 496
[content]
advertising = false
account_creation = false
digital_purchases = false
tracking = false
user_generated_content = false

View File

@@ -0,0 +1,18 @@
<!doctype html>
<html lang="en">
<meta charset="utf-8">
<meta name="viewport" content="width=device-width,initial-scale=1">
<title>IronStorage Privacy</title>
<style>body{font:17px system-ui;line-height:1.55;max-width:44rem;margin:3rem auto;padding:0 1rem;color:#1d1d1f}h1,h2{line-height:1.2}</style>
<h1>IronStorage Privacy</h1>
<p>Last updated: 16 August 2026</p>
<p>IronStorage does not collect analytics, advertising identifiers, diagnostics, or personal data for the developer, and it does not track you.</p>
<h2>Data on your device</h2>
<p>Your local Git repository and encrypted password entries remain on your device. Application tokens, imported or generated private-key material, and protected unlock data use Apple Keychain storage. Camera frames used to scan a key-transfer QR code are processed on the device and are not retained by IronStorage.</p>
<h2>Network access</h2>
<p>IronStorage works without a network service. If you later configure an HTTPS Git server, it connects only to that server using the credentials you provide to pull and push your password store. The developer does not operate an IronStorage service and does not receive this traffic.</p>
<h2>Deletion</h2>
<p>Remove configured credentials and key material in IronStorage before uninstalling when you want them deleted immediately. Keychain items otherwise follow Apples secure-storage lifecycle and may survive an app reinstall.</p>
<h2>Support</h2>
<p>Questions and issues can be filed at <a href="https://git.rfc1437.de/hugo/IronStorage/issues">the IronStorage issue tracker</a>.</p>
</html>

Binary file not shown.

After

Width:  |  Height:  |  Size: 235 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 205 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 364 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 148 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 213 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 221 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 35 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 45 KiB

View File

@@ -17,9 +17,11 @@
<key>CFBundlePackageType</key>
<string>XPC!</string>
<key>CFBundleShortVersionString</key>
<string>1.0</string>
<string>$(MARKETING_VERSION)</string>
<key>CFBundleVersion</key>
<string>1</string>
<string>$(CURRENT_PROJECT_VERSION)</string>
<key>ITSAppUsesNonExemptEncryption</key>
<false/>
<key>NSExtension</key>
<dict>
<key>NSExtensionPointIdentifier</key>

View File

@@ -619,6 +619,8 @@ public protocol MobileAuthenticationProtocol: AnyObject, Sendable {
func copyTotpCode(path: String, unixSeconds: UInt64) throws -> MobileEntryCopy
func createDirectory(parent: String, name: String) throws
func discardEntryEditor(editor: UInt64) throws
func entryEditor(editor: UInt64) throws -> MobileEntryEditorPage
@@ -823,6 +825,16 @@ open func copyTotpCode(path: String, unixSeconds: UInt64)throws -> MobileEntryC
})
}
open func createDirectory(parent: String, name: String)throws {try rustCallWithError(FfiConverterTypeMobileAuthenticationFfiError_lift) {
uniffiCallStatus in
uniffi_ironstorage_apple_fn_method_mobileauthentication_create_directory(
self.uniffiCloneHandle(),
FfiConverterString.lower(parent),
FfiConverterString.lower(name),uniffiCallStatus
)
}
}
open func discardEntryEditor(editor: UInt64)throws {try rustCallWithError(FfiConverterTypeMobileAuthenticationFfiError_lift) {
uniffiCallStatus in
uniffi_ironstorage_apple_fn_method_mobileauthentication_discard_entry_editor(
@@ -1551,7 +1563,7 @@ public protocol MobileKeyTransferImportProtocol: AnyObject, Sendable {
func addFrame(payload: String) throws -> MobileKeyTransferProgress
func `import`(passphrase: String?, makeDefault: Bool) throws -> MobileKeyTransferOutcome
func `import`(passphrase: String?, makeDefault: Bool, overwrite: Bool) throws -> MobileKeyTransferOutcome
}
open class MobileKeyTransferImport: MobileKeyTransferImportProtocol, @unchecked Sendable {
@@ -1617,13 +1629,14 @@ open func addFrame(payload: String)throws -> MobileKeyTransferProgress {
})
}
open func `import`(passphrase: String?, makeDefault: Bool)throws -> MobileKeyTransferOutcome {
open func `import`(passphrase: String?, makeDefault: Bool, overwrite: Bool)throws -> MobileKeyTransferOutcome {
return try FfiConverterTypeMobileKeyTransferOutcome_lift(try rustCallWithError(FfiConverterTypeMobileKeyTransferFfiError_lift) {
uniffiCallStatus in
uniffi_ironstorage_apple_fn_method_mobilekeytransferimport_import(
self.uniffiCloneHandle(),
FfiConverterOptionString.lower(passphrase),
FfiConverterBool.lower(makeDefault),uniffiCallStatus
FfiConverterBool.lower(makeDefault),
FfiConverterBool.lower(overwrite),uniffiCallStatus
)
})
}
@@ -1678,10 +1691,142 @@ public func FfiConverterTypeMobileKeyTransferImport_lower(_ value: MobileKeyTran
public protocol MobileLocalOnboardingOperationProtocol: AnyObject, Sendable {
func cancel()
func generate(userId: String, passphrase: String, replaceExistingKey: Bool) throws -> MobileOnboardingOutcome
}
open class MobileLocalOnboardingOperation: MobileLocalOnboardingOperationProtocol, @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_apple_fn_clone_mobilelocalonboardingoperation(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_apple_fn_free_mobilelocalonboardingoperation(handle, $0) }
}
open func cancel() {try! rustCall() {
uniffiCallStatus in
uniffi_ironstorage_apple_fn_method_mobilelocalonboardingoperation_cancel(
self.uniffiCloneHandle(),uniffiCallStatus
)
}
}
open func generate(userId: String, passphrase: String, replaceExistingKey: Bool)throws -> MobileOnboardingOutcome {
return try FfiConverterTypeMobileOnboardingOutcome_lift(try rustCallWithError(FfiConverterTypeMobileOnboardingFfiError_lift) {
uniffiCallStatus in
uniffi_ironstorage_apple_fn_method_mobilelocalonboardingoperation_generate(
self.uniffiCloneHandle(),
FfiConverterString.lower(userId),
FfiConverterString.lower(passphrase),
FfiConverterBool.lower(replaceExistingKey),uniffiCallStatus
)
})
}
}
#if swift(>=5.8)
@_documentation(visibility: private)
#endif
public struct FfiConverterTypeMobileLocalOnboardingOperation: FfiConverter {
typealias FfiType = UInt64
typealias SwiftType = MobileLocalOnboardingOperation
public static func lift(_ handle: UInt64) throws -> MobileLocalOnboardingOperation {
return MobileLocalOnboardingOperation(unsafeFromHandle: handle)
}
public static func lower(_ value: MobileLocalOnboardingOperation) -> UInt64 {
return value.uniffiCloneHandle()
}
public static func read(from buf: inout (data: Data, offset: Data.Index)) throws -> MobileLocalOnboardingOperation {
let handle: UInt64 = try readInt(&buf)
return try lift(handle)
}
public static func write(_ value: MobileLocalOnboardingOperation, into buf: inout [UInt8]) {
writeInt(&buf, lower(value))
}
}
#if swift(>=5.8)
@_documentation(visibility: private)
#endif
public func FfiConverterTypeMobileLocalOnboardingOperation_lift(_ handle: UInt64) throws -> MobileLocalOnboardingOperation {
return try FfiConverterTypeMobileLocalOnboardingOperation.lift(handle)
}
#if swift(>=5.8)
@_documentation(visibility: private)
#endif
public func FfiConverterTypeMobileLocalOnboardingOperation_lower(_ value: MobileLocalOnboardingOperation) -> UInt64 {
return FfiConverterTypeMobileLocalOnboardingOperation.lower(value)
}
public protocol MobileOnboardingOperationProtocol: AnyObject, Sendable {
func cancel()
func connectLocalStore() throws -> MobileOnboardingOutcome
func discover() throws -> MobileOnboardingDiscovery
func progress() -> MobileOnboardingProgress
@@ -1750,6 +1895,15 @@ open func cancel() {try! rustCall() {
}
}
open func connectLocalStore()throws -> MobileOnboardingOutcome {
return try FfiConverterTypeMobileOnboardingOutcome_lift(try rustCallWithError(FfiConverterTypeMobileOnboardingFfiError_lift) {
uniffiCallStatus in
uniffi_ironstorage_apple_fn_method_mobileonboardingoperation_connect_local_store(
self.uniffiCloneHandle(),uniffiCallStatus
)
})
}
open func discover()throws -> MobileOnboardingDiscovery {
return try FfiConverterTypeMobileOnboardingDiscovery_lift(try rustCallWithError(FfiConverterTypeMobileOnboardingFfiError_lift) {
uniffiCallStatus in
@@ -2929,6 +3083,7 @@ public func FfiConverterTypeMobileHomeNotice_lower(_ value: MobileHomeNotice) ->
public struct MobileHomePage: Equatable, Hashable {
public var remoteConfigured: Bool
public var freshness: MobileHomeFreshness
public var refreshedAt: Int64?
public var summaries: [MobileHomeSummaryRow]
@@ -2940,7 +3095,8 @@ public struct MobileHomePage: Equatable, Hashable {
// Default memberwise initializers are never public by default, so we
// declare one manually.
public init(freshness: MobileHomeFreshness, refreshedAt: Int64?, summaries: [MobileHomeSummaryRow], incoming: [MobileHomeCommit], outgoing: [MobileHomeCommit], incomingTotal: UInt32, outgoingTotal: UInt32, notice: MobileHomeNotice?) {
public init(remoteConfigured: Bool, freshness: MobileHomeFreshness, refreshedAt: Int64?, summaries: [MobileHomeSummaryRow], incoming: [MobileHomeCommit], outgoing: [MobileHomeCommit], incomingTotal: UInt32, outgoingTotal: UInt32, notice: MobileHomeNotice?) {
self.remoteConfigured = remoteConfigured
self.freshness = freshness
self.refreshedAt = refreshedAt
self.summaries = summaries
@@ -2967,6 +3123,7 @@ public struct FfiConverterTypeMobileHomePage: FfiConverterRustBuffer {
public static func read(from buf: inout (data: Data, offset: Data.Index)) throws -> MobileHomePage {
return
try MobileHomePage(
remoteConfigured: FfiConverterBool.read(from: &buf),
freshness: FfiConverterTypeMobileHomeFreshness.read(from: &buf),
refreshedAt: FfiConverterOptionInt64.read(from: &buf),
summaries: FfiConverterSequenceTypeMobileHomeSummaryRow.read(from: &buf),
@@ -2979,6 +3136,7 @@ public struct FfiConverterTypeMobileHomePage: FfiConverterRustBuffer {
}
public static func write(_ value: MobileHomePage, into buf: inout [UInt8]) {
FfiConverterBool.write(value.remoteConfigured, into: &buf)
FfiConverterTypeMobileHomeFreshness.write(value.freshness, into: &buf)
FfiConverterOptionInt64.write(value.refreshedAt, into: &buf)
FfiConverterSequenceTypeMobileHomeSummaryRow.write(value.summaries, into: &buf)
@@ -4081,6 +4239,7 @@ public func FfiConverterTypeMobilePasswordRow_lower(_ value: MobilePasswordRow)
public struct MobilePreferences: Equatable, Hashable {
public var syncConfigured: Bool
public var repositoryTitle: String
public var repositoryUrl: String
public var serverTitle: String
@@ -4097,7 +4256,8 @@ public struct MobilePreferences: Equatable, Hashable {
// Default memberwise initializers are never public by default, so we
// declare one manually.
public init(repositoryTitle: String, repositoryUrl: String, serverTitle: String, serverIdentity: String, applicationAccount: String?, defaultKeyTitle: String, defaultKeyFingerprint: String, authenticationTimeoutSeconds: UInt64, biometricUnlockEnabled: Bool, appearance: MobileAppearance, watchState: MobileWatchPreferenceState, watchTitle: String, watchDetail: String) {
public init(syncConfigured: Bool, repositoryTitle: String, repositoryUrl: String, serverTitle: String, serverIdentity: String, applicationAccount: String?, defaultKeyTitle: String, defaultKeyFingerprint: String, authenticationTimeoutSeconds: UInt64, biometricUnlockEnabled: Bool, appearance: MobileAppearance, watchState: MobileWatchPreferenceState, watchTitle: String, watchDetail: String) {
self.syncConfigured = syncConfigured
self.repositoryTitle = repositoryTitle
self.repositoryUrl = repositoryUrl
self.serverTitle = serverTitle
@@ -4129,6 +4289,7 @@ public struct FfiConverterTypeMobilePreferences: FfiConverterRustBuffer {
public static func read(from buf: inout (data: Data, offset: Data.Index)) throws -> MobilePreferences {
return
try MobilePreferences(
syncConfigured: FfiConverterBool.read(from: &buf),
repositoryTitle: FfiConverterString.read(from: &buf),
repositoryUrl: FfiConverterString.read(from: &buf),
serverTitle: FfiConverterString.read(from: &buf),
@@ -4146,6 +4307,7 @@ public struct FfiConverterTypeMobilePreferences: FfiConverterRustBuffer {
}
public static func write(_ value: MobilePreferences, into buf: inout [UInt8]) {
FfiConverterBool.write(value.syncConfigured, into: &buf)
FfiConverterString.write(value.repositoryTitle, into: &buf)
FfiConverterString.write(value.repositoryUrl, into: &buf)
FfiConverterString.write(value.serverTitle, into: &buf)
@@ -5674,12 +5836,78 @@ public func FfiConverterTypeMobileHomePhase_lower(_ value: MobileHomePhase) -> R
public enum MobileKeyTransferErrorKind: Equatable, Hashable {
case existingKey
case failed
}
#if compiler(>=6)
extension MobileKeyTransferErrorKind: Sendable {}
#endif
#if swift(>=5.8)
@_documentation(visibility: private)
#endif
public struct FfiConverterTypeMobileKeyTransferErrorKind: FfiConverterRustBuffer {
typealias SwiftType = MobileKeyTransferErrorKind
public static func read(from buf: inout (data: Data, offset: Data.Index)) throws -> MobileKeyTransferErrorKind {
let variant: Int32 = try readInt(&buf)
switch variant {
case 1: return .existingKey
case 2: return .failed
default: throw UniffiInternalError.unexpectedEnumCase
}
}
public static func write(_ value: MobileKeyTransferErrorKind, into buf: inout [UInt8]) {
switch value {
case .existingKey:
writeInt(&buf, Int32(1))
case .failed:
writeInt(&buf, Int32(2))
}
}
}
#if swift(>=5.8)
@_documentation(visibility: private)
#endif
public func FfiConverterTypeMobileKeyTransferErrorKind_lift(_ buf: RustBuffer) throws -> MobileKeyTransferErrorKind {
return try FfiConverterTypeMobileKeyTransferErrorKind.lift(buf)
}
#if swift(>=5.8)
@_documentation(visibility: private)
#endif
public func FfiConverterTypeMobileKeyTransferErrorKind_lower(_ value: MobileKeyTransferErrorKind) -> RustBuffer {
return FfiConverterTypeMobileKeyTransferErrorKind.lower(value)
}
public
enum MobileKeyTransferFfiError: Swift.Error, Equatable, Hashable, Foundation.LocalizedError {
case Failed(message: String
case Failed(kind: MobileKeyTransferErrorKind, message: String
)
@@ -5711,6 +5939,7 @@ public struct FfiConverterTypeMobileKeyTransferFfiError: FfiConverterRustBuffer
case 1: return .Failed(
kind: try FfiConverterTypeMobileKeyTransferErrorKind.read(from: &buf),
message: try FfiConverterString.read(from: &buf)
)
@@ -5725,8 +5954,9 @@ public struct FfiConverterTypeMobileKeyTransferFfiError: FfiConverterRustBuffer
case let .Failed(message):
case let .Failed(kind,message):
writeInt(&buf, Int32(1))
FfiConverterTypeMobileKeyTransferErrorKind.write(kind, into: &buf)
FfiConverterString.write(message, into: &buf)
}
@@ -5896,6 +6126,7 @@ public enum MobileOnboardingErrorKind: Equatable, Hashable {
case authentication
case repository
case existingClone
case existingKey
case interrupted
case secureStorage
case configuration
@@ -5931,13 +6162,15 @@ public struct FfiConverterTypeMobileOnboardingErrorKind: FfiConverterRustBuffer
case 5: return .existingClone
case 6: return .interrupted
case 6: return .existingKey
case 7: return .secureStorage
case 7: return .interrupted
case 8: return .configuration
case 8: return .secureStorage
case 9: return .alreadyConfigured
case 9: return .configuration
case 10: return .alreadyConfigured
default: throw UniffiInternalError.unexpectedEnumCase
}
@@ -5967,21 +6200,25 @@ public struct FfiConverterTypeMobileOnboardingErrorKind: FfiConverterRustBuffer
writeInt(&buf, Int32(5))
case .interrupted:
case .existingKey:
writeInt(&buf, Int32(6))
case .secureStorage:
case .interrupted:
writeInt(&buf, Int32(7))
case .configuration:
case .secureStorage:
writeInt(&buf, Int32(8))
case .alreadyConfigured:
case .configuration:
writeInt(&buf, Int32(9))
case .alreadyConfigured:
writeInt(&buf, Int32(10))
}
}
}
@@ -7574,6 +7811,20 @@ public func mobileKeyTransfer()throws -> MobileKeyTransfer {
)
})
}
public func mobileLocalKeyTransferImporter() -> MobileKeyTransferImport {
return try! FfiConverterTypeMobileKeyTransferImport_lift(try! rustCall() {
uniffiCallStatus in
uniffi_ironstorage_apple_fn_func_mobile_local_key_transfer_importer(uniffiCallStatus
)
})
}
public func mobileLocalOnboardingOperation() -> MobileLocalOnboardingOperation {
return try! FfiConverterTypeMobileLocalOnboardingOperation_lift(try! rustCall() {
uniffiCallStatus in
uniffi_ironstorage_apple_fn_func_mobile_local_onboarding_operation(uniffiCallStatus
)
})
}
public func mobileOnboardingOperation(serverUrl: String, account: String, repositoryPath: String, applicationToken: String)throws -> MobileOnboardingOperation {
return try FfiConverterTypeMobileOnboardingOperation_lift(try rustCallWithError(FfiConverterTypeMobileOnboardingFfiError_lift) {
uniffiCallStatus in
@@ -7670,6 +7921,12 @@ private let initializationResult: InitializationResult = {
if (uniffi_ironstorage_apple_checksum_func_mobile_key_transfer() != 57389) {
return InitializationResult.apiChecksumMismatch
}
if (uniffi_ironstorage_apple_checksum_func_mobile_local_key_transfer_importer() != 50238) {
return InitializationResult.apiChecksumMismatch
}
if (uniffi_ironstorage_apple_checksum_func_mobile_local_onboarding_operation() != 18004) {
return InitializationResult.apiChecksumMismatch
}
if (uniffi_ironstorage_apple_checksum_func_mobile_onboarding_operation() != 8354) {
return InitializationResult.apiChecksumMismatch
}
@@ -7721,6 +7978,9 @@ private let initializationResult: InitializationResult = {
if (uniffi_ironstorage_apple_checksum_method_mobileauthentication_copy_totp_code() != 8344) {
return InitializationResult.apiChecksumMismatch
}
if (uniffi_ironstorage_apple_checksum_method_mobileauthentication_create_directory() != 25476) {
return InitializationResult.apiChecksumMismatch
}
if (uniffi_ironstorage_apple_checksum_method_mobileauthentication_discard_entry_editor() != 28195) {
return InitializationResult.apiChecksumMismatch
}
@@ -7856,12 +8116,21 @@ private let initializationResult: InitializationResult = {
if (uniffi_ironstorage_apple_checksum_method_mobilekeytransferimport_add_frame() != 27319) {
return InitializationResult.apiChecksumMismatch
}
if (uniffi_ironstorage_apple_checksum_method_mobilekeytransferimport_import() != 37403) {
if (uniffi_ironstorage_apple_checksum_method_mobilekeytransferimport_import() != 44529) {
return InitializationResult.apiChecksumMismatch
}
if (uniffi_ironstorage_apple_checksum_method_mobilelocalonboardingoperation_cancel() != 26455) {
return InitializationResult.apiChecksumMismatch
}
if (uniffi_ironstorage_apple_checksum_method_mobilelocalonboardingoperation_generate() != 58931) {
return InitializationResult.apiChecksumMismatch
}
if (uniffi_ironstorage_apple_checksum_method_mobileonboardingoperation_cancel() != 49755) {
return InitializationResult.apiChecksumMismatch
}
if (uniffi_ironstorage_apple_checksum_method_mobileonboardingoperation_connect_local_store() != 57886) {
return InitializationResult.apiChecksumMismatch
}
if (uniffi_ironstorage_apple_checksum_method_mobileonboardingoperation_discover() != 2309) {
return InitializationResult.apiChecksumMismatch
}

View File

@@ -293,6 +293,11 @@ RustBuffer uniffi_ironstorage_apple_fn_method_mobileauthentication_copy_entry_fi
RustBuffer uniffi_ironstorage_apple_fn_method_mobileauthentication_copy_totp_code(uint64_t ptr, RustBuffer path, uint64_t unix_seconds, RustCallStatus *_Nonnull out_status
);
#endif
#ifndef UNIFFI_FFIDEF_UNIFFI_IRONSTORAGE_APPLE_FN_METHOD_MOBILEAUTHENTICATION_CREATE_DIRECTORY
#define UNIFFI_FFIDEF_UNIFFI_IRONSTORAGE_APPLE_FN_METHOD_MOBILEAUTHENTICATION_CREATE_DIRECTORY
void uniffi_ironstorage_apple_fn_method_mobileauthentication_create_directory(uint64_t ptr, RustBuffer parent, RustBuffer name, RustCallStatus *_Nonnull out_status
);
#endif
#ifndef UNIFFI_FFIDEF_UNIFFI_IRONSTORAGE_APPLE_FN_METHOD_MOBILEAUTHENTICATION_DISCARD_ENTRY_EDITOR
#define UNIFFI_FFIDEF_UNIFFI_IRONSTORAGE_APPLE_FN_METHOD_MOBILEAUTHENTICATION_DISCARD_ENTRY_EDITOR
void uniffi_ironstorage_apple_fn_method_mobileauthentication_discard_entry_editor(uint64_t ptr, uint64_t editor, RustCallStatus *_Nonnull out_status
@@ -550,7 +555,27 @@ RustBuffer uniffi_ironstorage_apple_fn_method_mobilekeytransferimport_add_frame(
#endif
#ifndef UNIFFI_FFIDEF_UNIFFI_IRONSTORAGE_APPLE_FN_METHOD_MOBILEKEYTRANSFERIMPORT_IMPORT
#define UNIFFI_FFIDEF_UNIFFI_IRONSTORAGE_APPLE_FN_METHOD_MOBILEKEYTRANSFERIMPORT_IMPORT
RustBuffer uniffi_ironstorage_apple_fn_method_mobilekeytransferimport_import(uint64_t ptr, RustBuffer passphrase, int8_t make_default, RustCallStatus *_Nonnull out_status
RustBuffer uniffi_ironstorage_apple_fn_method_mobilekeytransferimport_import(uint64_t ptr, RustBuffer passphrase, int8_t make_default, int8_t overwrite, RustCallStatus *_Nonnull out_status
);
#endif
#ifndef UNIFFI_FFIDEF_UNIFFI_IRONSTORAGE_APPLE_FN_CLONE_MOBILELOCALONBOARDINGOPERATION
#define UNIFFI_FFIDEF_UNIFFI_IRONSTORAGE_APPLE_FN_CLONE_MOBILELOCALONBOARDINGOPERATION
uint64_t uniffi_ironstorage_apple_fn_clone_mobilelocalonboardingoperation(uint64_t handle, RustCallStatus *_Nonnull out_status
);
#endif
#ifndef UNIFFI_FFIDEF_UNIFFI_IRONSTORAGE_APPLE_FN_FREE_MOBILELOCALONBOARDINGOPERATION
#define UNIFFI_FFIDEF_UNIFFI_IRONSTORAGE_APPLE_FN_FREE_MOBILELOCALONBOARDINGOPERATION
void uniffi_ironstorage_apple_fn_free_mobilelocalonboardingoperation(uint64_t handle, RustCallStatus *_Nonnull out_status
);
#endif
#ifndef UNIFFI_FFIDEF_UNIFFI_IRONSTORAGE_APPLE_FN_METHOD_MOBILELOCALONBOARDINGOPERATION_CANCEL
#define UNIFFI_FFIDEF_UNIFFI_IRONSTORAGE_APPLE_FN_METHOD_MOBILELOCALONBOARDINGOPERATION_CANCEL
void uniffi_ironstorage_apple_fn_method_mobilelocalonboardingoperation_cancel(uint64_t ptr, RustCallStatus *_Nonnull out_status
);
#endif
#ifndef UNIFFI_FFIDEF_UNIFFI_IRONSTORAGE_APPLE_FN_METHOD_MOBILELOCALONBOARDINGOPERATION_GENERATE
#define UNIFFI_FFIDEF_UNIFFI_IRONSTORAGE_APPLE_FN_METHOD_MOBILELOCALONBOARDINGOPERATION_GENERATE
RustBuffer uniffi_ironstorage_apple_fn_method_mobilelocalonboardingoperation_generate(uint64_t ptr, RustBuffer user_id, RustBuffer passphrase, int8_t replace_existing_key, RustCallStatus *_Nonnull out_status
);
#endif
#ifndef UNIFFI_FFIDEF_UNIFFI_IRONSTORAGE_APPLE_FN_CLONE_MOBILEONBOARDINGOPERATION
@@ -568,6 +593,11 @@ void uniffi_ironstorage_apple_fn_free_mobileonboardingoperation(uint64_t handle,
void uniffi_ironstorage_apple_fn_method_mobileonboardingoperation_cancel(uint64_t ptr, RustCallStatus *_Nonnull out_status
);
#endif
#ifndef UNIFFI_FFIDEF_UNIFFI_IRONSTORAGE_APPLE_FN_METHOD_MOBILEONBOARDINGOPERATION_CONNECT_LOCAL_STORE
#define UNIFFI_FFIDEF_UNIFFI_IRONSTORAGE_APPLE_FN_METHOD_MOBILEONBOARDINGOPERATION_CONNECT_LOCAL_STORE
RustBuffer uniffi_ironstorage_apple_fn_method_mobileonboardingoperation_connect_local_store(uint64_t ptr, RustCallStatus *_Nonnull out_status
);
#endif
#ifndef UNIFFI_FFIDEF_UNIFFI_IRONSTORAGE_APPLE_FN_METHOD_MOBILEONBOARDINGOPERATION_DISCOVER
#define UNIFFI_FFIDEF_UNIFFI_IRONSTORAGE_APPLE_FN_METHOD_MOBILEONBOARDINGOPERATION_DISCOVER
RustBuffer uniffi_ironstorage_apple_fn_method_mobileonboardingoperation_discover(uint64_t ptr, RustCallStatus *_Nonnull out_status
@@ -618,6 +648,18 @@ uint64_t uniffi_ironstorage_apple_fn_func_mobile_home_operation(RustBuffer authe
#define UNIFFI_FFIDEF_UNIFFI_IRONSTORAGE_APPLE_FN_FUNC_MOBILE_KEY_TRANSFER
uint64_t uniffi_ironstorage_apple_fn_func_mobile_key_transfer(RustCallStatus *_Nonnull out_status
);
#endif
#ifndef UNIFFI_FFIDEF_UNIFFI_IRONSTORAGE_APPLE_FN_FUNC_MOBILE_LOCAL_KEY_TRANSFER_IMPORTER
#define UNIFFI_FFIDEF_UNIFFI_IRONSTORAGE_APPLE_FN_FUNC_MOBILE_LOCAL_KEY_TRANSFER_IMPORTER
uint64_t uniffi_ironstorage_apple_fn_func_mobile_local_key_transfer_importer(RustCallStatus *_Nonnull out_status
);
#endif
#ifndef UNIFFI_FFIDEF_UNIFFI_IRONSTORAGE_APPLE_FN_FUNC_MOBILE_LOCAL_ONBOARDING_OPERATION
#define UNIFFI_FFIDEF_UNIFFI_IRONSTORAGE_APPLE_FN_FUNC_MOBILE_LOCAL_ONBOARDING_OPERATION
uint64_t uniffi_ironstorage_apple_fn_func_mobile_local_onboarding_operation(RustCallStatus *_Nonnull out_status
);
#endif
#ifndef UNIFFI_FFIDEF_UNIFFI_IRONSTORAGE_APPLE_FN_FUNC_MOBILE_ONBOARDING_OPERATION
@@ -944,6 +986,18 @@ uint16_t uniffi_ironstorage_apple_checksum_func_mobile_home_operation(void
#define UNIFFI_FFIDEF_UNIFFI_IRONSTORAGE_APPLE_CHECKSUM_FUNC_MOBILE_KEY_TRANSFER
uint16_t uniffi_ironstorage_apple_checksum_func_mobile_key_transfer(void
);
#endif
#ifndef UNIFFI_FFIDEF_UNIFFI_IRONSTORAGE_APPLE_CHECKSUM_FUNC_MOBILE_LOCAL_KEY_TRANSFER_IMPORTER
#define UNIFFI_FFIDEF_UNIFFI_IRONSTORAGE_APPLE_CHECKSUM_FUNC_MOBILE_LOCAL_KEY_TRANSFER_IMPORTER
uint16_t uniffi_ironstorage_apple_checksum_func_mobile_local_key_transfer_importer(void
);
#endif
#ifndef UNIFFI_FFIDEF_UNIFFI_IRONSTORAGE_APPLE_CHECKSUM_FUNC_MOBILE_LOCAL_ONBOARDING_OPERATION
#define UNIFFI_FFIDEF_UNIFFI_IRONSTORAGE_APPLE_CHECKSUM_FUNC_MOBILE_LOCAL_ONBOARDING_OPERATION
uint16_t uniffi_ironstorage_apple_checksum_func_mobile_local_onboarding_operation(void
);
#endif
#ifndef UNIFFI_FFIDEF_UNIFFI_IRONSTORAGE_APPLE_CHECKSUM_FUNC_MOBILE_ONBOARDING_OPERATION
@@ -1046,6 +1100,12 @@ uint16_t uniffi_ironstorage_apple_checksum_method_mobileauthentication_copy_entr
#define UNIFFI_FFIDEF_UNIFFI_IRONSTORAGE_APPLE_CHECKSUM_METHOD_MOBILEAUTHENTICATION_COPY_TOTP_CODE
uint16_t uniffi_ironstorage_apple_checksum_method_mobileauthentication_copy_totp_code(void
);
#endif
#ifndef UNIFFI_FFIDEF_UNIFFI_IRONSTORAGE_APPLE_CHECKSUM_METHOD_MOBILEAUTHENTICATION_CREATE_DIRECTORY
#define UNIFFI_FFIDEF_UNIFFI_IRONSTORAGE_APPLE_CHECKSUM_METHOD_MOBILEAUTHENTICATION_CREATE_DIRECTORY
uint16_t uniffi_ironstorage_apple_checksum_method_mobileauthentication_create_directory(void
);
#endif
#ifndef UNIFFI_FFIDEF_UNIFFI_IRONSTORAGE_APPLE_CHECKSUM_METHOD_MOBILEAUTHENTICATION_DISCARD_ENTRY_EDITOR
@@ -1322,12 +1382,30 @@ uint16_t uniffi_ironstorage_apple_checksum_method_mobilekeytransferimport_add_fr
#define UNIFFI_FFIDEF_UNIFFI_IRONSTORAGE_APPLE_CHECKSUM_METHOD_MOBILEKEYTRANSFERIMPORT_IMPORT
uint16_t uniffi_ironstorage_apple_checksum_method_mobilekeytransferimport_import(void
);
#endif
#ifndef UNIFFI_FFIDEF_UNIFFI_IRONSTORAGE_APPLE_CHECKSUM_METHOD_MOBILELOCALONBOARDINGOPERATION_CANCEL
#define UNIFFI_FFIDEF_UNIFFI_IRONSTORAGE_APPLE_CHECKSUM_METHOD_MOBILELOCALONBOARDINGOPERATION_CANCEL
uint16_t uniffi_ironstorage_apple_checksum_method_mobilelocalonboardingoperation_cancel(void
);
#endif
#ifndef UNIFFI_FFIDEF_UNIFFI_IRONSTORAGE_APPLE_CHECKSUM_METHOD_MOBILELOCALONBOARDINGOPERATION_GENERATE
#define UNIFFI_FFIDEF_UNIFFI_IRONSTORAGE_APPLE_CHECKSUM_METHOD_MOBILELOCALONBOARDINGOPERATION_GENERATE
uint16_t uniffi_ironstorage_apple_checksum_method_mobilelocalonboardingoperation_generate(void
);
#endif
#ifndef UNIFFI_FFIDEF_UNIFFI_IRONSTORAGE_APPLE_CHECKSUM_METHOD_MOBILEONBOARDINGOPERATION_CANCEL
#define UNIFFI_FFIDEF_UNIFFI_IRONSTORAGE_APPLE_CHECKSUM_METHOD_MOBILEONBOARDINGOPERATION_CANCEL
uint16_t uniffi_ironstorage_apple_checksum_method_mobileonboardingoperation_cancel(void
);
#endif
#ifndef UNIFFI_FFIDEF_UNIFFI_IRONSTORAGE_APPLE_CHECKSUM_METHOD_MOBILEONBOARDINGOPERATION_CONNECT_LOCAL_STORE
#define UNIFFI_FFIDEF_UNIFFI_IRONSTORAGE_APPLE_CHECKSUM_METHOD_MOBILEONBOARDINGOPERATION_CONNECT_LOCAL_STORE
uint16_t uniffi_ironstorage_apple_checksum_method_mobileonboardingoperation_connect_local_store(void
);
#endif
#ifndef UNIFFI_FFIDEF_UNIFFI_IRONSTORAGE_APPLE_CHECKSUM_METHOD_MOBILEONBOARDINGOPERATION_DISCOVER

View File

@@ -17,15 +17,15 @@
<key>CFBundlePackageType</key>
<string>APPL</string>
<key>CFBundleShortVersionString</key>
<string>1.0</string>
<string>$(MARKETING_VERSION)</string>
<key>CFBundleVersion</key>
<string>1</string>
<string>$(CURRENT_PROJECT_VERSION)</string>
<key>ITSAppUsesNonExemptEncryption</key>
<false/>
<key>NSFaceIDUsageDescription</key>
<string>Unlock your GPG key for protected password operations.</string>
<key>NSCameraUsageDescription</key>
<string>Scan GPG key transfer QR codes that you choose to import.</string>
<key>NSFaceIDUsageDescription</key>
<string>Unlock your GPG key for protected password operations.</string>
<key>UILaunchScreen</key>
<dict/>
<key>UISupportedInterfaceOrientations</key>

View File

@@ -0,0 +1,21 @@
MIT License
Copyright (c) 2026 rfc1437
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

File diff suppressed because it is too large Load Diff

View File

@@ -20,6 +20,9 @@ extension Notification.Name {
static let ironStorageKeyMaterialDidChange = Notification.Name(
"de.rfc1437.ironstorage.key-material-did-change"
)
static let ironStorageConfigurationDidChange = Notification.Name(
"de.rfc1437.ironstorage.configuration-did-change"
)
}
@MainActor
@@ -182,6 +185,12 @@ final class AppDelegate: UIResponder, UIApplicationDelegate {
name: .ironStorageKeyMaterialDidChange,
object: nil
)
NotificationCenter.default.addObserver(
self,
selector: #selector(keyMaterialDidChange),
name: .ironStorageConfigurationDidChange,
object: nil
)
}
deinit {
@@ -636,7 +645,7 @@ private final class ShellViewController: UITableViewController, MobileTabRoot {
}
@objc private func setupRequested() {
navigationController?.pushViewController(OnboardingViewController(), animated: true)
navigationController?.pushViewController(SetupChoiceViewController(), animated: true)
}
@objc private func tokenUpdateRequested() {
@@ -703,8 +712,10 @@ private final class ShellViewController: UITableViewController, MobileTabRoot {
)
case .empty:
content.image = UIImage(systemName: "checkmark.circle")
content.text = "No Remote Activity"
content.secondaryText = "There are no commits to pull or push."
content.text = homePage.remoteConfigured ? "No Remote Activity" : "Local Store Ready"
content.secondaryText = homePage.remoteConfigured
? "There are no commits to pull or push."
: "Changes are committed to the local Git repository."
cell.selectionStyle = .none
case .notice:
if let notice = homePage.notice {
@@ -734,6 +745,11 @@ private final class ShellViewController: UITableViewController, MobileTabRoot {
}
private func freshnessDescription(_ page: MobileHomePage) -> String {
if !page.remoteConfigured {
return page.freshness == .current
? "Current local Git status."
: "Local Git status from this device."
}
let refreshed = page.refreshedAt.map(formattedDate)
switch page.freshness {
case .neverRefreshed:
@@ -867,7 +883,7 @@ private final class ShellViewController: UITableViewController, MobileTabRoot {
private func showHomeLoading() {
var configuration = UIContentUnavailableConfiguration.loading()
configuration.text = "Loading Activity"
configuration.secondaryText = "Reading cached remote-branch state from storage."
configuration.secondaryText = "Reading Git history and password-store status."
contentUnavailableConfiguration = configuration
}
@@ -1078,7 +1094,8 @@ private final class PreferencesViewController: UITableViewController, MobileTabR
) -> Int {
switch section {
case 0: 2
case 1, 2, 3: 3
case 1: preferences?.syncConfigured == false ? 1 : 3
case 2, 3: 3
default: 1
}
}
@@ -1089,7 +1106,7 @@ private final class PreferencesViewController: UITableViewController, MobileTabR
) -> String? {
switch section {
case 0: "Password Store"
case 1: "Application Token"
case 1: preferences?.syncConfigured == false ? "Git Sync" : "Application Token"
case 2: "GPG Key & Recovery"
case 3: "Authentication"
case 4: "Appearance"
@@ -1106,7 +1123,9 @@ private final class PreferencesViewController: UITableViewController, MobileTabR
case 0:
"Non-secret repository and server identities from the shared configuration."
case 1:
"The application token is stored only in protected system storage and is never displayed."
preferences?.syncConfigured == false
? "The store has local Git history. An HTTPS remote can be configured later for synchronization."
: "The application token is stored only in protected system storage and is never displayed."
case 2:
"Import provides initial or recovery key setup. Private-key transfers require explicit confirmation and passphrase validation."
case 3:
@@ -1138,11 +1157,18 @@ private final class PreferencesViewController: UITableViewController, MobileTabR
content.secondaryText = preferences?.serverIdentity ?? "Server identity unavailable"
cell.selectionStyle = .none
case (1, 0):
content.image = UIImage(systemName: "key.fill")
content.text = preferences?.applicationAccount ?? "No Application Token"
content.secondaryText = preferences?.applicationAccount == nil
? "Git network operations require a replacement token"
: "Token stored in protected system storage"
if preferences?.syncConfigured == false {
content.image = UIImage(systemName: "network.badge.shield.half.filled")
content.text = "Add HTTPS Remote"
content.secondaryText = "Keep local history and enable optional synchronization"
cell.accessoryType = .disclosureIndicator
} else {
content.image = UIImage(systemName: "key.fill")
content.text = preferences?.applicationAccount ?? "No Application Token"
content.secondaryText = preferences?.applicationAccount == nil
? "Git network operations require a replacement token"
: "Token stored in protected system storage"
}
cell.selectionStyle = .none
case (1, 1):
content.image = UIImage(systemName: "arrow.triangle.2.circlepath")
@@ -1228,6 +1254,11 @@ private final class PreferencesViewController: UITableViewController, MobileTabR
override func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
tableView.deselectRow(at: indexPath, animated: true)
switch (indexPath.section, indexPath.row) {
case (1, 0) where preferences?.syncConfigured == false:
navigationController?.pushViewController(
OnboardingViewController(attachLocalStore: true),
animated: true
)
case (1, 1):
tokenUpdateRequested()
case (1, 2):
@@ -1461,8 +1492,9 @@ private final class PreferencesViewController: UITableViewController, MobileTabR
alert.addAction(UIAlertAction(title: "Cancel", style: .cancel))
alert.addAction(UIAlertAction(title: "Remove Token", style: .destructive) {
[weak self] _ in
guard let authentication = self?.authentication else { return }
self?.runPreferenceUpdate(announcement: "Application token removed") {
try self?.authentication?.removeApplicationToken()
try authentication.removeApplicationToken()
}
})
present(alert, animated: true)
@@ -1483,8 +1515,9 @@ private final class PreferencesViewController: UITableViewController, MobileTabR
alert.addAction(UIAlertAction(title: "Save", style: .default) {
[weak self, weak alert] _ in
let seconds = UInt64(alert?.textFields?.first?.text ?? "") ?? 0
guard let authentication = self?.authentication else { return }
self?.runPreferenceUpdate(announcement: "Inactivity timeout updated") {
try self?.authentication?.setAuthenticationTimeout(seconds: seconds)
try authentication.setAuthenticationTimeout(seconds: seconds)
}
})
present(alert, animated: true)
@@ -1498,8 +1531,9 @@ private final class PreferencesViewController: UITableViewController, MobileTabR
title: "\(selected ? "" : "")\(appearanceTitle(appearance))",
style: .default
) { [weak self] _ in
guard let authentication = self?.authentication else { return }
self?.runPreferenceUpdate(announcement: "Appearance updated") {
try self?.authentication?.setMobileAppearance(appearance: appearance)
try authentication.setMobileAppearance(appearance: appearance)
}
})
}
@@ -1656,6 +1690,14 @@ private final class PreferencesViewController: UITableViewController, MobileTabR
watchPaired: watchSnapshot.isPaired,
watchAppInstalled: watchSnapshot.isWatchAppInstalled
)
navigationItem.rightBarButtonItem = preferences?.syncConfigured == true
? UIBarButtonItem(
title: "Update Token",
style: .plain,
target: self,
action: #selector(tokenUpdateRequested)
)
: nil
if let appearance = preferences?.appearance {
tabBarController?.overrideUserInterfaceStyle = switch appearance {
case .system: .unspecified
@@ -1673,6 +1715,7 @@ private final class KeyImportScannerViewController: UIViewController,
DataScannerViewControllerDelegate
{
private let importer: MobileKeyTransferImport
private let localSetup: Bool
private let scanner = DataScannerViewController(
recognizedDataTypes: [.barcode(symbologies: [.qr])],
qualityLevel: .balanced,
@@ -1688,11 +1731,20 @@ private final class KeyImportScannerViewController: UIViewController,
init(transfer: MobileKeyTransfer) {
importer = transfer.importer()
localSetup = false
super.init(nibName: nil, bundle: nil)
title = "Import GPG Key"
navigationItem.largeTitleDisplayMode = .never
}
init(localImporter: MobileKeyTransferImport) {
importer = localImporter
localSetup = true
super.init(nibName: nil, bundle: nil)
title = "Import Local GPG Key"
navigationItem.largeTitleDisplayMode = .never
}
@available(*, unavailable)
required init?(coder: NSCoder) {
fatalError("init(coder:) is not supported")
@@ -1855,11 +1907,20 @@ private final class KeyImportScannerViewController: UIViewController,
private func confirmImport(_ key: MobileKeyTransferKey) {
let privateKey = key.kind == .private
if localSetup, !privateKey {
completing = false
presentFailure("A local store requires a private GPG key with an encryption subkey.") {
[weak self] in try? self?.scanner.startScanning()
}
return
}
let warning = privateKey
? "Import this private key only if you trust the device that displayed it."
: "Import this public key?"
let alert = UIAlertController(
title: privateKey ? "Import Private GPG Key?" : "Import Public GPG Key?",
title: localSetup
? "Create Local Store with This Key?"
: (privateKey ? "Import Private GPG Key?" : "Import Public GPG Key?"),
message: "\(warning)\n\n\(key.title)\n\(key.detail)",
preferredStyle: .alert
)
@@ -1874,21 +1935,37 @@ private final class KeyImportScannerViewController: UIViewController,
alert.addAction(UIAlertAction(title: "Cancel", style: .cancel) { [weak self] _ in
self?.navigationController?.popViewController(animated: true)
})
alert.addAction(UIAlertAction(title: "Import", style: .default) { [weak self, weak alert] _ in
self?.finishImport(passphrase: alert?.textFields?.first?.text, makeDefault: false)
})
if privateKey {
alert.addAction(UIAlertAction(title: "Import as Default", style: .default) {
if !localSetup {
alert.addAction(UIAlertAction(title: "Import", style: .default) {
[weak self, weak alert] _ in
self?.finishImport(passphrase: alert?.textFields?.first?.text, makeDefault: true)
self?.finishImport(
passphrase: alert?.textFields?.first?.text,
makeDefault: false
)
})
}
if privateKey {
alert.addAction(UIAlertAction(
title: localSetup ? "Create Local Store" : "Import as Default",
style: .default
) {
[weak self, weak alert] _ in
self?.finishImport(
passphrase: alert?.textFields?.first?.text,
makeDefault: true
)
})
}
present(alert, animated: true)
}
private func finishImport(passphrase: String?, makeDefault: Bool) {
private func finishImport(passphrase: String?, makeDefault: Bool, overwrite: Bool = false) {
do {
let outcome = try importer.import(passphrase: passphrase, makeDefault: makeDefault)
let outcome = try importer.import(
passphrase: passphrase,
makeDefault: makeDefault,
overwrite: overwrite
)
let alert = UIAlertController(
title: outcome.title,
message: outcome.detail,
@@ -1899,6 +1976,16 @@ private final class KeyImportScannerViewController: UIViewController,
NotificationCenter.default.post(name: .ironStorageKeyMaterialDidChange, object: nil)
})
present(alert, animated: true)
} catch let error as MobileKeyTransferFfiError {
if case let .Failed(kind, _) = error, kind == .existingKey, !overwrite {
confirmOverwrite(passphrase: passphrase, makeDefault: makeDefault)
return
}
completing = false
presentFailure(error.localizedDescription) { [weak self] in
guard let self else { return }
try? scanner.startScanning()
}
} catch {
completing = false
presentFailure(error.localizedDescription) { [weak self] in
@@ -1908,6 +1995,27 @@ private final class KeyImportScannerViewController: UIViewController,
}
}
private func confirmOverwrite(passphrase: String?, makeDefault: Bool) {
let alert = UIAlertController(
title: "Replace Existing GPG Key?",
message: "A key with this fingerprint already exists. Replacing key material can change which private key is used to unlock entries.",
preferredStyle: .alert
)
alert.addAction(UIAlertAction(title: "Cancel", style: .cancel) { [weak self] _ in
self?.completing = false
try? self?.scanner.startScanning()
})
alert.addAction(UIAlertAction(title: "Replace Key", style: .destructive) {
[weak self] _ in
self?.finishImport(
passphrase: passphrase,
makeDefault: makeDefault,
overwrite: true
)
})
present(alert, animated: true)
}
private func presentFailure(_ detail: String, completion: (() -> Void)? = nil) {
guard presentedViewController == nil else { return }
let alert = UIAlertController(
@@ -4202,6 +4310,55 @@ private final class PasswordDirectoryViewController: UITableViewController, Mobi
present(alert, animated: true)
}
private func createFolderRequested() {
let alert = UIAlertController(
title: "New Folder",
message: "Enter a name for the password folder in this folder.",
preferredStyle: .alert
)
alert.addTextField { field in
field.placeholder = "Folder Name"
field.autocapitalizationType = .words
field.clearButtonMode = .whileEditing
field.returnKeyType = .go
}
alert.addAction(UIAlertAction(title: "Cancel", style: .cancel))
alert.addAction(UIAlertAction(title: "Create", style: .default) { [weak self, weak alert] _ in
guard let name = alert?.textFields?.first?.text, !name.isEmpty else { return }
self?.beginCreateFolder(name: name)
})
present(alert, animated: true)
}
private func beginCreateFolder(name: String) {
guard let authentication else {
presentAuthenticationFailure(.unavailable)
return
}
createTask?.cancel()
let parent = path ?? ""
createTask = Task { [weak self] in
let result = await Task.detached(priority: .userInitiated) {
do {
try authentication.createDirectory(parent: parent, name: name)
return Result<Void, AuthenticationFailure>.success(())
} catch let error as MobileAuthenticationFfiError {
return .failure(AuthenticationFailure(error))
} catch {
return .failure(.unexpected)
}
}.value
guard !Task.isCancelled, let self else { return }
switch result {
case .success:
NotificationCenter.default.post(name: .ironStorageLocalStoreDidChange, object: nil)
reload()
case let .failure(failure):
presentAuthenticationFailure(failure)
}
}
}
private func beginCreate(name: String, passphrase: String?) {
guard let authentication else {
presentAuthenticationFailure(.unavailable)
@@ -4345,12 +4502,21 @@ private final class PasswordDirectoryViewController: UITableViewController, Mobi
case let .success(page):
directoryPage = page
title = page.title
navigationItem.rightBarButtonItem = UIBarButtonItem(
let add = UIBarButtonItem(
barButtonSystemItem: .add,
target: self,
action: #selector(createRequested)
target: nil,
action: nil
)
navigationItem.rightBarButtonItem?.accessibilityLabel = "Create password entry"
add.menu = UIMenu(children: [
UIAction(title: "New Password", image: UIImage(systemName: "key")) {
[weak self] _ in self?.createRequested()
},
UIAction(title: "New Folder", image: UIImage(systemName: "folder.badge.plus")) {
[weak self] _ in self?.createFolderRequested()
},
])
add.accessibilityLabel = "Create password or folder"
navigationItem.rightBarButtonItem = add
contentUnavailableConfiguration = nil
tableView.reloadData()
if page.rows.isEmpty {
@@ -6058,8 +6224,315 @@ private final class TokenUpdateViewController: UITableViewController {
}
}
@MainActor
private final class SetupChoiceViewController: UITableViewController {
init() {
super.init(style: .insetGrouped)
title = "Set Up IronStorage"
navigationItem.largeTitleDisplayMode = .never
}
@available(*, unavailable)
required init?(coder: NSCoder) {
fatalError("init(coder:) is not supported")
}
override func numberOfSections(in tableView: UITableView) -> Int { 2 }
override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
section == 0 ? 2 : 1
}
override func tableView(
_ tableView: UITableView,
titleForHeaderInSection section: Int
) -> String? {
section == 0 ? "Local Store" : "Existing Git Store"
}
override func tableView(
_ tableView: UITableView,
titleForFooterInSection section: Int
) -> String? {
section == 0
? "Creates a local Git repository. Add an HTTPS remote later when you want to sync."
: "Clone an existing password-store repository over HTTPS."
}
override func tableView(
_ tableView: UITableView,
cellForRowAt indexPath: IndexPath
) -> UITableViewCell {
let cell = UITableViewCell(style: .subtitle, reuseIdentifier: nil)
var content = cell.defaultContentConfiguration()
switch (indexPath.section, indexPath.row) {
case (0, 0):
content.image = UIImage(systemName: "key.badge.plus")
content.text = "Generate Local GPG Key"
content.secondaryText = "Create a protected key and local Git history"
case (0, 1):
content.image = UIImage(systemName: "qrcode.viewfinder")
content.text = "Import Local GPG Key"
content.secondaryText = "Scan a private IronStorage key transfer"
default:
content.image = UIImage(systemName: "network")
content.text = "Connect Existing Store"
content.secondaryText = "Clone an HTTPS Git repository"
}
content.secondaryTextProperties.numberOfLines = 0
cell.contentConfiguration = content
cell.accessoryType = .disclosureIndicator
return cell
}
override func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
tableView.deselectRow(at: indexPath, animated: true)
switch (indexPath.section, indexPath.row) {
case (0, 0):
navigationController?.pushViewController(LocalSetupViewController(), animated: true)
case (0, 1):
requestLocalKeyScanner()
default:
navigationController?.pushViewController(OnboardingViewController(), animated: true)
}
}
private func requestLocalKeyScanner() {
guard DataScannerViewController.isSupported else {
presentMessage("This device does not support live QR scanning.")
return
}
switch AVCaptureDevice.authorizationStatus(for: .video) {
case .authorized:
showLocalKeyScanner()
case .notDetermined:
AVCaptureDevice.requestAccess(for: .video) { [weak self] granted in
Task { @MainActor in
if granted {
self?.showLocalKeyScanner()
} else {
self?.presentMessage("Allow camera access in Settings to import a GPG key.")
}
}
}
default:
presentMessage("Allow camera access in Settings to import a GPG key.")
}
}
private func showLocalKeyScanner() {
guard DataScannerViewController.isAvailable else {
presentMessage("Close other camera apps and try again.")
return
}
navigationController?.pushViewController(
KeyImportScannerViewController(localImporter: mobileLocalKeyTransferImporter()),
animated: true
)
}
private func presentMessage(_ detail: String) {
let alert = UIAlertController(
title: "GPG Key Import Is Unavailable",
message: detail,
preferredStyle: .alert
)
alert.addAction(UIAlertAction(title: "OK", style: .default))
present(alert, animated: true)
}
}
@MainActor
private final class LocalSetupViewController: UITableViewController {
private let keyNameField = UITextField()
private let passphraseField = UITextField()
private let confirmationField = UITextField()
private var task: Task<Void, Never>?
private var operation: MobileLocalOnboardingOperation?
private var isWorking = false
init() {
super.init(style: .insetGrouped)
title = "Create Local Store"
navigationItem.largeTitleDisplayMode = .never
configureFields()
}
@available(*, unavailable)
required init?(coder: NSCoder) {
fatalError("init(coder:) is not supported")
}
deinit {
operation?.cancel()
task?.cancel()
}
override func numberOfSections(in tableView: UITableView) -> Int { 2 }
override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
section == 0 ? 3 : 1
}
override func tableView(
_ tableView: UITableView,
titleForHeaderInSection section: Int
) -> String? {
section == 0 ? "Protected GPG Key" : nil
}
override func tableView(
_ tableView: UITableView,
titleForFooterInSection section: Int
) -> String? {
section == 0
? "The passphrase protects the generated private key. Keep it somewhere safe; IronStorage cannot recover it."
: "Creates a pass-compatible password store with local Git versioning and no network connection."
}
override func tableView(
_ tableView: UITableView,
cellForRowAt indexPath: IndexPath
) -> UITableViewCell {
let cell = UITableViewCell(style: .default, reuseIdentifier: nil)
if indexPath.section == 1 {
cell.textLabel?.text = isWorking ? "Creating…" : "Create Local Store"
cell.textLabel?.textAlignment = .center
cell.textLabel?.textColor = isWorking ? .secondaryLabel : view.tintColor
cell.isUserInteractionEnabled = !isWorking
return cell
}
let field = [keyNameField, passphraseField, confirmationField][indexPath.row]
field.translatesAutoresizingMaskIntoConstraints = false
field.isEnabled = !isWorking
cell.contentView.addSubview(field)
NSLayoutConstraint.activate([
field.leadingAnchor.constraint(equalTo: cell.contentView.layoutMarginsGuide.leadingAnchor),
field.trailingAnchor.constraint(equalTo: cell.contentView.layoutMarginsGuide.trailingAnchor),
field.topAnchor.constraint(equalTo: cell.contentView.topAnchor, constant: 10),
field.bottomAnchor.constraint(equalTo: cell.contentView.bottomAnchor, constant: -10),
])
return cell
}
override func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
tableView.deselectRow(at: indexPath, animated: true)
guard indexPath.section == 1, !isWorking else { return }
create(replaceExistingKey: false)
}
private func configureFields() {
for field in [keyNameField, passphraseField, confirmationField] {
field.borderStyle = .none
field.clearButtonMode = .whileEditing
field.autocorrectionType = .no
field.adjustsFontForContentSizeCategory = true
field.font = .preferredFont(forTextStyle: .body)
}
keyNameField.placeholder = "GPG key name"
keyNameField.text = "IronStorage Local Key"
keyNameField.textContentType = .name
keyNameField.autocapitalizationType = .words
passphraseField.placeholder = "GPG key passphrase"
confirmationField.placeholder = "Confirm passphrase"
for field in [passphraseField, confirmationField] {
field.isSecureTextEntry = true
field.textContentType = .newPassword
field.autocapitalizationType = .none
}
}
private func create(replaceExistingKey: Bool) {
let keyName = keyNameField.text ?? ""
let passphrase = passphraseField.text ?? ""
guard !keyName.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty,
!passphrase.isEmpty,
passphrase == confirmationField.text
else {
presentFailure(
title: "Check GPG Key Details",
detail: "Enter a key name and matching non-empty passphrases."
)
return
}
let operation = mobileLocalOnboardingOperation()
self.operation = operation
isWorking = true
navigationItem.prompt = "Generating a protected GPG key and local Git repository."
tableView.reloadData()
task = Task { [weak self] in
let result = await Task.detached(priority: .userInitiated) {
do {
return Result<MobileOnboardingOutcome, OnboardingFailure>.success(
try operation.generate(
userId: keyName,
passphrase: passphrase,
replaceExistingKey: replaceExistingKey
)
)
} catch let error as MobileOnboardingFfiError {
return .failure(OnboardingFailure(error))
} catch {
return .failure(.unexpected)
}
}.value
self?.finish(result, keyName: keyName, passphrase: passphrase)
}
}
private func finish(
_ result: Result<MobileOnboardingOutcome, OnboardingFailure>,
keyName: String,
passphrase: String
) {
operation = nil
task = nil
isWorking = false
navigationItem.prompt = nil
tableView.reloadData()
switch result {
case let .success(outcome):
passphraseField.text = nil
confirmationField.text = nil
let alert = UIAlertController(
title: outcome.title,
message: outcome.detail,
preferredStyle: .alert
)
alert.addAction(UIAlertAction(title: "Done", style: .default) { _ in
NotificationCenter.default.post(name: .ironStorageKeyMaterialDidChange, object: nil)
})
present(alert, animated: true)
case let .failure(failure) where failure.kind == .existingKey:
let alert = UIAlertController(
title: failure.title,
message: failure.detail,
preferredStyle: .alert
)
alert.addAction(UIAlertAction(title: "Cancel", style: .cancel))
alert.addAction(UIAlertAction(title: "Replace Key", style: .destructive) {
[weak self] _ in
self?.keyNameField.text = keyName
self?.passphraseField.text = passphrase
self?.confirmationField.text = passphrase
self?.create(replaceExistingKey: true)
})
present(alert, animated: true)
case let .failure(failure):
presentFailure(title: failure.title, detail: failure.detail)
}
}
private func presentFailure(title: String, detail: String) {
let alert = UIAlertController(title: title, message: detail, preferredStyle: .alert)
alert.addAction(UIAlertAction(title: "OK", style: .default))
present(alert, animated: true)
}
}
@MainActor
private final class OnboardingViewController: UITableViewController {
private let attachLocalStore: Bool
private let serverField = UITextField()
private let accountField = UITextField()
private let repositoryField = UITextField()
@@ -6072,9 +6545,10 @@ private final class OnboardingViewController: UITableViewController {
private var generation = 0
private var isWorking = false
init() {
init(attachLocalStore: Bool = false) {
self.attachLocalStore = attachLocalStore
super.init(style: .insetGrouped)
title = "Connect Store"
title = attachLocalStore ? "Add Git Remote" : "Connect Store"
navigationItem.largeTitleDisplayMode = .never
configureFields()
}
@@ -6096,7 +6570,7 @@ private final class OnboardingViewController: UITableViewController {
}
override func numberOfSections(in tableView: UITableView) -> Int {
branches.isEmpty ? 2 : 3
attachLocalStore ? 2 : (branches.isEmpty ? 2 : 3)
}
override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
@@ -6113,7 +6587,7 @@ private final class OnboardingViewController: UITableViewController {
) -> String? {
switch section {
case 0: "HTTPS Repository"
case 1: branches.isEmpty ? nil : "Remote"
case 1: attachLocalStore || branches.isEmpty ? nil : "Remote"
default: "Local Clone"
}
}
@@ -6133,7 +6607,10 @@ private final class OnboardingViewController: UITableViewController {
return fieldCell(indexPath.row)
}
let cell = UITableViewCell(style: .value1, reuseIdentifier: nil)
if branches.isEmpty {
if attachLocalStore {
cell.textLabel?.text = "Connect Local Store"
cell.textLabel?.textColor = view.tintColor
} else if branches.isEmpty {
cell.textLabel?.text = "Discover Branches"
cell.textLabel?.textColor = view.tintColor
cell.accessoryType = .disclosureIndicator
@@ -6155,7 +6632,9 @@ private final class OnboardingViewController: UITableViewController {
override func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
tableView.deselectRow(at: indexPath, animated: true)
guard !isWorking else { return }
if branches.isEmpty, indexPath.section == 1 {
if attachLocalStore, indexPath.section == 1 {
connectLocalStore()
} else if branches.isEmpty, indexPath.section == 1 {
discoverBranches()
} else if indexPath.section == 1 {
chooseBranch(from: tableView.cellForRow(at: indexPath))
@@ -6221,6 +6700,17 @@ private final class OnboardingViewController: UITableViewController {
}
}
private func connectLocalStore() {
do {
let operation = try makeOperation()
begin(operation, title: "Connecting Git Remote") { operation in
.completed(try operation.connectLocalStore())
}
} catch {
present(error)
}
}
private func runSetup(useExisting: Bool) {
guard branches.indices.contains(selectedBranch) else { return }
do {
@@ -6290,7 +6780,14 @@ private final class OnboardingViewController: UITableViewController {
tokenField.text = nil
let alert = UIAlertController(title: outcome.title, message: outcome.detail, preferredStyle: .alert)
alert.addAction(UIAlertAction(title: "Done", style: .default) { [weak self] _ in
self?.navigationController?.popViewController(animated: true)
if self?.attachLocalStore == true {
NotificationCenter.default.post(
name: .ironStorageConfigurationDidChange,
object: nil
)
} else {
self?.navigationController?.popViewController(animated: true)
}
})
present(alert, animated: true)
case let .failure(failure):

View File

@@ -9,7 +9,7 @@ actually exercised.
| Area | Implemented boundary | Automated evidence | Manual release evidence |
| --- | --- | --- | --- |
| Home tab, remote activity, pull-to-refresh | UIKit renders Rust `MobileHomePage` and typed Commit/Fetch/Pull/Push actions | `mobile_home` unit tests, `git_embedded`, and `crates/apple` bridge tests | Exercise refresh and all four actions against a disposable HTTPS remote, including expired application-token recovery |
| Home tab, local history, and optional remote activity | UIKit renders Rust `MobileHomePage` and typed Commit/Fetch/Pull/Push actions | `mobile_home` unit tests, `git_embedded`, and `crates/apple` bridge tests | Verify a clean local store first, then attach a disposable HTTPS remote and exercise refresh and all four actions, including expired application-token recovery |
| Passwords tab, navigation, view, and edit | Rust supplies typed paths, fields, drafts, revisions, and mutations; UIKit presents them | `mobile_passwords`, `entry_documents`, `mobile_mutation`, and `mobile_authentication` tests | Open nested entries, edit/save/cancel, and confirm stale-edit recovery |
| Swipe move/copy/delete | Rust plans destinations, collisions, dirty-editor handling, and commits; UIKit also exposes accessibility actions | `plans_destinations_collisions_hidden_paths_and_stale_revisions` and authentication serialization tests | Exercise swipe and VoiceOver alternatives; confirm destructive prompts |
| TOTP tab and sharing | Rust discovers, caches, parses, selects, and generates OTP data | `mobile_totp`, `otp`, and `mobile_watch` integration tests | Discover entries, read a code, change Watch selection, and verify countdown |
@@ -46,8 +46,7 @@ Git, non-TOTP Watch features, and production AutoFill behavior.
- Face ID and camera access have purpose strings in `project.yml`. No custom
entitlement is currently required: default signing supplies each target's
application identifier and Keychain group, WatchConnectivity needs no added
capability, and no App Group is used. Alternative-distribution entitlements
belong to the final distribution issue.
capability, and no App Group is used.
`cargo test -p ironstorage --test apple_mobile_audit` enforces the source and privacy-manifest
parts of this review so those boundaries cannot silently regress.
@@ -83,7 +82,8 @@ the tester is ready to enter the passphrase again.
## Manual evidence still required
Record the device model/OS, build commit, and pass/fail result for each manual
row above. Physical paired-Watch deployment and WatchConnectivity testing are
currently skipped by project direction, so issues closed without that evidence
must carry the `untested` label. Do not describe a simulator build or an opened
Xcode project as device validation.
row above. Simulator validation is required during implementation. The final
distribution issue remains open until the approved App Store build is installed
and verified on the paired physical iPhone and Apple Watch. Never overwrite that
installation with a development-signed build, and do not describe a simulator
build or an opened Xcode project as physical-device validation.

View File

@@ -17,9 +17,11 @@
<key>CFBundlePackageType</key>
<string>APPL</string>
<key>CFBundleShortVersionString</key>
<string>1.0</string>
<string>$(MARKETING_VERSION)</string>
<key>CFBundleVersion</key>
<string>1</string>
<string>$(CURRENT_PROJECT_VERSION)</string>
<key>ITSAppUsesNonExemptEncryption</key>
<false/>
<key>WKApplication</key>
<true/>
<key>WKCompanionAppBundleIdentifier</key>

View File

@@ -4,6 +4,10 @@ options:
settings:
ENABLE_USER_SCRIPT_SANDBOXING: NO
SWIFT_VERSION: "5.0"
CODE_SIGN_STYLE: Automatic
DEVELOPMENT_TEAM: MU22FMRGK8
MARKETING_VERSION: "0.1.0"
CURRENT_PROJECT_VERSION: "1"
targets:
IronStorage:
type: application
@@ -11,8 +15,6 @@ targets:
deploymentTarget: "17.0"
settings:
PRODUCT_BUNDLE_IDENTIFIER: de.rfc1437.ironstorage
MARKETING_VERSION: "0.1.0"
CURRENT_PROJECT_VERSION: "1"
TARGETED_DEVICE_FAMILY: "1"
SWIFT_OBJC_BRIDGING_HEADER: IronStorage-Bridging-Header.h
LIBRARY_SEARCH_PATHS: "$(inherited) $(DERIVED_FILE_DIR)/rust"
@@ -21,6 +23,8 @@ targets:
path: Info.plist
properties:
CFBundleDisplayName: IronStorage
CFBundleShortVersionString: $(MARKETING_VERSION)
CFBundleVersion: $(CURRENT_PROJECT_VERSION)
ITSAppUsesNonExemptEncryption: false
NSFaceIDUsageDescription: Unlock your GPG key for protected password operations.
NSCameraUsageDescription: Scan GPG key transfer QR codes that you choose to import.
@@ -29,10 +33,14 @@ targets:
- UIInterfaceOrientationPortrait
sources:
- Assets.xcassets
- Resources/App/License.txt
- Resources/App/PrivacyInfo.xcprivacy
- Resources/App/ThirdPartyLicenses.txt
- Sources/App
- Generated/ironstorage_apple.swift
dependencies:
- target: IronStorageAutoFill
embed: true
- target: IronStorageWatch
embed: true
preBuildScripts:
@@ -57,6 +65,9 @@ targets:
path: AutoFill-Info.plist
properties:
CFBundleDisplayName: IronStorage AutoFill
CFBundleShortVersionString: $(MARKETING_VERSION)
CFBundleVersion: $(CURRENT_PROJECT_VERSION)
ITSAppUsesNonExemptEncryption: false
NSExtension:
NSExtensionPointIdentifier: com.apple.authentication-services-credential-provider-ui
NSExtensionPrincipalClass: $(PRODUCT_MODULE_NAME).CredentialProviderViewController
@@ -87,6 +98,9 @@ targets:
path: Watch-Info.plist
properties:
CFBundleDisplayName: IronStorage
CFBundleShortVersionString: $(MARKETING_VERSION)
CFBundleVersion: $(CURRENT_PROJECT_VERSION)
ITSAppUsesNonExemptEncryption: false
WKApplication: true
WKCompanionAppBundleIdentifier: de.rfc1437.ironstorage
sources: