diff --git a/Sesame-Watch Watch App/Sesame_WatchApp.swift b/Sesame-Watch Watch App/Sesame_WatchApp.swift index f0ceeec..1aa854f 100644 --- a/Sesame-Watch Watch App/Sesame_WatchApp.swift +++ b/Sesame-Watch Watch App/Sesame_WatchApp.swift @@ -1,8 +1,14 @@ import SwiftUI import SwiftData +private enum MainScreenSelection: Int { + case unlock = 0 + case settings = 1 + case history = 2 +} + @main -struct Sesame_Watch_Watch_AppApp: App { +struct SesameWatchApp: App { @State var modelContainer: ModelContainer @@ -13,36 +19,32 @@ struct Sesame_Watch_Watch_AppApp: App { let keyManagement = KeyManagement() @State - var selected: Int = 0 + private var selectedScreen: MainScreenSelection = .unlock @State var didLaunchFromComplication = false init() { - do { - let modelContainer = try ModelContainer(for: HistoryItem.self) - self.modelContainer = modelContainer - self.coordinator = .init(modelContext: modelContainer.mainContext) - } catch { - fatalError("Failed to create model container: \(error)") - } + let modelContainer = SesameWatchApp.loadModelContainer() + self.modelContainer = modelContainer + self.coordinator = .init(modelContext: modelContainer.mainContext) } var body: some Scene { WindowGroup { - TabView(selection: $selected) { + TabView(selection: $selectedScreen) { ContentView(coordinator: coordinator, didLaunchFromComplication: $didLaunchFromComplication) - .tag(1) + .tag(MainScreenSelection.unlock) SettingsView() .environmentObject(keyManagement) - .tag(2) + .tag(MainScreenSelection.settings) HistoryView() - .tag(3) + .tag(MainScreenSelection.history) } .tabViewStyle(PageTabViewStyle()) .onOpenURL { url in - selected = 0 didLaunchFromComplication = true + selectedScreen = .unlock } } .modelContainer(modelContainer) diff --git a/Sesame.xcodeproj/project.pbxproj b/Sesame.xcodeproj/project.pbxproj index 195369b..5968323 100644 --- a/Sesame.xcodeproj/project.pbxproj +++ b/Sesame.xcodeproj/project.pbxproj @@ -67,6 +67,8 @@ 88E197D429EDCE7600BF1D19 /* UInt32+Coding.swift in Sources */ = {isa = PBXBuildFile; fileRef = E2C5C1DC281B3AC400769EF6 /* UInt32+Coding.swift */; }; 88E197D729EDCFE800BF1D19 /* Date+Extensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 88E197D629EDCFE800BF1D19 /* Date+Extensions.swift */; }; 88E197D829EDD13B00BF1D19 /* SymmetricKey+Extensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 884A45CA27A464C000D6E650 /* SymmetricKey+Extensions.swift */; }; + 88E35EF52B3B0A9800485A66 /* App+Extensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 88E35EF42B3B0A9800485A66 /* App+Extensions.swift */; }; + 88E35EF62B3B0A9800485A66 /* App+Extensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 88E35EF42B3B0A9800485A66 /* App+Extensions.swift */; }; E240654B2A8153C6009C1AD8 /* SettingsListTextItem.swift in Sources */ = {isa = PBXBuildFile; fileRef = E240654A2A8153C6009C1AD8 /* SettingsListTextItem.swift */; }; E240654F2A8159B7009C1AD8 /* SettingsTextInputView.swift in Sources */ = {isa = PBXBuildFile; fileRef = E240654E2A8159B7009C1AD8 /* SettingsTextInputView.swift */; }; E24065512A819066009C1AD8 /* SettingsTextItemLink.swift in Sources */ = {isa = PBXBuildFile; fileRef = E24065502A819066009C1AD8 /* SettingsTextItemLink.swift */; }; @@ -160,6 +162,7 @@ 88E197B329EDC9BC00BF1D19 /* ContentView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ContentView.swift; sourceTree = ""; }; 88E197B529EDC9BD00BF1D19 /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; }; 88E197D629EDCFE800BF1D19 /* Date+Extensions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Date+Extensions.swift"; sourceTree = ""; }; + 88E35EF42B3B0A9800485A66 /* App+Extensions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "App+Extensions.swift"; sourceTree = ""; }; E240654A2A8153C6009C1AD8 /* SettingsListTextItem.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SettingsListTextItem.swift; sourceTree = ""; }; E240654E2A8159B7009C1AD8 /* SettingsTextInputView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SettingsTextInputView.swift; sourceTree = ""; }; E24065502A819066009C1AD8 /* SettingsTextItemLink.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SettingsTextItemLink.swift; sourceTree = ""; }; @@ -283,6 +286,7 @@ 884A45CA27A464C000D6E650 /* SymmetricKey+Extensions.swift */, 88AEE3802B22327F0034EDA9 /* UInt32+Random.swift */, 8860D76D2B246FC400849FAC /* Text+Extensions.swift */, + 88E35EF42B3B0A9800485A66 /* App+Extensions.swift */, ); path = Extensions; sourceTree = ""; @@ -554,6 +558,7 @@ E24EE77227FDCCC00011CFD2 /* Data+Hex.swift in Sources */, 8860D7482B23294600849FAC /* SignedMessage+Crypto.swift in Sources */, 8860D74C2B232A7700849FAC /* SesameHeader.swift in Sources */, + 88E35EF52B3B0A9800485A66 /* App+Extensions.swift in Sources */, 8860D7622B23803E00849FAC /* ServerChallenge.swift in Sources */, 8860D7432B22858600849FAC /* Date+Timestamp.swift in Sources */, 88AEE3862B22376D0034EDA9 /* Message+Crypto.swift in Sources */, @@ -595,6 +600,7 @@ 888362362A80F4420032BBB2 /* HistoryView.swift in Sources */, 8860D75B2B237FB600849FAC /* SignedMessage+Crypto.swift in Sources */, E240654F2A8159B7009C1AD8 /* SettingsTextInputView.swift in Sources */, + 88E35EF62B3B0A9800485A66 /* App+Extensions.swift in Sources */, 88E197D329EDCE6E00BF1D19 /* MessageResult.swift in Sources */, 88E197D129EDCE5F00BF1D19 /* Data+Hex.swift in Sources */, 8860D76C2B246F5E00849FAC /* UInt32+Random.swift in Sources */, diff --git a/Sesame/Common/Extensions/App+Extensions.swift b/Sesame/Common/Extensions/App+Extensions.swift new file mode 100644 index 0000000..c619cfc --- /dev/null +++ b/Sesame/Common/Extensions/App+Extensions.swift @@ -0,0 +1,37 @@ +import SwiftUI +import SwiftData + +extension App { + + static func loadModelContainer() -> ModelContainer { + do { + return try ModelContainer(for: HistoryItem.self) + } catch { + print("[WARNING] Removing default SwiftData storage") + removeDefaultModelContainer() + } + // Try again to load an empty container + do { + return try ModelContainer(for: HistoryItem.self) + } catch { + fatalError("Failed to create empty model container: \(error)") + } + } + + private static func removeDefaultModelContainer() { + guard let appSupportDir = FileManager.default.urls(for: .applicationSupportDirectory, in: .userDomainMask).last else { + fatalError("Failed to get application support directory") + } + do { + try FileManager.default.contentsOfDirectory(at: appSupportDir, includingPropertiesForKeys: nil) + .filter { $0.lastPathComponent.hasPrefix("default") } + .forEach { + try FileManager.default.removeItem(at: $0) + } + } catch { + print(error) + fatalError("Failed to remove default SwiftData database files") + } + } + +} diff --git a/Sesame/History/HistoryItem.swift b/Sesame/History/HistoryItem.swift index bd00cde..c06d791 100644 --- a/Sesame/History/HistoryItem.swift +++ b/Sesame/History/HistoryItem.swift @@ -6,7 +6,7 @@ final class HistoryItem { let startDate: Date - let message: Message + let message: HistoryMessage let route: TransmissionType @@ -14,7 +14,11 @@ final class HistoryItem { init(message: Message, startDate: Date, route: TransmissionType, finishDate: Date) { self.startDate = startDate - self.message = message + self.message = .init( + messageType: message.messageType, + clientChallenge: Int(message.clientChallenge), + serverChallenge: Int(message.serverChallenge), + result: message.result) self.finishDate = finishDate self.route = route } @@ -28,6 +32,59 @@ final class HistoryItem { } } +struct HistoryMessage { + + /// The type of message being sent. + let messageType: MessageType + + /** + * The random nonce created by the remote + * + * This nonce is a random number created by the remote, different for each unlock request. + * It is set for all message types. + */ + let clientChallenge: Int + + /** + * A random number to sign by the remote + * + * This nonce is set by the server after receiving an initial message. + * It is set for the message types `challenge`, `request`, and `response`. + */ + let serverChallenge: Int + + /** + * The response status for the previous message. + * + * It is set only for messages from the server, e.g. the `challenge` and `response` message types. + * Must be set to `MessageAccepted` for other messages. + */ + let result: MessageResult + } + +extension HistoryMessage: Encodable { + + func encode(to encoder: Encoder) throws { + var container = encoder.unkeyedContainer() + try container.encode(messageType) + try container.encode(clientChallenge) + try container.encode(serverChallenge) + try container.encode(result) + } +} + +extension HistoryMessage: Decodable { + + init(from decoder: Decoder) throws { + var container = try decoder.unkeyedContainer() + self.messageType = try container.decode(MessageType.self) + self.clientChallenge = try container.decode(Int.self) + self.serverChallenge = try container.decode(Int.self) + self.result = try container.decode(MessageResult.self) + } +} + + extension HistoryItem: Identifiable { var id: Double { diff --git a/Sesame/SesameApp.swift b/Sesame/SesameApp.swift index 620540c..4f69a3e 100644 --- a/Sesame/SesameApp.swift +++ b/Sesame/SesameApp.swift @@ -8,11 +8,7 @@ struct SesameApp: App { var modelContainer: ModelContainer init() { - do { - self.modelContainer = try ModelContainer(for: HistoryItem.self) - } catch { - fatalError("Failed to create model container: \(error)") - } + self.modelContainer = SesameApp.loadModelContainer() } var body: some Scene {