Compare commits

...

4 Commits

Author SHA1 Message Date
Christoph Hagen
5f9af35542 Add history and settings to watch app 2023-08-09 16:29:18 +02:00
Christoph Hagen
e5ea8c4951 Clean code and add network timeout 2023-08-09 16:27:34 +02:00
Christoph Hagen
f451715a11 Fix API 2023-08-09 16:27:15 +02:00
Christoph Hagen
32b4c8c81a Update API 2023-08-09 16:26:43 +02:00
26 changed files with 654 additions and 213 deletions

View File

@ -1,6 +1,7 @@
{ {
"images" : [ "images" : [
{ {
"filename" : "sesame.png",
"idiom" : "universal", "idiom" : "universal",
"platform" : "watchos", "platform" : "watchos",
"size" : "1024x1024" "size" : "1024x1024"

Binary file not shown.

After

Width:  |  Height:  |  Size: 113 KiB

View File

@ -1,4 +1,5 @@
import SwiftUI import SwiftUI
import SFSafeSymbols
import CryptoKit import CryptoKit
struct ContentView: View { struct ContentView: View {
@ -27,65 +28,43 @@ struct ContentView: View {
@State @State
var state: ClientState = .noKeyAvailable var state: ClientState = .noKeyAvailable
@State
private var hasActiveRequest = false
let server = Client() let server = Client()
let history = HistoryManager()
var buttonBackground: Color { var buttonBackground: Color {
state.allowsAction ? state.allowsAction ?
.white.opacity(0.2) : .white.opacity(0.2) :
.black.opacity(0.2) .black.opacity(0.2)
} }
let buttonBorderWidth: CGFloat = 3
var buttonColor: Color { var buttonColor: Color {
state.allowsAction ? .white : .gray state.allowsAction ? .white : .gray
} }
private let sidePaddingRatio: CGFloat = 0.05
private let buttonSizeRatio: CGFloat = 0.9
private let smallButtonHeight: CGFloat = 50
private let smallButtonWidth: CGFloat = 120
private let smallButtonBorderWidth: CGFloat = 1
var compensationTime: UInt32 {
isCompensatingDaylightTime ? 3600 : 0
}
var isPerformingRequests: Bool {
hasActiveRequest ||
state == .waitingForResponse
}
var body: some View { var body: some View {
HStack {
Spacer()
VStack(alignment: .center) { VStack(alignment: .center) {
Spacer() Image(systemSymbol: .lock)
GeometryReader { geo in .resizable()
HStack(alignment: .center) { .aspectRatio(contentMode: .fit)
Spacer() .fontWeight(.ultraLight)
let buttonWidth = min(geo.size.width, geo.size.height) .padding()
Text(state.actionText)
.frame(width: buttonWidth, height: buttonWidth)
.background(buttonBackground)
.cornerRadius(buttonWidth / 2)
.overlay(RoundedRectangle(cornerRadius: buttonWidth / 2)
.stroke(lineWidth: buttonBorderWidth).foregroundColor(buttonColor))
.foregroundColor(buttonColor)
.font(.title)
.disabled(!state.allowsAction)
.onTapGesture(perform: mainButtonPressed) .onTapGesture(perform: mainButtonPressed)
Spacer() .disabled(!state.allowsAction)
} Text(state.actionText)
.font(.subheadline)
} }
Spacer() Spacer()
} }
.background(state.color) .background(state.color)
.animation(.easeInOut, value: state.color) .animation(.easeInOut, value: state.color)
.onAppear {
if keyManager.hasAllKeys, state == .noKeyAvailable {
state = .ready
}
}
} }
func mainButtonPressed() { func mainButtonPressed() {
@ -97,8 +76,9 @@ struct ContentView: View {
let count = UInt32(nextMessageCounter) let count = UInt32(nextMessageCounter)
let sentTime = Date() let sentTime = Date()
// Add time to compensate that the device is using daylight savings time // Add time to compensate that the device is using daylight savings time
let timeCompensation: UInt32 = isCompensatingDaylightTime ? 3600 : 0
let content = Message.Content( let content = Message.Content(
time: sentTime.timestamp + compensationTime, time: sentTime.timestamp + timeCompensation,
id: count, id: count,
device: deviceId) device: deviceId)
let message = content.authenticate(using: key) let message = content.authenticate(using: key)
@ -138,7 +118,11 @@ struct ContentView: View {
} }
private func save(historyItem: HistoryItem) { private func save(historyItem: HistoryItem) {
do {
try history.save(item: historyItem)
} catch {
print("Failed to save item: \(error)")
}
} }
} }

View File

@ -0,0 +1,67 @@
import SwiftUI
import SFSafeSymbols
private let df: DateFormatter = {
let df = DateFormatter()
df.dateStyle = .short
df.timeStyle = .short
df.doesRelativeDateFormatting = true
return df
}()
struct HistoryItemDetail: View {
let item: HistoryItem
private var entryTime: String {
df.string(from: item.requestDate)
}
var counterText: String {
let sentCounter = item.request.id
let startText = "\(sentCounter)"
guard let rCounter = item.responseMessage?.id else {
return startText
}
guard sentCounter + 1 != rCounter && sentCounter != rCounter else {
return startText
}
return "\(sentCounter) -> \(rCounter)"
}
var body: some View {
List {
SettingsListTextItem(
title: "Status",
value: item.response?.description ?? "No response")
SettingsListTextItem(
title: "Date",
value: entryTime)
SettingsListTextItem(
title: "Connection",
value: item.usedLocalConnection ? "Local" : "Remote")
SettingsListTextItem(
title: "Device ID",
value: "\(item.request.deviceId!)")
SettingsListTextItem(
title: "Message Counter",
value: counterText)
if let time = item.roundTripTime {
SettingsListTextItem(
title: "Round Trip Time",
value: "\(Int(time * 1000)) ms")
}
if let offset = item.clockOffset {
SettingsListTextItem(
title: "Clock offset",
value: "\(offset) seconds")
}
}.navigationTitle("Details")
}
}
struct HistoryItemDetail_Previews: PreviewProvider {
static var previews: some View {
HistoryItemDetail(item: .mock)
}
}

View File

@ -0,0 +1,41 @@
import SwiftUI
import SFSafeSymbols
private let df: DateFormatter = {
let df = DateFormatter()
df.dateStyle = .short
df.timeStyle = .short
df.doesRelativeDateFormatting = true
return df
}()
struct HistoryListRow: View {
let item: HistoryItem
private var entryTime: String {
df.string(from: item.requestDate)
}
var body: some View {
VStack(alignment: .leading) {
HStack {
Image(systemSymbol: item.response?.symbol ?? .exclamationmarkTriangle)
Text(item.response?.description ?? "No response")
.font(.headline)
.foregroundColor(.primary)
}
Text(entryTime)
.font(.footnote)
.foregroundColor(.accentColor)
}
}
}
struct HistoryListRow_Previews: PreviewProvider {
static var previews: some View {
HistoryListRow(item: .mock)
}
}

View File

@ -1,13 +1,37 @@
import SwiftUI import SwiftUI
struct HistoryView: View { struct HistoryView: View {
let history: HistoryManagerProtocol
@State
private var items: [HistoryItem] = []
var body: some View { var body: some View {
Text(/*@START_MENU_TOKEN@*/"Hello, World!"/*@END_MENU_TOKEN@*/) NavigationStack {
List(items) { item in
NavigationLink {
HistoryItemDetail(item: item)
} label: {
HistoryListRow(item: item)
}
}
.navigationTitle("History")
}.onAppear(perform: loadItems)
}
private func loadItems() {
Task {
let entries = history.loadEntries()
DispatchQueue.main.async {
items = entries
}
}
} }
} }
struct HistoryView_Previews: PreviewProvider { struct HistoryView_Previews: PreviewProvider {
static var previews: some View { static var previews: some View {
HistoryView() HistoryView(history: HistoryManagerMock())
} }
} }

View File

@ -1,121 +0,0 @@
import Foundation
import CryptoKit
import SwiftUI
private let localKey: [UInt8] = [
0x98, 0x36, 0x91, 0x09, 0x29, 0xa0, 0x54, 0x44,
0x03, 0x0c, 0xa5, 0xb4, 0x20, 0x16, 0x10, 0x0d,
0xaf, 0x41, 0x9b, 0x26, 0x4f, 0x75, 0xa4, 0x61,
0xed, 0x15, 0x0c, 0xb3, 0x06, 0x39, 0x92, 0x59]
private let remoteKey: [UInt8] = [
0xfa, 0x23, 0xf6, 0x98, 0xea, 0x87, 0x23, 0xa0,
0xa0, 0xbe, 0x9a, 0xdb, 0x31, 0x28, 0xcb, 0x7d,
0xd3, 0xa5, 0x7b, 0xf0, 0xc0, 0xeb, 0x45, 0x65,
0x4d, 0x94, 0x50, 0x1a, 0x2f, 0x6f, 0xeb, 0x70]
private let authToken: [UInt8] = {
let s = "Y6QzDK5DaFK1w2oEX5OkzoC0nTqP8w5IxpvWAR1mpro="
let t = Data(base64Encoded: s.data(using: .utf8)!)!
return Array(t)
}()
extension KeyManagement {
enum KeyType: String, Identifiable, CaseIterable {
case deviceKey = "sesame-device"
case remoteKey = "sesame-remote"
case authToken = "sesame-remote-auth"
var id: String {
rawValue
}
var displayName: String {
switch self {
case .deviceKey:
return "Device Key"
case .remoteKey:
return "Remote Key"
case .authToken:
return "Authentication Token"
}
}
var keyLength: SymmetricKeySize {
.bits256
}
var usesHashing: Bool {
switch self {
case .authToken:
return true
default:
return false
}
}
}
}
extension KeyManagement.KeyType: CustomStringConvertible {
var description: String {
displayName
}
}
final class KeyManagement: ObservableObject {
@Published
private(set) var hasRemoteKey = true
@Published
private(set) var hasDeviceKey = true
@Published
private(set) var hasAuthToken = true
var hasAllKeys: Bool {
hasRemoteKey && hasDeviceKey && hasAuthToken
}
init() {}
func has(_ type: KeyType) -> Bool {
switch type {
case .deviceKey:
return hasDeviceKey
case .remoteKey:
return hasRemoteKey
case .authToken:
return hasAuthToken
}
}
func get(_ type: KeyType) -> SymmetricKey? {
let bytes: [UInt8] = get(type)
return SymmetricKey(data: bytes)
}
private func get(_ type: KeyType) -> [UInt8] {
switch type {
case .deviceKey:
return remoteKey
case .remoteKey:
return localKey
case .authToken:
return authToken
}
}
func delete(_ type: KeyType) {
}
func generate(_ type: KeyType) {
}
}

View File

@ -11,7 +11,8 @@ struct Sesame_Watch_Watch_AppApp: App {
ContentView() ContentView()
.environmentObject(keyManagement) .environmentObject(keyManagement)
SettingsView() SettingsView()
HistoryView() .environmentObject(keyManagement)
HistoryView(history: HistoryManager())
} }
.tabViewStyle(PageTabViewStyle()) .tabViewStyle(PageTabViewStyle())
} }

View File

@ -0,0 +1,77 @@
import SwiftUI
import CryptoKit
struct SettingsKeyInputView: View {
let type: KeyManagement.KeyType
@State
private var text: String = ""
let footnote: String
@EnvironmentObject
private var keys: KeyManagement
private var hasKey: Bool {
keys.has(type)
}
private var displayText: String {
keys.get(type)?.displayString ?? "-"
}
private var copyText: String {
guard let key = keys.get(type)?.data else {
return ""
}
guard type.usesHashing else {
return key.hexEncoded
}
return SHA256.hash(data: key).hexEncoded
}
var body: some View {
ScrollView {
VStack(alignment: .leading) {
TextField(type.displayName, text: $text)
.onSubmit(validateText)
.foregroundColor(.accentColor)
Text(footnote)
.font(.footnote)
.foregroundColor(.secondary)
}
.navigationTitle(type.displayName)
.onAppear {
if text == "" {
text = displayText
print("Text inserted")
}
}
}
}
private func validateText() {
let cleanText = text.replacingOccurrences(of: " ", with: "").trimmingCharacters(in: .whitespacesAndNewlines).lowercased()
guard let keyData = Data(fromHexEncodedString: cleanText) else {
print("Invalid key string")
return
}
let keyLength = type.keyLength.bitCount
guard keyData.count * 8 == keyLength else {
print("Invalid key length \(keyData.count * 8) bits, expected \(keyLength) (Input: '\(text)')")
return
}
keys.save(type, data: keyData)
print("Key \(type) saved")
}
}
struct SettingsKeyInputView_Previews: PreviewProvider {
static var previews: some View {
SettingsKeyInputView(
type: .remoteKey,
footnote: "Some text describing the purpose of the key.")
.environmentObject(KeyManagement())
}
}

View File

@ -0,0 +1,47 @@
import SwiftUI
struct SettingsKeyItemLink: View {
let type: KeyManagement.KeyType
let footnote: String
@EnvironmentObject
private var keys: KeyManagement
@State
private var keyText = "..."
var body: some View {
NavigationLink {
SettingsKeyInputView(
type: type,
footnote: footnote)
.environmentObject(keys)
} label: {
SettingsListTextItem(
title: type.displayName,
value: keyText)
.onAppear(perform: updateKeyText)
}
.buttonStyle(PlainButtonStyle())
}
private func updateKeyText() {
Task {
let key = keys.get(type)?.displayString ?? "Not set"
DispatchQueue.main.async {
keyText = key
}
}
}
}
struct SettingsKeyItemLink_Previews: PreviewProvider {
static var previews: some View {
SettingsKeyItemLink(
type: .deviceKey,
footnote: "Some text describing the purpose of the key.")
.environmentObject(KeyManagement())
}
}

View File

@ -0,0 +1,31 @@
import SwiftUI
struct SettingsListTextItem: View {
let title: String
let value: String
var body: some View {
VStack(alignment: .leading) {
HStack {
Text(title)
.foregroundColor(.primary)
Spacer()
}
Text(value)
.font(.footnote)
.foregroundColor(.secondary)
.lineLimit(1)
}
.padding()
}
}
struct SettingsListTextItem_Previews: PreviewProvider {
static var previews: some View {
SettingsListTextItem(
title: "Title",
value: "Some longer text")
}
}

View File

@ -0,0 +1,28 @@
import SwiftUI
struct SettingsListToggleItem: View {
let title: String
@Binding
var value: Bool
let subtitle: String
var body: some View {
VStack(alignment: .leading) {
Toggle(title, isOn: $value)
Text(subtitle)
.font(.footnote)
.foregroundColor(.secondary)
}
.padding()
.cornerRadius(8)
}
}
struct SettingsListToggleItem_Previews: PreviewProvider {
static var previews: some View {
SettingsListToggleItem(title: "Toggle", value: .constant(true), subtitle: "Some longer text explaining what the toggle does")
}
}

View File

@ -0,0 +1,45 @@
import SwiftUI
struct SettingsNumberInputView: View {
let title: String
@Binding
var value: Int
@State
private var text: String = ""
let footnote: String
var body: some View {
VStack(alignment: .leading) {
TextField(title, text: $text)
.onSubmit {
guard let newValue = Int(text) else {
return
}
value = newValue
}
.foregroundColor(.accentColor)
Text(footnote)
.font(.footnote)
.foregroundColor(.secondary)
Spacer()
}
.navigationTitle(title)
.navigationBarBackButtonHidden(false)
.onAppear {
text = "\(value)"
}
}
}
struct SettingsNumberInputView_Previews: PreviewProvider {
static var previews: some View {
SettingsNumberInputView(
title: "Title",
value: .constant(0),
footnote: "Some more text explaining the purpose of the text field.")
}
}

View File

@ -0,0 +1,30 @@
import SwiftUI
struct SettingsNumberItemLink: View {
let title: String
@Binding
var value: Int
let footnote: String
var body: some View {
NavigationLink {
SettingsNumberInputView(
title: title,
value: $value,
footnote: footnote
)
} label: {
SettingsListTextItem(title: title, value: "\(value)")
}
.buttonStyle(PlainButtonStyle())
}
}
struct SettingsNumberItemLink_Previews: PreviewProvider {
static var previews: some View {
SettingsNumberItemLink(title: "Title", value: .constant(0), footnote: "Some more text explaining the purpose of the text field.")
}
}

View File

@ -0,0 +1,33 @@
import SwiftUI
struct SettingsTextInputView: View {
let title: String
@Binding
var text: String
let footnote: String
var body: some View {
VStack(alignment: .leading) {
TextField(title, text: $text)
.foregroundColor(.accentColor)
Text(footnote)
.font(.footnote)
.foregroundColor(.secondary)
Spacer()
}
.navigationTitle(title)
.navigationBarBackButtonHidden(false)
}
}
struct SettingsTextInputView_Previews: PreviewProvider {
static var previews: some View {
SettingsTextInputView(
title: "Title",
text: .constant("Text"),
footnote: "Some more text explaining the purpose of the text field.")
}
}

View File

@ -0,0 +1,30 @@
import SwiftUI
struct SettingsTextItemLink: View {
let title: String
@Binding
var value: String
let footnote: String
var body: some View {
NavigationLink {
SettingsTextInputView(
title: title,
text: $value,
footnote: footnote
)
} label: {
SettingsListTextItem(title: title, value: value)
}
.buttonStyle(PlainButtonStyle())
}
}
struct SettingsTextItemLink_Previews: PreviewProvider {
static var previews: some View {
SettingsTextItemLink(title: "Title", value: .constant("Some value"), footnote: "Some more text explaining the purpose of the text field.")
}
}

View File

@ -1,19 +1,77 @@
import SwiftUI import SwiftUI
struct SettingsView: View { struct SettingsView: View {
@AppStorage("server")
var serverPath: String = "https://christophhagen.de/sesame/"
@AppStorage("localIP")
var localAddress: String = "192.168.178.104/"
@AppStorage("counter")
var nextMessageCounter: Int = 0
@AppStorage("compensate")
var isCompensatingDaylightTime: Bool = false
@AppStorage("local")
private var useLocalConnection = false
@AppStorage("deviceId")
private var deviceId: Int = 0
@EnvironmentObject
var keys: KeyManagement
var body: some View { var body: some View {
ScrollView { NavigationStack {
VStack { List {
Text(/*@START_MENU_TOKEN@*/"Hello, World!"/*@END_MENU_TOKEN@*/) SettingsTextItemLink(
} title: "Server url",
value: $serverPath,
footnote: "The url where the sesame server listens for incoming messages.")
SettingsTextItemLink(
title: "Local url",
value: $localAddress,
footnote: "The url where the device can be reached directly on the local WiFi network.")
SettingsListToggleItem(
title: "Local connection",
value: $useLocalConnection,
subtitle: "Attempt to communicate directly with the device, which requires a WiFi connection on the same network.")
SettingsNumberItemLink(
title: "Device ID",
value: $deviceId,
footnote: "The device ID is unique for each remote device, and is assigned by the system administrator.")
SettingsNumberItemLink(
title: "Message counter",
value: $nextMessageCounter,
footnote: "The message counter is increased after every message to the device, and used to prevent replay attacks.")
SettingsListToggleItem(
title: "Daylight savings",
value: $isCompensatingDaylightTime,
subtitle: "Compensate timestamps if the remote has daylight savings time wrongly set.")
SettingsKeyItemLink(
type: .deviceKey,
footnote: "Some text describing the purpose of the key.")
.environmentObject(keys)
SettingsKeyItemLink(
type: .remoteKey,
footnote: "Some text describing the purpose of the key.")
.environmentObject(keys)
SettingsKeyItemLink(
type: .authToken,
footnote: "Some text describing the purpose of the key.")
.environmentObject(keys)
} }
.navigationTitle("Settings") .navigationTitle("Settings")
} }
}
} }
struct SettingsView_Previews: PreviewProvider { struct SettingsView_Previews: PreviewProvider {
static var previews: some View { static var previews: some View {
SettingsView() SettingsView()
.previewDevice("Apple Watch Series 7 - 41mm") .previewDevice("Apple Watch Series 7 - 41mm")
.environmentObject(KeyManagement())
} }
} }

View File

@ -23,7 +23,6 @@
88E197B429EDC9BC00BF1D19 /* ContentView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 88E197B329EDC9BC00BF1D19 /* ContentView.swift */; }; 88E197B429EDC9BC00BF1D19 /* ContentView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 88E197B329EDC9BC00BF1D19 /* ContentView.swift */; };
88E197B629EDC9BD00BF1D19 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 88E197B529EDC9BD00BF1D19 /* Assets.xcassets */; }; 88E197B629EDC9BD00BF1D19 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 88E197B529EDC9BD00BF1D19 /* Assets.xcassets */; };
88E197B929EDC9BD00BF1D19 /* Preview Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 88E197B829EDC9BD00BF1D19 /* Preview Assets.xcassets */; }; 88E197B929EDC9BD00BF1D19 /* Preview Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 88E197B829EDC9BD00BF1D19 /* Preview Assets.xcassets */; };
88E197C229EDCB0900BF1D19 /* KeyManagement.swift in Sources */ = {isa = PBXBuildFile; fileRef = 88E197C129EDCB0900BF1D19 /* KeyManagement.swift */; };
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 */; };
88E197C829EDCCCE00BF1D19 /* ClientState.swift in Sources */ = {isa = PBXBuildFile; fileRef = 884A45C827A43D7900D6E650 /* ClientState.swift */; }; 88E197C829EDCCCE00BF1D19 /* ClientState.swift in Sources */ = {isa = PBXBuildFile; fileRef = 884A45C827A43D7900D6E650 /* ClientState.swift */; };
@ -39,6 +38,18 @@
88E197D729EDCFE800BF1D19 /* Date+Extensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 88E197D629EDCFE800BF1D19 /* Date+Extensions.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 */; }; 88E197D829EDD13B00BF1D19 /* SymmetricKey+Extensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 884A45CA27A464C000D6E650 /* SymmetricKey+Extensions.swift */; };
88E197D929EDD14D00BF1D19 /* HistoryItem.swift in Sources */ = {isa = PBXBuildFile; fileRef = E28DED34281EB17600259690 /* HistoryItem.swift */; }; 88E197D929EDD14D00BF1D19 /* HistoryItem.swift in Sources */ = {isa = PBXBuildFile; fileRef = E28DED34281EB17600259690 /* HistoryItem.swift */; };
E240654B2A8153C6009C1AD8 /* SettingsListTextItem.swift in Sources */ = {isa = PBXBuildFile; fileRef = E240654A2A8153C6009C1AD8 /* SettingsListTextItem.swift */; };
E240654D2A8155A3009C1AD8 /* SettingsListToggleItem.swift in Sources */ = {isa = PBXBuildFile; fileRef = E240654C2A8155A3009C1AD8 /* SettingsListToggleItem.swift */; };
E240654F2A8159B7009C1AD8 /* SettingsTextInputView.swift in Sources */ = {isa = PBXBuildFile; fileRef = E240654E2A8159B7009C1AD8 /* SettingsTextInputView.swift */; };
E24065512A819066009C1AD8 /* SettingsTextItemLink.swift in Sources */ = {isa = PBXBuildFile; fileRef = E24065502A819066009C1AD8 /* SettingsTextItemLink.swift */; };
E24065532A819614009C1AD8 /* SettingsNumberItemLink.swift in Sources */ = {isa = PBXBuildFile; fileRef = E24065522A819614009C1AD8 /* SettingsNumberItemLink.swift */; };
E24065552A819663009C1AD8 /* SettingsNumberInputView.swift in Sources */ = {isa = PBXBuildFile; fileRef = E24065542A819663009C1AD8 /* SettingsNumberInputView.swift */; };
E24065582A819AE3009C1AD8 /* SettingsKeyItemLink.swift in Sources */ = {isa = PBXBuildFile; fileRef = E24065572A819AE3009C1AD8 /* SettingsKeyItemLink.swift */; };
E240655A2A82218D009C1AD8 /* SettingsKeyInputView.swift in Sources */ = {isa = PBXBuildFile; fileRef = E24065592A82218D009C1AD8 /* SettingsKeyInputView.swift */; };
E240655B2A822397009C1AD8 /* KeyManagement.swift in Sources */ = {isa = PBXBuildFile; fileRef = 884A45C4279F4BBE00D6E650 /* KeyManagement.swift */; };
E240655C2A822C8E009C1AD8 /* HistoryManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = E28DED36281EC7FB00259690 /* HistoryManager.swift */; };
E240655E2A822E97009C1AD8 /* HistoryListRow.swift in Sources */ = {isa = PBXBuildFile; fileRef = E240655D2A822E97009C1AD8 /* HistoryListRow.swift */; };
E24065602A822ED9009C1AD8 /* HistoryItemDetail.swift in Sources */ = {isa = PBXBuildFile; fileRef = E240655F2A822ED9009C1AD8 /* HistoryItemDetail.swift */; };
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 */; };
@ -72,8 +83,17 @@
88E197B329EDC9BC00BF1D19 /* ContentView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ContentView.swift; sourceTree = "<group>"; }; 88E197B329EDC9BC00BF1D19 /* ContentView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ContentView.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>"; };
88E197B829EDC9BD00BF1D19 /* Preview Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = "Preview Assets.xcassets"; sourceTree = "<group>"; }; 88E197B829EDC9BD00BF1D19 /* Preview Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = "Preview Assets.xcassets"; sourceTree = "<group>"; };
88E197C129EDCB0900BF1D19 /* KeyManagement.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = KeyManagement.swift; 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>"; };
E240654A2A8153C6009C1AD8 /* SettingsListTextItem.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SettingsListTextItem.swift; sourceTree = "<group>"; };
E240654C2A8155A3009C1AD8 /* SettingsListToggleItem.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SettingsListToggleItem.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>"; };
E24065522A819614009C1AD8 /* SettingsNumberItemLink.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SettingsNumberItemLink.swift; sourceTree = "<group>"; };
E24065542A819663009C1AD8 /* SettingsNumberInputView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SettingsNumberInputView.swift; sourceTree = "<group>"; };
E24065572A819AE3009C1AD8 /* SettingsKeyItemLink.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SettingsKeyItemLink.swift; sourceTree = "<group>"; };
E24065592A82218D009C1AD8 /* SettingsKeyInputView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SettingsKeyInputView.swift; sourceTree = "<group>"; };
E240655D2A822E97009C1AD8 /* HistoryListRow.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HistoryListRow.swift; sourceTree = "<group>"; };
E240655F2A822ED9009C1AD8 /* HistoryItemDetail.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HistoryItemDetail.swift; sourceTree = "<group>"; };
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>"; };
@ -166,11 +186,13 @@
88E197B029EDC9BC00BF1D19 /* Sesame-Watch Watch App */ = { 88E197B029EDC9BC00BF1D19 /* Sesame-Watch Watch App */ = {
isa = PBXGroup; isa = PBXGroup;
children = ( children = (
E24065562A819AAD009C1AD8 /* Settings */,
88E197B129EDC9BC00BF1D19 /* Sesame_WatchApp.swift */, 88E197B129EDC9BC00BF1D19 /* Sesame_WatchApp.swift */,
88E197B329EDC9BC00BF1D19 /* ContentView.swift */, 88E197B329EDC9BC00BF1D19 /* ContentView.swift */,
888362332A80F3F90032BBB2 /* SettingsView.swift */, 888362332A80F3F90032BBB2 /* SettingsView.swift */,
888362352A80F4420032BBB2 /* HistoryView.swift */, 888362352A80F4420032BBB2 /* HistoryView.swift */,
88E197C129EDCB0900BF1D19 /* KeyManagement.swift */, E240655D2A822E97009C1AD8 /* HistoryListRow.swift */,
E240655F2A822ED9009C1AD8 /* HistoryItemDetail.swift */,
88E197B529EDC9BD00BF1D19 /* Assets.xcassets */, 88E197B529EDC9BD00BF1D19 /* Assets.xcassets */,
88E197B729EDC9BD00BF1D19 /* Preview Content */, 88E197B729EDC9BD00BF1D19 /* Preview Content */,
88E197D629EDCFE800BF1D19 /* Date+Extensions.swift */, 88E197D629EDCFE800BF1D19 /* Date+Extensions.swift */,
@ -193,6 +215,21 @@
name = Frameworks; name = Frameworks;
sourceTree = "<group>"; sourceTree = "<group>";
}; };
E24065562A819AAD009C1AD8 /* Settings */ = {
isa = PBXGroup;
children = (
E24065502A819066009C1AD8 /* SettingsTextItemLink.swift */,
E24065522A819614009C1AD8 /* SettingsNumberItemLink.swift */,
E24065542A819663009C1AD8 /* SettingsNumberInputView.swift */,
E240654E2A8159B7009C1AD8 /* SettingsTextInputView.swift */,
E240654A2A8153C6009C1AD8 /* SettingsListTextItem.swift */,
E240654C2A8155A3009C1AD8 /* SettingsListToggleItem.swift */,
E24065572A819AE3009C1AD8 /* SettingsKeyItemLink.swift */,
E24065592A82218D009C1AD8 /* SettingsKeyInputView.swift */,
);
path = Settings;
sourceTree = "<group>";
};
E2C5C1D92806FE4A00769EF6 /* API */ = { E2C5C1D92806FE4A00769EF6 /* API */ = {
isa = PBXGroup; isa = PBXGroup;
children = ( children = (
@ -351,20 +388,31 @@
files = ( files = (
888362342A80F3F90032BBB2 /* SettingsView.swift in Sources */, 888362342A80F3F90032BBB2 /* SettingsView.swift in Sources */,
88E197B429EDC9BC00BF1D19 /* ContentView.swift in Sources */, 88E197B429EDC9BC00BF1D19 /* ContentView.swift in Sources */,
E24065532A819614009C1AD8 /* SettingsNumberItemLink.swift in Sources */,
888362362A80F4420032BBB2 /* HistoryView.swift in Sources */, 888362362A80F4420032BBB2 /* HistoryView.swift in Sources */,
E240654F2A8159B7009C1AD8 /* SettingsTextInputView.swift in Sources */,
88E197D329EDCE6E00BF1D19 /* MessageResult.swift in Sources */, 88E197D329EDCE6E00BF1D19 /* MessageResult.swift in Sources */,
88E197D529EDCE8800BF1D19 /* ServerMessage.swift in Sources */, 88E197D529EDCE8800BF1D19 /* ServerMessage.swift in Sources */,
88E197D129EDCE5F00BF1D19 /* Data+Extensions.swift in Sources */, 88E197D129EDCE5F00BF1D19 /* Data+Extensions.swift in Sources */,
E240655A2A82218D009C1AD8 /* SettingsKeyInputView.swift in Sources */,
88E197D229EDCE6600BF1D19 /* RouteAPI.swift in Sources */, 88E197D229EDCE6600BF1D19 /* RouteAPI.swift in Sources */,
E24065512A819066009C1AD8 /* SettingsTextItemLink.swift in Sources */,
88E197D729EDCFE800BF1D19 /* Date+Extensions.swift in Sources */, 88E197D729EDCFE800BF1D19 /* Date+Extensions.swift in Sources */,
88E197C829EDCCCE00BF1D19 /* ClientState.swift in Sources */, 88E197C829EDCCCE00BF1D19 /* ClientState.swift in Sources */,
E24065602A822ED9009C1AD8 /* HistoryItemDetail.swift in Sources */,
88E197B229EDC9BC00BF1D19 /* Sesame_WatchApp.swift in Sources */, 88E197B229EDC9BC00BF1D19 /* Sesame_WatchApp.swift in Sources */,
E24065582A819AE3009C1AD8 /* SettingsKeyItemLink.swift in Sources */,
88E197C929EDCCE100BF1D19 /* Message.swift in Sources */, 88E197C929EDCCE100BF1D19 /* Message.swift in Sources */,
88E197D929EDD14D00BF1D19 /* HistoryItem.swift in Sources */, 88E197D929EDD14D00BF1D19 /* HistoryItem.swift in Sources */,
E240654B2A8153C6009C1AD8 /* SettingsListTextItem.swift in Sources */,
88E197C729EDCCBD00BF1D19 /* Client.swift in Sources */, 88E197C729EDCCBD00BF1D19 /* Client.swift in Sources */,
88E197D429EDCE7600BF1D19 /* UInt32+Extensions.swift in Sources */, 88E197D429EDCE7600BF1D19 /* UInt32+Extensions.swift in Sources */,
E240655B2A822397009C1AD8 /* KeyManagement.swift in Sources */,
E240654D2A8155A3009C1AD8 /* SettingsListToggleItem.swift in Sources */,
E24065552A819663009C1AD8 /* SettingsNumberInputView.swift in Sources */,
E240655E2A822E97009C1AD8 /* HistoryListRow.swift in Sources */,
88E197D829EDD13B00BF1D19 /* SymmetricKey+Extensions.swift in Sources */, 88E197D829EDD13B00BF1D19 /* SymmetricKey+Extensions.swift in Sources */,
88E197C229EDCB0900BF1D19 /* KeyManagement.swift in Sources */, E240655C2A822C8E009C1AD8 /* HistoryManager.swift in Sources */,
); );
runOnlyForDeploymentPostprocessing = 0; runOnlyForDeploymentPostprocessing = 0;
}; };

View File

@ -67,11 +67,16 @@
<key>orderHint</key> <key>orderHint</key>
<integer>0</integer> <integer>0</integer>
</dict> </dict>
<key>Sesame.xcscheme_^#shared#^_</key> <key>Sesame-Watch Watch App.xcscheme_^#shared#^_</key>
<dict> <dict>
<key>orderHint</key> <key>orderHint</key>
<integer>0</integer> <integer>0</integer>
</dict> </dict>
<key>Sesame.xcscheme_^#shared#^_</key>
<dict>
<key>orderHint</key>
<integer>1</integer>
</dict>
</dict> </dict>
</dict> </dict>
</plist> </plist>

View File

@ -27,8 +27,8 @@ struct DeviceResponse {
} }
/// Shorthand property for an invalid message. /// Shorthand property for an invalid message.
static var invalidMessageData: DeviceResponse { static var invalidMessageSize: DeviceResponse {
.init(event: .invalidMessageData) .init(event: .invalidMessageSize)
} }
/// Shorthand property for missing body data. /// Shorthand property for missing body data.
@ -83,8 +83,8 @@ struct DeviceResponse {
/// Get the reponse encoded in bytes. /// Get the reponse encoded in bytes.
var encoded: Data { var encoded: Data {
guard let message = response else { guard let message = response else {
return Data([event.rawValue]) return event.encoded
} }
return Data([event.rawValue]) + message.encoded return event.encoded + message.encoded
} }
} }

View File

@ -11,8 +11,8 @@ enum MessageResult: UInt8 {
/// A socket event on the device was unexpected (not binary data) /// A socket event on the device was unexpected (not binary data)
case unexpectedSocketEvent = 2 case unexpectedSocketEvent = 2
/// The size of the payload (i.e. message) was invalid, or the data could not be read /// The size of the payload (i.e. message) was invalid
case invalidMessageData = 3 case invalidMessageSize = 3
/// The transmitted message could not be authenticated using the key /// The transmitted message could not be authenticated using the key
case messageAuthenticationFailed = 4 case messageAuthenticationFailed = 4
@ -44,6 +44,10 @@ enum MessageResult: UInt8 {
/// The device is connected /// The device is connected
case deviceConnected = 15 case deviceConnected = 15
case invalidUrlParameter = 20
case invalidResponseAuthentication = 21
} }
extension MessageResult: CustomStringConvertible { extension MessageResult: CustomStringConvertible {
@ -54,7 +58,7 @@ extension MessageResult: CustomStringConvertible {
return "The device received unexpected text" return "The device received unexpected text"
case .unexpectedSocketEvent: case .unexpectedSocketEvent:
return "Unexpected socket event for the device" return "Unexpected socket event for the device"
case .invalidMessageData: case .invalidMessageSize:
return "Invalid message data" return "Invalid message data"
case .messageAuthenticationFailed: case .messageAuthenticationFailed:
return "Message authentication failed" return "Message authentication failed"
@ -76,6 +80,17 @@ extension MessageResult: CustomStringConvertible {
return "Another operation is in progress" return "Another operation is in progress"
case .deviceConnected: case .deviceConnected:
return "The device is connected" return "The device is connected"
case .invalidUrlParameter:
return "The url parameter could not be found"
case .invalidResponseAuthentication:
return "The response could not be authenticated"
} }
} }
} }
extension MessageResult {
var encoded: Data {
Data([rawValue])
}
}

