Harden Apple Watch snapshot synchronization

This commit is contained in:
2026-08-17 18:06:09 +02:00
parent 5c88d3ace0
commit 2a59803b6c
8 changed files with 372 additions and 64 deletions

View File

@@ -563,6 +563,8 @@ fileprivate struct FfiConverterData: FfiConverterRustBuffer {
public protocol WatchCoreProtocol: AnyObject, Sendable {
func applyAuthoritativeSnapshot(snapshot: Data) throws -> WatchSnapshotUpdate
func applySnapshot(snapshot: Data) throws -> WatchSnapshotUpdate
func noPersistedSnapshot() throws
@@ -633,6 +635,16 @@ open class WatchCore: WatchCoreProtocol, @unchecked Sendable {
open func applyAuthoritativeSnapshot(snapshot: Data)throws -> WatchSnapshotUpdate {
return try FfiConverterTypeWatchSnapshotUpdate_lift(try rustCallWithError(FfiConverterTypeWatchFfiError_lift) {
uniffiCallStatus in
uniffi_ironstorage_watch_fn_method_watchcore_apply_authoritative_snapshot(
self.uniffiCloneHandle(),
FfiConverterData.lower(snapshot),uniffiCallStatus
)
})
}
open func applySnapshot(snapshot: Data)throws -> WatchSnapshotUpdate {
return try FfiConverterTypeWatchSnapshotUpdate_lift(try rustCallWithError(FfiConverterTypeWatchFfiError_lift) {
uniffiCallStatus in
@@ -1384,6 +1396,9 @@ private let initializationResult: InitializationResult = {
if (uniffi_ironstorage_watch_checksum_func_watch_core() != 47212) {
return InitializationResult.apiChecksumMismatch
}
if (uniffi_ironstorage_watch_checksum_method_watchcore_apply_authoritative_snapshot() != 29816) {
return InitializationResult.apiChecksumMismatch
}
if (uniffi_ironstorage_watch_checksum_method_watchcore_apply_snapshot() != 6162) {
return InitializationResult.apiChecksumMismatch
}

View File

@@ -253,6 +253,11 @@ uint64_t uniffi_ironstorage_watch_fn_clone_watchcore(uint64_t handle, RustCallSt
void uniffi_ironstorage_watch_fn_free_watchcore(uint64_t handle, RustCallStatus *_Nonnull out_status
);
#endif
#ifndef UNIFFI_FFIDEF_UNIFFI_IRONSTORAGE_WATCH_FN_METHOD_WATCHCORE_APPLY_AUTHORITATIVE_SNAPSHOT
#define UNIFFI_FFIDEF_UNIFFI_IRONSTORAGE_WATCH_FN_METHOD_WATCHCORE_APPLY_AUTHORITATIVE_SNAPSHOT
RustBuffer uniffi_ironstorage_watch_fn_method_watchcore_apply_authoritative_snapshot(uint64_t ptr, RustBuffer snapshot, RustCallStatus *_Nonnull out_status
);
#endif
#ifndef UNIFFI_FFIDEF_UNIFFI_IRONSTORAGE_WATCH_FN_METHOD_WATCHCORE_APPLY_SNAPSHOT
#define UNIFFI_FFIDEF_UNIFFI_IRONSTORAGE_WATCH_FN_METHOD_WATCHCORE_APPLY_SNAPSHOT
RustBuffer uniffi_ironstorage_watch_fn_method_watchcore_apply_snapshot(uint64_t ptr, RustBuffer snapshot, RustCallStatus *_Nonnull out_status
@@ -563,6 +568,12 @@ void ffi_ironstorage_watch_rust_future_complete_void(uint64_t handle, RustCallSt
#define UNIFFI_FFIDEF_UNIFFI_IRONSTORAGE_WATCH_CHECKSUM_FUNC_WATCH_CORE
uint16_t uniffi_ironstorage_watch_checksum_func_watch_core(void
);
#endif
#ifndef UNIFFI_FFIDEF_UNIFFI_IRONSTORAGE_WATCH_CHECKSUM_METHOD_WATCHCORE_APPLY_AUTHORITATIVE_SNAPSHOT
#define UNIFFI_FFIDEF_UNIFFI_IRONSTORAGE_WATCH_CHECKSUM_METHOD_WATCHCORE_APPLY_AUTHORITATIVE_SNAPSHOT
uint16_t uniffi_ironstorage_watch_checksum_method_watchcore_apply_authoritative_snapshot(void
);
#endif
#ifndef UNIFFI_FFIDEF_UNIFFI_IRONSTORAGE_WATCH_CHECKSUM_METHOD_WATCHCORE_APPLY_SNAPSHOT

View File

@@ -46,6 +46,7 @@ private final class WatchSnapshotCoordinator: NSObject, WCSessionDelegate {
Notification.Name.ironStorageWatchSnapshotDidChange,
.ironStorageLocalStoreDidChange,
.ironStorageAuthenticationDidChange,
UIApplication.didBecomeActiveNotification,
] {
NotificationCenter.default.addObserver(
self,
@@ -103,21 +104,7 @@ private final class WatchSnapshotCoordinator: NSObject, WCSessionDelegate {
Self.snapshotKey: snapshot,
Self.deliveredReceiptKey: Data(transfer.deliveredReceipt),
]
do {
try session.updateApplicationContext(payload)
if session.isReachable {
session.sendMessage(payload, replyHandler: nil) { _ in
session.transferUserInfo(payload)
}
} else {
session.transferUserInfo(payload)
}
} catch {
_ = try? authentication.failWatchSnapshot(
revision: transfer.revision,
detail: "The latest Apple Watch snapshot could not be queued."
)
}
publish(payload, through: session)
notifyStatusChanged()
case .failure:
// A locked GPG key leaves the previous replacement pending. Authentication
@@ -127,6 +114,18 @@ private final class WatchSnapshotCoordinator: NSObject, WCSessionDelegate {
}
}
private func publish(_ payload: [String: Any], through session: WCSession) {
for transfer in session.outstandingUserInfoTransfers
where transfer.userInfo[Self.snapshotKey] != nil {
transfer.cancel()
}
try? session.updateApplicationContext(payload)
session.transferUserInfo(payload)
if session.isReachable {
session.sendMessage(payload, replyHandler: nil) { _ in }
}
}
private func recordUnavailable(unpaired: Bool, detail: String) {
guard let authentication else { return }
_ = try? authentication.setWatchSnapshotUnavailable(unpaired: unpaired, detail: detail)

View File

@@ -85,6 +85,12 @@ private final class WatchSnapshotTransport: NSObject, ObservableObject, WCSessio
func sceneBecameActive() {
restoreSnapshot()
guard WCSession.isSupported() else { return }
let session = WCSession.default
session.activate()
if session.activationState == .activated {
receive(session.receivedApplicationContext, session: session, authoritative: true)
}
}
func sceneBecameInactive() {
@@ -108,7 +114,11 @@ private final class WatchSnapshotTransport: NSObject, ObservableObject, WCSessio
}
}
private func receive(_ applicationContext: [String: Any], session: WCSession) {
private func receive(
_ applicationContext: [String: Any],
session: WCSession,
authoritative: Bool = false
) {
try? core.syncStarted()
guard var snapshot = applicationContext[Self.snapshotKey] as? Data else {
try? core.syncFinished()
@@ -117,7 +127,11 @@ private final class WatchSnapshotTransport: NSObject, ObservableObject, WCSessio
}
defer { snapshot.resetBytes(in: 0..<snapshot.count) }
do {
let update = try core.applySnapshot(snapshot: snapshot)
let update = if authoritative {
try core.applyAuthoritativeSnapshot(snapshot: snapshot)
} else {
try core.applySnapshot(snapshot: snapshot)
}
switch update.persistence {
case .keep:
break
@@ -128,20 +142,25 @@ private final class WatchSnapshotTransport: NSObject, ObservableObject, WCSessio
}
refreshPresentation()
guard !update.receipt.isEmpty else { return }
let acknowledgement = [Self.receiptKey: update.receipt]
if session.isReachable {
session.sendMessage(acknowledgement, replyHandler: nil) { _ in
session.transferUserInfo(acknowledgement)
}
} else {
session.transferUserInfo(acknowledgement)
}
acknowledge(update.receipt, through: session)
} catch {
try? core.syncFailed()
refreshPresentation()
}
}
private func acknowledge(_ receipt: Data, through session: WCSession) {
let acknowledgement = [Self.receiptKey: receipt]
for transfer in session.outstandingUserInfoTransfers
where transfer.userInfo[Self.receiptKey] != nil {
transfer.cancel()
}
session.transferUserInfo(acknowledgement)
if session.isReachable {
session.sendMessage(acknowledgement, replyHandler: nil) { _ in }
}
}
func refreshPresentation(at date: Date = .now) {
presentation = try? core.presentationAt(
unixSeconds: UInt64(date.timeIntervalSince1970)
@@ -160,7 +179,11 @@ private final class WatchSnapshotTransport: NSObject, ObservableObject, WCSessio
self.refreshPresentation()
return
}
self.receive(session.receivedApplicationContext, session: session)
self.receive(
session.receivedApplicationContext,
session: session,
authoritative: true
)
}
}
@@ -169,7 +192,7 @@ private final class WatchSnapshotTransport: NSObject, ObservableObject, WCSessio
didReceiveApplicationContext applicationContext: [String: Any]
) {
Task { @MainActor [weak self] in
self?.receive(applicationContext, session: session)
self?.receive(applicationContext, session: session, authoritative: true)
}
}