Compare commits

...

7 Commits

16 changed files with 306 additions and 118 deletions

Binary file not shown.

View File

@ -1,11 +1,11 @@
import SwiftUI import SwiftUI
struct SettingsNumberInputView: View { struct SettingsNumberInputView<Value>: View where Value: FixedWidthInteger {
let title: String let title: String
@Binding @Binding
var value: Int var value: Value
@State @State
private var text: String = "" private var text: String = ""
@ -16,7 +16,7 @@ struct SettingsNumberInputView: View {
VStack(alignment: .leading) { VStack(alignment: .leading) {
TextField(title, text: $text) TextField(title, text: $text)
.onSubmit { .onSubmit {
guard let newValue = Int(text) else { guard let newValue = Value(text) else {
return return
} }
value = newValue value = newValue

View File

@ -1,11 +1,11 @@
import SwiftUI import SwiftUI
struct SettingsNumberItemLink: View { struct SettingsNumberItemLink<Value>: View where Value: FixedWidthInteger {
let title: String let title: String
@Binding @Binding
var value: Int var value: Value
let footnote: String let footnote: String

View File

@ -11,6 +11,9 @@ struct SettingsView: View {
@AppStorage("localIP") @AppStorage("localIP")
var localAddress: String = "192.168.178.104/" var localAddress: String = "192.168.178.104/"
@AppStorage("localPort")
var localPort: UInt16 = 8888
@EnvironmentObject @EnvironmentObject
var keys: KeyManagement var keys: KeyManagement
@ -38,17 +41,21 @@ struct SettingsView: View {
title: "Local url", title: "Local url",
value: $localAddress, value: $localAddress,
footnote: "The url where the device can be reached directly on the local WiFi network.") 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( SettingsKeyItemLink(
type: .deviceKey, type: .deviceKey,
footnote: "Some text describing the purpose of the key.") footnote: "The key used by the device for responses")
.environmentObject(keys) .environmentObject(keys)
SettingsKeyItemLink( SettingsKeyItemLink(
type: .remoteKey, type: .remoteKey,
footnote: "Some text describing the purpose of the key.") footnote: "The key used by the remote for requests.")
.environmentObject(keys) .environmentObject(keys)
SettingsKeyItemLink( SettingsKeyItemLink(
type: .authToken, type: .authToken,
footnote: "Some text describing the purpose of the key.") footnote: "The authentication token of the remote for the server.")
.environmentObject(keys) .environmentObject(keys)
} }
.navigationTitle("Settings") .navigationTitle("Settings")

View File

@ -2,6 +2,7 @@ import SwiftUI
import SwiftData import SwiftData
import SFSafeSymbols import SFSafeSymbols
import CryptoKit import CryptoKit
import UIKit
struct ContentView: View { struct ContentView: View {
@ -40,7 +41,7 @@ struct ContentView: View {
.aspectRatio(contentMode: .fit) .aspectRatio(contentMode: .fit)
.fontWeight(.ultraLight) .fontWeight(.ultraLight)
.padding() .padding()
.onTapGesture(perform: coordinator.startUnlock) .onTapGesture { coordinator.startUnlock() }
if coordinator.isPerformingRequest { if coordinator.isPerformingRequest {
ProgressView() ProgressView()
.progressViewStyle(CircularProgressViewStyle()) .progressViewStyle(CircularProgressViewStyle())
@ -58,8 +59,8 @@ struct ContentView: View {
guard launched else { guard launched else {
return return
} }
coordinator.startUnlock(quitAfterSuccess: true)
didLaunchFromComplication = false didLaunchFromComplication = false
coordinator.startUnlock()
} }
} }
} }

View File

@ -8,7 +8,7 @@
/* Begin PBXBuildFile section */ /* Begin PBXBuildFile section */
884A45B7279F48C100D6E650 /* SesameApp.swift in Sources */ = {isa = PBXBuildFile; fileRef = 884A45B6279F48C100D6E650 /* SesameApp.swift */; }; 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 */; }; 884A45BB279F48C300D6E650 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 884A45BA279F48C300D6E650 /* Assets.xcassets */; };
884A45C5279F4BBE00D6E650 /* KeyManagement.swift in Sources */ = {isa = PBXBuildFile; fileRef = 884A45C4279F4BBE00D6E650 /* KeyManagement.swift */; }; 884A45C5279F4BBE00D6E650 /* KeyManagement.swift in Sources */ = {isa = PBXBuildFile; fileRef = 884A45C4279F4BBE00D6E650 /* KeyManagement.swift */; };
884A45CB27A464C000D6E650 /* SymmetricKey+Extensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 884A45CA27A464C000D6E650 /* SymmetricKey+Extensions.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 */; }; 88AEE3842B2236DC0034EDA9 /* SignedMessage.swift in Sources */ = {isa = PBXBuildFile; fileRef = 88AEE3832B2236DC0034EDA9 /* SignedMessage.swift */; };
88AEE3862B22376D0034EDA9 /* Message+Crypto.swift in Sources */ = {isa = PBXBuildFile; fileRef = 88AEE3852B22376D0034EDA9 /* Message+Crypto.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 */; }; 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 */; }; 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 */; }; 88E197B629EDC9BD00BF1D19 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 88E197B529EDC9BD00BF1D19 /* Assets.xcassets */; };
88E197C429EDCC8900BF1D19 /* Client.swift in Sources */ = {isa = PBXBuildFile; fileRef = 884A45CC27A465F500D6E650 /* Client.swift */; }; 88E197C429EDCC8900BF1D19 /* Client.swift in Sources */ = {isa = PBXBuildFile; fileRef = 884A45CC27A465F500D6E650 /* Client.swift */; };
88E197C729EDCCBD00BF1D19 /* Client.swift in Sources */ = {isa = PBXBuildFile; fileRef = 884A45CC27A465F500D6E650 /* Client.swift */; }; 88E197C729EDCCBD00BF1D19 /* Client.swift in Sources */ = {isa = PBXBuildFile; fileRef = 884A45CC27A465F500D6E650 /* Client.swift */; };
@ -85,6 +86,7 @@
E24EE77927FF95E00011CFD2 /* Message.swift in Sources */ = {isa = PBXBuildFile; fileRef = E24EE77827FF95E00011CFD2 /* Message.swift */; }; E24EE77927FF95E00011CFD2 /* Message.swift in Sources */ = {isa = PBXBuildFile; fileRef = E24EE77827FF95E00011CFD2 /* Message.swift */; };
E24F6C6E2A89749A0040F8C4 /* ConnectionStrategy.swift in Sources */ = {isa = PBXBuildFile; fileRef = E24F6C6D2A89749A0040F8C4 /* ConnectionStrategy.swift */; }; E24F6C6E2A89749A0040F8C4 /* ConnectionStrategy.swift in Sources */ = {isa = PBXBuildFile; fileRef = E24F6C6D2A89749A0040F8C4 /* ConnectionStrategy.swift */; };
E24F6C6F2A8974C60040F8C4 /* 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 */; }; E25317562A8A1ABF005A537D /* HistoryItem.swift in Sources */ = {isa = PBXBuildFile; fileRef = E28DED34281EB17600259690 /* HistoryItem.swift */; };
E268E0822A85302000185913 /* WidgetKit.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = E268E0532A852F8E00185913 /* WidgetKit.framework */; }; E268E0822A85302000185913 /* WidgetKit.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = E268E0532A852F8E00185913 /* WidgetKit.framework */; };
E268E0832A85302000185913 /* SwiftUI.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = E268E0552A852F8E00185913 /* SwiftUI.framework */; }; E268E0832A85302000185913 /* SwiftUI.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = E268E0552A852F8E00185913 /* SwiftUI.framework */; };
@ -131,7 +133,7 @@
/* Begin PBXFileReference section */ /* Begin PBXFileReference section */
884A45B3279F48C100D6E650 /* Sesame.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = Sesame.app; sourceTree = BUILT_PRODUCTS_DIR; }; 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>"; }; 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>"; }; 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>"; }; 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>"; }; 884A45CA27A464C000D6E650 /* SymmetricKey+Extensions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "SymmetricKey+Extensions.swift"; sourceTree = "<group>"; };
@ -157,9 +159,10 @@
88AEE3832B2236DC0034EDA9 /* SignedMessage.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SignedMessage.swift; sourceTree = "<group>"; }; 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>"; }; 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>"; }; 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; }; 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>"; }; 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>"; }; 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>"; }; 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>"; }; 88E35EF42B3B0A9800485A66 /* App+Extensions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "App+Extensions.swift"; sourceTree = "<group>"; };
@ -257,7 +260,7 @@
E2C5C1D92806FE4A00769EF6 /* API */, E2C5C1D92806FE4A00769EF6 /* API */,
8860D7442B2328B800849FAC /* API Extensions */, 8860D7442B2328B800849FAC /* API Extensions */,
884A45B6279F48C100D6E650 /* SesameApp.swift */, 884A45B6279F48C100D6E650 /* SesameApp.swift */,
884A45B8279F48C100D6E650 /* ContentView.swift */, 884A45B8279F48C100D6E650 /* MainView.swift */,
E28DED2C281E840B00259690 /* SettingsView.swift */, E28DED2C281E840B00259690 /* SettingsView.swift */,
E28DED2E281E8A0500259690 /* SingleKeyView.swift */, E28DED2E281E8A0500259690 /* SingleKeyView.swift */,
E25317542A8A1A07005A537D /* History */, E25317542A8A1A07005A537D /* History */,
@ -308,7 +311,7 @@
88E197B529EDC9BD00BF1D19 /* Assets.xcassets */, 88E197B529EDC9BD00BF1D19 /* Assets.xcassets */,
E24065562A819AAD009C1AD8 /* Settings */, E24065562A819AAD009C1AD8 /* Settings */,
88E197B129EDC9BC00BF1D19 /* Sesame_WatchApp.swift */, 88E197B129EDC9BC00BF1D19 /* Sesame_WatchApp.swift */,
88E197B329EDC9BC00BF1D19 /* ContentView.swift */, 88E197B329EDC9BC00BF1D19 /* UnlockView.swift */,
888362332A80F3F90032BBB2 /* SettingsView.swift */, 888362332A80F3F90032BBB2 /* SettingsView.swift */,
888362352A80F4420032BBB2 /* HistoryView.swift */, 888362352A80F4420032BBB2 /* HistoryView.swift */,
E240655D2A822E97009C1AD8 /* HistoryListRow.swift */, E240655D2A822E97009C1AD8 /* HistoryListRow.swift */,
@ -354,6 +357,7 @@
8860D7612B23803E00849FAC /* ServerChallenge.swift */, 8860D7612B23803E00849FAC /* ServerChallenge.swift */,
8860D7642B23B5B200849FAC /* RequestCoordinator.swift */, 8860D7642B23B5B200849FAC /* RequestCoordinator.swift */,
8860D7672B23D04100849FAC /* PendingOperation.swift */, 8860D7672B23D04100849FAC /* PendingOperation.swift */,
88BA7DD22BD41B8A008F2A3C /* UDPClient.swift */,
); );
path = Common; path = Common;
sourceTree = "<group>"; sourceTree = "<group>";
@ -548,7 +552,7 @@
buildActionMask = 2147483647; buildActionMask = 2147483647;
files = ( files = (
884A45CF27A5402D00D6E650 /* MessageResult.swift in Sources */, 884A45CF27A5402D00D6E650 /* MessageResult.swift in Sources */,
884A45B9279F48C100D6E650 /* ContentView.swift in Sources */, 884A45B9279F48C100D6E650 /* MainView.swift in Sources */,
88AEE3882B226FED0034EDA9 /* MessageResult+UI.swift in Sources */, 88AEE3882B226FED0034EDA9 /* MessageResult+UI.swift in Sources */,
E28DED2F281E8A0500259690 /* SingleKeyView.swift in Sources */, E28DED2F281E8A0500259690 /* SingleKeyView.swift in Sources */,
888A11332B32DBBB0099A12B /* UInt8+Extensions.swift in Sources */, 888A11332B32DBBB0099A12B /* UInt8+Extensions.swift in Sources */,
@ -579,6 +583,7 @@
88AEE3812B22327F0034EDA9 /* UInt32+Random.swift in Sources */, 88AEE3812B22327F0034EDA9 /* UInt32+Random.swift in Sources */,
E24F6C6E2A89749A0040F8C4 /* ConnectionStrategy.swift in Sources */, E24F6C6E2A89749A0040F8C4 /* ConnectionStrategy.swift in Sources */,
884A45C5279F4BBE00D6E650 /* KeyManagement.swift in Sources */, 884A45C5279F4BBE00D6E650 /* KeyManagement.swift in Sources */,
88BA7DD32BD41B8A008F2A3C /* UDPClient.swift in Sources */,
88AEE3842B2236DC0034EDA9 /* SignedMessage.swift in Sources */, 88AEE3842B2236DC0034EDA9 /* SignedMessage.swift in Sources */,
8860D74A2B2329CE00849FAC /* SignedMessage+Size.swift in Sources */, 8860D74A2B2329CE00849FAC /* SignedMessage+Size.swift in Sources */,
8860D7542B23489300849FAC /* ActiveRequestType.swift in Sources */, 8860D7542B23489300849FAC /* ActiveRequestType.swift in Sources */,
@ -594,7 +599,7 @@
8860D7662B23B5B200849FAC /* RequestCoordinator.swift in Sources */, 8860D7662B23B5B200849FAC /* RequestCoordinator.swift in Sources */,
8860D75D2B237FC000849FAC /* Data+Coding.swift in Sources */, 8860D75D2B237FC000849FAC /* Data+Coding.swift in Sources */,
888362342A80F3F90032BBB2 /* SettingsView.swift in Sources */, 888362342A80F3F90032BBB2 /* SettingsView.swift in Sources */,
88E197B429EDC9BC00BF1D19 /* ContentView.swift in Sources */, 88E197B429EDC9BC00BF1D19 /* UnlockView.swift in Sources */,
E2F5DCCB2A88E976002858B9 /* Array+Extensions.swift in Sources */, E2F5DCCB2A88E976002858B9 /* Array+Extensions.swift in Sources */,
E24065532A819614009C1AD8 /* SettingsNumberItemLink.swift in Sources */, E24065532A819614009C1AD8 /* SettingsNumberItemLink.swift in Sources */,
888362362A80F4420032BBB2 /* HistoryView.swift in Sources */, 888362362A80F4420032BBB2 /* HistoryView.swift in Sources */,
@ -606,6 +611,7 @@
8860D76C2B246F5E00849FAC /* UInt32+Random.swift in Sources */, 8860D76C2B246F5E00849FAC /* UInt32+Random.swift in Sources */,
E240655A2A82218D009C1AD8 /* SettingsKeyInputView.swift in Sources */, E240655A2A82218D009C1AD8 /* SettingsKeyInputView.swift in Sources */,
8860D76F2B246FC400849FAC /* Text+Extensions.swift in Sources */, 8860D76F2B246FC400849FAC /* Text+Extensions.swift in Sources */,
E25231782C0227B500FFE373 /* UDPClient.swift in Sources */,
88E197D229EDCE6600BF1D19 /* SesameRoute.swift in Sources */, 88E197D229EDCE6600BF1D19 /* SesameRoute.swift in Sources */,
8860D75E2B237FC600849FAC /* Message+Size.swift in Sources */, 8860D75E2B237FC600849FAC /* Message+Size.swift in Sources */,
8860D7552B237F9100849FAC /* TransmissionType.swift in Sources */, 8860D7552B237F9100849FAC /* TransmissionType.swift in Sources */,

View File

@ -67,6 +67,11 @@
<key>orderHint</key> <key>orderHint</key>
<integer>0</integer> <integer>0</integer>
</dict> </dict>
<key>Sesame Watch App.xcscheme_^#shared#^_</key>
<dict>
<key>orderHint</key>
<integer>1</integer>
</dict>
<key>Sesame-Watch Watch App.xcscheme_^#shared#^_</key> <key>Sesame-Watch Watch App.xcscheme_^#shared#^_</key>
<dict> <dict>
<key>orderHint</key> <key>orderHint</key>
@ -75,12 +80,12 @@
<key>Sesame-WidgetExtension.xcscheme_^#shared#^_</key> <key>Sesame-WidgetExtension.xcscheme_^#shared#^_</key>
<dict> <dict>
<key>orderHint</key> <key>orderHint</key>
<integer>2</integer> <integer>0</integer>
</dict> </dict>
<key>Sesame.xcscheme_^#shared#^_</key> <key>Sesame.xcscheme_^#shared#^_</key>
<dict> <dict>
<key>orderHint</key> <key>orderHint</key>
<integer>0</integer> <integer>2</integer>
</dict> </dict>
</dict> </dict>
</dict> </dict>

View File

@ -3,13 +3,9 @@ import CryptoKit
final class Client { final class Client {
private let localRequestRoute = "message"
private let urlMessageParameter = "m"
init() {} 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 sentTime = Date.now
let signedMessage = message.authenticate(using: keys.remote) let signedMessage = message.authenticate(using: keys.remote)
let response: Message let response: Message
@ -18,7 +14,7 @@ final class Client {
response = await send(signedMessage, toServerUrl: url, authenticateWith: keys.server, verifyUsing: keys.device) response = await send(signedMessage, toServerUrl: url, authenticateWith: keys.server, verifyUsing: keys.device)
case .overLocalWifi: 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 let receivedTime = Date.now
// Create best guess for creation of challenge. // Create best guess for creation of challenge.
@ -39,18 +35,19 @@ final class Client {
} }
return (response, serverChallenge) return (response, serverChallenge)
} }
private func send(_ message: SignedMessage, toLocalDeviceUrl server: String, verifyUsing deviceKey: SymmetricKey) async -> Message { private func send(_ message: SignedMessage, toLocalDevice host: String, port: UInt16, verifyUsing deviceKey: SymmetricKey) async -> Message {
let data = message.encoded.hexEncoded let client = UDPClient(host: host, port: port)
guard let url = URL(string: server)?.appendingPathComponent("\(localRequestRoute)?\(urlMessageParameter)=\(data)") else { let response: Data? = await withCheckedContinuation { continuation in
return message.message.with(result: .serverUrlInvalid) client.begin()
client.send(message: message.encoded) { res in
continuation.resume(returning: res)
}
} }
guard let data = response else {
var request = URLRequest(url: url) return message.message.with(result: .deviceNotConnected)
request.httpMethod = "POST" }
request.timeoutInterval = 10 return decode(data, inResponseTo: message.message, verifyUsing: deviceKey)
return await perform(request, inResponseTo: message.message, verifyUsing: deviceKey)
} }
private func send(_ message: SignedMessage, toServerUrl server: String, authenticateWith authToken: Data, verifyUsing deviceKey: SymmetricKey) async -> Message { 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 { guard response == .messageAccepted, let data = responseData else {
return message.with(result: response) 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 { guard data.count == SignedMessage.size else {
print("[WARN] Received message with \(data.count) bytes (\(Array(data)))") print("[WARN] Received message with \(data.count) bytes (\(Array(data)))")
return message.with(result: .invalidMessageSizeFromDevice) return message.with(result: .invalidMessageSizeFromDevice)

View File

@ -25,6 +25,9 @@ final class RequestCoordinator: ObservableObject {
@AppStorage("localIP") @AppStorage("localIP")
var localAddress: String = "192.168.178.104/" var localAddress: String = "192.168.178.104/"
@AppStorage("localPort")
var localPort: UInt16 = 8888
@AppStorage("connectionType") @AppStorage("connectionType")
var connectionType: ConnectionStrategy = .remoteFirst var connectionType: ConnectionStrategy = .remoteFirst
@ -58,17 +61,23 @@ final class RequestCoordinator: ObservableObject {
} }
} }
func startUnlock() { func startUnlock(quitAfterSuccess: Bool = false) {
guard !isPerformingRequest else { guard !isPerformingRequest else {
return return
} }
isPerformingRequest = true isPerformingRequest = true
Task { Task {
let finalResult = await performFullChallengeResponse() let finalResult = await performFullChallengeResponse()
DispatchQueue.main.async { DispatchQueue.main.async {
self.state = finalResult self.state = finalResult
self.isPerformingRequest = false self.isPerformingRequest = false
} }
if finalResult == .unlocked, quitAfterSuccess {
DispatchQueue.main.asyncAfter(deadline: .now() + .seconds(3)) {
exit(EXIT_SUCCESS)
}
}
scheduleReturnToReadyState() scheduleReturnToReadyState()
} }
} }
@ -165,7 +174,7 @@ final class RequestCoordinator: ObservableObject {
return (message.with(result: .noKeyAvailable), nil) return (message.with(result: .noKeyAvailable), nil)
} }
let url = url(for: route) 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() { func resetState() {
@ -190,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
}
}

View 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()
}
}
}

View File

@ -6,7 +6,11 @@ final class HistoryItem {
let startDate: Date let startDate: Date
let message: HistoryMessage let messageData: Data
var message: Message {
try! .init(decodeFrom: messageData)
}
let route: TransmissionType let route: TransmissionType
@ -14,11 +18,7 @@ final class HistoryItem {
init(message: Message, startDate: Date, route: TransmissionType, finishDate: Date) { init(message: Message, startDate: Date, route: TransmissionType, finishDate: Date) {
self.startDate = startDate self.startDate = startDate
self.message = .init( self.messageData = message.encoded
messageType: message.messageType,
clientChallenge: Int(message.clientChallenge),
serverChallenge: Int(message.serverChallenge),
result: message.result)
self.finishDate = finishDate self.finishDate = finishDate
self.route = route self.route = route
} }
@ -32,59 +32,6 @@ 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 { extension HistoryItem: Identifiable {
var id: Double { var id: Double {

View File

@ -22,12 +22,8 @@ struct HistoryListItem: View {
"\(Int(entry.roundTripTime * 1000)) ms" "\(Int(entry.roundTripTime * 1000)) ms"
} }
var clientNonceText: String { var noncesText: String {
"\(entry.message.clientChallenge)" "\(entry.message.clientChallenge)\(entry.message.serverChallenge)"
}
var serverNonceText: String {
"\(entry.message.serverChallenge)"
} }
var body: some View { var body: some View {
@ -42,10 +38,8 @@ struct HistoryListItem: View {
HStack { HStack {
Image(systemSymbol: .arrowUpArrowDownCircle) Image(systemSymbol: .arrowUpArrowDownCircle)
Text(roundTripText).padding(.trailing) Text(roundTripText).padding(.trailing)
Image(systemSymbol: .lockIphone) Image(systemSymbol: .keyHorizontal)
Text(clientNonceText).padding(.trailing) Text(noncesText)
Image(systemSymbol: .doorRightHandClosed)
Text(serverNonceText).padding(.trailing)
} }
.foregroundColor(.secondary) .foregroundColor(.secondary)
.font(.footnote) .font(.footnote)

View File

@ -2,8 +2,11 @@ import SwiftUI
import SwiftData import SwiftData
struct HistoryView: View { struct HistoryView: View {
@Environment(\.modelContext)
private var context
@Query @Query(sort: \HistoryItem.startDate, order: .reverse)
private var items: [HistoryItem] = [] private var items: [HistoryItem] = []
private var unlockCount: Int { private var unlockCount: Int {
@ -38,7 +41,17 @@ struct HistoryView: View {
} }
ForEach(items) {entry in ForEach(items) {entry in
HistoryListItem(entry: entry) 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") .navigationTitle("History")
} }

View File

@ -40,9 +40,8 @@ struct ContentView: View {
.background(.white.opacity(0.2)) .background(.white.opacity(0.2))
.cornerRadius(smallButtonSize / 2) .cornerRadius(smallButtonSize / 2)
.font(.title2) .font(.title2)
} }
Button("Unlock", action: coordinator.startUnlock) Button("Unlock", action: unlock)
.frame(width: unlockButtonSize, height: unlockButtonSize) .frame(width: unlockButtonSize, height: unlockButtonSize)
.background(buttonBackground) .background(buttonBackground)
.cornerRadius(unlockButtonSize / 2) .cornerRadius(unlockButtonSize / 2)
@ -76,13 +75,16 @@ struct ContentView: View {
keyManager: coordinator.keyManager, keyManager: coordinator.keyManager,
coordinator: coordinator, coordinator: coordinator,
serverAddress: $coordinator.serverPath, serverAddress: $coordinator.serverPath,
localAddress: $coordinator.localAddress) localAddress: $coordinator.localAddress,
localPort: $coordinator.localPort)
} }
.sheet(isPresented: $showHistorySheet) { HistoryView() } .sheet(isPresented: $showHistorySheet) { HistoryView() }
.preferredColorScheme(.dark) .preferredColorScheme(.dark)
} }
private func unlock() {
coordinator.startUnlock()
}
} }

View File

@ -1,6 +1,7 @@
import SwiftUI import SwiftUI
import SwiftData import SwiftData
import SFSafeSymbols import SFSafeSymbols
import Combine
struct SettingsView: View { struct SettingsView: View {
@ -15,6 +16,12 @@ struct SettingsView: View {
@Binding @Binding
var localAddress: String var localAddress: String
@Binding
var localPort: UInt16
@State
private var localPortString = ""
var body: some View { var body: some View {
NavigationView { NavigationView {
ScrollView { ScrollView {
@ -45,6 +52,19 @@ struct SettingsView: View {
TextField("Local address", text: $localAddress) TextField("Local address", text: $localAddress)
.foregroundColor(.secondary) .foregroundColor(.secondary)
.padding(.leading, 8) .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) }.padding(.vertical, 8)
ForEach(KeyManagement.KeyType.allCases) { keyType in ForEach(KeyManagement.KeyType.allCases) { keyType in
SingleKeyView( SingleKeyView(
@ -52,10 +72,8 @@ struct SettingsView: View {
type: keyType) type: keyType)
} }
}.padding() }.padding()
}.onDisappear { }.onAppear {
if !localAddress.hasSuffix("/") { self.localPortString = "\(localPort)"
localAddress += "/"
}
} }
.navigationTitle("Settings") .navigationTitle("Settings")
} }
@ -74,7 +92,8 @@ struct SettingsView: View {
keyManager: KeyManagement(), keyManager: KeyManagement(),
coordinator: .init(modelContext: container.mainContext), coordinator: .init(modelContext: container.mainContext),
serverAddress: .constant("https://example.com"), serverAddress: .constant("https://example.com"),
localAddress: .constant("192.168.178.42")) localAddress: .constant("192.168.178.42"),
localPort: .constant(1234))
} catch { } catch {
fatalError("Failed to create model container.") fatalError("Failed to create model container.")
} }