View File

@ -11,8 +11,6 @@ struct ServerMessage {
static let authTokenSize = SHA256.byteCount static let authTokenSize = SHA256.byteCount
static let length = authTokenSize + Message.length
let authToken: Data let authToken: Data
let message: Message let message: Message
@ -22,30 +20,7 @@ struct ServerMessage {
self.message = message self.message = message
} }
/**
Decode a message from a byte buffer.
The buffer must contain at least `ServerMessage.length` bytes, or it will return `nil`.
- Parameter buffer: The buffer containing the bytes.
*/
init?(decodeFrom buffer: ByteBuffer) {
guard let data = buffer.getBytes(at: 0, length: ServerMessage.length) else {
return nil
}
self.authToken = Data(data.prefix(ServerMessage.authTokenSize))
self.message = Message(decodeFrom: Data(data.dropFirst(ServerMessage.authTokenSize)))
}
var encoded: Data { var encoded: Data {
authToken + message.encoded authToken + message.encoded
} }
static func token(from buffer: ByteBuffer) -> Data? {
guard buffer.readableBytes == authTokenSize else {
return nil
}
guard let bytes = buffer.getBytes(at: 0, length: authTokenSize) else {
return nil
}
return Data(bytes)
}
} }

View File

@ -40,6 +40,7 @@ final class Client {
var request = URLRequest(url: url) var request = URLRequest(url: url)
request.httpBody = data request.httpBody = data
request.httpMethod = "POST" request.httpMethod = "POST"
request.timeoutInterval = 10
return await requestAndDecode(request) return await requestAndDecode(request)
} }

