From e54d91a83a41b6b8ff5af91f2965d5e6a165ff17 Mon Sep 17 00:00:00 2001 From: Chili Palmer Date: Sun, 16 Aug 2026 14:38:16 +0200 Subject: [PATCH] Implement Apple Watch TOTP interface (#57) --- apple/Generated/ironstorage_watch.swift | 387 ++++++++++++++---- apple/Generated/ironstorage_watchFFI.h | 70 +++- apple/Sources/Watch/IronStorageWatchApp.swift | 176 +++++++- crates/storage/src/mobile_watch.rs | 141 ++++++- crates/storage/tests/mobile_watch.rs | 41 +- crates/watch-apple/src/lib.rs | 100 ++++- 6 files changed, 789 insertions(+), 126 deletions(-) diff --git a/apple/Generated/ironstorage_watch.swift b/apple/Generated/ironstorage_watch.swift index b9e1e7c..ef6d925 100644 --- a/apple/Generated/ironstorage_watch.swift +++ b/apple/Generated/ironstorage_watch.swift @@ -398,7 +398,7 @@ private func uniffiTraitInterfaceCallWithError( callStatus.pointee.errorBuf = FfiConverterString.lower(String(describing: error)) } } -// Initial value and increment amount for handles. +// Initial value and increment amount for handles. // These ensure that SWIFT handles always have the lowest bit set fileprivate let UNIFFI_HANDLEMAP_INITIAL: UInt64 = 1 fileprivate let UNIFFI_HANDLEMAP_DELTA: UInt64 = 2 @@ -562,15 +562,23 @@ fileprivate struct FfiConverterData: FfiConverterRustBuffer { public protocol WatchCoreProtocol: AnyObject, Sendable { - + func applySnapshot(snapshot: Data) throws -> WatchSnapshotUpdate - - func noPersistedSnapshot() throws - - func protectedDataUnavailable() throws - - func recordsAt(unixSeconds: UInt64) throws -> [WatchTotpRecord] - + + func noPersistedSnapshot() throws + + func presentationAt(unixSeconds: UInt64) throws -> WatchPresentation + + func protectedDataUnavailable() throws + + func syncFailed() throws + + func syncFinished() throws + + func syncStarted() throws + + func syncUnavailable() throws + } open class WatchCore: WatchCoreProtocol, @unchecked Sendable { fileprivate let handle: UInt64 @@ -622,9 +630,9 @@ open class WatchCore: WatchCoreProtocol, @unchecked Sendable { try! rustCall { uniffi_ironstorage_watch_fn_free_watchcore(handle, $0) } } - - + + open func applySnapshot(snapshot: Data)throws -> WatchSnapshotUpdate { return try FfiConverterTypeWatchSnapshotUpdate_lift(try rustCallWithError(FfiConverterTypeWatchFfiError_lift) { uniffiCallStatus in @@ -634,7 +642,7 @@ open func applySnapshot(snapshot: Data)throws -> WatchSnapshotUpdate { ) }) } - + open func noPersistedSnapshot()throws {try rustCallWithError(FfiConverterTypeWatchFfiError_lift) { uniffiCallStatus in uniffi_ironstorage_watch_fn_method_watchcore_no_persisted_snapshot( @@ -642,7 +650,17 @@ open func noPersistedSnapshot()throws {try rustCallWithError(FfiConverterTypeW ) } } - + +open func presentationAt(unixSeconds: UInt64)throws -> WatchPresentation { + return try FfiConverterTypeWatchPresentation_lift(try rustCallWithError(FfiConverterTypeWatchFfiError_lift) { + uniffiCallStatus in + uniffi_ironstorage_watch_fn_method_watchcore_presentation_at( + self.uniffiCloneHandle(), + FfiConverterUInt64.lower(unixSeconds),uniffiCallStatus + ) +}) +} + open func protectedDataUnavailable()throws {try rustCallWithError(FfiConverterTypeWatchFfiError_lift) { uniffiCallStatus in uniffi_ironstorage_watch_fn_method_watchcore_protected_data_unavailable( @@ -650,19 +668,41 @@ open func protectedDataUnavailable()throws {try rustCallWithError(FfiConverter ) } } - -open func recordsAt(unixSeconds: UInt64)throws -> [WatchTotpRecord] { - return try FfiConverterSequenceTypeWatchTotpRecord.lift(try rustCallWithError(FfiConverterTypeWatchFfiError_lift) { - uniffiCallStatus in - uniffi_ironstorage_watch_fn_method_watchcore_records_at( - self.uniffiCloneHandle(), - FfiConverterUInt64.lower(unixSeconds),uniffiCallStatus - ) -}) -} - - +open func syncFailed()throws {try rustCallWithError(FfiConverterTypeWatchFfiError_lift) { + uniffiCallStatus in + uniffi_ironstorage_watch_fn_method_watchcore_sync_failed( + self.uniffiCloneHandle(),uniffiCallStatus + ) +} +} + +open func syncFinished()throws {try rustCallWithError(FfiConverterTypeWatchFfiError_lift) { + uniffiCallStatus in + uniffi_ironstorage_watch_fn_method_watchcore_sync_finished( + self.uniffiCloneHandle(),uniffiCallStatus + ) +} +} + +open func syncStarted()throws {try rustCallWithError(FfiConverterTypeWatchFfiError_lift) { + uniffiCallStatus in + uniffi_ironstorage_watch_fn_method_watchcore_sync_started( + self.uniffiCloneHandle(),uniffiCallStatus + ) +} +} + +open func syncUnavailable()throws {try rustCallWithError(FfiConverterTypeWatchFfiError_lift) { + uniffiCallStatus in + uniffi_ironstorage_watch_fn_method_watchcore_sync_unavailable( + self.uniffiCloneHandle(),uniffiCallStatus + ) +} +} + + + } @@ -709,6 +749,68 @@ public func FfiConverterTypeWatchCore_lower(_ value: WatchCore) -> UInt64 { +public struct WatchPresentation: Equatable, Hashable { + public var state: WatchPresentationState + public var title: String + public var detail: String + public var records: [WatchTotpRecord] + + // Default memberwise initializers are never public by default, so we + // declare one manually. + public init(state: WatchPresentationState, title: String, detail: String, records: [WatchTotpRecord]) { + self.state = state + self.title = title + self.detail = detail + self.records = records + } + + + + +} + +#if compiler(>=6) +extension WatchPresentation: Sendable {} +#endif + +#if swift(>=5.8) +@_documentation(visibility: private) +#endif +public struct FfiConverterTypeWatchPresentation: FfiConverterRustBuffer { + public static func read(from buf: inout (data: Data, offset: Data.Index)) throws -> WatchPresentation { + return + try WatchPresentation( + state: FfiConverterTypeWatchPresentationState.read(from: &buf), + title: FfiConverterString.read(from: &buf), + detail: FfiConverterString.read(from: &buf), + records: FfiConverterSequenceTypeWatchTotpRecord.read(from: &buf) + ) + } + + public static func write(_ value: WatchPresentation, into buf: inout [UInt8]) { + FfiConverterTypeWatchPresentationState.write(value.state, into: &buf) + FfiConverterString.write(value.title, into: &buf) + FfiConverterString.write(value.detail, into: &buf) + FfiConverterSequenceTypeWatchTotpRecord.write(value.records, into: &buf) + } +} + + +#if swift(>=5.8) +@_documentation(visibility: private) +#endif +public func FfiConverterTypeWatchPresentation_lift(_ buf: RustBuffer) throws -> WatchPresentation { + return try FfiConverterTypeWatchPresentation.lift(buf) +} + +#if swift(>=5.8) +@_documentation(visibility: private) +#endif +public func FfiConverterTypeWatchPresentation_lower(_ value: WatchPresentation) -> RustBuffer { + return FfiConverterTypeWatchPresentation.lower(value) +} + + public struct WatchSnapshotUpdate: Equatable, Hashable { public var apply: WatchSnapshotApply public var persistence: WatchPersistenceAction @@ -726,9 +828,9 @@ public struct WatchSnapshotUpdate: Equatable, Hashable { self.receipt = receipt } - - + + } #if compiler(>=6) @@ -742,10 +844,10 @@ public struct FfiConverterTypeWatchSnapshotUpdate: FfiConverterRustBuffer { public static func read(from buf: inout (data: Data, offset: Data.Index)) throws -> WatchSnapshotUpdate { return try WatchSnapshotUpdate( - apply: FfiConverterTypeWatchSnapshotApply.read(from: &buf), - persistence: FfiConverterTypeWatchPersistenceAction.read(from: &buf), - revision: FfiConverterOptionUInt64.read(from: &buf), - selectedEntries: FfiConverterUInt32.read(from: &buf), + apply: FfiConverterTypeWatchSnapshotApply.read(from: &buf), + persistence: FfiConverterTypeWatchPersistenceAction.read(from: &buf), + revision: FfiConverterOptionUInt64.read(from: &buf), + selectedEntries: FfiConverterUInt32.read(from: &buf), receipt: FfiConverterData.read(from: &buf) ) } @@ -782,21 +884,23 @@ public struct WatchTotpRecord: Equatable, Hashable { public var code: String public var period: UInt64 public var validUntil: UInt64 + public var remaining: UInt64 // Default memberwise initializers are never public by default, so we // declare one manually. - public init(path: String, issuer: String?, account: String, code: String, period: UInt64, validUntil: UInt64) { + public init(path: String, issuer: String?, account: String, code: String, period: UInt64, validUntil: UInt64, remaining: UInt64) { self.path = path self.issuer = issuer self.account = account self.code = code self.period = period self.validUntil = validUntil + self.remaining = remaining } - - + + } #if compiler(>=6) @@ -810,12 +914,13 @@ public struct FfiConverterTypeWatchTotpRecord: FfiConverterRustBuffer { public static func read(from buf: inout (data: Data, offset: Data.Index)) throws -> WatchTotpRecord { return try WatchTotpRecord( - path: FfiConverterString.read(from: &buf), - issuer: FfiConverterOptionString.read(from: &buf), - account: FfiConverterString.read(from: &buf), - code: FfiConverterString.read(from: &buf), - period: FfiConverterUInt64.read(from: &buf), - validUntil: FfiConverterUInt64.read(from: &buf) + path: FfiConverterString.read(from: &buf), + issuer: FfiConverterOptionString.read(from: &buf), + account: FfiConverterString.read(from: &buf), + code: FfiConverterString.read(from: &buf), + period: FfiConverterUInt64.read(from: &buf), + validUntil: FfiConverterUInt64.read(from: &buf), + remaining: FfiConverterUInt64.read(from: &buf) ) } @@ -826,6 +931,7 @@ public struct FfiConverterTypeWatchTotpRecord: FfiConverterRustBuffer { FfiConverterString.write(value.code, into: &buf) FfiConverterUInt64.write(value.period, into: &buf) FfiConverterUInt64.write(value.validUntil, into: &buf) + FfiConverterUInt64.write(value.remaining, into: &buf) } } @@ -845,23 +951,23 @@ public func FfiConverterTypeWatchTotpRecord_lower(_ value: WatchTotpRecord) -> R } -public +public enum WatchFfiError: Swift.Error, Equatable, Hashable, Foundation.LocalizedError { - - + + case Failed(message: String ) - - - + + + public var errorDescription: String? { String(reflecting: self) } - + } #if compiler(>=6) @@ -878,9 +984,9 @@ public struct FfiConverterTypeWatchFfiError: FfiConverterRustBuffer { let variant: Int32 = try readInt(&buf) switch variant { - - + + case 1: return .Failed( message: try FfiConverterString.read(from: &buf) ) @@ -892,14 +998,14 @@ public struct FfiConverterTypeWatchFfiError: FfiConverterRustBuffer { public static func write(_ value: WatchFfiError, into buf: inout [UInt8]) { switch value { - - - + + + case let .Failed(message): writeInt(&buf, Int32(1)) FfiConverterString.write(message, into: &buf) - + } } } @@ -922,7 +1028,7 @@ public func FfiConverterTypeWatchFfiError_lower(_ value: WatchFfiError) -> RustB public enum WatchPersistenceAction: Equatable, Hashable { - + case keep case replace case delete @@ -946,32 +1052,32 @@ public struct FfiConverterTypeWatchPersistenceAction: FfiConverterRustBuffer { public static func read(from buf: inout (data: Data, offset: Data.Index)) throws -> WatchPersistenceAction { let variant: Int32 = try readInt(&buf) switch variant { - + case 1: return .keep - + case 2: return .replace - + case 3: return .delete - + default: throw UniffiInternalError.unexpectedEnumCase } } public static func write(_ value: WatchPersistenceAction, into buf: inout [UInt8]) { switch value { - - + + case .keep: writeInt(&buf, Int32(1)) - - + + case .replace: writeInt(&buf, Int32(2)) - - + + case .delete: writeInt(&buf, Int32(3)) - + } } } @@ -994,8 +1100,109 @@ public func FfiConverterTypeWatchPersistenceAction_lower(_ value: WatchPersisten +public enum WatchPresentationState: Equatable, Hashable { + + case ready + case empty + case syncing + case stale + case locked + case unavailable + case error + + + + + +} + +#if compiler(>=6) +extension WatchPresentationState: Sendable {} +#endif + +#if swift(>=5.8) +@_documentation(visibility: private) +#endif +public struct FfiConverterTypeWatchPresentationState: FfiConverterRustBuffer { + typealias SwiftType = WatchPresentationState + + public static func read(from buf: inout (data: Data, offset: Data.Index)) throws -> WatchPresentationState { + let variant: Int32 = try readInt(&buf) + switch variant { + + case 1: return .ready + + case 2: return .empty + + case 3: return .syncing + + case 4: return .stale + + case 5: return .locked + + case 6: return .unavailable + + case 7: return .error + + default: throw UniffiInternalError.unexpectedEnumCase + } + } + + public static func write(_ value: WatchPresentationState, into buf: inout [UInt8]) { + switch value { + + + case .ready: + writeInt(&buf, Int32(1)) + + + case .empty: + writeInt(&buf, Int32(2)) + + + case .syncing: + writeInt(&buf, Int32(3)) + + + case .stale: + writeInt(&buf, Int32(4)) + + + case .locked: + writeInt(&buf, Int32(5)) + + + case .unavailable: + writeInt(&buf, Int32(6)) + + + case .error: + writeInt(&buf, Int32(7)) + + } + } +} + + +#if swift(>=5.8) +@_documentation(visibility: private) +#endif +public func FfiConverterTypeWatchPresentationState_lift(_ buf: RustBuffer) throws -> WatchPresentationState { + return try FfiConverterTypeWatchPresentationState.lift(buf) +} + +#if swift(>=5.8) +@_documentation(visibility: private) +#endif +public func FfiConverterTypeWatchPresentationState_lower(_ value: WatchPresentationState) -> RustBuffer { + return FfiConverterTypeWatchPresentationState.lower(value) +} + + + + public enum WatchSnapshotApply: Equatable, Hashable { - + case replaced case revoked case duplicate @@ -1021,44 +1228,44 @@ public struct FfiConverterTypeWatchSnapshotApply: FfiConverterRustBuffer { public static func read(from buf: inout (data: Data, offset: Data.Index)) throws -> WatchSnapshotApply { let variant: Int32 = try readInt(&buf) switch variant { - + case 1: return .replaced - + case 2: return .revoked - + case 3: return .duplicate - + case 4: return .stale - + case 5: return .pairingChanged - + default: throw UniffiInternalError.unexpectedEnumCase } } public static func write(_ value: WatchSnapshotApply, into buf: inout [UInt8]) { switch value { - - + + case .replaced: writeInt(&buf, Int32(1)) - - + + case .revoked: writeInt(&buf, Int32(2)) - - + + case .duplicate: writeInt(&buf, Int32(3)) - - + + case .stale: writeInt(&buf, Int32(4)) - - + + case .pairingChanged: writeInt(&buf, Int32(5)) - + } } } @@ -1183,10 +1390,22 @@ private let initializationResult: InitializationResult = { if (uniffi_ironstorage_watch_checksum_method_watchcore_no_persisted_snapshot() != 32214) { return InitializationResult.apiChecksumMismatch } + if (uniffi_ironstorage_watch_checksum_method_watchcore_presentation_at() != 27053) { + return InitializationResult.apiChecksumMismatch + } if (uniffi_ironstorage_watch_checksum_method_watchcore_protected_data_unavailable() != 42217) { return InitializationResult.apiChecksumMismatch } - if (uniffi_ironstorage_watch_checksum_method_watchcore_records_at() != 48946) { + if (uniffi_ironstorage_watch_checksum_method_watchcore_sync_failed() != 27399) { + return InitializationResult.apiChecksumMismatch + } + if (uniffi_ironstorage_watch_checksum_method_watchcore_sync_finished() != 42146) { + return InitializationResult.apiChecksumMismatch + } + if (uniffi_ironstorage_watch_checksum_method_watchcore_sync_started() != 37956) { + return InitializationResult.apiChecksumMismatch + } + if (uniffi_ironstorage_watch_checksum_method_watchcore_sync_unavailable() != 38367) { return InitializationResult.apiChecksumMismatch } diff --git a/apple/Generated/ironstorage_watchFFI.h b/apple/Generated/ironstorage_watchFFI.h index b0c264d..6c12c9d 100644 --- a/apple/Generated/ironstorage_watchFFI.h +++ b/apple/Generated/ironstorage_watchFFI.h @@ -263,20 +263,40 @@ RustBuffer uniffi_ironstorage_watch_fn_method_watchcore_apply_snapshot(uint64_t void uniffi_ironstorage_watch_fn_method_watchcore_no_persisted_snapshot(uint64_t ptr, RustCallStatus *_Nonnull out_status ); #endif +#ifndef UNIFFI_FFIDEF_UNIFFI_IRONSTORAGE_WATCH_FN_METHOD_WATCHCORE_PRESENTATION_AT +#define UNIFFI_FFIDEF_UNIFFI_IRONSTORAGE_WATCH_FN_METHOD_WATCHCORE_PRESENTATION_AT +RustBuffer uniffi_ironstorage_watch_fn_method_watchcore_presentation_at(uint64_t ptr, uint64_t unix_seconds, RustCallStatus *_Nonnull out_status +); +#endif #ifndef UNIFFI_FFIDEF_UNIFFI_IRONSTORAGE_WATCH_FN_METHOD_WATCHCORE_PROTECTED_DATA_UNAVAILABLE #define UNIFFI_FFIDEF_UNIFFI_IRONSTORAGE_WATCH_FN_METHOD_WATCHCORE_PROTECTED_DATA_UNAVAILABLE void uniffi_ironstorage_watch_fn_method_watchcore_protected_data_unavailable(uint64_t ptr, RustCallStatus *_Nonnull out_status ); #endif -#ifndef UNIFFI_FFIDEF_UNIFFI_IRONSTORAGE_WATCH_FN_METHOD_WATCHCORE_RECORDS_AT -#define UNIFFI_FFIDEF_UNIFFI_IRONSTORAGE_WATCH_FN_METHOD_WATCHCORE_RECORDS_AT -RustBuffer uniffi_ironstorage_watch_fn_method_watchcore_records_at(uint64_t ptr, uint64_t unix_seconds, RustCallStatus *_Nonnull out_status +#ifndef UNIFFI_FFIDEF_UNIFFI_IRONSTORAGE_WATCH_FN_METHOD_WATCHCORE_SYNC_FAILED +#define UNIFFI_FFIDEF_UNIFFI_IRONSTORAGE_WATCH_FN_METHOD_WATCHCORE_SYNC_FAILED +void uniffi_ironstorage_watch_fn_method_watchcore_sync_failed(uint64_t ptr, RustCallStatus *_Nonnull out_status +); +#endif +#ifndef UNIFFI_FFIDEF_UNIFFI_IRONSTORAGE_WATCH_FN_METHOD_WATCHCORE_SYNC_FINISHED +#define UNIFFI_FFIDEF_UNIFFI_IRONSTORAGE_WATCH_FN_METHOD_WATCHCORE_SYNC_FINISHED +void uniffi_ironstorage_watch_fn_method_watchcore_sync_finished(uint64_t ptr, RustCallStatus *_Nonnull out_status +); +#endif +#ifndef UNIFFI_FFIDEF_UNIFFI_IRONSTORAGE_WATCH_FN_METHOD_WATCHCORE_SYNC_STARTED +#define UNIFFI_FFIDEF_UNIFFI_IRONSTORAGE_WATCH_FN_METHOD_WATCHCORE_SYNC_STARTED +void uniffi_ironstorage_watch_fn_method_watchcore_sync_started(uint64_t ptr, RustCallStatus *_Nonnull out_status +); +#endif +#ifndef UNIFFI_FFIDEF_UNIFFI_IRONSTORAGE_WATCH_FN_METHOD_WATCHCORE_SYNC_UNAVAILABLE +#define UNIFFI_FFIDEF_UNIFFI_IRONSTORAGE_WATCH_FN_METHOD_WATCHCORE_SYNC_UNAVAILABLE +void uniffi_ironstorage_watch_fn_method_watchcore_sync_unavailable(uint64_t ptr, RustCallStatus *_Nonnull out_status ); #endif #ifndef UNIFFI_FFIDEF_UNIFFI_IRONSTORAGE_WATCH_FN_FUNC_WATCH_CORE #define UNIFFI_FFIDEF_UNIFFI_IRONSTORAGE_WATCH_FN_FUNC_WATCH_CORE uint64_t uniffi_ironstorage_watch_fn_func_watch_core(RustCallStatus *_Nonnull out_status - + ); #endif #ifndef UNIFFI_FFIDEF_FFI_IRONSTORAGE_WATCH_RUSTBUFFER_ALLOC @@ -542,37 +562,61 @@ void ffi_ironstorage_watch_rust_future_complete_void(uint64_t handle, RustCallSt #ifndef UNIFFI_FFIDEF_UNIFFI_IRONSTORAGE_WATCH_CHECKSUM_FUNC_WATCH_CORE #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_SNAPSHOT #define UNIFFI_FFIDEF_UNIFFI_IRONSTORAGE_WATCH_CHECKSUM_METHOD_WATCHCORE_APPLY_SNAPSHOT uint16_t uniffi_ironstorage_watch_checksum_method_watchcore_apply_snapshot(void - + ); #endif #ifndef UNIFFI_FFIDEF_UNIFFI_IRONSTORAGE_WATCH_CHECKSUM_METHOD_WATCHCORE_NO_PERSISTED_SNAPSHOT #define UNIFFI_FFIDEF_UNIFFI_IRONSTORAGE_WATCH_CHECKSUM_METHOD_WATCHCORE_NO_PERSISTED_SNAPSHOT uint16_t uniffi_ironstorage_watch_checksum_method_watchcore_no_persisted_snapshot(void - + +); +#endif +#ifndef UNIFFI_FFIDEF_UNIFFI_IRONSTORAGE_WATCH_CHECKSUM_METHOD_WATCHCORE_PRESENTATION_AT +#define UNIFFI_FFIDEF_UNIFFI_IRONSTORAGE_WATCH_CHECKSUM_METHOD_WATCHCORE_PRESENTATION_AT +uint16_t uniffi_ironstorage_watch_checksum_method_watchcore_presentation_at(void + ); #endif #ifndef UNIFFI_FFIDEF_UNIFFI_IRONSTORAGE_WATCH_CHECKSUM_METHOD_WATCHCORE_PROTECTED_DATA_UNAVAILABLE #define UNIFFI_FFIDEF_UNIFFI_IRONSTORAGE_WATCH_CHECKSUM_METHOD_WATCHCORE_PROTECTED_DATA_UNAVAILABLE uint16_t uniffi_ironstorage_watch_checksum_method_watchcore_protected_data_unavailable(void - + ); #endif -#ifndef UNIFFI_FFIDEF_UNIFFI_IRONSTORAGE_WATCH_CHECKSUM_METHOD_WATCHCORE_RECORDS_AT -#define UNIFFI_FFIDEF_UNIFFI_IRONSTORAGE_WATCH_CHECKSUM_METHOD_WATCHCORE_RECORDS_AT -uint16_t uniffi_ironstorage_watch_checksum_method_watchcore_records_at(void - +#ifndef UNIFFI_FFIDEF_UNIFFI_IRONSTORAGE_WATCH_CHECKSUM_METHOD_WATCHCORE_SYNC_FAILED +#define UNIFFI_FFIDEF_UNIFFI_IRONSTORAGE_WATCH_CHECKSUM_METHOD_WATCHCORE_SYNC_FAILED +uint16_t uniffi_ironstorage_watch_checksum_method_watchcore_sync_failed(void + +); +#endif +#ifndef UNIFFI_FFIDEF_UNIFFI_IRONSTORAGE_WATCH_CHECKSUM_METHOD_WATCHCORE_SYNC_FINISHED +#define UNIFFI_FFIDEF_UNIFFI_IRONSTORAGE_WATCH_CHECKSUM_METHOD_WATCHCORE_SYNC_FINISHED +uint16_t uniffi_ironstorage_watch_checksum_method_watchcore_sync_finished(void + +); +#endif +#ifndef UNIFFI_FFIDEF_UNIFFI_IRONSTORAGE_WATCH_CHECKSUM_METHOD_WATCHCORE_SYNC_STARTED +#define UNIFFI_FFIDEF_UNIFFI_IRONSTORAGE_WATCH_CHECKSUM_METHOD_WATCHCORE_SYNC_STARTED +uint16_t uniffi_ironstorage_watch_checksum_method_watchcore_sync_started(void + +); +#endif +#ifndef UNIFFI_FFIDEF_UNIFFI_IRONSTORAGE_WATCH_CHECKSUM_METHOD_WATCHCORE_SYNC_UNAVAILABLE +#define UNIFFI_FFIDEF_UNIFFI_IRONSTORAGE_WATCH_CHECKSUM_METHOD_WATCHCORE_SYNC_UNAVAILABLE +uint16_t uniffi_ironstorage_watch_checksum_method_watchcore_sync_unavailable(void + ); #endif #ifndef UNIFFI_FFIDEF_FFI_IRONSTORAGE_WATCH_UNIFFI_CONTRACT_VERSION #define UNIFFI_FFIDEF_FFI_IRONSTORAGE_WATCH_UNIFFI_CONTRACT_VERSION uint32_t ffi_ironstorage_watch_uniffi_contract_version(void - + ); #endif diff --git a/apple/Sources/Watch/IronStorageWatchApp.swift b/apple/Sources/Watch/IronStorageWatchApp.swift index 1ac94b5..9e33c05 100644 --- a/apple/Sources/Watch/IronStorageWatchApp.swift +++ b/apple/Sources/Watch/IronStorageWatchApp.swift @@ -1,3 +1,4 @@ +import Combine import Security import SwiftUI import WatchConnectivity @@ -64,15 +65,21 @@ private final class WatchSnapshotTransport: NSObject, ObservableObject, WCSessio private static let snapshotKey = "de.rfc1437.ironstorage.watch.snapshot" private static let receiptKey = "de.rfc1437.ironstorage.watch.delivered" - @Published private(set) var records: [WatchTotpRecord] = [] + @Published private(set) var presentation: WatchPresentation? private let core = watchCore() override init() { super.init() restoreSnapshot() - guard WCSession.isSupported() else { return } + guard WCSession.isSupported() else { + try? core.syncUnavailable() + refreshPresentation() + return + } let session = WCSession.default session.delegate = self + try? core.syncStarted() + refreshPresentation() session.activate() } @@ -82,7 +89,7 @@ private final class WatchSnapshotTransport: NSObject, ObservableObject, WCSessio func sceneBecameInactive() { try? core.protectedDataUnavailable() - records.removeAll(keepingCapacity: false) + refreshPresentation() } private func restoreSnapshot() { @@ -94,15 +101,20 @@ private final class WatchSnapshotTransport: NSObject, ObservableObject, WCSessio } else { try core.noPersistedSnapshot() } - refreshRecords() + refreshPresentation() } catch { - try? core.protectedDataUnavailable() - records.removeAll(keepingCapacity: false) + try? core.syncFailed() + refreshPresentation() } } private func receive(_ applicationContext: [String: Any], session: WCSession) { - guard var snapshot = applicationContext[Self.snapshotKey] as? Data else { return } + try? core.syncStarted() + guard var snapshot = applicationContext[Self.snapshotKey] as? Data else { + try? core.syncFinished() + refreshPresentation() + return + } defer { snapshot.resetBytes(in: 0.. u64 { self.valid_until } + pub fn remaining(&self) -> u64 { + self.remaining + } pub fn remaining_at(&self, unix_seconds: u64) -> u64 { self.valid_until.saturating_sub(unix_seconds) } @@ -363,14 +367,58 @@ impl fmt::Debug for WatchTotpRecord { .field("code", &"[REDACTED]") .field("period", &self.period) .field("valid_until", &self.valid_until) + .field("remaining", &self.remaining) .finish() } } -#[derive(Default)] +#[derive(Clone, Copy, Debug, Eq, PartialEq)] +pub enum WatchPresentationState { + Ready, + Empty, + Syncing, + Stale, + Locked, + Unavailable, + Error, +} + +pub struct WatchPresentation { + state: WatchPresentationState, + title: String, + detail: String, + records: Vec, +} + +impl WatchPresentation { + pub fn state(&self) -> WatchPresentationState { + self.state + } + pub fn title(&self) -> &str { + &self.title + } + pub fn detail(&self) -> &str { + &self.detail + } + pub fn records(&self) -> &[WatchTotpRecord] { + &self.records + } +} + pub struct WatchRuntime { receiver: WatchSnapshotReceiver, protected_data_available: bool, + presentation_state: WatchPresentationState, +} + +impl Default for WatchRuntime { + fn default() -> Self { + Self { + receiver: WatchSnapshotReceiver::default(), + protected_data_available: true, + presentation_state: WatchPresentationState::Syncing, + } + } } impl WatchRuntime { @@ -378,8 +426,21 @@ impl WatchRuntime { &mut self, bytes: Vec, ) -> Result { - let apply = self.receiver.apply(SecretBytes::new(bytes))?; + let apply = match self.receiver.apply(SecretBytes::new(bytes)) { + Ok(apply) => apply, + Err(error) => { + self.presentation_state = WatchPresentationState::Error; + return Err(error); + } + }; self.protected_data_available = true; + self.presentation_state = match apply { + WatchSnapshotApply::Revoked => WatchPresentationState::Empty, + WatchSnapshotApply::Stale => WatchPresentationState::Stale, + WatchSnapshotApply::Replaced + | WatchSnapshotApply::Duplicate + | WatchSnapshotApply::PairingChanged => WatchPresentationState::Ready, + }; let persistence = match apply { WatchSnapshotApply::Replaced | WatchSnapshotApply::PairingChanged => { WatchPersistenceAction::Replace @@ -434,19 +495,95 @@ impl WatchRuntime { code, period: entry.period, valid_until, + remaining: valid_until.saturating_sub(unix_seconds), }) }) .collect() } + pub fn presentation_at( + &self, + unix_seconds: u64, + ) -> Result { + let records = if matches!( + self.presentation_state, + WatchPresentationState::Ready | WatchPresentationState::Stale + ) { + self.records_at(unix_seconds)? + } else { + Vec::new() + }; + let (title, detail) = match self.presentation_state { + WatchPresentationState::Ready => ("TOTP Codes", "Select an entry to view its code."), + WatchPresentationState::Empty => ( + "No TOTP Codes", + "Select TOTP entries in IronStorage on your iPhone.", + ), + WatchPresentationState::Syncing => ( + "Syncing", + "Checking for TOTP entries shared by your iPhone.", + ), + WatchPresentationState::Stale => ( + "Update Delayed", + "Showing saved codes; the received update was older.", + ), + WatchPresentationState::Locked => ( + "Watch Locked", + "Unlock your Watch to access shared TOTP codes.", + ), + WatchPresentationState::Unavailable => ( + "Sync Unavailable", + "Open IronStorage on your iPhone to set up Watch sync.", + ), + WatchPresentationState::Error => ( + "Codes Unavailable", + "Open IronStorage on your iPhone and share the entries again.", + ), + }; + Ok(WatchPresentation { + state: self.presentation_state, + title: title.to_owned(), + detail: detail.to_owned(), + records, + }) + } + + pub fn sync_started(&mut self) { + if self.receiver.current().is_none() && self.protected_data_available { + self.presentation_state = WatchPresentationState::Syncing; + } + } + + pub fn sync_finished(&mut self) { + if self.receiver.current().is_none() + && self.protected_data_available + && self.presentation_state == WatchPresentationState::Syncing + { + self.presentation_state = WatchPresentationState::Empty; + } + } + + pub fn sync_unavailable(&mut self) { + if self.receiver.current().is_none() && self.protected_data_available { + self.presentation_state = WatchPresentationState::Unavailable; + } + } + + pub fn sync_failed(&mut self) { + self.receiver.clear_secrets(); + self.presentation_state = WatchPresentationState::Error; + } + pub fn protected_data_unavailable(&mut self) { self.receiver.clear_secrets(); self.protected_data_available = false; + self.presentation_state = WatchPresentationState::Locked; } pub fn no_persisted_snapshot(&mut self) { self.receiver.revoke(); self.protected_data_available = true; + self.presentation_state = WatchPresentationState::Empty; } } diff --git a/crates/storage/tests/mobile_watch.rs b/crates/storage/tests/mobile_watch.rs index 7d5da60..da74670 100644 --- a/crates/storage/tests/mobile_watch.rs +++ b/crates/storage/tests/mobile_watch.rs @@ -4,8 +4,8 @@ use std::fs; use ironstorage::{ mobile_watch::{ - MobileWatchSnapshotState, WatchPersistenceAction, WatchRuntime, WatchSnapshotApply, - WatchSnapshotEntry, WatchSnapshotReceiver, WatchSnapshotSender, + MobileWatchSnapshotState, WatchPersistenceAction, WatchPresentationState, WatchRuntime, + WatchSnapshotApply, WatchSnapshotEntry, WatchSnapshotReceiver, WatchSnapshotSender, }, otp::OtpAlgorithm, repository::{EntryPath, SecretBytes}, @@ -184,6 +184,22 @@ fn watch_runtime_generates_view_ready_totp_and_clears_secrets_when_locked() -> T )?; let mut runtime = WatchRuntime::default(); + assert_eq!( + runtime.presentation_at(59)?.state(), + WatchPresentationState::Syncing + ); + runtime.sync_finished(); + assert_eq!( + runtime.presentation_at(59)?.state(), + WatchPresentationState::Empty + ); + runtime.sync_started(); + runtime.sync_unavailable(); + assert_eq!( + runtime.presentation_at(59)?.state(), + WatchPresentationState::Unavailable + ); + runtime.sync_started(); let update = runtime.apply_snapshot(snapshot.snapshot().expose().to_vec())?; assert_eq!(update.apply(), WatchSnapshotApply::Replaced); assert_eq!(update.persistence(), WatchPersistenceAction::Replace); @@ -196,9 +212,16 @@ fn watch_runtime_generates_view_ready_totp_and_clears_secrets_when_locked() -> T assert_eq!(records[0].code().expose(), b"94287082"); assert_eq!(records[0].valid_until(), 60); assert_eq!(records[0].remaining_at(59), 1); + let presentation = runtime.presentation_at(59)?; + assert_eq!(presentation.state(), WatchPresentationState::Ready); + assert_eq!(presentation.records()[0].remaining(), 1); runtime.protected_data_unavailable(); assert!(runtime.records_at(59).is_err()); + assert_eq!( + runtime.presentation_at(59)?.state(), + WatchPresentationState::Locked + ); let restored = runtime.apply_snapshot(snapshot.snapshot().expose().to_vec())?; assert_eq!(restored.persistence(), WatchPersistenceAction::Replace); @@ -209,5 +232,19 @@ fn watch_runtime_generates_view_ready_totp_and_clears_secrets_when_locked() -> T assert_eq!(revoked.apply(), WatchSnapshotApply::Revoked); assert_eq!(revoked.persistence(), WatchPersistenceAction::Delete); assert!(runtime.records_at(59)?.is_empty()); + assert_eq!( + runtime.presentation_at(59)?.state(), + WatchPresentationState::Empty + ); + runtime.apply_snapshot(snapshot.snapshot().expose().to_vec())?; + assert_eq!( + runtime.presentation_at(59)?.state(), + WatchPresentationState::Stale + ); + assert!(runtime.apply_snapshot(vec![0; 64]).is_err()); + assert_eq!( + runtime.presentation_at(59)?.state(), + WatchPresentationState::Error + ); Ok(()) } diff --git a/crates/watch-apple/src/lib.rs b/crates/watch-apple/src/lib.rs index 01c696c..5299a16 100644 --- a/crates/watch-apple/src/lib.rs +++ b/crates/watch-apple/src/lib.rs @@ -10,7 +10,8 @@ use std::{ }; use ironstorage::mobile_watch::{ - WatchPersistenceAction as StoragePersistenceAction, WatchRuntime as StorageWatchRuntime, + WatchPersistenceAction as StoragePersistenceAction, WatchPresentation as StoragePresentation, + WatchPresentationState as StoragePresentationState, WatchRuntime as StorageWatchRuntime, WatchSnapshotApply as StorageSnapshotApply, WatchSnapshotError as StorageWatchError, WatchSnapshotUpdate as StorageSnapshotUpdate, WatchTotpRecord as StorageTotpRecord, }; @@ -83,10 +84,11 @@ pub struct WatchTotpRecord { pub code: String, pub period: u64, pub valid_until: u64, + pub remaining: u64, } -impl From for WatchTotpRecord { - fn from(value: StorageTotpRecord) -> Self { +impl From<&StorageTotpRecord> for WatchTotpRecord { + fn from(value: &StorageTotpRecord) -> Self { Self { path: value.path().to_owned(), issuer: value.issuer().map(str::to_owned), @@ -95,6 +97,51 @@ impl From for WatchTotpRecord { .expect("storage-generated TOTP codes are ASCII"), period: value.period(), valid_until: value.valid_until(), + remaining: value.remaining(), + } + } +} + +#[derive(Clone, Copy, Debug, Eq, PartialEq, uniffi::Enum)] +pub enum WatchPresentationState { + Ready, + Empty, + Syncing, + Stale, + Locked, + Unavailable, + Error, +} + +impl From for WatchPresentationState { + fn from(value: StoragePresentationState) -> Self { + match value { + StoragePresentationState::Ready => Self::Ready, + StoragePresentationState::Empty => Self::Empty, + StoragePresentationState::Syncing => Self::Syncing, + StoragePresentationState::Stale => Self::Stale, + StoragePresentationState::Locked => Self::Locked, + StoragePresentationState::Unavailable => Self::Unavailable, + StoragePresentationState::Error => Self::Error, + } + } +} + +#[derive(Clone, Debug, Eq, PartialEq, uniffi::Record)] +pub struct WatchPresentation { + pub state: WatchPresentationState, + pub title: String, + pub detail: String, + pub records: Vec, +} + +impl From for WatchPresentation { + fn from(value: StoragePresentation) -> Self { + Self { + state: value.state().into(), + title: value.title().to_owned(), + detail: value.detail().to_owned(), + records: value.records().iter().map(WatchTotpRecord::from).collect(), } } } @@ -138,15 +185,44 @@ impl WatchCore { .map_err(Into::into) } - pub fn records_at(&self, unix_seconds: u64) -> Result, WatchFfiError> { + pub fn presentation_at(&self, unix_seconds: u64) -> Result { self.runtime .lock() .map_err(|_| lock_error())? - .records_at(unix_seconds) - .map(|records| records.into_iter().map(Into::into).collect()) + .presentation_at(unix_seconds) + .map(Into::into) .map_err(Into::into) } + pub fn sync_started(&self) -> Result<(), WatchFfiError> { + self.runtime + .lock() + .map_err(|_| lock_error())? + .sync_started(); + Ok(()) + } + + pub fn sync_finished(&self) -> Result<(), WatchFfiError> { + self.runtime + .lock() + .map_err(|_| lock_error())? + .sync_finished(); + Ok(()) + } + + pub fn sync_unavailable(&self) -> Result<(), WatchFfiError> { + self.runtime + .lock() + .map_err(|_| lock_error())? + .sync_unavailable(); + Ok(()) + } + + pub fn sync_failed(&self) -> Result<(), WatchFfiError> { + self.runtime.lock().map_err(|_| lock_error())?.sync_failed(); + Ok(()) + } + pub fn protected_data_unavailable(&self) -> Result<(), WatchFfiError> { self.runtime .lock() @@ -183,8 +259,16 @@ mod tests { fn bridge_masks_records_when_protected_data_is_unavailable() { let core = super::watch_core(); core.no_persisted_snapshot().expect("available Keychain"); - assert!(core.records_at(59).expect("empty snapshot").is_empty()); + assert!( + core.presentation_at(59) + .expect("empty snapshot") + .records + .is_empty() + ); core.protected_data_unavailable().expect("lock transition"); - assert!(core.records_at(59).is_err()); + assert_eq!( + core.presentation_at(59).expect("locked presentation").state, + super::WatchPresentationState::Locked + ); } }