Compare commits
9 Commits
f284696e21
...
master
Author | SHA1 | Date | |
---|---|---|---|
3591842861 | |||
91af68a44b | |||
877bba56b4 | |||
c049f9c9d6 | |||
03735d9e72 | |||
e4f93d94a9 | |||
973f0cb1c1 | |||
e9d870bd12 | |||
9086c6a916 |
BIN
Banner.key
BIN
Banner.key
Binary file not shown.
@ -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)
|
||||
|
@ -1,11 +1,11 @@
|
||||
import SwiftUI
|
||||
|
||||
struct SettingsNumberInputView: View {
|
||||
struct SettingsNumberInputView<Value>: View where Value: FixedWidthInteger {
|
||||
|
||||
let title: String
|
||||
|
||||
@Binding
|
||||
var value: Int
|
||||
var value: Value
|
||||
|
||||
@State
|
||||
private var text: String = ""
|
||||
@ -16,7 +16,7 @@ struct SettingsNumberInputView: View {
|
||||
VStack(alignment: .leading) {
|
||||
TextField(title, text: $text)
|
||||
.onSubmit {
|
||||
guard let newValue = Int(text) else {
|
||||
guard let newValue = Value(text) else {
|
||||
return
|
||||
}
|
||||
value = newValue
|
||||
|
@ -1,11 +1,11 @@
|
||||
import SwiftUI
|
||||
|
||||
struct SettingsNumberItemLink: View {
|
||||
struct SettingsNumberItemLink<Value>: View where Value: FixedWidthInteger {
|
||||
|
||||
let title: String
|
||||
|
||||
@Binding
|
||||
var value: Int
|
||||
var value: Value
|
||||
|
||||
let footnote: String
|
||||
|
||||
|
@ -11,6 +11,9 @@ struct SettingsView: View {
|
||||
@AppStorage("localIP")
|
||||
var localAddress: String = "192.168.178.104/"
|
||||
|
||||
@AppStorage("localPort")
|
||||
var localPort: UInt16 = 8888
|
||||
|
||||
@EnvironmentObject
|
||||
var keys: KeyManagement
|
||||
|
||||
@ -38,17 +41,21 @@ struct SettingsView: View {
|
||||
title: "Local url",
|
||||
value: $localAddress,
|
||||
footnote: "The url where the device can be reached directly on the local WiFi network.")
|
||||
SettingsNumberItemLink(
|
||||
title: "Local port",
|
||||
value: $localPort,
|
||||
footnote: "The port for the local connection")
|
||||
SettingsKeyItemLink(
|
||||
type: .deviceKey,
|
||||
footnote: "Some text describing the purpose of the key.")
|
||||
footnote: "The key used by the device for responses")
|
||||
.environmentObject(keys)
|
||||
SettingsKeyItemLink(
|
||||
type: .remoteKey,
|
||||
footnote: "Some text describing the purpose of the key.")
|
||||
footnote: "The key used by the remote for requests.")
|
||||
.environmentObject(keys)
|
||||
SettingsKeyItemLink(
|
||||
type: .authToken,
|
||||
footnote: "Some text describing the purpose of the key.")
|
||||
footnote: "The authentication token of the remote for the server.")
|
||||
.environmentObject(keys)
|
||||
}
|
||||
.navigationTitle("Settings")
|
||||
|
@ -2,6 +2,7 @@ import SwiftUI
|
||||
import SwiftData
|
||||
import SFSafeSymbols
|
||||
import CryptoKit
|
||||
import UIKit
|
||||
|
||||
struct ContentView: View {
|
||||
|
||||
@ -40,7 +41,7 @@ struct ContentView: View {
|
||||
.aspectRatio(contentMode: .fit)
|
||||
.fontWeight(.ultraLight)
|
||||
.padding()
|
||||
.onTapGesture(perform: coordinator.startUnlock)
|
||||
.onTapGesture { coordinator.startUnlock() }
|
||||
if coordinator.isPerformingRequest {
|
||||
ProgressView()
|
||||
.progressViewStyle(CircularProgressViewStyle())
|
||||
@ -58,8 +59,8 @@ struct ContentView: View {
|
||||
guard launched else {
|
||||
return
|
||||
}
|
||||
coordinator.startUnlock(quitAfterSuccess: true)
|
||||
didLaunchFromComplication = false
|
||||
coordinator.startUnlock()
|
||||
}
|
||||
}
|
||||
}
|
@ -8,7 +8,7 @@
|
||||
|
||||
/* Begin PBXBuildFile section */
|
||||
884A45B7279F48C100D6E650 /* SesameApp.swift in Sources */ = {isa = PBXBuildFile; fileRef = 884A45B6279F48C100D6E650 /* SesameApp.swift */; };
|
||||
884A45B9279F48C100D6E650 /* ContentView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 884A45B8279F48C100D6E650 /* ContentView.swift */; };
|
||||
884A45B9279F48C100D6E650 /* MainView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 884A45B8279F48C100D6E650 /* MainView.swift */; };
|
||||
884A45BB279F48C300D6E650 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 884A45BA279F48C300D6E650 /* Assets.xcassets */; };
|
||||
884A45C5279F4BBE00D6E650 /* KeyManagement.swift in Sources */ = {isa = PBXBuildFile; fileRef = 884A45C4279F4BBE00D6E650 /* KeyManagement.swift */; };
|
||||
884A45CB27A464C000D6E650 /* SymmetricKey+Extensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 884A45CA27A464C000D6E650 /* SymmetricKey+Extensions.swift */; };
|
||||
@ -53,8 +53,9 @@
|
||||
88AEE3842B2236DC0034EDA9 /* SignedMessage.swift in Sources */ = {isa = PBXBuildFile; fileRef = 88AEE3832B2236DC0034EDA9 /* SignedMessage.swift */; };
|
||||
88AEE3862B22376D0034EDA9 /* Message+Crypto.swift in Sources */ = {isa = PBXBuildFile; fileRef = 88AEE3852B22376D0034EDA9 /* Message+Crypto.swift */; };
|
||||
88AEE3882B226FED0034EDA9 /* MessageResult+UI.swift in Sources */ = {isa = PBXBuildFile; fileRef = 88AEE3872B226FED0034EDA9 /* MessageResult+UI.swift */; };
|
||||
88BA7DD32BD41B8A008F2A3C /* UDPClient.swift in Sources */ = {isa = PBXBuildFile; fileRef = 88BA7DD22BD41B8A008F2A3C /* UDPClient.swift */; };
|
||||
88E197B229EDC9BC00BF1D19 /* Sesame_WatchApp.swift in Sources */ = {isa = PBXBuildFile; fileRef = 88E197B129EDC9BC00BF1D19 /* Sesame_WatchApp.swift */; };
|
||||
88E197B429EDC9BC00BF1D19 /* ContentView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 88E197B329EDC9BC00BF1D19 /* ContentView.swift */; };
|
||||
88E197B429EDC9BC00BF1D19 /* UnlockView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 88E197B329EDC9BC00BF1D19 /* UnlockView.swift */; };
|
||||
88E197B629EDC9BD00BF1D19 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 88E197B529EDC9BD00BF1D19 /* Assets.xcassets */; };
|
||||
88E197C429EDCC8900BF1D19 /* Client.swift in Sources */ = {isa = PBXBuildFile; fileRef = 884A45CC27A465F500D6E650 /* Client.swift */; };
|
||||
88E197C729EDCCBD00BF1D19 /* Client.swift in Sources */ = {isa = PBXBuildFile; fileRef = 884A45CC27A465F500D6E650 /* Client.swift */; };
|
||||
@ -67,6 +68,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 */; };
|
||||
@ -83,6 +86,7 @@
|
||||
E24EE77927FF95E00011CFD2 /* Message.swift in Sources */ = {isa = PBXBuildFile; fileRef = E24EE77827FF95E00011CFD2 /* Message.swift */; };
|
||||
E24F6C6E2A89749A0040F8C4 /* ConnectionStrategy.swift in Sources */ = {isa = PBXBuildFile; fileRef = E24F6C6D2A89749A0040F8C4 /* ConnectionStrategy.swift */; };
|
||||
E24F6C6F2A8974C60040F8C4 /* ConnectionStrategy.swift in Sources */ = {isa = PBXBuildFile; fileRef = E24F6C6D2A89749A0040F8C4 /* ConnectionStrategy.swift */; };
|
||||
E25231782C0227B500FFE373 /* UDPClient.swift in Sources */ = {isa = PBXBuildFile; fileRef = 88BA7DD22BD41B8A008F2A3C /* UDPClient.swift */; };
|
||||
E25317562A8A1ABF005A537D /* HistoryItem.swift in Sources */ = {isa = PBXBuildFile; fileRef = E28DED34281EB17600259690 /* HistoryItem.swift */; };
|
||||
E268E0822A85302000185913 /* WidgetKit.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = E268E0532A852F8E00185913 /* WidgetKit.framework */; };
|
||||
E268E0832A85302000185913 /* SwiftUI.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = E268E0552A852F8E00185913 /* SwiftUI.framework */; };
|
||||
@ -129,7 +133,7 @@
|
||||
/* Begin PBXFileReference section */
|
||||
884A45B3279F48C100D6E650 /* Sesame.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = Sesame.app; sourceTree = BUILT_PRODUCTS_DIR; };
|
||||
884A45B6279F48C100D6E650 /* SesameApp.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SesameApp.swift; sourceTree = "<group>"; };
|
||||
884A45B8279F48C100D6E650 /* ContentView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ContentView.swift; sourceTree = "<group>"; };
|
||||
884A45B8279F48C100D6E650 /* MainView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MainView.swift; sourceTree = "<group>"; };
|
||||
884A45BA279F48C300D6E650 /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = "<group>"; };
|
||||
884A45C4279F4BBE00D6E650 /* KeyManagement.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = KeyManagement.swift; sourceTree = "<group>"; };
|
||||
884A45CA27A464C000D6E650 /* SymmetricKey+Extensions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "SymmetricKey+Extensions.swift"; sourceTree = "<group>"; };
|
||||
@ -155,11 +159,13 @@
|
||||
88AEE3832B2236DC0034EDA9 /* SignedMessage.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SignedMessage.swift; sourceTree = "<group>"; };
|
||||
88AEE3852B22376D0034EDA9 /* Message+Crypto.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Message+Crypto.swift"; sourceTree = "<group>"; };
|
||||
88AEE3872B226FED0034EDA9 /* MessageResult+UI.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "MessageResult+UI.swift"; sourceTree = "<group>"; };
|
||||
88BA7DD22BD41B8A008F2A3C /* UDPClient.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UDPClient.swift; sourceTree = "<group>"; };
|
||||
88E197AC29EDC9BC00BF1D19 /* Sesame-Watch Watch App.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = "Sesame-Watch Watch App.app"; sourceTree = BUILT_PRODUCTS_DIR; };
|
||||
88E197B129EDC9BC00BF1D19 /* Sesame_WatchApp.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Sesame_WatchApp.swift; sourceTree = "<group>"; };
|
||||
88E197B329EDC9BC00BF1D19 /* ContentView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ContentView.swift; sourceTree = "<group>"; };
|
||||
88E197B329EDC9BC00BF1D19 /* UnlockView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UnlockView.swift; sourceTree = "<group>"; };
|
||||
88E197B529EDC9BD00BF1D19 /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = "<group>"; };
|
||||
88E197D629EDCFE800BF1D19 /* Date+Extensions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Date+Extensions.swift"; sourceTree = "<group>"; };
|
||||
88E35EF42B3B0A9800485A66 /* App+Extensions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "App+Extensions.swift"; sourceTree = "<group>"; };
|
||||
E240654A2A8153C6009C1AD8 /* SettingsListTextItem.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SettingsListTextItem.swift; sourceTree = "<group>"; };
|
||||
E240654E2A8159B7009C1AD8 /* SettingsTextInputView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SettingsTextInputView.swift; sourceTree = "<group>"; };
|
||||
E24065502A819066009C1AD8 /* SettingsTextItemLink.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SettingsTextItemLink.swift; sourceTree = "<group>"; };
|
||||
@ -254,7 +260,7 @@
|
||||
E2C5C1D92806FE4A00769EF6 /* API */,
|
||||
8860D7442B2328B800849FAC /* API Extensions */,
|
||||
884A45B6279F48C100D6E650 /* SesameApp.swift */,
|
||||
884A45B8279F48C100D6E650 /* ContentView.swift */,
|
||||
884A45B8279F48C100D6E650 /* MainView.swift */,
|
||||
E28DED2C281E840B00259690 /* SettingsView.swift */,
|
||||
E28DED2E281E8A0500259690 /* SingleKeyView.swift */,
|
||||
E25317542A8A1A07005A537D /* History */,
|
||||
@ -283,6 +289,7 @@
|
||||
884A45CA27A464C000D6E650 /* SymmetricKey+Extensions.swift */,
|
||||
88AEE3802B22327F0034EDA9 /* UInt32+Random.swift */,
|
||||
8860D76D2B246FC400849FAC /* Text+Extensions.swift */,
|
||||
88E35EF42B3B0A9800485A66 /* App+Extensions.swift */,
|
||||
);
|
||||
path = Extensions;
|
||||
sourceTree = "<group>";
|
||||
@ -304,7 +311,7 @@
|
||||
88E197B529EDC9BD00BF1D19 /* Assets.xcassets */,
|
||||
E24065562A819AAD009C1AD8 /* Settings */,
|
||||
88E197B129EDC9BC00BF1D19 /* Sesame_WatchApp.swift */,
|
||||
88E197B329EDC9BC00BF1D19 /* ContentView.swift */,
|
||||
88E197B329EDC9BC00BF1D19 /* UnlockView.swift */,
|
||||
888362332A80F3F90032BBB2 /* SettingsView.swift */,
|
||||
888362352A80F4420032BBB2 /* HistoryView.swift */,
|
||||
E240655D2A822E97009C1AD8 /* HistoryListRow.swift */,
|
||||
@ -350,6 +357,7 @@
|
||||
8860D7612B23803E00849FAC /* ServerChallenge.swift */,
|
||||
8860D7642B23B5B200849FAC /* RequestCoordinator.swift */,
|
||||
8860D7672B23D04100849FAC /* PendingOperation.swift */,
|
||||
88BA7DD22BD41B8A008F2A3C /* UDPClient.swift */,
|
||||
);
|
||||
path = Common;
|
||||
sourceTree = "<group>";
|
||||
@ -544,7 +552,7 @@
|
||||
buildActionMask = 2147483647;
|
||||
files = (
|
||||
884A45CF27A5402D00D6E650 /* MessageResult.swift in Sources */,
|
||||
884A45B9279F48C100D6E650 /* ContentView.swift in Sources */,
|
||||
884A45B9279F48C100D6E650 /* MainView.swift in Sources */,
|
||||
88AEE3882B226FED0034EDA9 /* MessageResult+UI.swift in Sources */,
|
||||
E28DED2F281E8A0500259690 /* SingleKeyView.swift in Sources */,
|
||||
888A11332B32DBBB0099A12B /* UInt8+Extensions.swift in Sources */,
|
||||
@ -554,6 +562,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 */,
|
||||
@ -574,6 +583,7 @@
|
||||
88AEE3812B22327F0034EDA9 /* UInt32+Random.swift in Sources */,
|
||||
E24F6C6E2A89749A0040F8C4 /* ConnectionStrategy.swift in Sources */,
|
||||
884A45C5279F4BBE00D6E650 /* KeyManagement.swift in Sources */,
|
||||
88BA7DD32BD41B8A008F2A3C /* UDPClient.swift in Sources */,
|
||||
88AEE3842B2236DC0034EDA9 /* SignedMessage.swift in Sources */,
|
||||
8860D74A2B2329CE00849FAC /* SignedMessage+Size.swift in Sources */,
|
||||
8860D7542B23489300849FAC /* ActiveRequestType.swift in Sources */,
|
||||
@ -589,17 +599,19 @@
|
||||
8860D7662B23B5B200849FAC /* RequestCoordinator.swift in Sources */,
|
||||
8860D75D2B237FC000849FAC /* Data+Coding.swift in Sources */,
|
||||
888362342A80F3F90032BBB2 /* SettingsView.swift in Sources */,
|
||||
88E197B429EDC9BC00BF1D19 /* ContentView.swift in Sources */,
|
||||
88E197B429EDC9BC00BF1D19 /* UnlockView.swift in Sources */,
|
||||
E2F5DCCB2A88E976002858B9 /* Array+Extensions.swift in Sources */,
|
||||
E24065532A819614009C1AD8 /* SettingsNumberItemLink.swift in Sources */,
|
||||
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 */,
|
||||
E240655A2A82218D009C1AD8 /* SettingsKeyInputView.swift in Sources */,
|
||||
8860D76F2B246FC400849FAC /* Text+Extensions.swift in Sources */,
|
||||
E25231782C0227B500FFE373 /* UDPClient.swift in Sources */,
|
||||
88E197D229EDCE6600BF1D19 /* SesameRoute.swift in Sources */,
|
||||
8860D75E2B237FC600849FAC /* Message+Size.swift in Sources */,
|
||||
8860D7552B237F9100849FAC /* TransmissionType.swift in Sources */,
|
||||
|
Binary file not shown.
@ -67,6 +67,11 @@
|
||||
<key>orderHint</key>
|
||||
<integer>0</integer>
|
||||
</dict>
|
||||
<key>Sesame Watch App.xcscheme_^#shared#^_</key>
|
||||
<dict>
|
||||
<key>orderHint</key>
|
||||
<integer>1</integer>
|
||||
</dict>
|
||||
<key>Sesame-Watch Watch App.xcscheme_^#shared#^_</key>
|
||||
<dict>
|
||||
<key>orderHint</key>
|
||||
@ -75,12 +80,12 @@
|
||||
<key>Sesame-WidgetExtension.xcscheme_^#shared#^_</key>
|
||||
<dict>
|
||||
<key>orderHint</key>
|
||||
<integer>2</integer>
|
||||
<integer>0</integer>
|
||||
</dict>
|
||||
<key>Sesame.xcscheme_^#shared#^_</key>
|
||||
<dict>
|
||||
<key>orderHint</key>
|
||||
<integer>0</integer>
|
||||
<integer>2</integer>
|
||||
</dict>
|
||||
</dict>
|
||||
</dict>
|
||||
|
@ -3,13 +3,9 @@ import CryptoKit
|
||||
|
||||
final class Client {
|
||||
|
||||
private let localRequestRoute = "message"
|
||||
|
||||
private let urlMessageParameter = "m"
|
||||
|
||||
init() {}
|
||||
|
||||
func send(_ message: Message, to url: String, through route: TransmissionType, using keys: KeySet) async -> ServerResponse {
|
||||
func send(_ message: Message, to url: String, port: UInt16, through route: TransmissionType, using keys: KeySet) async -> ServerResponse {
|
||||
let sentTime = Date.now
|
||||
let signedMessage = message.authenticate(using: keys.remote)
|
||||
let response: Message
|
||||
@ -18,7 +14,7 @@ final class Client {
|
||||
response = await send(signedMessage, toServerUrl: url, authenticateWith: keys.server, verifyUsing: keys.device)
|
||||
|
||||
case .overLocalWifi:
|
||||
response = await send(signedMessage, toLocalDeviceUrl: url, verifyUsing: keys.device)
|
||||
response = await send(signedMessage, toLocalDevice: url, port: port, verifyUsing: keys.device)
|
||||
}
|
||||
let receivedTime = Date.now
|
||||
// Create best guess for creation of challenge.
|
||||
@ -39,18 +35,19 @@ final class Client {
|
||||
}
|
||||
return (response, serverChallenge)
|
||||
}
|
||||
|
||||
|
||||
private func send(_ message: SignedMessage, toLocalDeviceUrl server: String, verifyUsing deviceKey: SymmetricKey) async -> Message {
|
||||
let data = message.encoded.hexEncoded
|
||||
guard let url = URL(string: server)?.appendingPathComponent("\(localRequestRoute)?\(urlMessageParameter)=\(data)") else {
|
||||
return message.message.with(result: .serverUrlInvalid)
|
||||
private func send(_ message: SignedMessage, toLocalDevice host: String, port: UInt16, verifyUsing deviceKey: SymmetricKey) async -> Message {
|
||||
let client = UDPClient(host: host, port: port)
|
||||
let response: Data? = await withCheckedContinuation { continuation in
|
||||
client.begin()
|
||||
client.send(message: message.encoded) { res in
|
||||
continuation.resume(returning: res)
|
||||
}
|
||||
}
|
||||
|
||||
var request = URLRequest(url: url)
|
||||
request.httpMethod = "POST"
|
||||
request.timeoutInterval = 10
|
||||
return await perform(request, inResponseTo: message.message, verifyUsing: deviceKey)
|
||||
guard let data = response else {
|
||||
return message.message.with(result: .deviceNotConnected)
|
||||
}
|
||||
return decode(data, inResponseTo: message.message, verifyUsing: deviceKey)
|
||||
}
|
||||
|
||||
private func send(_ message: SignedMessage, toServerUrl server: String, authenticateWith authToken: Data, verifyUsing deviceKey: SymmetricKey) async -> Message {
|
||||
@ -71,6 +68,10 @@ final class Client {
|
||||
guard response == .messageAccepted, let data = responseData else {
|
||||
return message.with(result: response)
|
||||
}
|
||||
return decode(data, inResponseTo: message, verifyUsing: deviceKey)
|
||||
}
|
||||
|
||||
private func decode(_ data: Data, inResponseTo message: Message, verifyUsing deviceKey: SymmetricKey) -> Message {
|
||||
guard data.count == SignedMessage.size else {
|
||||
print("[WARN] Received message with \(data.count) bytes (\(Array(data)))")
|
||||
return message.with(result: .invalidMessageSizeFromDevice)
|
||||
|
37
Sesame/Common/Extensions/App+Extensions.swift
Normal file
37
Sesame/Common/Extensions/App+Extensions.swift
Normal file
@ -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")
|
||||
}
|
||||
}
|
||||
|
||||
}
|
@ -25,6 +25,9 @@ final class RequestCoordinator: ObservableObject {
|
||||
@AppStorage("localIP")
|
||||
var localAddress: String = "192.168.178.104/"
|
||||
|
||||
@AppStorage("localPort")
|
||||
var localPort: UInt16 = 8888
|
||||
|
||||
@AppStorage("connectionType")
|
||||
var connectionType: ConnectionStrategy = .remoteFirst
|
||||
|
||||
@ -41,17 +44,40 @@ final class RequestCoordinator: ObservableObject {
|
||||
}
|
||||
}
|
||||
|
||||
func startUnlock() {
|
||||
func checkConnection(using route: TransmissionType? = nil) {
|
||||
guard !isPerformingRequest else {
|
||||
return
|
||||
}
|
||||
isPerformingRequest = true
|
||||
Task {
|
||||
let route = route ?? connectionType.transmissionTypes.first!
|
||||
let (finalResult, _) = await performChallenge(route: route)
|
||||
DispatchQueue.main.async {
|
||||
self.state = finalResult.result
|
||||
self.isPerformingRequest = false
|
||||
}
|
||||
print("Finished connection test: \(finalResult)")
|
||||
scheduleReturnToReadyState()
|
||||
}
|
||||
}
|
||||
|
||||
func startUnlock(quitAfterSuccess: Bool = false) {
|
||||
guard !isPerformingRequest else {
|
||||
return
|
||||
}
|
||||
isPerformingRequest = true
|
||||
Task {
|
||||
let finalResult = await performFullChallengeResponse()
|
||||
|
||||
DispatchQueue.main.async {
|
||||
self.state = finalResult
|
||||
self.isPerformingRequest = false
|
||||
}
|
||||
if finalResult == .unlocked, quitAfterSuccess {
|
||||
DispatchQueue.main.asyncAfter(deadline: .now() + .seconds(3)) {
|
||||
exit(EXIT_SUCCESS)
|
||||
}
|
||||
}
|
||||
scheduleReturnToReadyState()
|
||||
}
|
||||
}
|
||||
@ -148,7 +174,7 @@ final class RequestCoordinator: ObservableObject {
|
||||
return (message.with(result: .noKeyAvailable), nil)
|
||||
}
|
||||
let url = url(for: route)
|
||||
return await client.send(message, to: url, through: route, using: keys)
|
||||
return await client.send(message, to: url, port: localPort, through: route, using: keys)
|
||||
}
|
||||
|
||||
func resetState() {
|
||||
@ -173,3 +199,17 @@ final class RequestCoordinator: ObservableObject {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
extension UInt16: RawRepresentable {
|
||||
|
||||
public var rawValue: String {
|
||||
"\(self)"
|
||||
}
|
||||
|
||||
public init?(rawValue: String) {
|
||||
guard let value = UInt16(rawValue) else {
|
||||
return nil
|
||||
}
|
||||
self = value
|
||||
}
|
||||
}
|
||||
|
170
Sesame/Common/UDPClient.swift
Normal file
170
Sesame/Common/UDPClient.swift
Normal file
@ -0,0 +1,170 @@
|
||||
import Foundation
|
||||
import Network
|
||||
|
||||
enum UDPState: String {
|
||||
case initial
|
||||
case connectionCreated
|
||||
case preparingConnection
|
||||
case sending
|
||||
case waitingForResponse
|
||||
}
|
||||
|
||||
final class UDPClient {
|
||||
|
||||
let host: NWEndpoint.Host
|
||||
|
||||
let port: NWEndpoint.Port
|
||||
|
||||
private var connection: NWConnection?
|
||||
|
||||
private var completion: ((Data?) -> Void)?
|
||||
|
||||
private var state: UDPState = .initial
|
||||
|
||||
init(host: String, port: UInt16) {
|
||||
self.host = .init("192.168.188.118")
|
||||
self.port = .init(rawValue: port)!
|
||||
}
|
||||
|
||||
deinit {
|
||||
print("Destroying UDP Client")
|
||||
finish()
|
||||
}
|
||||
|
||||
func begin() {
|
||||
guard state == .initial else {
|
||||
print("Invalid state for begin(): \(state)")
|
||||
return
|
||||
}
|
||||
connection = NWConnection(host: host, port: port, using: .udp)
|
||||
state = .connectionCreated
|
||||
print("Created connection: \(connection != nil)")
|
||||
}
|
||||
|
||||
func send(message: Data, completion: @escaping (Data?) -> Void) {
|
||||
print("Sending message to \(host) at port \(port)")
|
||||
guard state == .connectionCreated else {
|
||||
print("Invalid state preparing for send: \(state)")
|
||||
return
|
||||
}
|
||||
guard let connection else {
|
||||
print("Failed to send, no connection")
|
||||
completion(nil)
|
||||
return
|
||||
}
|
||||
|
||||
self.completion = completion
|
||||
connection.stateUpdateHandler = { [weak self] (newState) in
|
||||
switch (newState) {
|
||||
case .ready:
|
||||
print("State: Ready\n")
|
||||
self?.send(message, over: connection)
|
||||
case .setup:
|
||||
print("State: Setup\n")
|
||||
case .cancelled:
|
||||
print("Cancelled UDP connection")
|
||||
self?.finish()
|
||||
case .preparing:
|
||||
print("Preparing UDP connection")
|
||||
case .failed(let error):
|
||||
print("Failed to start UDP connection: \(error)")
|
||||
self?.finish()
|
||||
// default:
|
||||
// print("ERROR! State not defined!\n")
|
||||
// self?.finish()
|
||||
case .waiting(_):
|
||||
print("Waiting for UDP connection path change")
|
||||
@unknown default:
|
||||
print("Unknown UDP connection state: \(newState)")
|
||||
self?.finish()
|
||||
}
|
||||
}
|
||||
print("Preparing connection")
|
||||
state = .preparingConnection
|
||||
connection.start(queue: .global())
|
||||
|
||||
DispatchQueue.main.asyncAfter(deadline: .now() + .seconds(5)) {
|
||||
guard self.state == .preparingConnection else {
|
||||
return
|
||||
}
|
||||
print("Timed out preparing connection")
|
||||
self.finish()
|
||||
}
|
||||
}
|
||||
|
||||
private func finish(_ data: Data? = nil) {
|
||||
completion?(data)
|
||||
completion = nil
|
||||
connection?.stateUpdateHandler = nil
|
||||
connection?.cancel()
|
||||
connection = nil
|
||||
state = .initial
|
||||
}
|
||||
|
||||
private func send(_ data: Data, over connection: NWConnection) {
|
||||
guard state == .preparingConnection else {
|
||||
print("Invalid state for send: \(state)")
|
||||
return
|
||||
}
|
||||
|
||||
connection.stateUpdateHandler = nil
|
||||
|
||||
let completion = NWConnection.SendCompletion.contentProcessed { [weak self] error in
|
||||
if let error {
|
||||
print("Failed to send UDP packet: \(error)")
|
||||
self?.finish()
|
||||
} else {
|
||||
print("Finished sending message")
|
||||
self?.waitForResponse(over: connection)
|
||||
}
|
||||
}
|
||||
state = .sending
|
||||
connection.send(content: data, completion: completion)
|
||||
print("Started to send message")
|
||||
|
||||
DispatchQueue.main.asyncAfter(deadline: .now() + .seconds(5)) {
|
||||
guard self.state == .sending else {
|
||||
return
|
||||
}
|
||||
print("Timed out waiting for for send to complete")
|
||||
self.finish()
|
||||
}
|
||||
}
|
||||
|
||||
private func waitForResponse(over connection: NWConnection) {
|
||||
guard state == .sending else {
|
||||
print("Invalid state for send: \(state)")
|
||||
return
|
||||
}
|
||||
state = .waitingForResponse
|
||||
connection.receiveMessage { [weak self] (data, context, isComplete, error) in
|
||||
guard self?.state == .waitingForResponse else {
|
||||
return
|
||||
}
|
||||
guard isComplete else {
|
||||
if let error {
|
||||
print("Failed to receive UDP message: \(error)")
|
||||
} else {
|
||||
print("Failed to receive complete UDP message without error")
|
||||
}
|
||||
self?.finish()
|
||||
return
|
||||
}
|
||||
guard let data else {
|
||||
print("Received UDP message without data")
|
||||
self?.finish()
|
||||
return
|
||||
}
|
||||
print("Received \(data.count) bytes")
|
||||
self?.finish(data)
|
||||
}
|
||||
|
||||
DispatchQueue.main.asyncAfter(deadline: .now() + .seconds(5)) {
|
||||
guard self.state == .waitingForResponse else {
|
||||
return
|
||||
}
|
||||
print("Timed out waiting for response")
|
||||
self.finish()
|
||||
}
|
||||
}
|
||||
}
|
@ -6,7 +6,11 @@ final class HistoryItem {
|
||||
|
||||
let startDate: Date
|
||||
|
||||
let message: Message
|
||||
let messageData: Data
|
||||
|
||||
var message: Message {
|
||||
try! .init(decodeFrom: messageData)
|
||||
}
|
||||
|
||||
let route: TransmissionType
|
||||
|
||||
@ -14,7 +18,7 @@ final class HistoryItem {
|
||||
|
||||
init(message: Message, startDate: Date, route: TransmissionType, finishDate: Date) {
|
||||
self.startDate = startDate
|
||||
self.message = message
|
||||
self.messageData = message.encoded
|
||||
self.finishDate = finishDate
|
||||
self.route = route
|
||||
}
|
||||
|
@ -22,12 +22,8 @@ struct HistoryListItem: View {
|
||||
"\(Int(entry.roundTripTime * 1000)) ms"
|
||||
}
|
||||
|
||||
var clientNonceText: String {
|
||||
"\(entry.message.clientChallenge)"
|
||||
}
|
||||
|
||||
var serverNonceText: String {
|
||||
"\(entry.message.serverChallenge)"
|
||||
var noncesText: String {
|
||||
"\(entry.message.clientChallenge) → \(entry.message.serverChallenge)"
|
||||
}
|
||||
|
||||
var body: some View {
|
||||
@ -42,10 +38,8 @@ struct HistoryListItem: View {
|
||||
HStack {
|
||||
Image(systemSymbol: .arrowUpArrowDownCircle)
|
||||
Text(roundTripText).padding(.trailing)
|
||||
Image(systemSymbol: .lockIphone)
|
||||
Text(clientNonceText).padding(.trailing)
|
||||
Image(systemSymbol: .doorRightHandClosed)
|
||||
Text(serverNonceText).padding(.trailing)
|
||||
Image(systemSymbol: .keyHorizontal)
|
||||
Text(noncesText)
|
||||
}
|
||||
.foregroundColor(.secondary)
|
||||
.font(.footnote)
|
||||
|
@ -2,8 +2,11 @@ import SwiftUI
|
||||
import SwiftData
|
||||
|
||||
struct HistoryView: View {
|
||||
|
||||
@Environment(\.modelContext)
|
||||
private var context
|
||||
|
||||
@Query
|
||||
@Query(sort: \HistoryItem.startDate, order: .reverse)
|
||||
private var items: [HistoryItem] = []
|
||||
|
||||
private var unlockCount: Int {
|
||||
@ -38,7 +41,17 @@ struct HistoryView: View {
|
||||
}
|
||||
ForEach(items) {entry in
|
||||
HistoryListItem(entry: entry)
|
||||
}
|
||||
}.onDelete(perform: { indexSet in
|
||||
let objects = indexSet.map { items[$0] }
|
||||
for object in objects {
|
||||
context.delete(object)
|
||||
}
|
||||
do {
|
||||
try context.save()
|
||||
} catch {
|
||||
print("Failed to save after deleting \(objects.count) object(s): \(error)")
|
||||
}
|
||||
})
|
||||
}
|
||||
.navigationTitle("History")
|
||||
}
|
||||
|
@ -40,9 +40,8 @@ struct ContentView: View {
|
||||
.background(.white.opacity(0.2))
|
||||
.cornerRadius(smallButtonSize / 2)
|
||||
.font(.title2)
|
||||
|
||||
}
|
||||
Button("Unlock", action: coordinator.startUnlock)
|
||||
Button("Unlock", action: unlock)
|
||||
.frame(width: unlockButtonSize, height: unlockButtonSize)
|
||||
.background(buttonBackground)
|
||||
.cornerRadius(unlockButtonSize / 2)
|
||||
@ -73,15 +72,19 @@ struct ContentView: View {
|
||||
.animation(.easeInOut, value: coordinator.state.color)
|
||||
.sheet(isPresented: $showSettingsSheet) {
|
||||
SettingsView(
|
||||
keyManager: coordinator.keyManager,
|
||||
keyManager: coordinator.keyManager,
|
||||
coordinator: coordinator,
|
||||
serverAddress: $coordinator.serverPath,
|
||||
localAddress: $coordinator.localAddress)
|
||||
localAddress: $coordinator.localAddress,
|
||||
localPort: $coordinator.localPort)
|
||||
}
|
||||
.sheet(isPresented: $showHistorySheet) { HistoryView() }
|
||||
.preferredColorScheme(.dark)
|
||||
}
|
||||
|
||||
|
||||
private func unlock() {
|
||||
coordinator.startUnlock()
|
||||
}
|
||||
|
||||
}
|
||||
|
@ -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 {
|
||||
|
@ -1,15 +1,27 @@
|
||||
import SwiftUI
|
||||
import SwiftData
|
||||
import SFSafeSymbols
|
||||
import Combine
|
||||
|
||||
struct SettingsView: View {
|
||||
|
||||
let keyManager: KeyManagement
|
||||
|
||||
@ObservedObject
|
||||
var coordinator: RequestCoordinator
|
||||
|
||||
@Binding
|
||||
var serverAddress: String
|
||||
|
||||
@Binding
|
||||
var localAddress: String
|
||||
|
||||
@Binding
|
||||
var localPort: UInt16
|
||||
|
||||
@State
|
||||
private var localPortString = ""
|
||||
|
||||
var body: some View {
|
||||
NavigationView {
|
||||
ScrollView {
|
||||
@ -20,6 +32,19 @@ struct SettingsView: View {
|
||||
TextField("Server address", text: $serverAddress)
|
||||
.foregroundColor(.secondary)
|
||||
.padding(.leading, 8)
|
||||
HStack {
|
||||
Button("Test") {
|
||||
coordinator.checkConnection(using: .throughServer)
|
||||
}.padding(8)
|
||||
if coordinator.state == .deviceAvailable {
|
||||
Image(systemSymbol: .checkmarkCircle)
|
||||
.foregroundColor(.green)
|
||||
} else if coordinator.state != .notChecked {
|
||||
Text(coordinator.state.description)
|
||||
.font(.caption)
|
||||
.foregroundStyle(.secondary)
|
||||
}
|
||||
}
|
||||
}.padding(.vertical, 8)
|
||||
VStack(alignment: .leading) {
|
||||
Text("Local address")
|
||||
@ -27,6 +52,19 @@ struct SettingsView: View {
|
||||
TextField("Local address", text: $localAddress)
|
||||
.foregroundColor(.secondary)
|
||||
.padding(.leading, 8)
|
||||
TextField("UDP Port", text: $localPortString)
|
||||
.keyboardType(.numberPad)
|
||||
.onReceive(Just(localPortString)) { newValue in
|
||||
let filtered = newValue.filter { "0123456789".contains($0) }
|
||||
if filtered != newValue {
|
||||
self.localPortString = filtered
|
||||
if let value = UInt16(filtered) {
|
||||
self.localPort = value
|
||||
}
|
||||
}
|
||||
}
|
||||
.foregroundColor(.secondary)
|
||||
.padding(.leading, 8)
|
||||
}.padding(.vertical, 8)
|
||||
ForEach(KeyManagement.KeyType.allCases) { keyType in
|
||||
SingleKeyView(
|
||||
@ -34,21 +72,29 @@ struct SettingsView: View {
|
||||
type: keyType)
|
||||
}
|
||||
}.padding()
|
||||
}.onDisappear {
|
||||
if !localAddress.hasSuffix("/") {
|
||||
localAddress += "/"
|
||||
}
|
||||
}.onAppear {
|
||||
self.localPortString = "\(localPort)"
|
||||
}
|
||||
.navigationTitle("Settings")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
struct SettingsView_Previews: PreviewProvider {
|
||||
static var previews: some View {
|
||||
SettingsView(
|
||||
#Preview {
|
||||
do {
|
||||
let config = ModelConfiguration(isStoredInMemoryOnly: true)
|
||||
let container = try ModelContainer(for: HistoryItem.self, configurations: config)
|
||||
|
||||
let item = HistoryItem.mock
|
||||
container.mainContext.insert(item)
|
||||
try container.mainContext.save()
|
||||
return SettingsView(
|
||||
keyManager: KeyManagement(),
|
||||
coordinator: .init(modelContext: container.mainContext),
|
||||
serverAddress: .constant("https://example.com"),
|
||||
localAddress: .constant("192.168.178.42"))
|
||||
localAddress: .constant("192.168.178.42"),
|
||||
localPort: .constant(1234))
|
||||
} catch {
|
||||
fatalError("Failed to create model container.")
|
||||
}
|
||||
}
|
||||
|
Reference in New Issue
Block a user