View File

@ -1,5 +1,6 @@
import Foundation import Foundation
import SwiftUI import SwiftUI
import SFSafeSymbols
enum ConnectionError { enum ConnectionError {
case serverNotReached case serverNotReached
@ -97,7 +98,8 @@ enum ClientState {
self = .openSesame self = .openSesame
case .messageDeviceInvalid: case .messageDeviceInvalid:
self = .messageRejected(.invalidDeviceId) self = .messageRejected(.invalidDeviceId)
case .noBodyData, .invalidMessageData, .textReceived, .unexpectedSocketEvent: case .noBodyData, .invalidMessageSize, .textReceived, .unexpectedSocketEvent, .invalidUrlParameter, .invalidResponseAuthentication:
print("Unexpected internal error: \(keyResult)")
self = .internalError(keyResult.description) self = .internalError(keyResult.description)
case .deviceNotConnected: case .deviceNotConnected:
self = .deviceNotAvailable(.deviceDisconnected) self = .deviceNotAvailable(.deviceDisconnected)
@ -242,7 +244,7 @@ extension ClientState {
} }
case .openSesame: case .openSesame:
return 17 return 17
case .internalError(_): case .internalError:
return 18 return 18
} }
} }
@ -294,3 +296,26 @@ extension ClientState {
} }
} }
} }
extension ClientState {
@available(iOS 16, *)
var symbol: SFSymbol {
switch self {
case .deviceNotAvailable:
return .wifiExclamationmark
case .internalError:
return .applewatchSlash
case .noKeyAvailable:
return .lockTrianglebadgeExclamationmark
case .openSesame:
return .lockOpen
case .messageRejected:
return .nosign
case .responseRejected:
return .exclamationmarkTriangle
case .requestingStatus, .ready, .waitingForResponse:
return .wifiExclamationmark
}
}
}

View File

@ -54,23 +54,19 @@ struct HistoryListItem: View {
HStack { HStack {
if let roundTripText { if let roundTripText {
Image(systemSymbol: entry.usedLocalConnection ? .wifi : .network) Image(systemSymbol: entry.usedLocalConnection ? .wifi : .network)
//Image(systemSymbol: .arrowUpArrowDownCircle)
Text(roundTripText) Text(roundTripText)
.font(.subheadline) .font(.subheadline)
} }
//Spacer()
Image(systemSymbol: .personalhotspot) Image(systemSymbol: .personalhotspot)
Text(counterText) Text(counterText)
.font(.subheadline) .font(.subheadline)
if let timeOffsetText { if let timeOffsetText {
//Spacer()
Image(systemSymbol: .stopwatch) Image(systemSymbol: .stopwatch)
Text(timeOffsetText) Text(timeOffsetText)
.font(.subheadline) .font(.subheadline)
} }
}.foregroundColor(.secondary) }.foregroundColor(.secondary)
} }
//.padding()
} }
} }