Add option to connect locally to device

This commit is contained in:
Christoph Hagen 2023-04-11 18:18:31 +02:00
parent 9beb2e423e
commit 8a17eef19b
12 changed files with 396 additions and 244 deletions

View File

@ -16,11 +16,13 @@
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 */; };
884A45CD27A465F500D6E650 /* Client.swift in Sources */ = {isa = PBXBuildFile; fileRef = 884A45CC27A465F500D6E650 /* Client.swift */; }; 884A45CD27A465F500D6E650 /* Client.swift in Sources */ = {isa = PBXBuildFile; fileRef = 884A45CC27A465F500D6E650 /* Client.swift */; };
884A45CF27A5402D00D6E650 /* MessageResult.swift in Sources */ = {isa = PBXBuildFile; fileRef = 884A45CE27A5402D00D6E650 /* MessageResult.swift */; }; 884A45CF27A5402D00D6E650 /* MessageResult.swift in Sources */ = {isa = PBXBuildFile; fileRef = 884A45CE27A5402D00D6E650 /* MessageResult.swift */; };
8864664F29E5684C004FE2BE /* CBORCoding in Frameworks */ = {isa = PBXBuildFile; productRef = 8864664E29E5684C004FE2BE /* CBORCoding */; };
8864665229E5939C004FE2BE /* SFSafeSymbols in Frameworks */ = {isa = PBXBuildFile; productRef = 8864665129E5939C004FE2BE /* SFSafeSymbols */; };
E24EE77227FDCCC00011CFD2 /* Data+Extensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = E24EE77127FDCCC00011CFD2 /* Data+Extensions.swift */; }; E24EE77227FDCCC00011CFD2 /* Data+Extensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = E24EE77127FDCCC00011CFD2 /* Data+Extensions.swift */; };
E24EE77427FF95920011CFD2 /* DeviceResponse.swift in Sources */ = {isa = PBXBuildFile; fileRef = E24EE77327FF95920011CFD2 /* DeviceResponse.swift */; }; E24EE77427FF95920011CFD2 /* DeviceResponse.swift in Sources */ = {isa = PBXBuildFile; fileRef = E24EE77327FF95920011CFD2 /* DeviceResponse.swift */; };
E24EE77727FF95C00011CFD2 /* NIOCore in Frameworks */ = {isa = PBXBuildFile; productRef = E24EE77627FF95C00011CFD2 /* NIOCore */; }; E24EE77727FF95C00011CFD2 /* NIOCore in Frameworks */ = {isa = PBXBuildFile; productRef = E24EE77627FF95C00011CFD2 /* NIOCore */; };
E24EE77927FF95E00011CFD2 /* Message.swift in Sources */ = {isa = PBXBuildFile; fileRef = E24EE77827FF95E00011CFD2 /* Message.swift */; }; E24EE77927FF95E00011CFD2 /* Message.swift in Sources */ = {isa = PBXBuildFile; fileRef = E24EE77827FF95E00011CFD2 /* Message.swift */; };
E28DED2D281E840B00259690 /* KeyView.swift in Sources */ = {isa = PBXBuildFile; fileRef = E28DED2C281E840B00259690 /* KeyView.swift */; }; E28DED2D281E840B00259690 /* SettingsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = E28DED2C281E840B00259690 /* SettingsView.swift */; };
E28DED2F281E8A0500259690 /* SingleKeyView.swift in Sources */ = {isa = PBXBuildFile; fileRef = E28DED2E281E8A0500259690 /* SingleKeyView.swift */; }; E28DED2F281E8A0500259690 /* SingleKeyView.swift in Sources */ = {isa = PBXBuildFile; fileRef = E28DED2E281E8A0500259690 /* SingleKeyView.swift */; };
E28DED31281EAE9100259690 /* HistoryView.swift in Sources */ = {isa = PBXBuildFile; fileRef = E28DED30281EAE9100259690 /* HistoryView.swift */; }; E28DED31281EAE9100259690 /* HistoryView.swift in Sources */ = {isa = PBXBuildFile; fileRef = E28DED30281EAE9100259690 /* HistoryView.swift */; };
E28DED33281EB15B00259690 /* HistoryListItem.swift in Sources */ = {isa = PBXBuildFile; fileRef = E28DED32281EB15B00259690 /* HistoryListItem.swift */; }; E28DED33281EB15B00259690 /* HistoryListItem.swift in Sources */ = {isa = PBXBuildFile; fileRef = E28DED32281EB15B00259690 /* HistoryListItem.swift */; };
@ -45,7 +47,7 @@
E24EE77127FDCCC00011CFD2 /* Data+Extensions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Data+Extensions.swift"; sourceTree = "<group>"; }; E24EE77127FDCCC00011CFD2 /* Data+Extensions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Data+Extensions.swift"; sourceTree = "<group>"; };
E24EE77327FF95920011CFD2 /* DeviceResponse.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DeviceResponse.swift; sourceTree = "<group>"; }; E24EE77327FF95920011CFD2 /* DeviceResponse.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DeviceResponse.swift; sourceTree = "<group>"; };
E24EE77827FF95E00011CFD2 /* Message.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Message.swift; sourceTree = "<group>"; }; E24EE77827FF95E00011CFD2 /* Message.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Message.swift; sourceTree = "<group>"; };
E28DED2C281E840B00259690 /* KeyView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = KeyView.swift; sourceTree = "<group>"; }; E28DED2C281E840B00259690 /* SettingsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SettingsView.swift; sourceTree = "<group>"; };
E28DED2E281E8A0500259690 /* SingleKeyView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SingleKeyView.swift; sourceTree = "<group>"; }; E28DED2E281E8A0500259690 /* SingleKeyView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SingleKeyView.swift; sourceTree = "<group>"; };
E28DED30281EAE9100259690 /* HistoryView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HistoryView.swift; sourceTree = "<group>"; }; E28DED30281EAE9100259690 /* HistoryView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HistoryView.swift; sourceTree = "<group>"; };
E28DED32281EB15B00259690 /* HistoryListItem.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HistoryListItem.swift; sourceTree = "<group>"; }; E28DED32281EB15B00259690 /* HistoryListItem.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HistoryListItem.swift; sourceTree = "<group>"; };
@ -62,6 +64,8 @@
isa = PBXFrameworksBuildPhase; isa = PBXFrameworksBuildPhase;
buildActionMask = 2147483647; buildActionMask = 2147483647;
files = ( files = (
8864665229E5939C004FE2BE /* SFSafeSymbols in Frameworks */,
8864664F29E5684C004FE2BE /* CBORCoding in Frameworks */,
E24EE77727FF95C00011CFD2 /* NIOCore in Frameworks */, E24EE77727FF95C00011CFD2 /* NIOCore in Frameworks */,
); );
runOnlyForDeploymentPostprocessing = 0; runOnlyForDeploymentPostprocessing = 0;
@ -96,7 +100,7 @@
E28DED32281EB15B00259690 /* HistoryListItem.swift */, E28DED32281EB15B00259690 /* HistoryListItem.swift */,
E28DED34281EB17600259690 /* HistoryItem.swift */, E28DED34281EB17600259690 /* HistoryItem.swift */,
E28DED36281EC7FB00259690 /* HistoryManager.swift */, E28DED36281EC7FB00259690 /* HistoryManager.swift */,
E28DED2C281E840B00259690 /* KeyView.swift */, E28DED2C281E840B00259690 /* SettingsView.swift */,
E28DED2E281E8A0500259690 /* SingleKeyView.swift */, E28DED2E281E8A0500259690 /* SingleKeyView.swift */,
884A45CC27A465F500D6E650 /* Client.swift */, 884A45CC27A465F500D6E650 /* Client.swift */,
884A45C827A43D7900D6E650 /* ClientState.swift */, 884A45C827A43D7900D6E650 /* ClientState.swift */,
@ -148,6 +152,8 @@
name = Sesame; name = Sesame;
packageProductDependencies = ( packageProductDependencies = (
E24EE77627FF95C00011CFD2 /* NIOCore */, E24EE77627FF95C00011CFD2 /* NIOCore */,
8864664E29E5684C004FE2BE /* CBORCoding */,
8864665129E5939C004FE2BE /* SFSafeSymbols */,
); );
productName = Sesame; productName = Sesame;
productReference = 884A45B3279F48C100D6E650 /* Sesame.app */; productReference = 884A45B3279F48C100D6E650 /* Sesame.app */;
@ -165,6 +171,7 @@
TargetAttributes = { TargetAttributes = {
884A45B2279F48C100D6E650 = { 884A45B2279F48C100D6E650 = {
CreatedOnToolsVersion = 13.2.1; CreatedOnToolsVersion = 13.2.1;
LastSwiftMigration = 1430;
}; };
}; };
}; };
@ -179,6 +186,8 @@
mainGroup = 884A45AA279F48C100D6E650; mainGroup = 884A45AA279F48C100D6E650;
packageReferences = ( packageReferences = (
E24EE77527FF95C00011CFD2 /* XCRemoteSwiftPackageReference "swift-nio" */, E24EE77527FF95C00011CFD2 /* XCRemoteSwiftPackageReference "swift-nio" */,
8864664D29E5684C004FE2BE /* XCRemoteSwiftPackageReference "CBORCoding" */,
8864665029E5939C004FE2BE /* XCRemoteSwiftPackageReference "SFSafeSymbols" */,
); );
productRefGroup = 884A45B4279F48C100D6E650 /* Products */; productRefGroup = 884A45B4279F48C100D6E650 /* Products */;
projectDirPath = ""; projectDirPath = "";
@ -221,7 +230,7 @@
E28DED35281EB17600259690 /* HistoryItem.swift in Sources */, E28DED35281EB17600259690 /* HistoryItem.swift in Sources */,
884A45C927A43D7900D6E650 /* ClientState.swift in Sources */, 884A45C927A43D7900D6E650 /* ClientState.swift in Sources */,
E28DED33281EB15B00259690 /* HistoryListItem.swift in Sources */, E28DED33281EB15B00259690 /* HistoryListItem.swift in Sources */,
E28DED2D281E840B00259690 /* KeyView.swift in Sources */, E28DED2D281E840B00259690 /* SettingsView.swift in Sources */,
884A45B7279F48C100D6E650 /* SesameApp.swift in Sources */, 884A45B7279F48C100D6E650 /* SesameApp.swift in Sources */,
884A45C5279F4BBE00D6E650 /* KeyManagement.swift in Sources */, 884A45C5279F4BBE00D6E650 /* KeyManagement.swift in Sources */,
E2C5C1F8281E769F00769EF6 /* ServerMessage.swift in Sources */, E2C5C1F8281E769F00769EF6 /* ServerMessage.swift in Sources */,
@ -352,6 +361,7 @@
buildSettings = { buildSettings = {
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor;
CLANG_ENABLE_MODULES = YES;
CODE_SIGN_STYLE = Automatic; CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 1; CURRENT_PROJECT_VERSION = 1;
DEVELOPMENT_ASSET_PATHS = "\"Sesame/Preview Content\""; DEVELOPMENT_ASSET_PATHS = "\"Sesame/Preview Content\"";
@ -373,6 +383,8 @@
PRODUCT_BUNDLE_IDENTIFIER = de.christophhagen.Sesame; PRODUCT_BUNDLE_IDENTIFIER = de.christophhagen.Sesame;
PRODUCT_NAME = "$(TARGET_NAME)"; PRODUCT_NAME = "$(TARGET_NAME)";
SWIFT_EMIT_LOC_STRINGS = YES; SWIFT_EMIT_LOC_STRINGS = YES;
SWIFT_OBJC_BRIDGING_HEADER = "";
SWIFT_OPTIMIZATION_LEVEL = "-Onone";
SWIFT_VERSION = 5.0; SWIFT_VERSION = 5.0;
TARGETED_DEVICE_FAMILY = 1; TARGETED_DEVICE_FAMILY = 1;
}; };
@ -383,6 +395,7 @@
buildSettings = { buildSettings = {
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor;
CLANG_ENABLE_MODULES = YES;
CODE_SIGN_STYLE = Automatic; CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 1; CURRENT_PROJECT_VERSION = 1;
DEVELOPMENT_ASSET_PATHS = "\"Sesame/Preview Content\""; DEVELOPMENT_ASSET_PATHS = "\"Sesame/Preview Content\"";
@ -404,6 +417,7 @@
PRODUCT_BUNDLE_IDENTIFIER = de.christophhagen.Sesame; PRODUCT_BUNDLE_IDENTIFIER = de.christophhagen.Sesame;
PRODUCT_NAME = "$(TARGET_NAME)"; PRODUCT_NAME = "$(TARGET_NAME)";
SWIFT_EMIT_LOC_STRINGS = YES; SWIFT_EMIT_LOC_STRINGS = YES;
SWIFT_OBJC_BRIDGING_HEADER = "";
SWIFT_VERSION = 5.0; SWIFT_VERSION = 5.0;
TARGETED_DEVICE_FAMILY = 1; TARGETED_DEVICE_FAMILY = 1;
}; };
@ -433,6 +447,22 @@
/* End XCConfigurationList section */ /* End XCConfigurationList section */
/* Begin XCRemoteSwiftPackageReference section */ /* Begin XCRemoteSwiftPackageReference section */
8864664D29E5684C004FE2BE /* XCRemoteSwiftPackageReference "CBORCoding" */ = {
isa = XCRemoteSwiftPackageReference;
repositoryURL = "https://github.com/christophhagen/CBORCoding";
requirement = {
kind = upToNextMajorVersion;
minimumVersion = 1.0.0;
};
};
8864665029E5939C004FE2BE /* XCRemoteSwiftPackageReference "SFSafeSymbols" */ = {
isa = XCRemoteSwiftPackageReference;
repositoryURL = "https://github.com/SFSafeSymbols/SFSafeSymbols";
requirement = {
kind = upToNextMajorVersion;
minimumVersion = 4.0.0;
};
};
E24EE77527FF95C00011CFD2 /* XCRemoteSwiftPackageReference "swift-nio" */ = { E24EE77527FF95C00011CFD2 /* XCRemoteSwiftPackageReference "swift-nio" */ = {
isa = XCRemoteSwiftPackageReference; isa = XCRemoteSwiftPackageReference;
repositoryURL = "https://github.com/apple/swift-nio.git"; repositoryURL = "https://github.com/apple/swift-nio.git";
@ -444,6 +474,16 @@
/* End XCRemoteSwiftPackageReference section */ /* End XCRemoteSwiftPackageReference section */
/* Begin XCSwiftPackageProductDependency section */ /* Begin XCSwiftPackageProductDependency section */
8864664E29E5684C004FE2BE /* CBORCoding */ = {
isa = XCSwiftPackageProductDependency;
package = 8864664D29E5684C004FE2BE /* XCRemoteSwiftPackageReference "CBORCoding" */;
productName = CBORCoding;
};
8864665129E5939C004FE2BE /* SFSafeSymbols */ = {
isa = XCSwiftPackageProductDependency;
package = 8864665029E5939C004FE2BE /* XCRemoteSwiftPackageReference "SFSafeSymbols" */;
productName = SFSafeSymbols;
};
E24EE77627FF95C00011CFD2 /* NIOCore */ = { E24EE77627FF95C00011CFD2 /* NIOCore */ = {
isa = XCSwiftPackageProductDependency; isa = XCSwiftPackageProductDependency;
package = E24EE77527FF95C00011CFD2 /* XCRemoteSwiftPackageReference "swift-nio" */; package = E24EE77527FF95C00011CFD2 /* XCRemoteSwiftPackageReference "swift-nio" */;

View File

@ -1,5 +1,23 @@
{ {
"pins" : [ "pins" : [
{
"identity" : "cborcoding",
"kind" : "remoteSourceControl",
"location" : "https://github.com/christophhagen/CBORCoding",
"state" : {
"revision" : "1e52c77523fca12cc290b17eed12fadb50ad72af",
"version" : "1.0.0"
}
},
{
"identity" : "sfsafesymbols",
"kind" : "remoteSourceControl",
"location" : "https://github.com/SFSafeSymbols/SFSafeSymbols",
"state" : {
"revision" : "7cca2d60925876b5953a2cf7341cd80fbeac983c",
"version" : "4.1.1"
}
},
{ {
"identity" : "swift-nio", "identity" : "swift-nio",
"kind" : "remoteSourceControl", "kind" : "remoteSourceControl",

View File

@ -29,6 +29,14 @@ struct Message: Equatable, Hashable {
} }
} }
extension Message: Codable {
enum CodingKeys: Int, CodingKey {
case mac = 1
case content = 2
}
}
extension Message { extension Message {
/** /**
@ -74,7 +82,14 @@ extension Message {
time.encoded + id.encoded time.encoded + id.encoded
} }
} }
}
extension Message.Content: Codable {
enum CodingKeys: Int, CodingKey {
case time = 1
case id = 2
}
} }
extension Message { extension Message {

View File

@ -1,30 +1,49 @@
import Foundation import Foundation
import CryptoKit import CryptoKit
struct Client { final class Client {
let server: URL
// TODO: Use or delete
private let delegate = NeverCacheDelegate() private let delegate = NeverCacheDelegate()
init(server: URL) { init() {}
self.server = server
func deviceStatus(authToken: Data, server: String) async -> ClientState {
await send(path: .getDeviceStatus, server: server, data: authToken).state
} }
func deviceStatus(authToken: Data) async -> ClientState { func sendMessageOverLocalNetwork(_ message: Message, server: String) async -> (state: ClientState, response: Message?) {
await send(path: .getDeviceStatus, data: authToken).state let data = message.encoded.hexEncoded
guard let url = URL(string: server + "message?m=\(data)") else {
return (.internalError("Invalid server url"), nil)
}
var request = URLRequest(url: url)
request.httpMethod = "POST"
return await requestAndDecode(request)
} }
func send(_ message: Message, authToken: Data) async -> (state: ClientState, response: Message?) { func send(_ message: Message, server: String, authToken: Data) async -> (state: ClientState, response: Message?) {
let serverMessage = ServerMessage(authToken: authToken, message: message) let serverMessage = ServerMessage(authToken: authToken, message: message)
return await send(path: .postMessage, data: serverMessage.encoded) return await send(path: .postMessage, server: server, data: serverMessage.encoded)
} }
private func send(path: RouteAPI, data: Data) async -> (state: ClientState, response: Message?) { private func send(path: RouteAPI, server: String, data: Data) async -> (state: ClientState, response: Message?) {
let url = server.appendingPathComponent(path.rawValue) guard let url = URL(string: server) else {
return (.internalError("Invalid server url"), nil)
}
let fullUrl = url.appendingPathComponent(path.rawValue)
return await send(to: fullUrl, data: data)
}
private func send(to url: URL, data: Data) async -> (state: ClientState, response: Message?) {
var request = URLRequest(url: url) var request = URLRequest(url: url)
request.httpBody = data request.httpBody = data
request.httpMethod = "POST" request.httpMethod = "POST"
return await requestAndDecode(request)
}
private func requestAndDecode(_ request: URLRequest) async -> (state: ClientState, response: Message?) {
guard let data = await fulfill(request) else { guard let data = await fulfill(request) else {
return (.deviceNotAvailable(.serverNotReached), nil) return (.deviceNotAvailable(.serverNotReached), nil)
} }
@ -36,6 +55,9 @@ struct Client {
} }
let result = ClientState(keyResult: status) let result = ClientState(keyResult: status)
guard data.count == Message.length + 1 else { guard data.count == Message.length + 1 else {
if data.count != 1 {
print("Device response with only \(data.count) bytes")
}
return (result, nil) return (result, nil)
} }
let messageData = Array(data.advanced(by: 1)) let messageData = Array(data.advanced(by: 1))

View File

@ -137,7 +137,7 @@ enum ClientState {
var allowsAction: Bool { var allowsAction: Bool {
switch self { switch self {
case .requestingStatus, .deviceNotAvailable, .waitingForResponse, .noKeyAvailable: case .noKeyAvailable:
return false return false
default: default:
return true return true
@ -188,7 +188,7 @@ extension ClientState {
Data([code]) Data([code])
} }
private var code: UInt8 { var code: UInt8 {
switch self { switch self {
case .noKeyAvailable: case .noKeyAvailable:
return 1 return 1

View File

@ -1,16 +1,23 @@
import SwiftUI import SwiftUI
import CryptoKit import CryptoKit
let server = Client(server: URL(string: "https://christophhagen.de/sesame/")!)
struct ContentView: View { struct ContentView: View {
@AppStorage("server")
var serverPath: String = "https://christophhagen.de/sesame/"
@AppStorage("localIP")
var localAddress: String = "192.168.178.104/"
@AppStorage("counter") @AppStorage("counter")
var nextMessageCounter: Int = 0 var nextMessageCounter: Int = 0
@AppStorage("compensate") @AppStorage("compensate")
var isCompensatingDaylightTime: Bool = false var isCompensatingDaylightTime: Bool = false
@AppStorage("local")
private var useLocalConnection = false
@State @State
var keyManager = KeyManagement() var keyManager = KeyManagement()
@ -29,11 +36,16 @@ struct ContentView: View {
private var responseTime: Date? = nil private var responseTime: Date? = nil
@State @State
private var showKeySheet = false private var showSettingsSheet = false
@State @State
private var showHistorySheet = false private var showHistorySheet = false
@State
private var didShowKeySheetOnce = false
let server = Client()
var compensationTime: UInt32 { var compensationTime: UInt32 {
isCompensatingDaylightTime ? 3600 : 0 isCompensatingDaylightTime ? 3600 : 0
} }
@ -77,7 +89,7 @@ struct ContentView: View {
.font(.title2) .font(.title2)
.padding() .padding()
Spacer() Spacer()
Button("Keys", action: { showKeySheet = true }) Button("Settings", action: { showSettingsSheet = true })
.frame(width: smallButtonWidth, .frame(width: smallButtonWidth,
height: smallButtonHeight) height: smallButtonHeight)
.background(.white.opacity(0.2)) .background(.white.opacity(0.2))
@ -97,7 +109,8 @@ struct ContentView: View {
height: buttonWidth) height: buttonWidth)
.background(buttonBackground) .background(buttonBackground)
.cornerRadius(buttonWidth / 2) .cornerRadius(buttonWidth / 2)
.overlay(RoundedRectangle(cornerRadius: buttonWidth / 2).stroke(lineWidth: buttonBorderWidth).foregroundColor(buttonColor)) .overlay(RoundedRectangle(cornerRadius: buttonWidth / 2)
.stroke(lineWidth: buttonBorderWidth).foregroundColor(buttonColor))
.foregroundColor(buttonColor) .foregroundColor(buttonColor)
.font(.title) .font(.title)
.disabled(!state.allowsAction) .disabled(!state.allowsAction)
@ -115,8 +128,13 @@ struct ContentView: View {
} }
.frame(width: geo.size.width, height: geo.size.height) .frame(width: geo.size.width, height: geo.size.height)
.animation(.easeInOut, value: state.color) .animation(.easeInOut, value: state.color)
.sheet(isPresented: $showKeySheet) { .sheet(isPresented: $showSettingsSheet) {
KeyView(keyManager: $keyManager, isCompensatingDaylightTime: $isCompensatingDaylightTime) SettingsView(
keyManager: $keyManager,
serverAddress: $serverPath,
localAddress: $localAddress,
isCompensatingDaylightTime: $isCompensatingDaylightTime,
useLocalConnection: $useLocalConnection)
} }
.sheet(isPresented: $showHistorySheet) { .sheet(isPresented: $showHistorySheet) {
HistoryView(manager: history) HistoryView(manager: history)
@ -138,35 +156,39 @@ struct ContentView: View {
time: sentTime.timestamp + compensationTime, time: sentTime.timestamp + compensationTime,
id: count) id: count)
let message = content.authenticate(using: key) let message = content.authenticate(using: key)
let historyItem = HistoryItem(sent: message, date: sentTime) let historyItem = HistoryItem(sent: message.content, date: sentTime, local: useLocalConnection)
state = .waitingForResponse state = .waitingForResponse
print("Sending message \(count)") print("Sending message \(count)")
Task { Task {
let (newState, message) = await server.send(message, authToken: token) let (newState, responseMessage) = await send(message, authToken: token)
let receivedTime = Date.now let receivedTime = Date.now
responseTime = receivedTime responseTime = receivedTime
state = newState state = newState
let finishedItem = historyItem.didReceive(response: newState, date: receivedTime, message: message) let finishedItem = historyItem.didReceive(response: newState, date: receivedTime, message: responseMessage?.content)
process(item: finishedItem) guard let key = keyManager.get(.deviceKey) else {
save(historyItem: finishedItem.notAuthenticated())
return
}
guard let responseMessage else {
save(historyItem: finishedItem)
return
}
guard responseMessage.isValid(using: key) else {
save(historyItem: finishedItem.invalidated())
return
}
nextMessageCounter = Int(responseMessage.content.id)
save(historyItem: finishedItem)
} }
} }
private func process(item: HistoryItem) { private func send(_ message: Message, authToken: Data) async -> (state: ClientState, response: Message?) {
guard let message = item.incomingMessage else { if useLocalConnection {
save(historyItem: item) return await server.sendMessageOverLocalNetwork(message, server: localAddress)
return } else {
return await server.send(message, server: serverPath, authToken: authToken)
} }
guard let key = keyManager.get(.deviceKey) else {
save(historyItem: item.notAuthenticated())
return
}
guard message.isValid(using: key) else {
save(historyItem: item.invalidated())
return
}
nextMessageCounter = Int(message.content.id)
save(historyItem: item)
} }
private func save(historyItem: HistoryItem) { private func save(historyItem: HistoryItem) {
@ -194,7 +216,14 @@ struct ContentView: View {
} }
func checkDeviceStatus(_ timer: Timer) { func checkDeviceStatus(_ timer: Timer) {
guard !useLocalConnection else {
return
}
guard let authToken = keyManager.get(.authToken) else { guard let authToken = keyManager.get(.authToken) else {
if !didShowKeySheetOnce {
didShowKeySheetOnce = true
//showSettingsSheet = true
}
return return
} }
guard !hasActiveRequest else { guard !hasActiveRequest else {
@ -202,7 +231,7 @@ struct ContentView: View {
} }
hasActiveRequest = true hasActiveRequest = true
Task { Task {
let newState = await server.deviceStatus(authToken: authToken.data) let newState = await server.deviceStatus(authToken: authToken.data, server: serverPath)
hasActiveRequest = false hasActiveRequest = false
switch state { switch state {
case .noKeyAvailable: case .noKeyAvailable:

View File

@ -1,168 +1,131 @@
import Foundation import Foundation
struct HistoryItem { struct HistoryItem {
let outgoingDate: Date
let outgoingMessage: Message /// The sent/received date (local time, not including compensation offset)
let requestDate: Date
let incomingDate: Date? let request: Message.Content
let incomingMessage: Message? let usedLocalConnection: Bool
let response: ClientState? let response: ClientState?
init(sent message: Message, date: Date) { let responseMessage: Message.Content?
self.outgoingDate = date
self.outgoingMessage = message let responseDate: Date?
self.incomingDate = nil
self.incomingMessage = nil init(sent message: Message.Content, date: Date, local: Bool) {
self.requestDate = date
self.request = message
self.responseMessage = nil
self.response = nil self.response = nil
self.responseDate = nil
self.usedLocalConnection = local
} }
func didReceive(response: ClientState, date: Date?, message: Message?) -> HistoryItem { func didReceive(response: ClientState, date: Date?, message: Message.Content?) -> HistoryItem {
.init(sent: self, response: response, date: date, message: message) .init(sent: self, response: response, date: date, message: message)
} }
func invalidated() -> HistoryItem { func invalidated() -> HistoryItem {
didReceive(response: .responseRejected(.invalidAuthentication), date: incomingDate, message: incomingMessage) didReceive(response: .responseRejected(.invalidAuthentication), date: responseDate, message: responseMessage)
} }
func notAuthenticated() -> HistoryItem { func notAuthenticated() -> HistoryItem {
didReceive(response: .responseRejected(.missingKey), date: incomingDate, message: incomingMessage) didReceive(response: .responseRejected(.missingKey), date: responseDate, message: responseMessage)
} }
private init(sent: HistoryItem, response: ClientState, date: Date?, message: Message?) { private init(sent: HistoryItem, response: ClientState, date: Date?, message: Message.Content?) {
self.outgoingDate = sent.outgoingDate self.requestDate = sent.requestDate
self.outgoingMessage = sent.outgoingMessage self.request = sent.request
self.incomingDate = date self.responseDate = date
self.incomingMessage = message self.responseMessage = message
self.response = response self.response = response
self.usedLocalConnection = sent.usedLocalConnection
} }
// MARK: Statistics // MARK: Statistics
var roundTripTime: TimeInterval? { var roundTripTime: TimeInterval? {
incomingDate?.timeIntervalSince(outgoingDate) responseDate?.timeIntervalSince(requestDate)
} }
var deviceTime: Date? { var deviceTime: Date? {
guard let timestamp = incomingMessage?.content.time else { guard let timestamp = responseMessage?.time else {
return nil return nil
} }
return Date(timestamp: timestamp) return Date(timestamp: timestamp)
} }
var requestLatency: TimeInterval? { var requestLatency: TimeInterval? {
deviceTime?.timeIntervalSince(outgoingDate) deviceTime?.timeIntervalSince(requestDate)
} }
var responseLatency: TimeInterval? { var responseLatency: TimeInterval? {
guard let deviceTime = deviceTime else { guard let deviceTime = deviceTime else {
return nil return nil
} }
return incomingDate?.timeIntervalSince(deviceTime) return responseDate?.timeIntervalSince(deviceTime)
} }
var clockOffset: Int? { var clockOffset: Int? {
guard let interval = roundTripTime, let deviceTime = deviceTime else { guard let interval = roundTripTime, let deviceTime = deviceTime else {
return nil return nil
} }
let estimatedArrival = outgoingDate.advanced(by: interval / 2) let estimatedArrival = requestDate.advanced(by: interval / 2)
return Int(deviceTime.timeIntervalSince(estimatedArrival)) return Int(deviceTime.timeIntervalSince(estimatedArrival))
} }
// MARK: Coding }
static func testEncoding() { extension HistoryItem: Codable {
} enum CodingKeys: Int, CodingKey {
case requestDate = 1
var encoded: Data { case request = 2
var result = outgoingDate.encoded + outgoingMessage.encoded case usedLocalConnection = 3
if let date = incomingDate { case response = 4
result += Data([1]) + date.encoded case responseMessage = 5
} else { case responseDate = 6
result += Data([0])
}
if let message = incomingMessage {
result += Data([1]) + message.encoded
} else {
result += Data([0])
}
result += response?.encoded ?? Data([0])
return result
}
init?(decodeFrom data: Data, index: inout Int) {
guard let outgoingDate = Date(decodeFrom: data, index: &index) else {
return nil
}
self.outgoingDate = outgoingDate
guard let outgoingMessage = Message(decodeFrom: data, index: &index) else {
return nil
}
self.outgoingMessage = outgoingMessage
if data[index] > 0 {
index += 1
guard let incomingDate = Date(decodeFrom: data, index: &index) else {
return nil
}
self.incomingDate = incomingDate
} else {
self.incomingDate = nil
index += 1
}
if data[index] > 0 {
index += 1
guard let incomingMessage = Message(decodeFrom: data, index: &index) else {
return nil
}
self.incomingMessage = incomingMessage
} else {
self.incomingMessage = nil
index += 1
}
guard index < data.count else {
return nil
}
self.response = ClientState(code: data[index])
index += 1
} }
} }
private extension Date { extension ClientState: Codable {
static var encodedSize: Int { init(from decoder: Decoder) throws {
MemoryLayout<Double>.size let code = try decoder.singleValueContainer().decode(UInt8.self)
self.init(code: code)
} }
var encoded: Data { func encode(to encoder: Encoder) throws {
.init(from: timeIntervalSince1970) var container = encoder.singleValueContainer()
} try container.encode(code)
init?(decodeFrom data: Data, index: inout Int) {
guard index + Date.encodedSize <= data.count else {
return nil
}
self.init(timeIntervalSince1970: data.advanced(by: index).convert(into: .zero))
index += Date.encodedSize
} }
} }
extension HistoryItem: Identifiable { extension HistoryItem: Identifiable {
var id: UInt32 { var id: UInt32 {
outgoingDate.timestamp requestDate.timestamp
} }
} }
extension HistoryItem: Comparable { extension HistoryItem: Comparable {
static func < (lhs: HistoryItem, rhs: HistoryItem) -> Bool { static func < (lhs: HistoryItem, rhs: HistoryItem) -> Bool {
lhs.outgoingDate < rhs.outgoingDate lhs.requestDate < rhs.requestDate
}
}
extension HistoryItem {
static var mock: HistoryItem {
let content = Message.Content(time: Date.now.timestamp, id: 123)
let content2 = Message.Content(time: (Date.now + 1).timestamp, id: 124)
return .init(sent: content, date: .now, local: false)
.didReceive(response: .openSesame, date: .now + 2, message: content2)
} }
} }

View File

@ -1,4 +1,5 @@
import SwiftUI import SwiftUI
import SFSafeSymbols
private let df: DateFormatter = { private let df: DateFormatter = {
let df = DateFormatter() let df = DateFormatter()
@ -12,20 +13,20 @@ struct HistoryListItem: View {
let entry: HistoryItem let entry: HistoryItem
var entryTime: String { var entryTime: String {
df.string(from: entry.outgoingDate) df.string(from: entry.requestDate)
} }
var roundTripText: String { var roundTripText: String? {
guard let time = entry.roundTripTime else { guard let time = entry.roundTripTime else {
return "" return nil
} }
return "\(Int(time * 1000)) ms" return "\(Int(time * 1000)) ms"
} }
var counterText: String { var counterText: String {
let sentCounter = entry.outgoingMessage.content.id let sentCounter = entry.request.id
let startText = "🔗 \(sentCounter)" let startText = "\(sentCounter)"
guard let rCounter = entry.incomingMessage?.content.id else { guard let rCounter = entry.responseMessage?.id else {
return startText return startText
} }
let diff = Int(rCounter) - Int(sentCounter) let diff = Int(rCounter) - Int(sentCounter)
@ -35,15 +36,15 @@ struct HistoryListItem: View {
return startText + " (\(diff))" return startText + " (\(diff))"
} }
var timeOffsetText: String { var timeOffsetText: String? {
guard let offset = entry.clockOffset, offset != 0 else { guard let offset = entry.clockOffset else {
return "" return nil
} }
return "🕓 \(offset) s" return "\(offset) s"
} }
var body: some View { var body: some View {
VStack { VStack(alignment: .leading) {
HStack { HStack {
Text(entry.response?.description ?? "") Text(entry.response?.description ?? "")
.font(.headline) .font(.headline)
@ -51,18 +52,25 @@ struct HistoryListItem: View {
Text(entryTime) Text(entryTime)
}.padding(.bottom, 1) }.padding(.bottom, 1)
HStack { HStack {
Text(roundTripText) if let roundTripText {
.font(.subheadline) Image(systemSymbol: entry.usedLocalConnection ? .wifi : .network)
.foregroundColor(.secondary) //Image(systemSymbol: .arrowUpArrowDownCircle)
Text(roundTripText)
.font(.subheadline)
}
//Spacer()
Image(systemSymbol: .personalhotspot)
Text(counterText) Text(counterText)
.font(.subheadline) .font(.subheadline)
.foregroundColor(.secondary) if let timeOffsetText {
Text(timeOffsetText) //Spacer()
.font(.subheadline) Image(systemSymbol: .stopwatch)
.foregroundColor(.secondary) Text(timeOffsetText)
Spacer() .font(.subheadline)
} }
}.padding() }.foregroundColor(.secondary)
}
//.padding()
} }
} }
@ -71,19 +79,3 @@ struct HistoryListItem_Previews: PreviewProvider {
HistoryListItem(entry: .mock) HistoryListItem(entry: .mock)
} }
} }
private extension HistoryItem {
static var mock: HistoryItem {
let mac = Data(repeating: 42, count: 32)
let content = Message.Content(time: Date.now.timestamp, id: 123)
let content2 = Message.Content(time: (Date.now + 1).timestamp, id: 124)
return .init(
sent: Message(mac: mac, content: content),
date: .now)
.didReceive(
response: .openSesame,
date: .now + 2,
message: Message(mac: mac, content: content2))
}
}

View File

@ -1,59 +1,92 @@
import Foundation import Foundation
import CBORCoding
final class HistoryManager { protocol HistoryManagerProtocol {
func loadEntries() -> [HistoryItem]
func save(item: HistoryItem) throws
}
final class HistoryManager: HistoryManagerProtocol {
private let encoder = CBOREncoder(dateEncodingStrategy: .secondsSince1970)
private var fm: FileManager { private var fm: FileManager {
.default .default
} }
var documentDirectory: URL { static var documentDirectory: URL {
try! fm.url( try! FileManager.default.url(
for: .documentDirectory, for: .documentDirectory,
in: .userDomainMask, in: .userDomainMask,
appropriateFor: nil, create: true) appropriateFor: nil, create: true)
} }
private var fileUrl: URL { private let fileUrl: URL
documentDirectory.appendingPathComponent("history.bin")
init() {
self.fileUrl = HistoryManager.documentDirectory.appendingPathComponent("history2.bin")
} }
func loadEntries() -> [HistoryItem] { func loadEntries() -> [HistoryItem] {
let url = fileUrl guard fm.fileExists(atPath: fileUrl.path) else {
guard fm.fileExists(atPath: url.path) else {
print("No history data found") print("No history data found")
return [] return []
} }
let content: Data let content: Data
do { do {
content = try Data(contentsOf: url) content = try Data(contentsOf: fileUrl)
} catch { } catch {
print("Failed to read history data: \(error)") print("Failed to read history data: \(error)")
return [] return []
} }
let decoder = CBORDecoder()
var index = 0 var index = 0
var entries = [HistoryItem]() var entries = [HistoryItem]()
while index < content.count { while index < content.count {
guard let entry = HistoryItem(decodeFrom: content, index: &index) else { let length = Int(content[index])
print("Failed to read entry at index \(index)") index += 1
if index + length > content.count {
print("Missing bytes in history file: needed \(length), has only \(content.count - index)")
return entries
}
let entryData = content[index..<index+length]
index += length
do {
let entry: HistoryItem = try decoder.decode(from: entryData)
entries.append(entry)
} catch {
print("Failed to decode history (index: \(index), length \(length)): \(error)")
return entries return entries
} }
entries.append(entry)
} }
return entries.sorted().reversed() return entries.sorted().reversed()
} }
func save(item: HistoryItem) throws { func save(item: HistoryItem) throws {
let url = fileUrl let entryData = try encoder.encode(item)
let data = item.encoded let data = Data([UInt8(entryData.count)]) + entryData
guard fm.fileExists(atPath: url.path) else { guard fm.fileExists(atPath: fileUrl.path) else {
try data.write(to: url) try data.write(to: fileUrl)
print("First history item written") print("First history item written (\(data[0]))")
return return
} }
let handle = try FileHandle(forWritingTo: url) let handle = try FileHandle(forWritingTo: fileUrl)
try handle.seekToEnd() try handle.seekToEnd()
try handle.write(contentsOf: data) try handle.write(contentsOf: data)
try handle.close() try handle.close()
print("History item written") print("History item written (\(data[0]))")
}
}
final class HistoryManagerMock: HistoryManagerProtocol {
func loadEntries() -> [HistoryItem] {
[.mock]
}
func save(item: HistoryItem) throws {
} }
} }

View File

@ -2,17 +2,20 @@ import SwiftUI
struct HistoryView: View { struct HistoryView: View {
let manager: HistoryManager let manager: HistoryManagerProtocol
var body: some View { var body: some View {
List(manager.loadEntries()) { entry in NavigationView {
HistoryListItem(entry: entry) List(manager.loadEntries()) { entry in
HistoryListItem(entry: entry)
}
.navigationTitle("History")
} }
} }
} }
struct HistoryView_Previews: PreviewProvider { struct HistoryView_Previews: PreviewProvider {
static var previews: some View { static var previews: some View {
HistoryView(manager: .init()) HistoryView(manager: HistoryManagerMock())
} }
} }

View File

@ -1,36 +0,0 @@
import SwiftUI
struct KeyView: View {
@Binding
var keyManager: KeyManagement
@Binding
var isCompensatingDaylightTime: Bool
var body: some View {
ScrollView {
VStack(alignment: .leading, spacing: 8) {
ForEach(KeyManagement.KeyType.allCases) { keyType in
SingleKeyView(
keyManager: $keyManager,
type: keyType)
}
Toggle(isOn: $isCompensatingDaylightTime) {
Text("Compensate daylight savings time")
}
Text("If the remote has daylight savings time wrongly set, then the time validation will fail. Use this option to send messages with adjusted timestamps. Warning: Incorrect use of this option will allow replay attacks.")
.font(.caption)
.foregroundColor(.secondary)
}.padding()
}
}
}
struct KeyView_Previews: PreviewProvider {
static var previews: some View {
KeyView(
keyManager: .constant(KeyManagement()),
isCompensatingDaylightTime: .constant(true))
}
}

73
Sesame/SettingsView.swift Normal file
View File

@ -0,0 +1,73 @@
import SwiftUI
struct SettingsView: View {
@Binding
var keyManager: KeyManagement
@Binding
var serverAddress: String
@Binding
var localAddress: String
@Binding
var isCompensatingDaylightTime: Bool
@Binding
var useLocalConnection: Bool
var body: some View {
NavigationView {
ScrollView {
VStack(alignment: .leading, spacing: 8) {
VStack(alignment: .leading) {
Text("Server address")
.bold()
TextField("Server address", text: $serverAddress)
.padding(.leading, 8)
}.padding(.vertical, 8)
VStack(alignment: .leading) {
Text("Local address")
.bold()
TextField("Local address", text: $localAddress)
.padding(.leading, 8)
}.padding(.vertical, 8)
Toggle(isOn: $useLocalConnection) {
Text("Use direct connection to device")
}
Text("Attempt to communicate directly with the device. This is useful if the server is unavailable. Requires a WiFi connection on the same network as the device.")
.font(.caption)
.foregroundColor(.secondary)
ForEach(KeyManagement.KeyType.allCases) { keyType in
SingleKeyView(
keyManager: $keyManager,
type: keyType)
}
Toggle(isOn: $isCompensatingDaylightTime) {
Text("Compensate daylight savings time")
}
Text("If the remote has daylight savings time wrongly set, then the time validation will fail. Use this option to send messages with adjusted timestamps. Warning: Incorrect use of this option will allow replay attacks.")
.font(.caption)
.foregroundColor(.secondary)
}.padding()
}.onDisappear {
if !localAddress.hasSuffix("/") {
localAddress += "/"
}
}
.navigationTitle("Settings")
}
}
}
struct SettingsView_Previews: PreviewProvider {
static var previews: some View {
SettingsView(
keyManager: .constant(KeyManagement()),
serverAddress: .constant("https://example.com"),
localAddress: .constant("192.168.178.42"),
isCompensatingDaylightTime: .constant(true),
useLocalConnection: .constant(false))
}
}