Compare commits
7 Commits
1.0.0
...
5f9af35542
Author | SHA1 | Date | |
---|---|---|---|
5f9af35542 | |||
e5ea8c4951 | |||
f451715a11 | |||
32b4c8c81a | |||
f599cb790b | |||
9b14f442b0 | |||
8a17eef19b |
@ -0,0 +1,11 @@
|
|||||||
|
{
|
||||||
|
"colors" : [
|
||||||
|
{
|
||||||
|
"idiom" : "universal"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"info" : {
|
||||||
|
"author" : "xcode",
|
||||||
|
"version" : 1
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,14 @@
|
|||||||
|
{
|
||||||
|
"images" : [
|
||||||
|
{
|
||||||
|
"filename" : "sesame.png",
|
||||||
|
"idiom" : "universal",
|
||||||
|
"platform" : "watchos",
|
||||||
|
"size" : "1024x1024"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"info" : {
|
||||||
|
"author" : "xcode",
|
||||||
|
"version" : 1
|
||||||
|
}
|
||||||
|
}
|
Binary file not shown.
After Width: | Height: | Size: 113 KiB |
6
Sesame-Watch Watch App/Assets.xcassets/Contents.json
Normal file
6
Sesame-Watch Watch App/Assets.xcassets/Contents.json
Normal file
@ -0,0 +1,6 @@
|
|||||||
|
{
|
||||||
|
"info" : {
|
||||||
|
"author" : "xcode",
|
||||||
|
"version" : 1
|
||||||
|
}
|
||||||
|
}
|
134
Sesame-Watch Watch App/ContentView.swift
Normal file
134
Sesame-Watch Watch App/ContentView.swift
Normal file
@ -0,0 +1,134 @@
|
|||||||
|
import SwiftUI
|
||||||
|
import SFSafeSymbols
|
||||||
|
import CryptoKit
|
||||||
|
|
||||||
|
struct ContentView: 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 keyManager: KeyManagement
|
||||||
|
|
||||||
|
@State
|
||||||
|
var state: ClientState = .noKeyAvailable
|
||||||
|
|
||||||
|
let server = Client()
|
||||||
|
|
||||||
|
let history = HistoryManager()
|
||||||
|
|
||||||
|
var buttonBackground: Color {
|
||||||
|
state.allowsAction ?
|
||||||
|
.white.opacity(0.2) :
|
||||||
|
.black.opacity(0.2)
|
||||||
|
}
|
||||||
|
|
||||||
|
var buttonColor: Color {
|
||||||
|
state.allowsAction ? .white : .gray
|
||||||
|
}
|
||||||
|
|
||||||
|
var body: some View {
|
||||||
|
HStack {
|
||||||
|
Spacer()
|
||||||
|
VStack(alignment: .center) {
|
||||||
|
Image(systemSymbol: .lock)
|
||||||
|
.resizable()
|
||||||
|
.aspectRatio(contentMode: .fit)
|
||||||
|
.fontWeight(.ultraLight)
|
||||||
|
.padding()
|
||||||
|
.onTapGesture(perform: mainButtonPressed)
|
||||||
|
.disabled(!state.allowsAction)
|
||||||
|
Text(state.actionText)
|
||||||
|
.font(.subheadline)
|
||||||
|
}
|
||||||
|
Spacer()
|
||||||
|
}
|
||||||
|
.background(state.color)
|
||||||
|
.animation(.easeInOut, value: state.color)
|
||||||
|
.onAppear {
|
||||||
|
if keyManager.hasAllKeys, state == .noKeyAvailable {
|
||||||
|
state = .ready
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func mainButtonPressed() {
|
||||||
|
guard let key = keyManager.get(.remoteKey),
|
||||||
|
let token = keyManager.get(.authToken)?.data,
|
||||||
|
let deviceId = UInt8(exactly: deviceId) else {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
let count = UInt32(nextMessageCounter)
|
||||||
|
let sentTime = Date()
|
||||||
|
// Add time to compensate that the device is using daylight savings time
|
||||||
|
let timeCompensation: UInt32 = isCompensatingDaylightTime ? 3600 : 0
|
||||||
|
let content = Message.Content(
|
||||||
|
time: sentTime.timestamp + timeCompensation,
|
||||||
|
id: count,
|
||||||
|
device: deviceId)
|
||||||
|
let message = content.authenticate(using: key)
|
||||||
|
let historyItem = HistoryItem(sent: message.content, date: sentTime, local: useLocalConnection)
|
||||||
|
state = .waitingForResponse
|
||||||
|
print("Sending message \(count)")
|
||||||
|
Task {
|
||||||
|
let (newState, responseMessage) = await send(message, authToken: token)
|
||||||
|
let receivedTime = Date.now
|
||||||
|
//responseTime = receivedTime
|
||||||
|
state = newState
|
||||||
|
let finishedItem = historyItem.didReceive(response: newState, date: receivedTime, message: responseMessage?.content)
|
||||||
|
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 send(_ message: Message, authToken: Data) async -> (state: ClientState, response: Message?) {
|
||||||
|
if useLocalConnection {
|
||||||
|
return await server.sendMessageOverLocalNetwork(message, server: localAddress)
|
||||||
|
} else {
|
||||||
|
return await server.send(message, server: serverPath, authToken: authToken)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private func save(historyItem: HistoryItem) {
|
||||||
|
do {
|
||||||
|
try history.save(item: historyItem)
|
||||||
|
} catch {
|
||||||
|
print("Failed to save item: \(error)")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
struct ContentView_Previews: PreviewProvider {
|
||||||
|
static var previews: some View {
|
||||||
|
ContentView()
|
||||||
|
.environmentObject(KeyManagement())
|
||||||
|
}
|
||||||
|
}
|
12
Sesame-Watch Watch App/Date+Extensions.swift
Normal file
12
Sesame-Watch Watch App/Date+Extensions.swift
Normal file
@ -0,0 +1,12 @@
|
|||||||
|
import Foundation
|
||||||
|
|
||||||
|
extension Date {
|
||||||
|
|
||||||
|
var timestamp: UInt32 {
|
||||||
|
UInt32(timeIntervalSince1970.rounded())
|
||||||
|
}
|
||||||
|
|
||||||
|
init(timestamp: UInt32) {
|
||||||
|
self.init(timeIntervalSince1970: TimeInterval(timestamp))
|
||||||
|
}
|
||||||
|
}
|
67
Sesame-Watch Watch App/HistoryItemDetail.swift
Normal file
67
Sesame-Watch Watch App/HistoryItemDetail.swift
Normal 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)
|
||||||
|
}
|
||||||
|
}
|
41
Sesame-Watch Watch App/HistoryListRow.swift
Normal file
41
Sesame-Watch Watch App/HistoryListRow.swift
Normal 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)
|
||||||
|
}
|
||||||
|
}
|
37
Sesame-Watch Watch App/HistoryView.swift
Normal file
37
Sesame-Watch Watch App/HistoryView.swift
Normal file
@ -0,0 +1,37 @@
|
|||||||
|
import SwiftUI
|
||||||
|
|
||||||
|
struct HistoryView: View {
|
||||||
|
|
||||||
|
let history: HistoryManagerProtocol
|
||||||
|
|
||||||
|
@State
|
||||||
|
private var items: [HistoryItem] = []
|
||||||
|
|
||||||
|
var body: some View {
|
||||||
|
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 {
|
||||||
|
static var previews: some View {
|
||||||
|
HistoryView(history: HistoryManagerMock())
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,6 @@
|
|||||||
|
{
|
||||||
|
"info" : {
|
||||||
|
"author" : "xcode",
|
||||||
|
"version" : 1
|
||||||
|
}
|
||||||
|
}
|
20
Sesame-Watch Watch App/Sesame_WatchApp.swift
Normal file
20
Sesame-Watch Watch App/Sesame_WatchApp.swift
Normal file
@ -0,0 +1,20 @@
|
|||||||
|
import SwiftUI
|
||||||
|
|
||||||
|
@main
|
||||||
|
struct Sesame_Watch_Watch_AppApp: App {
|
||||||
|
|
||||||
|
let keyManagement = KeyManagement()
|
||||||
|
|
||||||
|
var body: some Scene {
|
||||||
|
WindowGroup {
|
||||||
|
TabView {
|
||||||
|
ContentView()
|
||||||
|
.environmentObject(keyManagement)
|
||||||
|
SettingsView()
|
||||||
|
.environmentObject(keyManagement)
|
||||||
|
HistoryView(history: HistoryManager())
|
||||||
|
}
|
||||||
|
.tabViewStyle(PageTabViewStyle())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
77
Sesame-Watch Watch App/Settings/SettingsKeyInputView.swift
Normal file
77
Sesame-Watch Watch App/Settings/SettingsKeyInputView.swift
Normal 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())
|
||||||
|
}
|
||||||
|
}
|
47
Sesame-Watch Watch App/Settings/SettingsKeyItemLink.swift
Normal file
47
Sesame-Watch Watch App/Settings/SettingsKeyItemLink.swift
Normal 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())
|
||||||
|
}
|
||||||
|
}
|
31
Sesame-Watch Watch App/Settings/SettingsListTextItem.swift
Normal file
31
Sesame-Watch Watch App/Settings/SettingsListTextItem.swift
Normal 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")
|
||||||
|
}
|
||||||
|
}
|
28
Sesame-Watch Watch App/Settings/SettingsListToggleItem.swift
Normal file
28
Sesame-Watch Watch App/Settings/SettingsListToggleItem.swift
Normal 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")
|
||||||
|
}
|
||||||
|
}
|
@ -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.")
|
||||||
|
}
|
||||||
|
}
|
30
Sesame-Watch Watch App/Settings/SettingsNumberItemLink.swift
Normal file
30
Sesame-Watch Watch App/Settings/SettingsNumberItemLink.swift
Normal 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.")
|
||||||
|
}
|
||||||
|
}
|
33
Sesame-Watch Watch App/Settings/SettingsTextInputView.swift
Normal file
33
Sesame-Watch Watch App/Settings/SettingsTextInputView.swift
Normal 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.")
|
||||||
|
}
|
||||||
|
}
|
30
Sesame-Watch Watch App/Settings/SettingsTextItemLink.swift
Normal file
30
Sesame-Watch Watch App/Settings/SettingsTextItemLink.swift
Normal 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.")
|
||||||
|
}
|
||||||
|
}
|
77
Sesame-Watch Watch App/SettingsView.swift
Normal file
77
Sesame-Watch Watch App/SettingsView.swift
Normal file
@ -0,0 +1,77 @@
|
|||||||
|
import SwiftUI
|
||||||
|
|
||||||
|
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 {
|
||||||
|
NavigationStack {
|
||||||
|
List {
|
||||||
|
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")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
struct SettingsView_Previews: PreviewProvider {
|
||||||
|
static var previews: some View {
|
||||||
|
SettingsView()
|
||||||
|
.previewDevice("Apple Watch Series 7 - 41mm")
|
||||||
|
.environmentObject(KeyManagement())
|
||||||
|
}
|
||||||
|
}
|
@ -14,13 +14,47 @@
|
|||||||
884A45C5279F4BBE00D6E650 /* KeyManagement.swift in Sources */ = {isa = PBXBuildFile; fileRef = 884A45C4279F4BBE00D6E650 /* KeyManagement.swift */; };
|
884A45C5279F4BBE00D6E650 /* KeyManagement.swift in Sources */ = {isa = PBXBuildFile; fileRef = 884A45C4279F4BBE00D6E650 /* KeyManagement.swift */; };
|
||||||
884A45C927A43D7900D6E650 /* ClientState.swift in Sources */ = {isa = PBXBuildFile; fileRef = 884A45C827A43D7900D6E650 /* ClientState.swift */; };
|
884A45C927A43D7900D6E650 /* ClientState.swift in Sources */ = {isa = PBXBuildFile; fileRef = 884A45C827A43D7900D6E650 /* ClientState.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 */; };
|
||||||
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 */; };
|
||||||
|
888362342A80F3F90032BBB2 /* SettingsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 888362332A80F3F90032BBB2 /* SettingsView.swift */; };
|
||||||
|
888362362A80F4420032BBB2 /* HistoryView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 888362352A80F4420032BBB2 /* HistoryView.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 */; };
|
||||||
|
88E197B629EDC9BD00BF1D19 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 88E197B529EDC9BD00BF1D19 /* Assets.xcassets */; };
|
||||||
|
88E197B929EDC9BD00BF1D19 /* Preview Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 88E197B829EDC9BD00BF1D19 /* Preview Assets.xcassets */; };
|
||||||
|
88E197C429EDCC8900BF1D19 /* 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 */; };
|
||||||
|
88E197C929EDCCE100BF1D19 /* Message.swift in Sources */ = {isa = PBXBuildFile; fileRef = E24EE77827FF95E00011CFD2 /* Message.swift */; };
|
||||||
|
88E197CC29EDCD4900BF1D19 /* NIOCore in Frameworks */ = {isa = PBXBuildFile; productRef = 88E197CB29EDCD4900BF1D19 /* NIOCore */; };
|
||||||
|
88E197CE29EDCD7500BF1D19 /* CBORCoding in Frameworks */ = {isa = PBXBuildFile; productRef = 88E197CD29EDCD7500BF1D19 /* CBORCoding */; };
|
||||||
|
88E197D029EDCD7D00BF1D19 /* SFSafeSymbols in Frameworks */ = {isa = PBXBuildFile; productRef = 88E197CF29EDCD7D00BF1D19 /* SFSafeSymbols */; };
|
||||||
|
88E197D129EDCE5F00BF1D19 /* Data+Extensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = E24EE77127FDCCC00011CFD2 /* Data+Extensions.swift */; };
|
||||||
|
88E197D229EDCE6600BF1D19 /* RouteAPI.swift in Sources */ = {isa = PBXBuildFile; fileRef = E2C5C1DA2806FE8900769EF6 /* RouteAPI.swift */; };
|
||||||
|
88E197D329EDCE6E00BF1D19 /* MessageResult.swift in Sources */ = {isa = PBXBuildFile; fileRef = 884A45CE27A5402D00D6E650 /* MessageResult.swift */; };
|
||||||
|
88E197D429EDCE7600BF1D19 /* UInt32+Extensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = E2C5C1DC281B3AC400769EF6 /* UInt32+Extensions.swift */; };
|
||||||
|
88E197D529EDCE8800BF1D19 /* ServerMessage.swift in Sources */ = {isa = PBXBuildFile; fileRef = E2C5C1F7281E769F00769EF6 /* ServerMessage.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 */; };
|
||||||
|
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 */; };
|
||||||
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 */; };
|
||||||
@ -42,10 +76,28 @@
|
|||||||
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>"; };
|
||||||
884A45CC27A465F500D6E650 /* Client.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Client.swift; sourceTree = "<group>"; };
|
884A45CC27A465F500D6E650 /* Client.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Client.swift; sourceTree = "<group>"; };
|
||||||
884A45CE27A5402D00D6E650 /* MessageResult.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MessageResult.swift; sourceTree = "<group>"; };
|
884A45CE27A5402D00D6E650 /* MessageResult.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MessageResult.swift; sourceTree = "<group>"; };
|
||||||
|
888362332A80F3F90032BBB2 /* SettingsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SettingsView.swift; sourceTree = "<group>"; };
|
||||||
|
888362352A80F4420032BBB2 /* HistoryView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HistoryView.swift; sourceTree = "<group>"; };
|
||||||
|
88E197AC29EDC9BC00BF1D19 /* Sesame-Watch Watch App.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = "Sesame-Watch Watch App.app"; sourceTree = BUILT_PRODUCTS_DIR; };
|
||||||
|
88E197B129EDC9BC00BF1D19 /* Sesame_WatchApp.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Sesame_WatchApp.swift; sourceTree = "<group>"; };
|
||||||
|
88E197B329EDC9BC00BF1D19 /* ContentView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ContentView.swift; sourceTree = "<group>"; };
|
||||||
|
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>"; };
|
||||||
|
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>"; };
|
||||||
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,10 +114,22 @@
|
|||||||
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;
|
||||||
};
|
};
|
||||||
|
88E197A929EDC9BC00BF1D19 /* Frameworks */ = {
|
||||||
|
isa = PBXFrameworksBuildPhase;
|
||||||
|
buildActionMask = 2147483647;
|
||||||
|
files = (
|
||||||
|
88E197D029EDCD7D00BF1D19 /* SFSafeSymbols in Frameworks */,
|
||||||
|
88E197CE29EDCD7500BF1D19 /* CBORCoding in Frameworks */,
|
||||||
|
88E197CC29EDCD4900BF1D19 /* NIOCore in Frameworks */,
|
||||||
|
);
|
||||||
|
runOnlyForDeploymentPostprocessing = 0;
|
||||||
|
};
|
||||||
/* End PBXFrameworksBuildPhase section */
|
/* End PBXFrameworksBuildPhase section */
|
||||||
|
|
||||||
/* Begin PBXGroup section */
|
/* Begin PBXGroup section */
|
||||||
@ -73,7 +137,9 @@
|
|||||||
isa = PBXGroup;
|
isa = PBXGroup;
|
||||||
children = (
|
children = (
|
||||||
884A45B5279F48C100D6E650 /* Sesame */,
|
884A45B5279F48C100D6E650 /* Sesame */,
|
||||||
|
88E197B029EDC9BC00BF1D19 /* Sesame-Watch Watch App */,
|
||||||
884A45B4279F48C100D6E650 /* Products */,
|
884A45B4279F48C100D6E650 /* Products */,
|
||||||
|
88E197CA29EDCD4900BF1D19 /* Frameworks */,
|
||||||
);
|
);
|
||||||
sourceTree = "<group>";
|
sourceTree = "<group>";
|
||||||
};
|
};
|
||||||
@ -81,6 +147,7 @@
|
|||||||
isa = PBXGroup;
|
isa = PBXGroup;
|
||||||
children = (
|
children = (
|
||||||
884A45B3279F48C100D6E650 /* Sesame.app */,
|
884A45B3279F48C100D6E650 /* Sesame.app */,
|
||||||
|
88E197AC29EDC9BC00BF1D19 /* Sesame-Watch Watch App.app */,
|
||||||
);
|
);
|
||||||
name = Products;
|
name = Products;
|
||||||
sourceTree = "<group>";
|
sourceTree = "<group>";
|
||||||
@ -96,10 +163,10 @@
|
|||||||
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 */,
|
|
||||||
884A45C827A43D7900D6E650 /* ClientState.swift */,
|
884A45C827A43D7900D6E650 /* ClientState.swift */,
|
||||||
|
884A45CC27A465F500D6E650 /* Client.swift */,
|
||||||
884A45C4279F4BBE00D6E650 /* KeyManagement.swift */,
|
884A45C4279F4BBE00D6E650 /* KeyManagement.swift */,
|
||||||
884A45CA27A464C000D6E650 /* SymmetricKey+Extensions.swift */,
|
884A45CA27A464C000D6E650 /* SymmetricKey+Extensions.swift */,
|
||||||
884A45BA279F48C300D6E650 /* Assets.xcassets */,
|
884A45BA279F48C300D6E650 /* Assets.xcassets */,
|
||||||
@ -116,6 +183,53 @@
|
|||||||
path = "Preview Content";
|
path = "Preview Content";
|
||||||
sourceTree = "<group>";
|
sourceTree = "<group>";
|
||||||
};
|
};
|
||||||
|
88E197B029EDC9BC00BF1D19 /* Sesame-Watch Watch App */ = {
|
||||||
|
isa = PBXGroup;
|
||||||
|
children = (
|
||||||
|
E24065562A819AAD009C1AD8 /* Settings */,
|
||||||
|
88E197B129EDC9BC00BF1D19 /* Sesame_WatchApp.swift */,
|
||||||
|
88E197B329EDC9BC00BF1D19 /* ContentView.swift */,
|
||||||
|
888362332A80F3F90032BBB2 /* SettingsView.swift */,
|
||||||
|
888362352A80F4420032BBB2 /* HistoryView.swift */,
|
||||||
|
E240655D2A822E97009C1AD8 /* HistoryListRow.swift */,
|
||||||
|
E240655F2A822ED9009C1AD8 /* HistoryItemDetail.swift */,
|
||||||
|
88E197B529EDC9BD00BF1D19 /* Assets.xcassets */,
|
||||||
|
88E197B729EDC9BD00BF1D19 /* Preview Content */,
|
||||||
|
88E197D629EDCFE800BF1D19 /* Date+Extensions.swift */,
|
||||||
|
);
|
||||||
|
path = "Sesame-Watch Watch App";
|
||||||
|
sourceTree = "<group>";
|
||||||
|
};
|
||||||
|
88E197B729EDC9BD00BF1D19 /* Preview Content */ = {
|
||||||
|
isa = PBXGroup;
|
||||||
|
children = (
|
||||||
|
88E197B829EDC9BD00BF1D19 /* Preview Assets.xcassets */,
|
||||||
|
);
|
||||||
|
path = "Preview Content";
|
||||||
|
sourceTree = "<group>";
|
||||||
|
};
|
||||||
|
88E197CA29EDCD4900BF1D19 /* Frameworks */ = {
|
||||||
|
isa = PBXGroup;
|
||||||
|
children = (
|
||||||
|
);
|
||||||
|
name = Frameworks;
|
||||||
|
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 = (
|
||||||
@ -148,11 +262,35 @@
|
|||||||
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 */;
|
||||||
productType = "com.apple.product-type.application";
|
productType = "com.apple.product-type.application";
|
||||||
};
|
};
|
||||||
|
88E197AB29EDC9BC00BF1D19 /* Sesame-Watch Watch App */ = {
|
||||||
|
isa = PBXNativeTarget;
|
||||||
|
buildConfigurationList = 88E197BF29EDC9BD00BF1D19 /* Build configuration list for PBXNativeTarget "Sesame-Watch Watch App" */;
|
||||||
|
buildPhases = (
|
||||||
|
88E197A829EDC9BC00BF1D19 /* Sources */,
|
||||||
|
88E197A929EDC9BC00BF1D19 /* Frameworks */,
|
||||||
|
88E197AA29EDC9BC00BF1D19 /* Resources */,
|
||||||
|
);
|
||||||
|
buildRules = (
|
||||||
|
);
|
||||||
|
dependencies = (
|
||||||
|
);
|
||||||
|
name = "Sesame-Watch Watch App";
|
||||||
|
packageProductDependencies = (
|
||||||
|
88E197CB29EDCD4900BF1D19 /* NIOCore */,
|
||||||
|
88E197CD29EDCD7500BF1D19 /* CBORCoding */,
|
||||||
|
88E197CF29EDCD7D00BF1D19 /* SFSafeSymbols */,
|
||||||
|
);
|
||||||
|
productName = "Sesame-Watch Watch App";
|
||||||
|
productReference = 88E197AC29EDC9BC00BF1D19 /* Sesame-Watch Watch App.app */;
|
||||||
|
productType = "com.apple.product-type.application";
|
||||||
|
};
|
||||||
/* End PBXNativeTarget section */
|
/* End PBXNativeTarget section */
|
||||||
|
|
||||||
/* Begin PBXProject section */
|
/* Begin PBXProject section */
|
||||||
@ -160,11 +298,15 @@
|
|||||||
isa = PBXProject;
|
isa = PBXProject;
|
||||||
attributes = {
|
attributes = {
|
||||||
BuildIndependentTargetsInParallel = 1;
|
BuildIndependentTargetsInParallel = 1;
|
||||||
LastSwiftUpdateCheck = 1320;
|
LastSwiftUpdateCheck = 1430;
|
||||||
LastUpgradeCheck = 1320;
|
LastUpgradeCheck = 1320;
|
||||||
TargetAttributes = {
|
TargetAttributes = {
|
||||||
884A45B2279F48C100D6E650 = {
|
884A45B2279F48C100D6E650 = {
|
||||||
CreatedOnToolsVersion = 13.2.1;
|
CreatedOnToolsVersion = 13.2.1;
|
||||||
|
LastSwiftMigration = 1430;
|
||||||
|
};
|
||||||
|
88E197AB29EDC9BC00BF1D19 = {
|
||||||
|
CreatedOnToolsVersion = 14.3;
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
@ -179,12 +321,15 @@
|
|||||||
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 = "";
|
||||||
projectRoot = "";
|
projectRoot = "";
|
||||||
targets = (
|
targets = (
|
||||||
884A45B2279F48C100D6E650 /* Sesame */,
|
884A45B2279F48C100D6E650 /* Sesame */,
|
||||||
|
88E197AB29EDC9BC00BF1D19 /* Sesame-Watch Watch App */,
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
/* End PBXProject section */
|
/* End PBXProject section */
|
||||||
@ -199,6 +344,15 @@
|
|||||||
);
|
);
|
||||||
runOnlyForDeploymentPostprocessing = 0;
|
runOnlyForDeploymentPostprocessing = 0;
|
||||||
};
|
};
|
||||||
|
88E197AA29EDC9BC00BF1D19 /* Resources */ = {
|
||||||
|
isa = PBXResourcesBuildPhase;
|
||||||
|
buildActionMask = 2147483647;
|
||||||
|
files = (
|
||||||
|
88E197B929EDC9BD00BF1D19 /* Preview Assets.xcassets in Resources */,
|
||||||
|
88E197B629EDC9BD00BF1D19 /* Assets.xcassets in Resources */,
|
||||||
|
);
|
||||||
|
runOnlyForDeploymentPostprocessing = 0;
|
||||||
|
};
|
||||||
/* End PBXResourcesBuildPhase section */
|
/* End PBXResourcesBuildPhase section */
|
||||||
|
|
||||||
/* Begin PBXSourcesBuildPhase section */
|
/* Begin PBXSourcesBuildPhase section */
|
||||||
@ -212,7 +366,6 @@
|
|||||||
E28DED37281EC7FB00259690 /* HistoryManager.swift in Sources */,
|
E28DED37281EC7FB00259690 /* HistoryManager.swift in Sources */,
|
||||||
E2C5C1DB2806FE8900769EF6 /* RouteAPI.swift in Sources */,
|
E2C5C1DB2806FE8900769EF6 /* RouteAPI.swift in Sources */,
|
||||||
E2C5C1DD281B3AC400769EF6 /* UInt32+Extensions.swift in Sources */,
|
E2C5C1DD281B3AC400769EF6 /* UInt32+Extensions.swift in Sources */,
|
||||||
884A45CD27A465F500D6E650 /* Client.swift in Sources */,
|
|
||||||
E24EE77227FDCCC00011CFD2 /* Data+Extensions.swift in Sources */,
|
E24EE77227FDCCC00011CFD2 /* Data+Extensions.swift in Sources */,
|
||||||
E24EE77427FF95920011CFD2 /* DeviceResponse.swift in Sources */,
|
E24EE77427FF95920011CFD2 /* DeviceResponse.swift in Sources */,
|
||||||
884A45CB27A464C000D6E650 /* SymmetricKey+Extensions.swift in Sources */,
|
884A45CB27A464C000D6E650 /* SymmetricKey+Extensions.swift in Sources */,
|
||||||
@ -221,13 +374,48 @@
|
|||||||
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 */,
|
||||||
|
88E197C429EDCC8900BF1D19 /* Client.swift in Sources */,
|
||||||
884A45C5279F4BBE00D6E650 /* KeyManagement.swift in Sources */,
|
884A45C5279F4BBE00D6E650 /* KeyManagement.swift in Sources */,
|
||||||
E2C5C1F8281E769F00769EF6 /* ServerMessage.swift in Sources */,
|
E2C5C1F8281E769F00769EF6 /* ServerMessage.swift in Sources */,
|
||||||
);
|
);
|
||||||
runOnlyForDeploymentPostprocessing = 0;
|
runOnlyForDeploymentPostprocessing = 0;
|
||||||
};
|
};
|
||||||
|
88E197A829EDC9BC00BF1D19 /* Sources */ = {
|
||||||
|
isa = PBXSourcesBuildPhase;
|
||||||
|
buildActionMask = 2147483647;
|
||||||
|
files = (
|
||||||
|
888362342A80F3F90032BBB2 /* SettingsView.swift in Sources */,
|
||||||
|
88E197B429EDC9BC00BF1D19 /* ContentView.swift in Sources */,
|
||||||
|
E24065532A819614009C1AD8 /* SettingsNumberItemLink.swift in Sources */,
|
||||||
|
888362362A80F4420032BBB2 /* HistoryView.swift in Sources */,
|
||||||
|
E240654F2A8159B7009C1AD8 /* SettingsTextInputView.swift in Sources */,
|
||||||
|
88E197D329EDCE6E00BF1D19 /* MessageResult.swift in Sources */,
|
||||||
|
88E197D529EDCE8800BF1D19 /* ServerMessage.swift in Sources */,
|
||||||
|
88E197D129EDCE5F00BF1D19 /* Data+Extensions.swift in Sources */,
|
||||||
|
E240655A2A82218D009C1AD8 /* SettingsKeyInputView.swift in Sources */,
|
||||||
|
88E197D229EDCE6600BF1D19 /* RouteAPI.swift in Sources */,
|
||||||
|
E24065512A819066009C1AD8 /* SettingsTextItemLink.swift in Sources */,
|
||||||
|
88E197D729EDCFE800BF1D19 /* Date+Extensions.swift in Sources */,
|
||||||
|
88E197C829EDCCCE00BF1D19 /* ClientState.swift in Sources */,
|
||||||
|
E24065602A822ED9009C1AD8 /* HistoryItemDetail.swift in Sources */,
|
||||||
|
88E197B229EDC9BC00BF1D19 /* Sesame_WatchApp.swift in Sources */,
|
||||||
|
E24065582A819AE3009C1AD8 /* SettingsKeyItemLink.swift in Sources */,
|
||||||
|
88E197C929EDCCE100BF1D19 /* Message.swift in Sources */,
|
||||||
|
88E197D929EDD14D00BF1D19 /* HistoryItem.swift in Sources */,
|
||||||
|
E240654B2A8153C6009C1AD8 /* SettingsListTextItem.swift in Sources */,
|
||||||
|
88E197C729EDCCBD00BF1D19 /* Client.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 */,
|
||||||
|
E240655C2A822C8E009C1AD8 /* HistoryManager.swift in Sources */,
|
||||||
|
);
|
||||||
|
runOnlyForDeploymentPostprocessing = 0;
|
||||||
|
};
|
||||||
/* End PBXSourcesBuildPhase section */
|
/* End PBXSourcesBuildPhase section */
|
||||||
|
|
||||||
/* Begin XCBuildConfiguration section */
|
/* Begin XCBuildConfiguration section */
|
||||||
@ -352,6 +540,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 +562,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 +574,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,11 +596,74 @@
|
|||||||
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;
|
||||||
};
|
};
|
||||||
name = Release;
|
name = Release;
|
||||||
};
|
};
|
||||||
|
88E197BD29EDC9BD00BF1D19 /* Debug */ = {
|
||||||
|
isa = XCBuildConfiguration;
|
||||||
|
buildSettings = {
|
||||||
|
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
|
||||||
|
ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor;
|
||||||
|
CLANG_CXX_LANGUAGE_STANDARD = "gnu++20";
|
||||||
|
CODE_SIGN_STYLE = Automatic;
|
||||||
|
CURRENT_PROJECT_VERSION = 1;
|
||||||
|
DEVELOPMENT_ASSET_PATHS = "\"Sesame-Watch Watch App/Preview Content\"";
|
||||||
|
DEVELOPMENT_TEAM = H8WR4M6QQ4;
|
||||||
|
ENABLE_PREVIEWS = YES;
|
||||||
|
GENERATE_INFOPLIST_FILE = YES;
|
||||||
|
INFOPLIST_KEY_CFBundleDisplayName = "Sesame-Watch";
|
||||||
|
INFOPLIST_KEY_UISupportedInterfaceOrientations = "UIInterfaceOrientationPortrait UIInterfaceOrientationPortraitUpsideDown";
|
||||||
|
INFOPLIST_KEY_WKWatchOnly = YES;
|
||||||
|
LD_RUNPATH_SEARCH_PATHS = (
|
||||||
|
"$(inherited)",
|
||||||
|
"@executable_path/Frameworks",
|
||||||
|
);
|
||||||
|
MARKETING_VERSION = 1.0;
|
||||||
|
PRODUCT_BUNDLE_IDENTIFIER = "de.christophhagen.Sesame-Watch.watchkitapp";
|
||||||
|
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||||
|
SDKROOT = watchos;
|
||||||
|
SKIP_INSTALL = YES;
|
||||||
|
SWIFT_EMIT_LOC_STRINGS = YES;
|
||||||
|
SWIFT_VERSION = 5.0;
|
||||||
|
TARGETED_DEVICE_FAMILY = 4;
|
||||||
|
WATCHOS_DEPLOYMENT_TARGET = 9.4;
|
||||||
|
};
|
||||||
|
name = Debug;
|
||||||
|
};
|
||||||
|
88E197BE29EDC9BD00BF1D19 /* Release */ = {
|
||||||
|
isa = XCBuildConfiguration;
|
||||||
|
buildSettings = {
|
||||||
|
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
|
||||||
|
ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor;
|
||||||
|
CLANG_CXX_LANGUAGE_STANDARD = "gnu++20";
|
||||||
|
CODE_SIGN_STYLE = Automatic;
|
||||||
|
CURRENT_PROJECT_VERSION = 1;
|
||||||
|
DEVELOPMENT_ASSET_PATHS = "\"Sesame-Watch Watch App/Preview Content\"";
|
||||||
|
DEVELOPMENT_TEAM = H8WR4M6QQ4;
|
||||||
|
ENABLE_PREVIEWS = YES;
|
||||||
|
GENERATE_INFOPLIST_FILE = YES;
|
||||||
|
INFOPLIST_KEY_CFBundleDisplayName = "Sesame-Watch";
|
||||||
|
INFOPLIST_KEY_UISupportedInterfaceOrientations = "UIInterfaceOrientationPortrait UIInterfaceOrientationPortraitUpsideDown";
|
||||||
|
INFOPLIST_KEY_WKWatchOnly = YES;
|
||||||
|
LD_RUNPATH_SEARCH_PATHS = (
|
||||||
|
"$(inherited)",
|
||||||
|
"@executable_path/Frameworks",
|
||||||
|
);
|
||||||
|
MARKETING_VERSION = 1.0;
|
||||||
|
PRODUCT_BUNDLE_IDENTIFIER = "de.christophhagen.Sesame-Watch.watchkitapp";
|
||||||
|
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||||
|
SDKROOT = watchos;
|
||||||
|
SKIP_INSTALL = YES;
|
||||||
|
SWIFT_EMIT_LOC_STRINGS = YES;
|
||||||
|
SWIFT_VERSION = 5.0;
|
||||||
|
TARGETED_DEVICE_FAMILY = 4;
|
||||||
|
WATCHOS_DEPLOYMENT_TARGET = 9.4;
|
||||||
|
};
|
||||||
|
name = Release;
|
||||||
|
};
|
||||||
/* End XCBuildConfiguration section */
|
/* End XCBuildConfiguration section */
|
||||||
|
|
||||||
/* Begin XCConfigurationList section */
|
/* Begin XCConfigurationList section */
|
||||||
@ -430,9 +685,34 @@
|
|||||||
defaultConfigurationIsVisible = 0;
|
defaultConfigurationIsVisible = 0;
|
||||||
defaultConfigurationName = Release;
|
defaultConfigurationName = Release;
|
||||||
};
|
};
|
||||||
|
88E197BF29EDC9BD00BF1D19 /* Build configuration list for PBXNativeTarget "Sesame-Watch Watch App" */ = {
|
||||||
|
isa = XCConfigurationList;
|
||||||
|
buildConfigurations = (
|
||||||
|
88E197BD29EDC9BD00BF1D19 /* Debug */,
|
||||||
|
88E197BE29EDC9BD00BF1D19 /* Release */,
|
||||||
|
);
|
||||||
|
defaultConfigurationIsVisible = 0;
|
||||||
|
defaultConfigurationName = Release;
|
||||||
|
};
|
||||||
/* 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 +724,31 @@
|
|||||||
/* 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;
|
||||||
|
};
|
||||||
|
88E197CB29EDCD4900BF1D19 /* NIOCore */ = {
|
||||||
|
isa = XCSwiftPackageProductDependency;
|
||||||
|
package = E24EE77527FF95C00011CFD2 /* XCRemoteSwiftPackageReference "swift-nio" */;
|
||||||
|
productName = NIOCore;
|
||||||
|
};
|
||||||
|
88E197CD29EDCD7500BF1D19 /* CBORCoding */ = {
|
||||||
|
isa = XCSwiftPackageProductDependency;
|
||||||
|
package = 8864664D29E5684C004FE2BE /* XCRemoteSwiftPackageReference "CBORCoding" */;
|
||||||
|
productName = CBORCoding;
|
||||||
|
};
|
||||||
|
88E197CF29EDCD7D00BF1D19 /* 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" */;
|
||||||
|
@ -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",
|
||||||
|
Binary file not shown.
Binary file not shown.
@ -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>
|
||||||
|
@ -4,11 +4,16 @@
|
|||||||
<dict>
|
<dict>
|
||||||
<key>SchemeUserState</key>
|
<key>SchemeUserState</key>
|
||||||
<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>
|
||||||
|
@ -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
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -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 {
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -41,15 +49,18 @@ extension Message {
|
|||||||
|
|
||||||
/// The counter of the message (for freshness)
|
/// The counter of the message (for freshness)
|
||||||
let id: UInt32
|
let id: UInt32
|
||||||
|
|
||||||
|
let deviceId: UInt8?
|
||||||
|
|
||||||
/**
|
/**
|
||||||
Create new message content.
|
Create new message content.
|
||||||
- Parameter time: The time of message creation,
|
- Parameter time: The time of message creation,
|
||||||
- Parameter id: The counter of the message
|
- Parameter id: The counter of the message
|
||||||
*/
|
*/
|
||||||
init(time: UInt32, id: UInt32) {
|
init(time: UInt32, id: UInt32, device: UInt8) {
|
||||||
self.time = time
|
self.time = time
|
||||||
self.id = id
|
self.id = id
|
||||||
|
self.deviceId = device
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -61,20 +72,29 @@ extension Message {
|
|||||||
*/
|
*/
|
||||||
init<T: Sequence>(decodeFrom data: T) where T.Element == UInt8 {
|
init<T: Sequence>(decodeFrom data: T) where T.Element == UInt8 {
|
||||||
self.time = UInt32(data: Data(data.prefix(MemoryLayout<UInt32>.size)))
|
self.time = UInt32(data: Data(data.prefix(MemoryLayout<UInt32>.size)))
|
||||||
self.id = UInt32(data: Data(data.dropFirst(MemoryLayout<UInt32>.size)))
|
self.id = UInt32(data: Data(data.dropLast().suffix(MemoryLayout<UInt32>.size)))
|
||||||
|
self.deviceId = data.suffix(1).last!
|
||||||
}
|
}
|
||||||
|
|
||||||
/// The byte length of an encoded message content
|
/// The byte length of an encoded message content
|
||||||
static var length: Int {
|
static var length: Int {
|
||||||
MemoryLayout<UInt32>.size * 2
|
MemoryLayout<UInt32>.size * 2 + 1
|
||||||
}
|
}
|
||||||
|
|
||||||
/// The message content encoded to data
|
/// The message content encoded to data
|
||||||
var encoded: Data {
|
var encoded: Data {
|
||||||
time.encoded + id.encoded
|
time.encoded + id.encoded + Data([deviceId ?? 0])
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
extension Message.Content: Codable {
|
||||||
|
|
||||||
|
enum CodingKeys: Int, CodingKey {
|
||||||
|
case time = 1
|
||||||
|
case id = 2
|
||||||
|
case deviceId = 3
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
extension Message {
|
extension Message {
|
||||||
|
@ -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
|
||||||
@ -25,6 +25,9 @@ enum MessageResult: UInt8 {
|
|||||||
|
|
||||||
/// The key was accepted by the device, and the door will be opened
|
/// The key was accepted by the device, and the door will be opened
|
||||||
case messageAccepted = 7
|
case messageAccepted = 7
|
||||||
|
|
||||||
|
/// The device id is invalid
|
||||||
|
case messageDeviceInvalid = 8
|
||||||
|
|
||||||
|
|
||||||
/// The request did not contain body data with the key
|
/// The request did not contain body data with the key
|
||||||
@ -41,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 {
|
||||||
@ -51,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"
|
||||||
@ -61,6 +68,8 @@ extension MessageResult: CustomStringConvertible {
|
|||||||
return "Message counter invalid"
|
return "Message counter invalid"
|
||||||
case .messageAccepted:
|
case .messageAccepted:
|
||||||
return "Message accepted"
|
return "Message accepted"
|
||||||
|
case .messageDeviceInvalid:
|
||||||
|
return "Invalid device ID"
|
||||||
case .noBodyData:
|
case .noBodyData:
|
||||||
return "No body data included in the request"
|
return "No body data included in the request"
|
||||||
case .deviceNotConnected:
|
case .deviceNotConnected:
|
||||||
@ -71,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])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
@ -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
|
||||||
@ -21,31 +19,8 @@ struct ServerMessage {
|
|||||||
self.authToken = authToken
|
self.authToken = authToken
|
||||||
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)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
@ -1,30 +1,50 @@
|
|||||||
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) async -> ClientState {
|
func deviceStatus(authToken: Data, server: String) async -> ClientState {
|
||||||
await send(path: .getDeviceStatus, data: authToken).state
|
await send(path: .getDeviceStatus, server: server, data: authToken).state
|
||||||
}
|
}
|
||||||
|
|
||||||
func send(_ message: Message, authToken: Data) async -> (state: ClientState, response: Message?) {
|
func sendMessageOverLocalNetwork(_ message: Message, server: String) async -> (state: ClientState, response: Message?) {
|
||||||
|
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, 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, server: String, data: Data) async -> (state: ClientState, response: Message?) {
|
||||||
|
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(path: RouteAPI, data: Data) async -> (state: ClientState, response: Message?) {
|
private func send(to url: URL, data: Data) async -> (state: ClientState, response: Message?) {
|
||||||
let url = server.appendingPathComponent(path.rawValue)
|
|
||||||
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)
|
||||||
|
}
|
||||||
|
|
||||||
|
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 +56,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))
|
||||||
|
@ -1,5 +1,6 @@
|
|||||||
import Foundation
|
import Foundation
|
||||||
import SwiftUI
|
import SwiftUI
|
||||||
|
import SFSafeSymbols
|
||||||
|
|
||||||
enum ConnectionError {
|
enum ConnectionError {
|
||||||
case serverNotReached
|
case serverNotReached
|
||||||
@ -19,6 +20,7 @@ extension ConnectionError: CustomStringConvertible {
|
|||||||
}
|
}
|
||||||
|
|
||||||
enum RejectionCause {
|
enum RejectionCause {
|
||||||
|
case invalidDeviceId
|
||||||
case invalidCounter
|
case invalidCounter
|
||||||
case invalidTime
|
case invalidTime
|
||||||
case invalidAuthentication
|
case invalidAuthentication
|
||||||
@ -30,6 +32,8 @@ extension RejectionCause: CustomStringConvertible {
|
|||||||
|
|
||||||
var description: String {
|
var description: String {
|
||||||
switch self {
|
switch self {
|
||||||
|
case .invalidDeviceId:
|
||||||
|
return "Invalid device ID"
|
||||||
case .invalidCounter:
|
case .invalidCounter:
|
||||||
return "Invalid counter"
|
return "Invalid counter"
|
||||||
case .invalidTime:
|
case .invalidTime:
|
||||||
@ -92,7 +96,10 @@ enum ClientState {
|
|||||||
self = .messageRejected(.timeout)
|
self = .messageRejected(.timeout)
|
||||||
case .messageAccepted:
|
case .messageAccepted:
|
||||||
self = .openSesame
|
self = .openSesame
|
||||||
case .noBodyData, .invalidMessageData, .textReceived, .unexpectedSocketEvent:
|
case .messageDeviceInvalid:
|
||||||
|
self = .messageRejected(.invalidDeviceId)
|
||||||
|
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)
|
||||||
@ -137,7 +144,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 +195,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
|
||||||
@ -207,6 +214,8 @@ extension ClientState {
|
|||||||
return 6
|
return 6
|
||||||
case .messageRejected(let rejectionCause):
|
case .messageRejected(let rejectionCause):
|
||||||
switch rejectionCause {
|
switch rejectionCause {
|
||||||
|
case .invalidDeviceId:
|
||||||
|
return 19
|
||||||
case .invalidCounter:
|
case .invalidCounter:
|
||||||
return 7
|
return 7
|
||||||
case .invalidTime:
|
case .invalidTime:
|
||||||
@ -230,10 +239,12 @@ extension ClientState {
|
|||||||
return 15
|
return 15
|
||||||
case .missingKey:
|
case .missingKey:
|
||||||
return 16
|
return 16
|
||||||
|
case .invalidDeviceId:
|
||||||
|
return 20
|
||||||
}
|
}
|
||||||
case .openSesame:
|
case .openSesame:
|
||||||
return 17
|
return 17
|
||||||
case .internalError(_):
|
case .internalError:
|
||||||
return 18
|
return 18
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -276,8 +287,35 @@ extension ClientState {
|
|||||||
self = .openSesame
|
self = .openSesame
|
||||||
case 18:
|
case 18:
|
||||||
self = .internalError("")
|
self = .internalError("")
|
||||||
|
case 19:
|
||||||
|
self = .messageRejected(.invalidDeviceId)
|
||||||
|
case 20:
|
||||||
|
self = .responseRejected(.invalidDeviceId)
|
||||||
default:
|
default:
|
||||||
self = .internalError("Unknown code \(code)")
|
self = .internalError("Unknown code \(code)")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
@ -1,15 +1,25 @@
|
|||||||
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
|
||||||
|
|
||||||
|
@AppStorage("deviceID")
|
||||||
|
private var deviceID: Int = 0
|
||||||
|
|
||||||
@State
|
@State
|
||||||
var keyManager = KeyManagement()
|
var keyManager = KeyManagement()
|
||||||
@ -29,10 +39,15 @@ 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 +92,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 +112,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 +131,15 @@ 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,
|
||||||
|
deviceID: $deviceID,
|
||||||
|
nextMessageCounter: $nextMessageCounter,
|
||||||
|
isCompensatingDaylightTime: $isCompensatingDaylightTime,
|
||||||
|
useLocalConnection: $useLocalConnection)
|
||||||
}
|
}
|
||||||
.sheet(isPresented: $showHistorySheet) {
|
.sheet(isPresented: $showHistorySheet) {
|
||||||
HistoryView(manager: history)
|
HistoryView(manager: history)
|
||||||
@ -127,7 +150,8 @@ struct ContentView: View {
|
|||||||
|
|
||||||
func mainButtonPressed() {
|
func mainButtonPressed() {
|
||||||
guard let key = keyManager.get(.remoteKey),
|
guard let key = keyManager.get(.remoteKey),
|
||||||
let token = keyManager.get(.authToken)?.data else {
|
let token = keyManager.get(.authToken)?.data,
|
||||||
|
let deviceId = UInt8(exactly: deviceID) else {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -136,37 +160,42 @@ struct ContentView: View {
|
|||||||
// 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 content = Message.Content(
|
let content = Message.Content(
|
||||||
time: sentTime.timestamp + compensationTime,
|
time: sentTime.timestamp + compensationTime,
|
||||||
id: count)
|
id: count,
|
||||||
|
device: deviceId)
|
||||||
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 +223,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 +238,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:
|
||||||
|
@ -1,168 +1,131 @@
|
|||||||
import Foundation
|
import Foundation
|
||||||
|
|
||||||
|
|
||||||
struct HistoryItem {
|
struct HistoryItem {
|
||||||
|
|
||||||
|
|
||||||
|
/// The sent/received date (local time, not including compensation offset)
|
||||||
|
let requestDate: Date
|
||||||
|
|
||||||
let outgoingDate: Date
|
let request: Message.Content
|
||||||
|
|
||||||
let outgoingMessage: Message
|
let usedLocalConnection: Bool
|
||||||
|
|
||||||
let incomingDate: Date?
|
|
||||||
|
|
||||||
let incomingMessage: Message?
|
|
||||||
|
|
||||||
let response: ClientState?
|
let response: ClientState?
|
||||||
|
|
||||||
|
let responseMessage: Message.Content?
|
||||||
|
|
||||||
|
let responseDate: Date?
|
||||||
|
|
||||||
init(sent message: Message, date: Date) {
|
init(sent message: Message.Content, date: Date, local: Bool) {
|
||||||
self.outgoingDate = date
|
self.requestDate = date
|
||||||
self.outgoingMessage = message
|
self.request = message
|
||||||
self.incomingDate = nil
|
self.responseMessage = nil
|
||||||
self.incomingMessage = 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, device: 0)
|
||||||
|
let content2 = Message.Content(time: (Date.now + 1).timestamp, id: 124, device: 0)
|
||||||
|
return .init(sent: content, date: .now, local: false)
|
||||||
|
.didReceive(response: .openSesame, date: .now + 2, message: content2)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -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,21 @@ 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)
|
Text(roundTripText)
|
||||||
|
.font(.subheadline)
|
||||||
|
}
|
||||||
|
Image(systemSymbol: .personalhotspot)
|
||||||
Text(counterText)
|
Text(counterText)
|
||||||
.font(.subheadline)
|
.font(.subheadline)
|
||||||
.foregroundColor(.secondary)
|
if let timeOffsetText {
|
||||||
Text(timeOffsetText)
|
Image(systemSymbol: .stopwatch)
|
||||||
.font(.subheadline)
|
Text(timeOffsetText)
|
||||||
.foregroundColor(.secondary)
|
.font(.subheadline)
|
||||||
Spacer()
|
}
|
||||||
}
|
}.foregroundColor(.secondary)
|
||||||
}.padding()
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -71,19 +75,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))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
@ -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 {
|
||||||
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -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())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -17,11 +17,11 @@ extension KeyManagement {
|
|||||||
var displayName: String {
|
var displayName: String {
|
||||||
switch self {
|
switch self {
|
||||||
case .deviceKey:
|
case .deviceKey:
|
||||||
return "Device Key"
|
return "Unlock Key"
|
||||||
case .remoteKey:
|
case .remoteKey:
|
||||||
return "Remote Key"
|
return "Response Key"
|
||||||
case .authToken:
|
case .authToken:
|
||||||
return "Authentication Token"
|
return "Server Token"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -148,6 +148,15 @@ final class KeyManagement: ObservableObject {
|
|||||||
|
|
||||||
func generate(_ type: KeyType) {
|
func generate(_ type: KeyType) {
|
||||||
let key = SymmetricKey(size: type.keyLength)
|
let key = SymmetricKey(size: type.keyLength)
|
||||||
|
save(type, key: key)
|
||||||
|
}
|
||||||
|
|
||||||
|
func save(_ type: KeyType, data: Data) {
|
||||||
|
let key = SymmetricKey(data: data)
|
||||||
|
save(type, key: key)
|
||||||
|
}
|
||||||
|
|
||||||
|
private func save(_ type: KeyType, key: SymmetricKey) {
|
||||||
if keyChain.has(type) {
|
if keyChain.has(type) {
|
||||||
keyChain.delete(type)
|
keyChain.delete(type)
|
||||||
}
|
}
|
||||||
|
@ -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))
|
|
||||||
}
|
|
||||||
}
|
|
168
Sesame/SettingsView.swift
Normal file
168
Sesame/SettingsView.swift
Normal file
@ -0,0 +1,168 @@
|
|||||||
|
import SwiftUI
|
||||||
|
|
||||||
|
struct SettingsView: View {
|
||||||
|
|
||||||
|
@Binding
|
||||||
|
var keyManager: KeyManagement
|
||||||
|
|
||||||
|
@Binding
|
||||||
|
var serverAddress: String
|
||||||
|
|
||||||
|
@Binding
|
||||||
|
var localAddress: String
|
||||||
|
|
||||||
|
@Binding
|
||||||
|
var deviceID: Int
|
||||||
|
|
||||||
|
@Binding
|
||||||
|
var nextMessageCounter: Int
|
||||||
|
|
||||||
|
@Binding
|
||||||
|
var isCompensatingDaylightTime: Bool
|
||||||
|
|
||||||
|
@Binding
|
||||||
|
var useLocalConnection: Bool
|
||||||
|
|
||||||
|
@State
|
||||||
|
private var showDeviceIdInput = false
|
||||||
|
|
||||||
|
@State
|
||||||
|
private var deviceIdText = ""
|
||||||
|
|
||||||
|
@State
|
||||||
|
private var showCounterInput = false
|
||||||
|
|
||||||
|
@State
|
||||||
|
private var counterText = ""
|
||||||
|
|
||||||
|
|
||||||
|
var body: some View {
|
||||||
|
NavigationView {
|
||||||
|
ScrollView {
|
||||||
|
VStack(alignment: .leading, spacing: 8) {
|
||||||
|
VStack(alignment: .leading) {
|
||||||
|
Text("Server address")
|
||||||
|
.bold()
|
||||||
|
TextField("Server address", text: $serverAddress)
|
||||||
|
.foregroundColor(.secondary)
|
||||||
|
.padding(.leading, 8)
|
||||||
|
}.padding(.vertical, 8)
|
||||||
|
VStack(alignment: .leading) {
|
||||||
|
Text("Local address")
|
||||||
|
.bold()
|
||||||
|
TextField("Local address", text: $localAddress)
|
||||||
|
.foregroundColor(.secondary)
|
||||||
|
.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)
|
||||||
|
VStack(alignment: .leading) {
|
||||||
|
Text("Device id")
|
||||||
|
.bold()
|
||||||
|
HStack(alignment: .bottom) {
|
||||||
|
Text("\(deviceID)")
|
||||||
|
.font(.system(.body, design: .monospaced))
|
||||||
|
.foregroundColor(.secondary)
|
||||||
|
.padding([.trailing, .bottom])
|
||||||
|
Button("Edit", action: showAlertToChangeDeviceID)
|
||||||
|
.padding([.horizontal, .bottom])
|
||||||
|
.padding(.top, 4)
|
||||||
|
}
|
||||||
|
}.padding(.vertical, 8)
|
||||||
|
VStack(alignment: .leading) {
|
||||||
|
Text("Message counter")
|
||||||
|
.bold()
|
||||||
|
HStack(alignment: .bottom) {
|
||||||
|
Text("\(nextMessageCounter)")
|
||||||
|
.font(.system(.body, design: .monospaced))
|
||||||
|
.foregroundColor(.secondary)
|
||||||
|
.padding([.trailing, .bottom])
|
||||||
|
Button("Edit", action: showAlertToChangeCounter)
|
||||||
|
.padding([.horizontal, .bottom])
|
||||||
|
.padding(.top, 4)
|
||||||
|
}
|
||||||
|
}.padding(.vertical, 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()
|
||||||
|
}.onDisappear {
|
||||||
|
if !localAddress.hasSuffix("/") {
|
||||||
|
localAddress += "/"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.navigationTitle("Settings")
|
||||||
|
.alert("Update device ID", isPresented: $showDeviceIdInput, actions: {
|
||||||
|
TextField("Device ID", text: $deviceIdText)
|
||||||
|
.keyboardType(.decimalPad)
|
||||||
|
.font(.system(.body, design: .monospaced))
|
||||||
|
.foregroundColor(.black)
|
||||||
|
Button("Save", action: saveDeviceID)
|
||||||
|
Button("Cancel", role: .cancel, action: {})
|
||||||
|
}, message: {
|
||||||
|
Text("Enter the device ID")
|
||||||
|
})
|
||||||
|
.alert("Update message counter", isPresented: $showCounterInput, actions: {
|
||||||
|
TextField("Message counter", text: $counterText)
|
||||||
|
.keyboardType(.decimalPad)
|
||||||
|
.font(.system(.body, design: .monospaced))
|
||||||
|
.foregroundColor(.black)
|
||||||
|
Button("Save", action: saveCounter)
|
||||||
|
Button("Cancel", role: .cancel, action: {})
|
||||||
|
}, message: {
|
||||||
|
Text("Enter the message counter")
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private func showAlertToChangeDeviceID() {
|
||||||
|
deviceIdText = "\(deviceID)"
|
||||||
|
showDeviceIdInput = true
|
||||||
|
}
|
||||||
|
|
||||||
|
private func saveDeviceID() {
|
||||||
|
guard let id = UInt8(deviceIdText) else {
|
||||||
|
print("Invalid device id '\(deviceIdText)'")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
self.deviceID = Int(id)
|
||||||
|
}
|
||||||
|
|
||||||
|
private func showAlertToChangeCounter() {
|
||||||
|
counterText = "\(nextMessageCounter)"
|
||||||
|
showCounterInput = true
|
||||||
|
}
|
||||||
|
|
||||||
|
private func saveCounter() {
|
||||||
|
guard let id = UInt32(counterText) else {
|
||||||
|
print("Invalid message counter '\(counterText)'")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
self.nextMessageCounter = Int(id)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
struct SettingsView_Previews: PreviewProvider {
|
||||||
|
static var previews: some View {
|
||||||
|
SettingsView(
|
||||||
|
keyManager: .constant(KeyManagement()),
|
||||||
|
serverAddress: .constant("https://example.com"),
|
||||||
|
localAddress: .constant("192.168.178.42"),
|
||||||
|
deviceID: .constant(0),
|
||||||
|
nextMessageCounter: .constant(12345678),
|
||||||
|
isCompensatingDaylightTime: .constant(true),
|
||||||
|
useLocalConnection: .constant(false))
|
||||||
|
}
|
||||||
|
}
|
@ -8,6 +8,12 @@ struct SingleKeyView: View {
|
|||||||
|
|
||||||
@Binding
|
@Binding
|
||||||
var keyManager: KeyManagement
|
var keyManager: KeyManagement
|
||||||
|
|
||||||
|
@State
|
||||||
|
private var showEditWindow = false
|
||||||
|
|
||||||
|
@State
|
||||||
|
private var keyText = ""
|
||||||
|
|
||||||
let type: KeyManagement.KeyType
|
let type: KeyManagement.KeyType
|
||||||
|
|
||||||
@ -54,9 +60,41 @@ struct SingleKeyView: View {
|
|||||||
.disabled(!hasKey)
|
.disabled(!hasKey)
|
||||||
.padding([.horizontal, .bottom])
|
.padding([.horizontal, .bottom])
|
||||||
.padding(.top, 4)
|
.padding(.top, 4)
|
||||||
|
Button("Edit") {
|
||||||
|
keyText = keyManager.get(type)?.displayString ?? ""
|
||||||
|
print("Set key text to '\(keyText)'")
|
||||||
|
showEditWindow = true
|
||||||
|
}
|
||||||
|
.padding([.horizontal, .bottom])
|
||||||
|
.padding(.top, 4)
|
||||||
Spacer()
|
Spacer()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
.alert("Update key", isPresented: $showEditWindow, actions: {
|
||||||
|
TextField("Key data", text: $keyText)
|
||||||
|
.lineLimit(4)
|
||||||
|
.font(.system(.body, design: .monospaced))
|
||||||
|
.foregroundColor(.black)
|
||||||
|
Button("Save", action: saveKey)
|
||||||
|
Button("Cancel", role: .cancel, action: {})
|
||||||
|
}, message: {
|
||||||
|
Text("Enter the hex encoded key")
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
private func saveKey() {
|
||||||
|
let cleanText = keyText.replacingOccurrences(of: " ", with: "")
|
||||||
|
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)")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
keyManager.save(type, data: keyData)
|
||||||
|
print("Key \(type) saved")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -65,5 +103,6 @@ struct SingleKeyView_Previews: PreviewProvider {
|
|||||||
SingleKeyView(
|
SingleKeyView(
|
||||||
keyManager: .constant(KeyManagement()),
|
keyManager: .constant(KeyManagement()),
|
||||||
type: .deviceKey)
|
type: .deviceKey)
|
||||||
|
.previewLayout(.fixed(width: 350, height: 100))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -46,3 +46,26 @@ extension String {
|
|||||||
return results.map { String($0) }
|
return results.map { String($0) }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
let protocolSalt = "CryptoKit Playgrounds Putting It Together".data(using: .utf8)!
|
||||||
|
|
||||||
|
/// Generates an ephemeral key agreement key and performs key agreement to get the shared secret and derive the symmetric encryption key.
|
||||||
|
func encrypt(_ data: Data, to theirEncryptionKey: Curve25519.KeyAgreement.PublicKey, signedBy ourSigningKey: Curve25519.Signing.PrivateKey) throws ->
|
||||||
|
(ephemeralPublicKeyData: Data, ciphertext: Data, signature: Data) {
|
||||||
|
let ephemeralKey = Curve25519.KeyAgreement.PrivateKey()
|
||||||
|
let ephemeralPublicKey = ephemeralKey.publicKey.rawRepresentation
|
||||||
|
|
||||||
|
let sharedSecret = try ephemeralKey.sharedSecretFromKeyAgreement(with: theirEncryptionKey)
|
||||||
|
|
||||||
|
let symmetricKey = sharedSecret.hkdfDerivedSymmetricKey(using: SHA256.self,
|
||||||
|
salt: protocolSalt,
|
||||||
|
sharedInfo: ephemeralPublicKey +
|
||||||
|
theirEncryptionKey.rawRepresentation +
|
||||||
|
ourSigningKey.publicKey.rawRepresentation,
|
||||||
|
outputByteCount: 32)
|
||||||
|
|
||||||
|
let ciphertext = try ChaChaPoly.seal(data, using: symmetricKey).combined
|
||||||
|
let signature = try ourSigningKey.signature(for: ciphertext + ephemeralPublicKey + theirEncryptionKey.rawRepresentation)
|
||||||
|
|
||||||
|
return (ephemeralPublicKey, ciphertext, signature)
|
||||||
|
}
|
||||||
|
Reference in New Issue
Block a user