Challenge-response, SwiftData, new UI
This commit is contained in:
parent
7a443d51b3
commit
941aebd9ca
@ -1,4 +1,5 @@
|
||||
import SwiftUI
|
||||
import SwiftData
|
||||
import SFSafeSymbols
|
||||
import CryptoKit
|
||||
|
||||
@ -7,73 +8,20 @@ struct ContentView: View {
|
||||
@Binding
|
||||
var didLaunchFromComplication: Bool
|
||||
|
||||
@AppStorage("connectionType")
|
||||
var connectionType: ConnectionStrategy = .remoteFirst
|
||||
@ObservedObject
|
||||
var coordinator: RequestCoordinator
|
||||
|
||||
@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("deviceId")
|
||||
private var deviceId: Int = 0
|
||||
|
||||
@EnvironmentObject
|
||||
var keyManager: KeyManagement
|
||||
|
||||
@EnvironmentObject
|
||||
var history: HistoryManager
|
||||
|
||||
@State
|
||||
var state: ClientState = .noKeyAvailable
|
||||
|
||||
@State
|
||||
var stateResetTimer: Timer?
|
||||
|
||||
let server = Client()
|
||||
|
||||
private var firstTryIsLocalConnection: Bool {
|
||||
switch connectionType {
|
||||
case .local, .localFirst:
|
||||
return true
|
||||
case .remote, .remoteFirst:
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
private var hasSecondTry: Bool {
|
||||
switch connectionType {
|
||||
case .localFirst, .remoteFirst:
|
||||
return true
|
||||
default:
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
private var secondTryIsLocalConnection: Bool {
|
||||
switch connectionType {
|
||||
case .local, .localFirst:
|
||||
return false
|
||||
case .remote, .remoteFirst:
|
||||
return true
|
||||
}
|
||||
init(coordinator: RequestCoordinator, didLaunchFromComplication: Binding<Bool>) {
|
||||
self._didLaunchFromComplication = didLaunchFromComplication
|
||||
self.coordinator = coordinator
|
||||
}
|
||||
|
||||
var buttonBackground: Color {
|
||||
state.allowsAction ?
|
||||
.white.opacity(0.2) :
|
||||
.black.opacity(0.2)
|
||||
.white.opacity(0.2)
|
||||
}
|
||||
|
||||
var buttonColor: Color {
|
||||
state.allowsAction ? .white : .gray
|
||||
.white
|
||||
}
|
||||
|
||||
var body: some View {
|
||||
@ -85,145 +33,43 @@ struct ContentView: View {
|
||||
.aspectRatio(contentMode: .fit)
|
||||
.fontWeight(.ultraLight)
|
||||
.padding()
|
||||
.onTapGesture(perform: mainButtonPressed)
|
||||
.disabled(!state.allowsAction)
|
||||
if state == .waitingForResponse {
|
||||
.onTapGesture(perform: coordinator.startUnlock)
|
||||
if coordinator.isPerformingRequest {
|
||||
ProgressView()
|
||||
.progressViewStyle(CircularProgressViewStyle())
|
||||
.frame(width: 20, height: 20)
|
||||
} else {
|
||||
Text(state.actionText)
|
||||
Text("Unlock")
|
||||
.font(.subheadline)
|
||||
}
|
||||
}
|
||||
Spacer()
|
||||
}
|
||||
.background(state.color)
|
||||
.animation(.easeInOut, value: state.color)
|
||||
.onAppear {
|
||||
if state == .noKeyAvailable, keyManager.hasAllKeys {
|
||||
state = .ready
|
||||
}
|
||||
}
|
||||
.onChange(of: didLaunchFromComplication) { launched in
|
||||
.background(coordinator.state.color)
|
||||
.animation(.easeInOut, value: coordinator.state.color)
|
||||
.onChange(of: didLaunchFromComplication) { _, launched in
|
||||
guard launched else {
|
||||
return
|
||||
}
|
||||
didLaunchFromComplication = false
|
||||
mainButtonPressed()
|
||||
}
|
||||
}
|
||||
|
||||
func mainButtonPressed() {
|
||||
guard let keys = keyManager.getAllKeys(),
|
||||
let deviceId = UInt8(exactly: deviceId) else {
|
||||
return
|
||||
}
|
||||
sendMessage(from: deviceId, using: keys, isFirstTry: true)
|
||||
}
|
||||
|
||||
private func sendMessage(from deviceId: UInt8, using keys: KeySet, isFirstTry: Bool) {
|
||||
preventStateReset()
|
||||
state = .waitingForResponse
|
||||
let localConnection = isFirstTry ? firstTryIsLocalConnection : secondTryIsLocalConnection
|
||||
Task {
|
||||
let response = await send(
|
||||
count: UInt32(nextMessageCounter),
|
||||
from: deviceId,
|
||||
using: keys,
|
||||
to: server,
|
||||
over: localConnection,
|
||||
while: isCompensatingDaylightTime,
|
||||
localAddress: localAddress,
|
||||
remoteAddress: serverPath)
|
||||
|
||||
DispatchQueue.main.async {
|
||||
state = response.response
|
||||
scheduleStateReset()
|
||||
if let counter = response.responseMessage?.id {
|
||||
nextMessageCounter = Int(counter)
|
||||
}
|
||||
}
|
||||
save(historyItem: response)
|
||||
guard isFirstTry, hasSecondTry else {
|
||||
return
|
||||
}
|
||||
DispatchQueue.main.async {
|
||||
sendMessage(from: deviceId, using: keys, isFirstTry: false)
|
||||
coordinator.startUnlock()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private func preventStateReset() {
|
||||
stateResetTimer?.invalidate()
|
||||
stateResetTimer = nil
|
||||
}
|
||||
|
||||
private func scheduleStateReset() {
|
||||
stateResetTimer?.invalidate()
|
||||
stateResetTimer = Timer.scheduledTimer(withTimeInterval: 8.0, repeats: false) { _ in
|
||||
DispatchQueue.main.async {
|
||||
resetState()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private func resetState() {
|
||||
state = keyManager.hasAllKeys ? .ready : .noKeyAvailable
|
||||
preventStateReset()
|
||||
}
|
||||
|
||||
private func save(historyItem: HistoryItem) {
|
||||
#Preview {
|
||||
do {
|
||||
try history.save(item: historyItem)
|
||||
let config = ModelConfiguration(isStoredInMemoryOnly: true)
|
||||
let container = try ModelContainer(for: HistoryItem.self, configurations: config)
|
||||
|
||||
let item = HistoryItem.mock
|
||||
container.mainContext.insert(item)
|
||||
try container.mainContext.save()
|
||||
let coordinator = RequestCoordinator(modelContext: container.mainContext)
|
||||
return ContentView(coordinator: coordinator, didLaunchFromComplication: .constant(false))
|
||||
.modelContainer(container)
|
||||
} catch {
|
||||
print("Failed to save item: \(error)")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private func send(count: UInt32, from deviceId: UInt8, using keys: KeySet, to server: Client, over localConnection: Bool, while compensatingTime: Bool, localAddress: String, remoteAddress: String) async -> HistoryItem {
|
||||
let sentTime = Date()
|
||||
// Add time to compensate that the device is using daylight savings time
|
||||
let timeCompensation: UInt32 = compensatingTime ? 3600 : 0
|
||||
let content = Message.Content(
|
||||
time: sentTime.timestamp + timeCompensation,
|
||||
id: count,
|
||||
device: deviceId)
|
||||
let message = content.authenticate(using: keys.remote)
|
||||
print("Sending message \(count)")
|
||||
let address = localConnection ? localAddress : remoteAddress
|
||||
let (newState, responseMessage) = await send(message, to: server, using: keys.server, local: localConnection, address: address)
|
||||
var historyItem = HistoryItem(
|
||||
sent: message.content,
|
||||
sentDate: sentTime,
|
||||
local: localConnection,
|
||||
response: newState,
|
||||
responseDate: .now,
|
||||
responseMessage: responseMessage?.content)
|
||||
|
||||
guard let responseMessage else {
|
||||
return historyItem
|
||||
}
|
||||
guard responseMessage.isValid(using: keys.device) else {
|
||||
historyItem.response = .responseRejected(.invalidAuthentication)
|
||||
return historyItem
|
||||
}
|
||||
return historyItem
|
||||
}
|
||||
|
||||
private func send(_ message: Message, to server: Client, using authToken: Data, local: Bool, address: String) async -> (state: ClientState, response: Message?) {
|
||||
if local {
|
||||
return await server.sendMessageOverLocalNetwork(message, server: address)
|
||||
} else {
|
||||
return await server.send(message, server: address, authToken: authToken)
|
||||
fatalError("Failed to create model container.")
|
||||
}
|
||||
}
|
||||
|
||||
struct ContentView_Previews: PreviewProvider {
|
||||
static var previews: some View {
|
||||
ContentView(didLaunchFromComplication: .constant(false))
|
||||
.environmentObject(KeyManagement())
|
||||
.environmentObject(HistoryManager())
|
||||
}
|
||||
}
|
||||
|
@ -1,4 +1,5 @@
|
||||
import SwiftUI
|
||||
import SwiftData
|
||||
import SFSafeSymbols
|
||||
|
||||
private let df: DateFormatter = {
|
||||
@ -11,26 +12,15 @@ private let df: DateFormatter = {
|
||||
|
||||
struct HistoryItemDetail: View {
|
||||
|
||||
let item: HistoryItem
|
||||
@Environment(\.modelContext)
|
||||
private var modelContext
|
||||
|
||||
let history: HistoryManagerProtocol
|
||||
let item: HistoryItem
|
||||
|
||||
@Environment(\.dismiss) private var dismiss
|
||||
|
||||
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)"
|
||||
df.string(from: item.startDate)
|
||||
}
|
||||
|
||||
var body: some View {
|
||||
@ -43,21 +33,16 @@ struct HistoryItemDetail: View {
|
||||
value: entryTime)
|
||||
SettingsListTextItem(
|
||||
title: "Connection",
|
||||
value: item.usedLocalConnection ? "Local" : "Remote")
|
||||
SettingsListTextItem(
|
||||
title: "Device ID",
|
||||
value: "\(item.request.deviceId!)")
|
||||
SettingsListTextItem(
|
||||
title: "Message Counter",
|
||||
value: counterText)
|
||||
value: item.route.displayName)
|
||||
SettingsListTextItem(
|
||||
title: "Round Trip Time",
|
||||
value: "\(Int(item.roundTripTime * 1000)) ms")
|
||||
if let offset = item.clockOffset {
|
||||
SettingsListTextItem(
|
||||
title: "Clock offset",
|
||||
value: "\(offset) seconds")
|
||||
}
|
||||
title: "Client challenge",
|
||||
value: "\(item.message.clientChallenge)")
|
||||
SettingsListTextItem(
|
||||
title: "Server challenge",
|
||||
value: "\(item.message.serverChallenge)")
|
||||
Button {
|
||||
delete(item: item)
|
||||
} label: {
|
||||
@ -76,15 +61,22 @@ struct HistoryItemDetail: View {
|
||||
}
|
||||
|
||||
private func delete(item: HistoryItem) {
|
||||
guard history.delete(item: item) else {
|
||||
return
|
||||
}
|
||||
modelContext.delete(item)
|
||||
dismiss()
|
||||
}
|
||||
}
|
||||
|
||||
struct HistoryItemDetail_Previews: PreviewProvider {
|
||||
static var previews: some View {
|
||||
HistoryItemDetail(item: .mock, history: HistoryManagerMock())
|
||||
#Preview {
|
||||
do {
|
||||
let config = ModelConfiguration(isStoredInMemoryOnly: true)
|
||||
let container = try ModelContainer(for: HistoryItem.self, configurations: config)
|
||||
|
||||
let item = HistoryItem.mock
|
||||
container.mainContext.insert(item)
|
||||
try container.mainContext.save()
|
||||
return HistoryItemDetail(item: .mock)
|
||||
.modelContainer(container)
|
||||
} catch {
|
||||
fatalError("Failed to create model container.")
|
||||
}
|
||||
}
|
||||
|
@ -14,7 +14,7 @@ struct HistoryListRow: View {
|
||||
let item: HistoryItem
|
||||
|
||||
private var entryTime: String {
|
||||
df.string(from: item.requestDate)
|
||||
df.string(from: item.startDate)
|
||||
}
|
||||
|
||||
var body: some View {
|
||||
|
@ -1,21 +1,23 @@
|
||||
import SwiftUI
|
||||
import SwiftData
|
||||
|
||||
struct HistoryView: View {
|
||||
|
||||
@ObservedObject
|
||||
var history: HistoryManager
|
||||
@Environment(\.modelContext)
|
||||
private var modelContext
|
||||
|
||||
@Query(sort: \HistoryItem.startDate, order: .reverse)
|
||||
var history: [HistoryItem] = []
|
||||
|
||||
private var unlockCount: Int {
|
||||
history.entries.count {
|
||||
$0.response == .openSesame
|
||||
}
|
||||
history.count { $0.response == .unlocked }
|
||||
}
|
||||
|
||||
private var percentage: Double {
|
||||
guard history.entries.count > 0 else {
|
||||
guard history.count > 0 else {
|
||||
return 0
|
||||
}
|
||||
return Double(unlockCount * 100) / Double(history.entries.count)
|
||||
return Double(unlockCount * 100) / Double(history.count)
|
||||
}
|
||||
|
||||
var body: some View {
|
||||
@ -23,7 +25,7 @@ struct HistoryView: View {
|
||||
List {
|
||||
HStack {
|
||||
VStack(alignment: .leading) {
|
||||
Text("\(history.entries.count) requests")
|
||||
Text("\(history.count) requests")
|
||||
.foregroundColor(.primary)
|
||||
.font(.body)
|
||||
Text(String(format: "%.1f %% success", percentage))
|
||||
@ -34,9 +36,9 @@ struct HistoryView: View {
|
||||
}
|
||||
.listRowBackground(Color.clear)
|
||||
|
||||
ForEach(history.entries) { item in
|
||||
ForEach(history) { item in
|
||||
NavigationLink {
|
||||
HistoryItemDetail(item: item, history: history)
|
||||
HistoryItemDetail(item: item)
|
||||
} label: {
|
||||
HistoryListRow(item: item)
|
||||
}
|
||||
@ -54,14 +56,21 @@ struct HistoryView: View {
|
||||
}
|
||||
|
||||
private func delete(item: HistoryItem) {
|
||||
guard history.delete(item: item) else {
|
||||
return
|
||||
}
|
||||
modelContext.delete(item)
|
||||
}
|
||||
}
|
||||
|
||||
struct HistoryView_Previews: PreviewProvider {
|
||||
static var previews: some View {
|
||||
HistoryView(history: HistoryManager())
|
||||
#Preview {
|
||||
do {
|
||||
let config = ModelConfiguration(isStoredInMemoryOnly: true)
|
||||
let container = try ModelContainer(for: HistoryItem.self, configurations: config)
|
||||
|
||||
let item = HistoryItem.mock
|
||||
container.mainContext.insert(item)
|
||||
try container.mainContext.save()
|
||||
return HistoryView()
|
||||
.modelContainer(container)
|
||||
} catch {
|
||||
fatalError("Failed to create model container.")
|
||||
}
|
||||
}
|
||||
|
@ -1,11 +1,16 @@
|
||||
import SwiftUI
|
||||
import SwiftData
|
||||
|
||||
@main
|
||||
struct Sesame_Watch_Watch_AppApp: App {
|
||||
|
||||
let keyManagement = KeyManagement()
|
||||
@State
|
||||
var modelContainer: ModelContainer
|
||||
|
||||
let history = HistoryManager()
|
||||
@ObservedObject
|
||||
var coordinator: RequestCoordinator
|
||||
|
||||
let keyManagement = KeyManagement()
|
||||
|
||||
@State
|
||||
var selected: Int = 0
|
||||
@ -13,17 +18,25 @@ struct Sesame_Watch_Watch_AppApp: App {
|
||||
@State
|
||||
var didLaunchFromComplication = false
|
||||
|
||||
init() {
|
||||
do {
|
||||
let modelContainer = try ModelContainer(for: HistoryItem.self)
|
||||
self.modelContainer = modelContainer
|
||||
self.coordinator = .init(modelContext: modelContainer.mainContext)
|
||||
} catch {
|
||||
fatalError("Failed to create model container: \(error)")
|
||||
}
|
||||
}
|
||||
|
||||
var body: some Scene {
|
||||
WindowGroup {
|
||||
TabView(selection: $selected) {
|
||||
ContentView(didLaunchFromComplication: $didLaunchFromComplication)
|
||||
.environmentObject(keyManagement)
|
||||
.environmentObject(history)
|
||||
ContentView(coordinator: coordinator, didLaunchFromComplication: $didLaunchFromComplication)
|
||||
.tag(1)
|
||||
SettingsView()
|
||||
.environmentObject(keyManagement)
|
||||
.tag(2)
|
||||
HistoryView(history: history)
|
||||
HistoryView()
|
||||
.tag(3)
|
||||
}
|
||||
.tabViewStyle(PageTabViewStyle())
|
||||
@ -32,5 +45,6 @@ struct Sesame_Watch_Watch_AppApp: App {
|
||||
didLaunchFromComplication = true
|
||||
}
|
||||
}
|
||||
.modelContainer(modelContainer)
|
||||
}
|
||||
}
|
||||
|
@ -1,28 +0,0 @@
|
||||
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")
|
||||
}
|
||||
}
|
@ -11,29 +11,22 @@ struct SettingsView: View {
|
||||
@AppStorage("localIP")
|
||||
var localAddress: String = "192.168.178.104/"
|
||||
|
||||
@AppStorage("counter")
|
||||
var nextMessageCounter: Int = 0
|
||||
|
||||
@AppStorage("compensate")
|
||||
var isCompensatingDaylightTime: Bool = false
|
||||
|
||||
@AppStorage("deviceId")
|
||||
private var deviceId: Int = 0
|
||||
|
||||
@EnvironmentObject
|
||||
var keys: KeyManagement
|
||||
|
||||
var some: String { "some" }
|
||||
|
||||
var body: some View {
|
||||
NavigationStack {
|
||||
List {
|
||||
Picker("Connection", selection: $connectionType) {
|
||||
Text(ConnectionStrategy.local.rawValue)
|
||||
Text(display: ConnectionStrategy.local)
|
||||
.tag(ConnectionStrategy.local)
|
||||
Text(ConnectionStrategy.localFirst.rawValue)
|
||||
Text(display: ConnectionStrategy.localFirst)
|
||||
.tag(ConnectionStrategy.localFirst)
|
||||
Text(ConnectionStrategy.remote.rawValue)
|
||||
Text(display: ConnectionStrategy.remote)
|
||||
.tag(ConnectionStrategy.remote)
|
||||
Text(ConnectionStrategy.remoteFirst.rawValue)
|
||||
Text(display: ConnectionStrategy.remoteFirst)
|
||||
.tag(ConnectionStrategy.remoteFirst)
|
||||
}
|
||||
.padding(.leading)
|
||||
@ -45,18 +38,6 @@ struct SettingsView: View {
|
||||
title: "Local url",
|
||||
value: $localAddress,
|
||||
footnote: "The url where the device can be reached directly on the local WiFi network.")
|
||||
SettingsNumberItemLink(
|
||||
title: "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.")
|
||||
|
@ -11,32 +11,61 @@
|
||||
884A45B9279F48C100D6E650 /* ContentView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 884A45B8279F48C100D6E650 /* ContentView.swift */; };
|
||||
884A45BB279F48C300D6E650 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 884A45BA279F48C300D6E650 /* Assets.xcassets */; };
|
||||
884A45C5279F4BBE00D6E650 /* KeyManagement.swift in Sources */ = {isa = PBXBuildFile; fileRef = 884A45C4279F4BBE00D6E650 /* KeyManagement.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 */; };
|
||||
884A45CF27A5402D00D6E650 /* MessageResult.swift in Sources */ = {isa = PBXBuildFile; fileRef = 884A45CE27A5402D00D6E650 /* MessageResult.swift */; };
|
||||
8860D7432B22858600849FAC /* Date+Timestamp.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8860D7422B22858600849FAC /* Date+Timestamp.swift */; };
|
||||
8860D7462B2328EC00849FAC /* Message+Size.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8860D7452B2328EC00849FAC /* Message+Size.swift */; };
|
||||
8860D7482B23294600849FAC /* SignedMessage+Crypto.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8860D7472B23294600849FAC /* SignedMessage+Crypto.swift */; };
|
||||
8860D74A2B2329CE00849FAC /* SignedMessage+Size.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8860D7492B2329CE00849FAC /* SignedMessage+Size.swift */; };
|
||||
8860D74C2B232A7700849FAC /* SesameHeader.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8860D74B2B232A7700849FAC /* SesameHeader.swift */; };
|
||||
8860D74E2B232AED00849FAC /* Data+Coding.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8860D74D2B232AED00849FAC /* Data+Coding.swift */; };
|
||||
8860D7522B233BEA00849FAC /* TransmissionType.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8860D7512B233BEA00849FAC /* TransmissionType.swift */; };
|
||||
8860D7542B23489300849FAC /* ActiveRequestType.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8860D7532B23489300849FAC /* ActiveRequestType.swift */; };
|
||||
8860D7552B237F9100849FAC /* TransmissionType.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8860D7512B233BEA00849FAC /* TransmissionType.swift */; };
|
||||
8860D7562B237F9400849FAC /* ActiveRequestType.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8860D7532B23489300849FAC /* ActiveRequestType.swift */; };
|
||||
8860D7572B237FAD00849FAC /* MessageType.swift in Sources */ = {isa = PBXBuildFile; fileRef = 88AEE37E2B2217050034EDA9 /* MessageType.swift */; };
|
||||
8860D7582B237FB000849FAC /* Message.swift in Sources */ = {isa = PBXBuildFile; fileRef = E24EE77827FF95E00011CFD2 /* Message.swift */; };
|
||||
8860D7592B237FB200849FAC /* Message+Crypto.swift in Sources */ = {isa = PBXBuildFile; fileRef = 88AEE3852B22376D0034EDA9 /* Message+Crypto.swift */; };
|
||||
8860D75A2B237FB400849FAC /* SignedMessage.swift in Sources */ = {isa = PBXBuildFile; fileRef = 88AEE3832B2236DC0034EDA9 /* SignedMessage.swift */; };
|
||||
8860D75B2B237FB600849FAC /* SignedMessage+Crypto.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8860D7472B23294600849FAC /* SignedMessage+Crypto.swift */; };
|
||||
8860D75C2B237FB900849FAC /* MessageResult+UI.swift in Sources */ = {isa = PBXBuildFile; fileRef = 88AEE3872B226FED0034EDA9 /* MessageResult+UI.swift */; };
|
||||
8860D75D2B237FC000849FAC /* Data+Coding.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8860D74D2B232AED00849FAC /* Data+Coding.swift */; };
|
||||
8860D75E2B237FC600849FAC /* Message+Size.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8860D7452B2328EC00849FAC /* Message+Size.swift */; };
|
||||
8860D75F2B237FC900849FAC /* SignedMessage+Size.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8860D7492B2329CE00849FAC /* SignedMessage+Size.swift */; };
|
||||
8860D7602B237FCC00849FAC /* SesameHeader.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8860D74B2B232A7700849FAC /* SesameHeader.swift */; };
|
||||
8860D7622B23803E00849FAC /* ServerChallenge.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8860D7612B23803E00849FAC /* ServerChallenge.swift */; };
|
||||
8860D7632B23803E00849FAC /* ServerChallenge.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8860D7612B23803E00849FAC /* ServerChallenge.swift */; };
|
||||
8860D7652B23B5B200849FAC /* RequestCoordinator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8860D7642B23B5B200849FAC /* RequestCoordinator.swift */; };
|
||||
8860D7662B23B5B200849FAC /* RequestCoordinator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8860D7642B23B5B200849FAC /* RequestCoordinator.swift */; };
|
||||
8860D7682B23D04100849FAC /* PendingOperation.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8860D7672B23D04100849FAC /* PendingOperation.swift */; };
|
||||
8860D7692B23D04100849FAC /* PendingOperation.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8860D7672B23D04100849FAC /* PendingOperation.swift */; };
|
||||
8860D76C2B246F5E00849FAC /* UInt32+Random.swift in Sources */ = {isa = PBXBuildFile; fileRef = 88AEE3802B22327F0034EDA9 /* UInt32+Random.swift */; };
|
||||
8860D76E2B246FC400849FAC /* Text+Extensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8860D76D2B246FC400849FAC /* Text+Extensions.swift */; };
|
||||
8860D76F2B246FC400849FAC /* Text+Extensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8860D76D2B246FC400849FAC /* Text+Extensions.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 */; };
|
||||
88AEE37F2B2217050034EDA9 /* MessageType.swift in Sources */ = {isa = PBXBuildFile; fileRef = 88AEE37E2B2217050034EDA9 /* MessageType.swift */; };
|
||||
88AEE3812B22327F0034EDA9 /* UInt32+Random.swift in Sources */ = {isa = PBXBuildFile; fileRef = 88AEE3802B22327F0034EDA9 /* UInt32+Random.swift */; };
|
||||
88AEE3842B2236DC0034EDA9 /* SignedMessage.swift in Sources */ = {isa = PBXBuildFile; fileRef = 88AEE3832B2236DC0034EDA9 /* SignedMessage.swift */; };
|
||||
88AEE3862B22376D0034EDA9 /* Message+Crypto.swift in Sources */ = {isa = PBXBuildFile; fileRef = 88AEE3852B22376D0034EDA9 /* Message+Crypto.swift */; };
|
||||
88AEE3882B226FED0034EDA9 /* MessageResult+UI.swift in Sources */ = {isa = PBXBuildFile; fileRef = 88AEE3872B226FED0034EDA9 /* MessageResult+UI.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 */; };
|
||||
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 */; };
|
||||
88E197D129EDCE5F00BF1D19 /* Data+Hex.swift in Sources */ = {isa = PBXBuildFile; fileRef = E24EE77127FDCCC00011CFD2 /* Data+Hex.swift */; };
|
||||
88E197D229EDCE6600BF1D19 /* SesameRoute.swift in Sources */ = {isa = PBXBuildFile; fileRef = E2C5C1DA2806FE8900769EF6 /* SesameRoute.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 */; };
|
||||
88E197D429EDCE7600BF1D19 /* UInt32+Coding.swift in Sources */ = {isa = PBXBuildFile; fileRef = E2C5C1DC281B3AC400769EF6 /* UInt32+Coding.swift */; };
|
||||
88E197D729EDCFE800BF1D19 /* Date+Extensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 88E197D629EDCFE800BF1D19 /* Date+Extensions.swift */; };
|
||||
88E197D829EDD13B00BF1D19 /* SymmetricKey+Extensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 884A45CA27A464C000D6E650 /* SymmetricKey+Extensions.swift */; };
|
||||
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 */; };
|
||||
@ -47,8 +76,7 @@
|
||||
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 */; };
|
||||
E24EE77427FF95920011CFD2 /* DeviceResponse.swift in Sources */ = {isa = PBXBuildFile; fileRef = E24EE77327FF95920011CFD2 /* DeviceResponse.swift */; };
|
||||
E24EE77227FDCCC00011CFD2 /* Data+Hex.swift in Sources */ = {isa = PBXBuildFile; fileRef = E24EE77127FDCCC00011CFD2 /* Data+Hex.swift */; };
|
||||
E24EE77727FF95C00011CFD2 /* NIOCore in Frameworks */ = {isa = PBXBuildFile; productRef = E24EE77627FF95C00011CFD2 /* NIOCore */; };
|
||||
E24EE77927FF95E00011CFD2 /* Message.swift in Sources */ = {isa = PBXBuildFile; fileRef = E24EE77827FF95E00011CFD2 /* Message.swift */; };
|
||||
E24F6C6E2A89749A0040F8C4 /* ConnectionStrategy.swift in Sources */ = {isa = PBXBuildFile; fileRef = E24F6C6D2A89749A0040F8C4 /* ConnectionStrategy.swift */; };
|
||||
@ -66,9 +94,8 @@
|
||||
E28DED33281EB15B00259690 /* HistoryListItem.swift in Sources */ = {isa = PBXBuildFile; fileRef = E28DED32281EB15B00259690 /* HistoryListItem.swift */; };
|
||||
E28DED35281EB17600259690 /* HistoryItem.swift in Sources */ = {isa = PBXBuildFile; fileRef = E28DED34281EB17600259690 /* HistoryItem.swift */; };
|
||||
E28DED37281EC7FB00259690 /* HistoryManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = E28DED36281EC7FB00259690 /* HistoryManager.swift */; };
|
||||
E2C5C1DB2806FE8900769EF6 /* RouteAPI.swift in Sources */ = {isa = PBXBuildFile; fileRef = E2C5C1DA2806FE8900769EF6 /* RouteAPI.swift */; };
|
||||
E2C5C1DD281B3AC400769EF6 /* UInt32+Extensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = E2C5C1DC281B3AC400769EF6 /* UInt32+Extensions.swift */; };
|
||||
E2C5C1F8281E769F00769EF6 /* ServerMessage.swift in Sources */ = {isa = PBXBuildFile; fileRef = E2C5C1F7281E769F00769EF6 /* ServerMessage.swift */; };
|
||||
E2C5C1DB2806FE8900769EF6 /* SesameRoute.swift in Sources */ = {isa = PBXBuildFile; fileRef = E2C5C1DA2806FE8900769EF6 /* SesameRoute.swift */; };
|
||||
E2C5C1DD281B3AC400769EF6 /* UInt32+Coding.swift in Sources */ = {isa = PBXBuildFile; fileRef = E2C5C1DC281B3AC400769EF6 /* UInt32+Coding.swift */; };
|
||||
E2F5DCCA2A88E913002858B9 /* Array+Extensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = E2F5DCC92A88E913002858B9 /* Array+Extensions.swift */; };
|
||||
E2F5DCCB2A88E976002858B9 /* Array+Extensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = E2F5DCC92A88E913002858B9 /* Array+Extensions.swift */; };
|
||||
/* End PBXBuildFile section */
|
||||
@ -103,19 +130,34 @@
|
||||
884A45B8279F48C100D6E650 /* ContentView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ContentView.swift; sourceTree = "<group>"; };
|
||||
884A45BA279F48C300D6E650 /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = "<group>"; };
|
||||
884A45C4279F4BBE00D6E650 /* KeyManagement.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = KeyManagement.swift; sourceTree = "<group>"; };
|
||||
884A45C827A43D7900D6E650 /* ClientState.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ClientState.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>"; };
|
||||
884A45CE27A5402D00D6E650 /* MessageResult.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MessageResult.swift; sourceTree = "<group>"; };
|
||||
8860D7422B22858600849FAC /* Date+Timestamp.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Date+Timestamp.swift"; sourceTree = "<group>"; };
|
||||
8860D7452B2328EC00849FAC /* Message+Size.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Message+Size.swift"; sourceTree = "<group>"; };
|
||||
8860D7472B23294600849FAC /* SignedMessage+Crypto.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "SignedMessage+Crypto.swift"; sourceTree = "<group>"; };
|
||||
8860D7492B2329CE00849FAC /* SignedMessage+Size.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "SignedMessage+Size.swift"; sourceTree = "<group>"; };
|
||||
8860D74B2B232A7700849FAC /* SesameHeader.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SesameHeader.swift; sourceTree = "<group>"; };
|
||||
8860D74D2B232AED00849FAC /* Data+Coding.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Data+Coding.swift"; sourceTree = "<group>"; };
|
||||
8860D7512B233BEA00849FAC /* TransmissionType.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TransmissionType.swift; sourceTree = "<group>"; };
|
||||
8860D7532B23489300849FAC /* ActiveRequestType.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ActiveRequestType.swift; sourceTree = "<group>"; };
|
||||
8860D7612B23803E00849FAC /* ServerChallenge.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ServerChallenge.swift; sourceTree = "<group>"; };
|
||||
8860D7642B23B5B200849FAC /* RequestCoordinator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RequestCoordinator.swift; sourceTree = "<group>"; };
|
||||
8860D7672B23D04100849FAC /* PendingOperation.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PendingOperation.swift; sourceTree = "<group>"; };
|
||||
8860D76D2B246FC400849FAC /* Text+Extensions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Text+Extensions.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>"; };
|
||||
88AEE37E2B2217050034EDA9 /* MessageType.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MessageType.swift; sourceTree = "<group>"; };
|
||||
88AEE3802B22327F0034EDA9 /* UInt32+Random.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "UInt32+Random.swift"; sourceTree = "<group>"; };
|
||||
88AEE3832B2236DC0034EDA9 /* SignedMessage.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SignedMessage.swift; sourceTree = "<group>"; };
|
||||
88AEE3852B22376D0034EDA9 /* Message+Crypto.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Message+Crypto.swift"; sourceTree = "<group>"; };
|
||||
88AEE3872B226FED0034EDA9 /* MessageResult+UI.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "MessageResult+UI.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>"; };
|
||||
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>"; };
|
||||
@ -124,8 +166,7 @@
|
||||
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>"; };
|
||||
E24EE77327FF95920011CFD2 /* DeviceResponse.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DeviceResponse.swift; sourceTree = "<group>"; };
|
||||
E24EE77127FDCCC00011CFD2 /* Data+Hex.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Data+Hex.swift"; sourceTree = "<group>"; };
|
||||
E24EE77827FF95E00011CFD2 /* Message.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Message.swift; sourceTree = "<group>"; };
|
||||
E24F6C6D2A89749A0040F8C4 /* ConnectionStrategy.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ConnectionStrategy.swift; sourceTree = "<group>"; };
|
||||
E268E0532A852F8E00185913 /* WidgetKit.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = WidgetKit.framework; path = System/Library/Frameworks/WidgetKit.framework; sourceTree = SDKROOT; };
|
||||
@ -141,9 +182,8 @@
|
||||
E28DED34281EB17600259690 /* HistoryItem.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HistoryItem.swift; sourceTree = "<group>"; };
|
||||
E28DED36281EC7FB00259690 /* HistoryManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HistoryManager.swift; sourceTree = "<group>"; };
|
||||
E28DED38281EE9CF00259690 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist; path = Info.plist; sourceTree = "<group>"; };
|
||||
E2C5C1DA2806FE8900769EF6 /* RouteAPI.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RouteAPI.swift; sourceTree = "<group>"; };
|
||||
E2C5C1DC281B3AC400769EF6 /* UInt32+Extensions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "UInt32+Extensions.swift"; sourceTree = "<group>"; };
|
||||
E2C5C1F7281E769F00769EF6 /* ServerMessage.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ServerMessage.swift; sourceTree = "<group>"; };
|
||||
E2C5C1DA2806FE8900769EF6 /* SesameRoute.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SesameRoute.swift; sourceTree = "<group>"; };
|
||||
E2C5C1DC281B3AC400769EF6 /* UInt32+Coding.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "UInt32+Coding.swift"; sourceTree = "<group>"; };
|
||||
E2F5DCC92A88E913002858B9 /* Array+Extensions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Array+Extensions.swift"; sourceTree = "<group>"; };
|
||||
/* End PBXFileReference section */
|
||||
|
||||
@ -209,17 +249,51 @@
|
||||
884A45BA279F48C300D6E650 /* Assets.xcassets */,
|
||||
E24F6C6C2A89748B0040F8C4 /* Common */,
|
||||
E2C5C1D92806FE4A00769EF6 /* API */,
|
||||
8860D7442B2328B800849FAC /* API Extensions */,
|
||||
884A45B6279F48C100D6E650 /* SesameApp.swift */,
|
||||
884A45B8279F48C100D6E650 /* ContentView.swift */,
|
||||
E28DED2C281E840B00259690 /* SettingsView.swift */,
|
||||
E28DED2E281E8A0500259690 /* SingleKeyView.swift */,
|
||||
E28DED30281EAE9100259690 /* HistoryView.swift */,
|
||||
E25317542A8A1A07005A537D /* History */,
|
||||
E25317552A8A1A32005A537D /* Extensions */,
|
||||
);
|
||||
path = Sesame;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
8860D7442B2328B800849FAC /* API Extensions */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
88AEE3872B226FED0034EDA9 /* MessageResult+UI.swift */,
|
||||
88AEE37E2B2217050034EDA9 /* MessageType.swift */,
|
||||
E24EE77827FF95E00011CFD2 /* Message.swift */,
|
||||
88AEE3852B22376D0034EDA9 /* Message+Crypto.swift */,
|
||||
88AEE3832B2236DC0034EDA9 /* SignedMessage.swift */,
|
||||
8860D7472B23294600849FAC /* SignedMessage+Crypto.swift */,
|
||||
);
|
||||
path = "API Extensions";
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
8860D76B2B246F5600849FAC /* Extensions */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
E2F5DCC92A88E913002858B9 /* Array+Extensions.swift */,
|
||||
884A45CA27A464C000D6E650 /* SymmetricKey+Extensions.swift */,
|
||||
88AEE3802B22327F0034EDA9 /* UInt32+Random.swift */,
|
||||
8860D76D2B246FC400849FAC /* Text+Extensions.swift */,
|
||||
);
|
||||
path = Extensions;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
88AEE3822B22331E0034EDA9 /* Extensions */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
E24EE77127FDCCC00011CFD2 /* Data+Hex.swift */,
|
||||
8860D74D2B232AED00849FAC /* Data+Coding.swift */,
|
||||
E2C5C1DC281B3AC400769EF6 /* UInt32+Coding.swift */,
|
||||
);
|
||||
path = Extensions;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
88E197B029EDC9BC00BF1D19 /* Sesame-Watch Watch App */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
@ -253,7 +327,6 @@
|
||||
E24065542A819663009C1AD8 /* SettingsNumberInputView.swift */,
|
||||
E240654E2A8159B7009C1AD8 /* SettingsTextInputView.swift */,
|
||||
E240654A2A8153C6009C1AD8 /* SettingsListTextItem.swift */,
|
||||
E240654C2A8155A3009C1AD8 /* SettingsListToggleItem.swift */,
|
||||
E24065572A819AE3009C1AD8 /* SettingsKeyItemLink.swift */,
|
||||
E24065592A82218D009C1AD8 /* SettingsKeyInputView.swift */,
|
||||
);
|
||||
@ -263,11 +336,16 @@
|
||||
E24F6C6C2A89748B0040F8C4 /* Common */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
8860D76B2B246F5600849FAC /* Extensions */,
|
||||
884A45CC27A465F500D6E650 /* Client.swift */,
|
||||
884A45C827A43D7900D6E650 /* ClientState.swift */,
|
||||
E24F6C6D2A89749A0040F8C4 /* ConnectionStrategy.swift */,
|
||||
E28DED36281EC7FB00259690 /* HistoryManager.swift */,
|
||||
884A45C4279F4BBE00D6E650 /* KeyManagement.swift */,
|
||||
8860D7512B233BEA00849FAC /* TransmissionType.swift */,
|
||||
8860D7532B23489300849FAC /* ActiveRequestType.swift */,
|
||||
8860D7612B23803E00849FAC /* ServerChallenge.swift */,
|
||||
8860D7642B23B5B200849FAC /* RequestCoordinator.swift */,
|
||||
8860D7672B23D04100849FAC /* PendingOperation.swift */,
|
||||
);
|
||||
path = Common;
|
||||
sourceTree = "<group>";
|
||||
@ -275,6 +353,7 @@
|
||||
E25317542A8A1A07005A537D /* History */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
E28DED30281EAE9100259690 /* HistoryView.swift */,
|
||||
E28DED32281EB15B00259690 /* HistoryListItem.swift */,
|
||||
E28DED34281EB17600259690 /* HistoryItem.swift */,
|
||||
);
|
||||
@ -284,8 +363,7 @@
|
||||
E25317552A8A1A32005A537D /* Extensions */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
E2F5DCC92A88E913002858B9 /* Array+Extensions.swift */,
|
||||
884A45CA27A464C000D6E650 /* SymmetricKey+Extensions.swift */,
|
||||
8860D7422B22858600849FAC /* Date+Timestamp.swift */,
|
||||
);
|
||||
path = Extensions;
|
||||
sourceTree = "<group>";
|
||||
@ -303,13 +381,12 @@
|
||||
E2C5C1D92806FE4A00769EF6 /* API */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
E24EE77127FDCCC00011CFD2 /* Data+Extensions.swift */,
|
||||
E24EE77327FF95920011CFD2 /* DeviceResponse.swift */,
|
||||
E24EE77827FF95E00011CFD2 /* Message.swift */,
|
||||
88AEE3822B22331E0034EDA9 /* Extensions */,
|
||||
884A45CE27A5402D00D6E650 /* MessageResult.swift */,
|
||||
E2C5C1DA2806FE8900769EF6 /* RouteAPI.swift */,
|
||||
E2C5C1F7281E769F00769EF6 /* ServerMessage.swift */,
|
||||
E2C5C1DC281B3AC400769EF6 /* UInt32+Extensions.swift */,
|
||||
E2C5C1DA2806FE8900769EF6 /* SesameRoute.swift */,
|
||||
8860D7452B2328EC00849FAC /* Message+Size.swift */,
|
||||
8860D7492B2329CE00849FAC /* SignedMessage+Size.swift */,
|
||||
8860D74B2B232A7700849FAC /* SesameHeader.swift */,
|
||||
);
|
||||
path = API;
|
||||
sourceTree = "<group>";
|
||||
@ -391,7 +468,7 @@
|
||||
attributes = {
|
||||
BuildIndependentTargetsInParallel = 1;
|
||||
LastSwiftUpdateCheck = 1430;
|
||||
LastUpgradeCheck = 1320;
|
||||
LastUpgradeCheck = 1500;
|
||||
TargetAttributes = {
|
||||
884A45B2279F48C100D6E650 = {
|
||||
CreatedOnToolsVersion = 13.2.1;
|
||||
@ -464,25 +541,39 @@
|
||||
files = (
|
||||
884A45CF27A5402D00D6E650 /* MessageResult.swift in Sources */,
|
||||
884A45B9279F48C100D6E650 /* ContentView.swift in Sources */,
|
||||
88AEE3882B226FED0034EDA9 /* MessageResult+UI.swift in Sources */,
|
||||
E28DED2F281E8A0500259690 /* SingleKeyView.swift in Sources */,
|
||||
E28DED37281EC7FB00259690 /* HistoryManager.swift in Sources */,
|
||||
E2C5C1DB2806FE8900769EF6 /* RouteAPI.swift in Sources */,
|
||||
E2C5C1DD281B3AC400769EF6 /* UInt32+Extensions.swift in Sources */,
|
||||
E24EE77227FDCCC00011CFD2 /* Data+Extensions.swift in Sources */,
|
||||
E24EE77427FF95920011CFD2 /* DeviceResponse.swift in Sources */,
|
||||
E2C5C1DB2806FE8900769EF6 /* SesameRoute.swift in Sources */,
|
||||
E2C5C1DD281B3AC400769EF6 /* UInt32+Coding.swift in Sources */,
|
||||
E24EE77227FDCCC00011CFD2 /* Data+Hex.swift in Sources */,
|
||||
8860D7482B23294600849FAC /* SignedMessage+Crypto.swift in Sources */,
|
||||
8860D74C2B232A7700849FAC /* SesameHeader.swift in Sources */,
|
||||
8860D7622B23803E00849FAC /* ServerChallenge.swift in Sources */,
|
||||
8860D7432B22858600849FAC /* Date+Timestamp.swift in Sources */,
|
||||
88AEE3862B22376D0034EDA9 /* Message+Crypto.swift in Sources */,
|
||||
8860D7682B23D04100849FAC /* PendingOperation.swift in Sources */,
|
||||
8860D74E2B232AED00849FAC /* Data+Coding.swift in Sources */,
|
||||
8860D7522B233BEA00849FAC /* TransmissionType.swift in Sources */,
|
||||
884A45CB27A464C000D6E650 /* SymmetricKey+Extensions.swift in Sources */,
|
||||
E28DED31281EAE9100259690 /* HistoryView.swift in Sources */,
|
||||
E24EE77927FF95E00011CFD2 /* Message.swift in Sources */,
|
||||
E2F5DCCA2A88E913002858B9 /* Array+Extensions.swift in Sources */,
|
||||
E28DED35281EB17600259690 /* HistoryItem.swift in Sources */,
|
||||
884A45C927A43D7900D6E650 /* ClientState.swift in Sources */,
|
||||
E28DED33281EB15B00259690 /* HistoryListItem.swift in Sources */,
|
||||
E28DED2D281E840B00259690 /* SettingsView.swift in Sources */,
|
||||
884A45B7279F48C100D6E650 /* SesameApp.swift in Sources */,
|
||||
8860D76E2B246FC400849FAC /* Text+Extensions.swift in Sources */,
|
||||
88E197C429EDCC8900BF1D19 /* Client.swift in Sources */,
|
||||
8860D7652B23B5B200849FAC /* RequestCoordinator.swift in Sources */,
|
||||
88AEE3812B22327F0034EDA9 /* UInt32+Random.swift in Sources */,
|
||||
E24F6C6E2A89749A0040F8C4 /* ConnectionStrategy.swift in Sources */,
|
||||
884A45C5279F4BBE00D6E650 /* KeyManagement.swift in Sources */,
|
||||
E2C5C1F8281E769F00769EF6 /* ServerMessage.swift in Sources */,
|
||||
88AEE3842B2236DC0034EDA9 /* SignedMessage.swift in Sources */,
|
||||
8860D74A2B2329CE00849FAC /* SignedMessage+Size.swift in Sources */,
|
||||
8860D7542B23489300849FAC /* ActiveRequestType.swift in Sources */,
|
||||
88AEE37F2B2217050034EDA9 /* MessageType.swift in Sources */,
|
||||
8860D7462B2328EC00849FAC /* Message+Size.swift in Sources */,
|
||||
);
|
||||
runOnlyForDeploymentPostprocessing = 0;
|
||||
};
|
||||
@ -490,33 +581,46 @@
|
||||
isa = PBXSourcesBuildPhase;
|
||||
buildActionMask = 2147483647;
|
||||
files = (
|
||||
8860D7662B23B5B200849FAC /* RequestCoordinator.swift in Sources */,
|
||||
8860D75D2B237FC000849FAC /* Data+Coding.swift in Sources */,
|
||||
888362342A80F3F90032BBB2 /* SettingsView.swift in Sources */,
|
||||
88E197B429EDC9BC00BF1D19 /* ContentView.swift in Sources */,
|
||||
E2F5DCCB2A88E976002858B9 /* Array+Extensions.swift in Sources */,
|
||||
E24065532A819614009C1AD8 /* SettingsNumberItemLink.swift in Sources */,
|
||||
888362362A80F4420032BBB2 /* HistoryView.swift in Sources */,
|
||||
8860D75B2B237FB600849FAC /* SignedMessage+Crypto.swift in Sources */,
|
||||
E240654F2A8159B7009C1AD8 /* SettingsTextInputView.swift in Sources */,
|
||||
88E197D329EDCE6E00BF1D19 /* MessageResult.swift in Sources */,
|
||||
88E197D529EDCE8800BF1D19 /* ServerMessage.swift in Sources */,
|
||||
88E197D129EDCE5F00BF1D19 /* Data+Extensions.swift in Sources */,
|
||||
88E197D129EDCE5F00BF1D19 /* Data+Hex.swift in Sources */,
|
||||
8860D76C2B246F5E00849FAC /* UInt32+Random.swift in Sources */,
|
||||
E240655A2A82218D009C1AD8 /* SettingsKeyInputView.swift in Sources */,
|
||||
88E197D229EDCE6600BF1D19 /* RouteAPI.swift in Sources */,
|
||||
8860D76F2B246FC400849FAC /* Text+Extensions.swift in Sources */,
|
||||
88E197D229EDCE6600BF1D19 /* SesameRoute.swift in Sources */,
|
||||
8860D75E2B237FC600849FAC /* Message+Size.swift in Sources */,
|
||||
8860D7552B237F9100849FAC /* TransmissionType.swift in Sources */,
|
||||
8860D75C2B237FB900849FAC /* MessageResult+UI.swift in Sources */,
|
||||
8860D7632B23803E00849FAC /* ServerChallenge.swift in Sources */,
|
||||
E24065512A819066009C1AD8 /* SettingsTextItemLink.swift in Sources */,
|
||||
88E197D729EDCFE800BF1D19 /* Date+Extensions.swift in Sources */,
|
||||
88E197C829EDCCCE00BF1D19 /* ClientState.swift in Sources */,
|
||||
8860D75A2B237FB400849FAC /* SignedMessage.swift in Sources */,
|
||||
E24065602A822ED9009C1AD8 /* HistoryItemDetail.swift in Sources */,
|
||||
8860D75F2B237FC900849FAC /* SignedMessage+Size.swift in Sources */,
|
||||
88E197B229EDC9BC00BF1D19 /* Sesame_WatchApp.swift in Sources */,
|
||||
E24065582A819AE3009C1AD8 /* SettingsKeyItemLink.swift in Sources */,
|
||||
88E197C929EDCCE100BF1D19 /* Message.swift in Sources */,
|
||||
8860D7602B237FCC00849FAC /* SesameHeader.swift in Sources */,
|
||||
8860D7592B237FB200849FAC /* Message+Crypto.swift in Sources */,
|
||||
E24F6C6F2A8974C60040F8C4 /* ConnectionStrategy.swift in Sources */,
|
||||
8860D7562B237F9400849FAC /* ActiveRequestType.swift in Sources */,
|
||||
E240654B2A8153C6009C1AD8 /* SettingsListTextItem.swift in Sources */,
|
||||
8860D7692B23D04100849FAC /* PendingOperation.swift in Sources */,
|
||||
88E197C729EDCCBD00BF1D19 /* Client.swift in Sources */,
|
||||
88E197D429EDCE7600BF1D19 /* UInt32+Extensions.swift in Sources */,
|
||||
88E197D429EDCE7600BF1D19 /* UInt32+Coding.swift in Sources */,
|
||||
E240655B2A822397009C1AD8 /* KeyManagement.swift in Sources */,
|
||||
E240654D2A8155A3009C1AD8 /* SettingsListToggleItem.swift in Sources */,
|
||||
E24065552A819663009C1AD8 /* SettingsNumberInputView.swift in Sources */,
|
||||
8860D7572B237FAD00849FAC /* MessageType.swift in Sources */,
|
||||
E240655E2A822E97009C1AD8 /* HistoryListRow.swift in Sources */,
|
||||
E25317562A8A1ABF005A537D /* HistoryItem.swift in Sources */,
|
||||
8860D7582B237FB000849FAC /* Message.swift in Sources */,
|
||||
88E197D829EDD13B00BF1D19 /* SymmetricKey+Extensions.swift in Sources */,
|
||||
E240655C2A822C8E009C1AD8 /* HistoryManager.swift in Sources */,
|
||||
);
|
||||
@ -545,6 +649,7 @@
|
||||
isa = XCBuildConfiguration;
|
||||
buildSettings = {
|
||||
ALWAYS_SEARCH_USER_PATHS = NO;
|
||||
ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES;
|
||||
CLANG_ANALYZER_NONNULL = YES;
|
||||
CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE;
|
||||
CLANG_CXX_LANGUAGE_STANDARD = "gnu++17";
|
||||
@ -578,6 +683,7 @@
|
||||
DEBUG_INFORMATION_FORMAT = dwarf;
|
||||
ENABLE_STRICT_OBJC_MSGSEND = YES;
|
||||
ENABLE_TESTABILITY = YES;
|
||||
ENABLE_USER_SCRIPT_SANDBOXING = YES;
|
||||
GCC_C_LANGUAGE_STANDARD = gnu11;
|
||||
GCC_DYNAMIC_NO_PIC = NO;
|
||||
GCC_NO_COMMON_BLOCKS = YES;
|
||||
@ -592,7 +698,7 @@
|
||||
GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
|
||||
GCC_WARN_UNUSED_FUNCTION = YES;
|
||||
GCC_WARN_UNUSED_VARIABLE = YES;
|
||||
IPHONEOS_DEPLOYMENT_TARGET = 15.2;
|
||||
IPHONEOS_DEPLOYMENT_TARGET = 17.0;
|
||||
MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE;
|
||||
MTL_FAST_MATH = YES;
|
||||
ONLY_ACTIVE_ARCH = YES;
|
||||
@ -606,6 +712,7 @@
|
||||
isa = XCBuildConfiguration;
|
||||
buildSettings = {
|
||||
ALWAYS_SEARCH_USER_PATHS = NO;
|
||||
ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES;
|
||||
CLANG_ANALYZER_NONNULL = YES;
|
||||
CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE;
|
||||
CLANG_CXX_LANGUAGE_STANDARD = "gnu++17";
|
||||
@ -639,6 +746,7 @@
|
||||
DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym";
|
||||
ENABLE_NS_ASSERTIONS = NO;
|
||||
ENABLE_STRICT_OBJC_MSGSEND = YES;
|
||||
ENABLE_USER_SCRIPT_SANDBOXING = YES;
|
||||
GCC_C_LANGUAGE_STANDARD = gnu11;
|
||||
GCC_NO_COMMON_BLOCKS = YES;
|
||||
GCC_WARN_64_TO_32_BIT_CONVERSION = YES;
|
||||
@ -647,7 +755,7 @@
|
||||
GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
|
||||
GCC_WARN_UNUSED_FUNCTION = YES;
|
||||
GCC_WARN_UNUSED_VARIABLE = YES;
|
||||
IPHONEOS_DEPLOYMENT_TARGET = 15.2;
|
||||
IPHONEOS_DEPLOYMENT_TARGET = 17.0;
|
||||
MTL_ENABLE_DEBUG_INFO = NO;
|
||||
MTL_FAST_MATH = YES;
|
||||
SDKROOT = iphoneos;
|
||||
@ -676,7 +784,7 @@
|
||||
INFOPLIST_KEY_UISupportedInterfaceOrientations = UIInterfaceOrientationPortrait;
|
||||
INFOPLIST_KEY_UISupportedInterfaceOrientations_iPad = "UIInterfaceOrientationPortrait UIInterfaceOrientationPortraitUpsideDown UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight";
|
||||
INFOPLIST_KEY_UISupportedInterfaceOrientations_iPhone = UIInterfaceOrientationPortrait;
|
||||
IPHONEOS_DEPLOYMENT_TARGET = 16.0;
|
||||
IPHONEOS_DEPLOYMENT_TARGET = 17.0;
|
||||
LD_RUNPATH_SEARCH_PATHS = (
|
||||
"$(inherited)",
|
||||
"@executable_path/Frameworks",
|
||||
@ -711,7 +819,7 @@
|
||||
INFOPLIST_KEY_UISupportedInterfaceOrientations = UIInterfaceOrientationPortrait;
|
||||
INFOPLIST_KEY_UISupportedInterfaceOrientations_iPad = "UIInterfaceOrientationPortrait UIInterfaceOrientationPortraitUpsideDown UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight";
|
||||
INFOPLIST_KEY_UISupportedInterfaceOrientations_iPhone = UIInterfaceOrientationPortrait;
|
||||
IPHONEOS_DEPLOYMENT_TARGET = 16.0;
|
||||
IPHONEOS_DEPLOYMENT_TARGET = 17.0;
|
||||
LD_RUNPATH_SEARCH_PATHS = (
|
||||
"$(inherited)",
|
||||
"@executable_path/Frameworks",
|
||||
@ -754,7 +862,7 @@
|
||||
SWIFT_EMIT_LOC_STRINGS = YES;
|
||||
SWIFT_VERSION = 5.0;
|
||||
TARGETED_DEVICE_FAMILY = 4;
|
||||
WATCHOS_DEPLOYMENT_TARGET = 9.4;
|
||||
WATCHOS_DEPLOYMENT_TARGET = 10.0;
|
||||
};
|
||||
name = Debug;
|
||||
};
|
||||
@ -786,7 +894,7 @@
|
||||
SWIFT_EMIT_LOC_STRINGS = YES;
|
||||
SWIFT_VERSION = 5.0;
|
||||
TARGETED_DEVICE_FAMILY = 4;
|
||||
WATCHOS_DEPLOYMENT_TARGET = 9.4;
|
||||
WATCHOS_DEPLOYMENT_TARGET = 10.0;
|
||||
};
|
||||
name = Release;
|
||||
};
|
||||
@ -817,7 +925,7 @@
|
||||
SWIFT_EMIT_LOC_STRINGS = YES;
|
||||
SWIFT_VERSION = 5.0;
|
||||
TARGETED_DEVICE_FAMILY = 4;
|
||||
WATCHOS_DEPLOYMENT_TARGET = 9.4;
|
||||
WATCHOS_DEPLOYMENT_TARGET = 10.0;
|
||||
};
|
||||
name = Debug;
|
||||
};
|
||||
@ -848,7 +956,7 @@
|
||||
SWIFT_EMIT_LOC_STRINGS = YES;
|
||||
SWIFT_VERSION = 5.0;
|
||||
TARGETED_DEVICE_FAMILY = 4;
|
||||
WATCHOS_DEPLOYMENT_TARGET = 9.4;
|
||||
WATCHOS_DEPLOYMENT_TARGET = 10.0;
|
||||
};
|
||||
name = Release;
|
||||
};
|
||||
|
Binary file not shown.
@ -0,0 +1,77 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<Scheme
|
||||
LastUpgradeVersion = "1500"
|
||||
version = "1.7">
|
||||
<BuildAction
|
||||
parallelizeBuildables = "YES"
|
||||
buildImplicitDependencies = "YES">
|
||||
<BuildActionEntries>
|
||||
<BuildActionEntry
|
||||
buildForTesting = "YES"
|
||||
buildForRunning = "YES"
|
||||
buildForProfiling = "YES"
|
||||
buildForArchiving = "YES"
|
||||
buildForAnalyzing = "YES">
|
||||
<BuildableReference
|
||||
BuildableIdentifier = "primary"
|
||||
BlueprintIdentifier = "88E197AB29EDC9BC00BF1D19"
|
||||
BuildableName = "Sesame-Watch Watch App.app"
|
||||
BlueprintName = "Sesame-Watch Watch App"
|
||||
ReferencedContainer = "container:Sesame.xcodeproj">
|
||||
</BuildableReference>
|
||||
</BuildActionEntry>
|
||||
</BuildActionEntries>
|
||||
</BuildAction>
|
||||
<TestAction
|
||||
buildConfiguration = "Debug"
|
||||
selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
|
||||
selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
|
||||
shouldUseLaunchSchemeArgsEnv = "YES"
|
||||
shouldAutocreateTestPlan = "YES">
|
||||
</TestAction>
|
||||
<LaunchAction
|
||||
buildConfiguration = "Debug"
|
||||
selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
|
||||
selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
|
||||
launchStyle = "0"
|
||||
useCustomWorkingDirectory = "NO"
|
||||
ignoresPersistentStateOnLaunch = "NO"
|
||||
debugDocumentVersioning = "YES"
|
||||
debugServiceExtension = "internal"
|
||||
allowLocationSimulation = "YES">
|
||||
<BuildableProductRunnable
|
||||
runnableDebuggingMode = "0">
|
||||
<BuildableReference
|
||||
BuildableIdentifier = "primary"
|
||||
BlueprintIdentifier = "88E197AB29EDC9BC00BF1D19"
|
||||
BuildableName = "Sesame-Watch Watch App.app"
|
||||
BlueprintName = "Sesame-Watch Watch App"
|
||||
ReferencedContainer = "container:Sesame.xcodeproj">
|
||||
</BuildableReference>
|
||||
</BuildableProductRunnable>
|
||||
</LaunchAction>
|
||||
<ProfileAction
|
||||
buildConfiguration = "Release"
|
||||
shouldUseLaunchSchemeArgsEnv = "YES"
|
||||
savedToolIdentifier = ""
|
||||
useCustomWorkingDirectory = "NO"
|
||||
debugDocumentVersioning = "YES">
|
||||
<BuildableProductRunnable
|
||||
runnableDebuggingMode = "0">
|
||||
<BuildableReference
|
||||
BuildableIdentifier = "primary"
|
||||
BlueprintIdentifier = "88E197AB29EDC9BC00BF1D19"
|
||||
BuildableName = "Sesame-Watch Watch App.app"
|
||||
BlueprintName = "Sesame-Watch Watch App"
|
||||
ReferencedContainer = "container:Sesame.xcodeproj">
|
||||
</BuildableReference>
|
||||
</BuildableProductRunnable>
|
||||
</ProfileAction>
|
||||
<AnalyzeAction
|
||||
buildConfiguration = "Debug">
|
||||
</AnalyzeAction>
|
||||
<ArchiveAction
|
||||
buildConfiguration = "Release"
|
||||
revealArchiveInOrganizer = "YES">
|
||||
</ArchiveAction>
|
||||
</Scheme>
|
@ -0,0 +1,111 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<Scheme
|
||||
LastUpgradeVersion = "1500"
|
||||
wasCreatedForAppExtension = "YES"
|
||||
version = "2.0">
|
||||
<BuildAction
|
||||
parallelizeBuildables = "YES"
|
||||
buildImplicitDependencies = "YES">
|
||||
<BuildActionEntries>
|
||||
<BuildActionEntry
|
||||
buildForTesting = "YES"
|
||||
buildForRunning = "YES"
|
||||
buildForProfiling = "YES"
|
||||
buildForArchiving = "YES"
|
||||
buildForAnalyzing = "YES">
|
||||
<BuildableReference
|
||||
BuildableIdentifier = "primary"
|
||||
BlueprintIdentifier = "E268E0802A85302000185913"
|
||||
BuildableName = "Sesame-WidgetExtension.appex"
|
||||
BlueprintName = "Sesame-WidgetExtension"
|
||||
ReferencedContainer = "container:Sesame.xcodeproj">
|
||||
</BuildableReference>
|
||||
</BuildActionEntry>
|
||||
<BuildActionEntry
|
||||
buildForTesting = "YES"
|
||||
buildForRunning = "YES"
|
||||
buildForProfiling = "YES"
|
||||
buildForArchiving = "YES"
|
||||
buildForAnalyzing = "YES">
|
||||
<BuildableReference
|
||||
BuildableIdentifier = "primary"
|
||||
BlueprintIdentifier = "88E197AB29EDC9BC00BF1D19"
|
||||
BuildableName = "Sesame-Watch Watch App.app"
|
||||
BlueprintName = "Sesame-Watch Watch App"
|
||||
ReferencedContainer = "container:Sesame.xcodeproj">
|
||||
</BuildableReference>
|
||||
</BuildActionEntry>
|
||||
</BuildActionEntries>
|
||||
</BuildAction>
|
||||
<TestAction
|
||||
buildConfiguration = "Debug"
|
||||
selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
|
||||
selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
|
||||
shouldUseLaunchSchemeArgsEnv = "YES"
|
||||
shouldAutocreateTestPlan = "YES">
|
||||
</TestAction>
|
||||
<LaunchAction
|
||||
buildConfiguration = "Debug"
|
||||
selectedDebuggerIdentifier = ""
|
||||
selectedLauncherIdentifier = "Xcode.IDEFoundation.Launcher.PosixSpawn"
|
||||
launchStyle = "0"
|
||||
askForAppToLaunch = "Yes"
|
||||
useCustomWorkingDirectory = "NO"
|
||||
ignoresPersistentStateOnLaunch = "NO"
|
||||
debugDocumentVersioning = "YES"
|
||||
debugServiceExtension = "internal"
|
||||
allowLocationSimulation = "YES"
|
||||
launchAutomaticallySubstyle = "2">
|
||||
<BuildableProductRunnable
|
||||
runnableDebuggingMode = "0">
|
||||
<BuildableReference
|
||||
BuildableIdentifier = "primary"
|
||||
BlueprintIdentifier = "88E197AB29EDC9BC00BF1D19"
|
||||
BuildableName = "Sesame-Watch Watch App.app"
|
||||
BlueprintName = "Sesame-Watch Watch App"
|
||||
ReferencedContainer = "container:Sesame.xcodeproj">
|
||||
</BuildableReference>
|
||||
</BuildableProductRunnable>
|
||||
<EnvironmentVariables>
|
||||
<EnvironmentVariable
|
||||
key = "_XCWidgetKind"
|
||||
value = ""
|
||||
isEnabled = "YES">
|
||||
</EnvironmentVariable>
|
||||
<EnvironmentVariable
|
||||
key = "_XCWidgetDefaultView"
|
||||
value = "timeline"
|
||||
isEnabled = "YES">
|
||||
</EnvironmentVariable>
|
||||
<EnvironmentVariable
|
||||
key = "_XCWidgetFamily"
|
||||
value = "systemMedium"
|
||||
isEnabled = "YES">
|
||||
</EnvironmentVariable>
|
||||
</EnvironmentVariables>
|
||||
</LaunchAction>
|
||||
<ProfileAction
|
||||
buildConfiguration = "Release"
|
||||
shouldUseLaunchSchemeArgsEnv = "YES"
|
||||
savedToolIdentifier = ""
|
||||
useCustomWorkingDirectory = "NO"
|
||||
debugDocumentVersioning = "YES">
|
||||
<BuildableProductRunnable
|
||||
runnableDebuggingMode = "0">
|
||||
<BuildableReference
|
||||
BuildableIdentifier = "primary"
|
||||
BlueprintIdentifier = "E268E0802A85302000185913"
|
||||
BuildableName = "Sesame-WidgetExtension.appex"
|
||||
BlueprintName = "Sesame-WidgetExtension"
|
||||
ReferencedContainer = "container:Sesame.xcodeproj">
|
||||
</BuildableReference>
|
||||
</BuildableProductRunnable>
|
||||
</ProfileAction>
|
||||
<AnalyzeAction
|
||||
buildConfiguration = "Debug">
|
||||
</AnalyzeAction>
|
||||
<ArchiveAction
|
||||
buildConfiguration = "Release"
|
||||
revealArchiveInOrganizer = "YES">
|
||||
</ArchiveAction>
|
||||
</Scheme>
|
@ -0,0 +1,6 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<Bucket
|
||||
uuid = "47155A8E-2113-40C3-89ED-8DEEEFF66A4F"
|
||||
type = "1"
|
||||
version = "2.0">
|
||||
</Bucket>
|
@ -4,16 +4,39 @@
|
||||
<dict>
|
||||
<key>SchemeUserState</key>
|
||||
<dict>
|
||||
<key>Sesame Watch App.xcscheme_^#shared#^_</key>
|
||||
<dict>
|
||||
<key>orderHint</key>
|
||||
<integer>2</integer>
|
||||
</dict>
|
||||
<key>Sesame-Watch Watch App.xcscheme_^#shared#^_</key>
|
||||
<dict>
|
||||
<key>orderHint</key>
|
||||
<integer>0</integer>
|
||||
<integer>2</integer>
|
||||
</dict>
|
||||
<key>Sesame.xcscheme_^#shared#^_</key>
|
||||
<key>Sesame-WidgetExtension.xcscheme_^#shared#^_</key>
|
||||
<dict>
|
||||
<key>orderHint</key>
|
||||
<integer>1</integer>
|
||||
</dict>
|
||||
<key>Sesame.xcscheme_^#shared#^_</key>
|
||||
<dict>
|
||||
<key>orderHint</key>
|
||||
<integer>0</integer>
|
||||
</dict>
|
||||
</dict>
|
||||
<key>SuppressBuildableAutocreation</key>
|
||||
<dict>
|
||||
<key>88E197AB29EDC9BC00BF1D19</key>
|
||||
<dict>
|
||||
<key>primary</key>
|
||||
<true/>
|
||||
</dict>
|
||||
<key>E268E0802A85302000185913</key>
|
||||
<dict>
|
||||
<key>primary</key>
|
||||
<true/>
|
||||
</dict>
|
||||
</dict>
|
||||
</dict>
|
||||
</plist>
|
||||
|
26
Sesame/API Extensions/Message+Crypto.swift
Normal file
26
Sesame/API Extensions/Message+Crypto.swift
Normal file
@ -0,0 +1,26 @@
|
||||
import Foundation
|
||||
import CryptoKit
|
||||
|
||||
extension Message {
|
||||
|
||||
/**
|
||||
Calculate an authentication code for the message content.
|
||||
- Parameter key: The key to use to sign the content.
|
||||
- Returns: The new message signed with the key.
|
||||
*/
|
||||
func authenticate(using key: SymmetricKey) -> SignedMessage {
|
||||
let mac = HMAC<SHA256>.authenticationCode(for: encoded, using: key)
|
||||
return .init(mac: Data(mac.map { $0 }), message: self)
|
||||
}
|
||||
|
||||
/**
|
||||
Calculate an authentication code for the message content and convert everything to data.
|
||||
- Parameter key: The key to use to sign the content.
|
||||
- Returns: The new message signed with the key, serialized to bytes.
|
||||
*/
|
||||
func authenticateAndSerialize(using key: SymmetricKey) -> Data {
|
||||
let encoded = self.encoded
|
||||
let mac = HMAC<SHA256>.authenticationCode(for: encoded, using: key)
|
||||
return Data(mac.map { $0 }) + encoded
|
||||
}
|
||||
}
|
123
Sesame/API Extensions/Message.swift
Normal file
123
Sesame/API Extensions/Message.swift
Normal file
@ -0,0 +1,123 @@
|
||||
import Foundation
|
||||
|
||||
/**
|
||||
The message content without authentication.
|
||||
*/
|
||||
struct Message: Equatable, Hashable {
|
||||
|
||||
/// The type of message being sent.
|
||||
let messageType: MessageType
|
||||
|
||||
/**
|
||||
* The random nonce created by the remote
|
||||
*
|
||||
* This nonce is a random number created by the remote, different for each unlock request.
|
||||
* It is set for all message types.
|
||||
*/
|
||||
let clientChallenge: UInt32
|
||||
|
||||
/**
|
||||
* A random number to sign by the remote
|
||||
*
|
||||
* This nonce is set by the server after receiving an initial message.
|
||||
* It is set for the message types `challenge`, `request`, and `response`.
|
||||
*/
|
||||
let serverChallenge: UInt32
|
||||
|
||||
/**
|
||||
* The response status for the previous message.
|
||||
*
|
||||
* It is set only for messages from the server, e.g. the `challenge` and `response` message types.
|
||||
* Must be set to `MessageAccepted` for other messages.
|
||||
*/
|
||||
let result: MessageResult
|
||||
|
||||
init(messageType: MessageType, clientChallenge: UInt32, serverChallenge: UInt32, result: MessageResult) {
|
||||
self.messageType = messageType
|
||||
self.clientChallenge = clientChallenge
|
||||
self.serverChallenge = serverChallenge
|
||||
self.result = result
|
||||
}
|
||||
|
||||
/**
|
||||
Decode message content from data.
|
||||
|
||||
The data consists of two `UInt32` encoded in little endian format
|
||||
- Warning: The sequence must contain at least 8 bytes, or the function will crash.
|
||||
- Parameter data: The sequence containing the bytes.
|
||||
*/
|
||||
init(decodeFrom data: Data) throws {
|
||||
guard data.count == Message.size else {
|
||||
print("Invalid message size \(data.count)")
|
||||
throw MessageResult.invalidMessageSizeFromDevice
|
||||
}
|
||||
guard let messageType = MessageType(rawValue: data.first!) else {
|
||||
print("Invalid message type \(data.first!)")
|
||||
throw MessageResult.invalidMessageTypeFromDevice
|
||||
}
|
||||
self.messageType = messageType
|
||||
self.clientChallenge = UInt32(data: data.dropFirst().prefix(UInt32.byteSize))
|
||||
self.serverChallenge = UInt32(data: data.dropFirst(UInt32.byteSize+1).prefix(UInt32.byteSize))
|
||||
guard let result = MessageResult(rawValue: data.last!) else {
|
||||
print("Invalid message result \(data.last!)")
|
||||
throw MessageResult.unknownMessageResultFromDevice
|
||||
}
|
||||
self.result = result
|
||||
}
|
||||
|
||||
/// The message content encoded to data
|
||||
var encoded: Data {
|
||||
messageType.encoded + clientChallenge.encoded + serverChallenge.encoded + result.encoded
|
||||
}
|
||||
}
|
||||
|
||||
extension Message: Codable {
|
||||
|
||||
enum CodingKeys: Int, CodingKey {
|
||||
case messageType = 1
|
||||
case clientChallenge = 2
|
||||
case serverChallenge = 3
|
||||
case result = 4
|
||||
}
|
||||
}
|
||||
|
||||
extension Message {
|
||||
|
||||
init(error: MessageResult, type: MessageType) {
|
||||
self.init(messageType: type, clientChallenge: 0, serverChallenge: 0, result: error)
|
||||
}
|
||||
|
||||
static func initial() -> Message {
|
||||
.init(
|
||||
messageType: .initial,
|
||||
clientChallenge: .random(),
|
||||
serverChallenge: 0,
|
||||
result: .messageAccepted)
|
||||
}
|
||||
|
||||
func with(result: MessageResult) -> Message {
|
||||
.init(
|
||||
messageType: messageType.responseType,
|
||||
clientChallenge: clientChallenge,
|
||||
serverChallenge: serverChallenge,
|
||||
result: result)
|
||||
}
|
||||
|
||||
/**
|
||||
Create the message to respond to this challenge
|
||||
*/
|
||||
func requestMessage() -> Message {
|
||||
.init(
|
||||
messageType: .request,
|
||||
clientChallenge: clientChallenge,
|
||||
serverChallenge: serverChallenge,
|
||||
result: .messageAccepted)
|
||||
}
|
||||
}
|
||||
|
||||
extension Message: CustomStringConvertible {
|
||||
|
||||
var description: String {
|
||||
"\(messageType)(\(clientChallenge)->\(serverChallenge), \(result))"
|
||||
}
|
||||
}
|
118
Sesame/API Extensions/MessageResult+UI.swift
Normal file
118
Sesame/API Extensions/MessageResult+UI.swift
Normal file
@ -0,0 +1,118 @@
|
||||
import Foundation
|
||||
import SwiftUI
|
||||
import SFSafeSymbols
|
||||
|
||||
extension MessageResult {
|
||||
|
||||
var color: Color {
|
||||
switch self {
|
||||
|
||||
// Initial state when not configured
|
||||
case .noKeyAvailable:
|
||||
return Color(red: 50/255, green: 50/255, blue: 50/255)
|
||||
|
||||
// All ready states
|
||||
case .notChecked,
|
||||
.messageAccepted,
|
||||
.deviceAvailable:
|
||||
return Color(red: 115/255, green: 140/255, blue: 90/255)
|
||||
|
||||
case .unlocked:
|
||||
return Color(red: 65/255, green: 110/255, blue: 60/255)
|
||||
|
||||
// All implementation errors
|
||||
case .textReceived,
|
||||
.unexpectedSocketEvent,
|
||||
.invalidMessageSizeFromDevice,
|
||||
.invalidMessageSizeFromRemote,
|
||||
.invalidMessageTypeFromDevice,
|
||||
.invalidMessageTypeFromRemote,
|
||||
.unknownMessageResultFromDevice,
|
||||
.invalidUrlParameter,
|
||||
.noOrInvalidBodyDataFromRemote,
|
||||
.invalidMessageResultFromRemote,
|
||||
.unexpectedUrlResponseType,
|
||||
.unexpectedServerResponseCode,
|
||||
.internalServerError,
|
||||
.pathOnServerNotFound,
|
||||
.missingOrInvalidAuthenticationHeaderFromRemote:
|
||||
return Color(red: 30/255, green: 30/255, blue: 160/255)
|
||||
|
||||
// All security errors
|
||||
case .invalidSignatureFromRemote,
|
||||
.invalidServerChallengeFromDevice,
|
||||
.invalidServerChallengeFromRemote,
|
||||
.invalidClientChallengeFromDevice,
|
||||
.invalidClientChallengeFromRemote,
|
||||
.invalidSignatureFromDevice:
|
||||
return Color(red: 160/255, green: 30/255, blue: 30/255)
|
||||
|
||||
// Connection errors
|
||||
case .tooManyRequests,
|
||||
.deviceTimedOut,
|
||||
.deviceNotConnected,
|
||||
.serviceBehindProxyUnavailable:
|
||||
return Color(red: 150/255, green: 90/255, blue: 90/255)
|
||||
|
||||
// Configuration errors
|
||||
case .serverUrlInvalid, .invalidServerAuthenticationFromRemote:
|
||||
return Color(red: 100/255, green: 100/255, blue: 140/255)
|
||||
}
|
||||
}
|
||||
|
||||
var symbol: SFSymbol {
|
||||
switch self {
|
||||
|
||||
// Initial state when not configured
|
||||
case .noKeyAvailable:
|
||||
return .questionmarkKeyFilled // .keySlash in 5.0
|
||||
|
||||
// All ready states
|
||||
case .notChecked,
|
||||
.messageAccepted,
|
||||
.deviceAvailable:
|
||||
return .checkmark
|
||||
|
||||
case .unlocked:
|
||||
return .lockOpen
|
||||
|
||||
// All implementation errors
|
||||
case .textReceived,
|
||||
.unexpectedSocketEvent,
|
||||
.invalidMessageSizeFromDevice,
|
||||
.invalidMessageSizeFromRemote,
|
||||
.invalidMessageTypeFromDevice,
|
||||
.invalidMessageTypeFromRemote,
|
||||
.unknownMessageResultFromDevice,
|
||||
.invalidUrlParameter,
|
||||
.noOrInvalidBodyDataFromRemote,
|
||||
.invalidMessageResultFromRemote,
|
||||
.unexpectedUrlResponseType,
|
||||
.unexpectedServerResponseCode,
|
||||
.internalServerError,
|
||||
.pathOnServerNotFound,
|
||||
.missingOrInvalidAuthenticationHeaderFromRemote:
|
||||
return .questionmarkDiamond
|
||||
|
||||
// All security errors
|
||||
case .invalidSignatureFromRemote,
|
||||
.invalidServerChallengeFromDevice,
|
||||
.invalidServerChallengeFromRemote,
|
||||
.invalidClientChallengeFromDevice,
|
||||
.invalidClientChallengeFromRemote,
|
||||
.invalidSignatureFromDevice:
|
||||
return .lockTrianglebadgeExclamationmark
|
||||
|
||||
// Connection errors
|
||||
case .tooManyRequests,
|
||||
.deviceTimedOut,
|
||||
.deviceNotConnected,
|
||||
.serviceBehindProxyUnavailable:
|
||||
return .antennaRadiowavesLeftAndRightSlash
|
||||
|
||||
// Configuration errors
|
||||
case .serverUrlInvalid, .invalidServerAuthenticationFromRemote:
|
||||
return .gearBadgeQuestionmark
|
||||
}
|
||||
}
|
||||
}
|
57
Sesame/API Extensions/MessageType.swift
Normal file
57
Sesame/API Extensions/MessageType.swift
Normal file
@ -0,0 +1,57 @@
|
||||
import Foundation
|
||||
|
||||
enum MessageType: UInt8 {
|
||||
|
||||
/// The initial message from remote to device to request a challenge.
|
||||
case initial = 0
|
||||
|
||||
/// The second message in an unlock with the challenge from the device to the remote
|
||||
case challenge = 1
|
||||
|
||||
/// The third message with the signed challenge from the remote to the device
|
||||
case request = 2
|
||||
|
||||
/// The final message with the unlock result from the device to the remote
|
||||
case response = 3
|
||||
}
|
||||
|
||||
extension MessageType {
|
||||
|
||||
var encoded: Data {
|
||||
Data([rawValue])
|
||||
}
|
||||
}
|
||||
|
||||
extension MessageType: Codable {
|
||||
|
||||
}
|
||||
|
||||
extension MessageType {
|
||||
|
||||
var responseType: MessageType {
|
||||
switch self {
|
||||
case .initial:
|
||||
return .challenge
|
||||
case .challenge:
|
||||
return .request
|
||||
case .request, .response:
|
||||
return .response
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
extension MessageType: CustomStringConvertible {
|
||||
|
||||
var description: String {
|
||||
switch self {
|
||||
case .initial:
|
||||
return "Initial"
|
||||
case .challenge:
|
||||
return "Challenge"
|
||||
case .request:
|
||||
return "Request"
|
||||
case .response:
|
||||
return "Response"
|
||||
}
|
||||
}
|
||||
}
|
38
Sesame/API Extensions/SignedMessage+Crypto.swift
Normal file
38
Sesame/API Extensions/SignedMessage+Crypto.swift
Normal file
@ -0,0 +1,38 @@
|
||||
import Foundation
|
||||
import CryptoKit
|
||||
|
||||
extension SignedMessage {
|
||||
|
||||
/// The message encoded to data
|
||||
var encoded: Data {
|
||||
mac + message.encoded
|
||||
}
|
||||
|
||||
var bytes: [UInt8] {
|
||||
Array(encoded)
|
||||
}
|
||||
|
||||
/**
|
||||
Create a message from received bytes.
|
||||
- Parameter data: The sequence of bytes
|
||||
- Note: The sequence must contain at least `Message.length` bytes, or the function will crash.
|
||||
*/
|
||||
init(decodeFrom data: Data) throws {
|
||||
guard data.count == SignedMessage.size else {
|
||||
print("Invalid signed message size \(data.count)")
|
||||
throw MessageResult.invalidMessageSizeFromDevice
|
||||
}
|
||||
let count = SHA256.byteCount
|
||||
self.mac = data.prefix(count)
|
||||
self.message = try Message(decodeFrom: data.dropFirst(count))
|
||||
}
|
||||
|
||||
/**
|
||||
Check if the message contains a valid authentication code
|
||||
- Parameter key: The key used to sign the message.
|
||||
- Returns: `true`, if the message is valid.
|
||||
*/
|
||||
func isValid(using key: SymmetricKey) -> Bool {
|
||||
HMAC<SHA256>.isValidAuthenticationCode(mac, authenticating: message.encoded, using: key)
|
||||
}
|
||||
}
|
31
Sesame/API Extensions/SignedMessage.swift
Normal file
31
Sesame/API Extensions/SignedMessage.swift
Normal file
@ -0,0 +1,31 @@
|
||||
import Foundation
|
||||
|
||||
/**
|
||||
An authenticated message to or from the device.
|
||||
*/
|
||||
struct SignedMessage: Equatable, Hashable {
|
||||
|
||||
/// The message authentication code for the message (32 bytes)
|
||||
let mac: Data
|
||||
|
||||
/// The message content
|
||||
let message: Message
|
||||
|
||||
/**
|
||||
Create an authenticated message
|
||||
- Parameter mac: The message authentication code
|
||||
- Parameter content: The message content
|
||||
*/
|
||||
init(mac: Data, message: Message) {
|
||||
self.mac = mac
|
||||
self.message = message
|
||||
}
|
||||
}
|
||||
|
||||
extension SignedMessage: Codable {
|
||||
|
||||
enum CodingKeys: Int, CodingKey {
|
||||
case mac = 1
|
||||
case message = 2
|
||||
}
|
||||
}
|
@ -1,90 +0,0 @@
|
||||
import Foundation
|
||||
import NIOCore
|
||||
|
||||
/**
|
||||
Encapsulates a response from a device.
|
||||
*/
|
||||
struct DeviceResponse {
|
||||
|
||||
/// Shorthand property for a timeout event.
|
||||
static var deviceTimedOut: DeviceResponse {
|
||||
.init(event: .deviceTimedOut)
|
||||
}
|
||||
|
||||
/// Shorthand property for a disconnected event.
|
||||
static var deviceNotConnected: DeviceResponse {
|
||||
.init(event: .deviceNotConnected)
|
||||
}
|
||||
|
||||
/// Shorthand property for a connected event.
|
||||
static var deviceConnected: DeviceResponse {
|
||||
.init(event: .deviceConnected)
|
||||
}
|
||||
|
||||
/// Shorthand property for an unexpected socket event.
|
||||
static var unexpectedSocketEvent: DeviceResponse {
|
||||
.init(event: .unexpectedSocketEvent)
|
||||
}
|
||||
|
||||
/// Shorthand property for an invalid message.
|
||||
static var invalidMessageSize: DeviceResponse {
|
||||
.init(event: .invalidMessageSize)
|
||||
}
|
||||
|
||||
/// Shorthand property for missing body data.
|
||||
static var noBodyData: DeviceResponse {
|
||||
.init(event: .noBodyData)
|
||||
}
|
||||
|
||||
/// Shorthand property for a busy connection
|
||||
static var operationInProgress: DeviceResponse {
|
||||
.init(event: .operationInProgress)
|
||||
}
|
||||
|
||||
/// The response to a key from the server
|
||||
let event: MessageResult
|
||||
|
||||
/// The index of the next key to use
|
||||
let response: Message?
|
||||
|
||||
/**
|
||||
Decode a message from a buffer.
|
||||
|
||||
The buffer must contain `Message.length+1` bytes. The first byte denotes the event type,
|
||||
the remaining bytes contain the message.
|
||||
- Parameter buffer: The buffer where the message bytes are stored
|
||||
*/
|
||||
init?(_ buffer: ByteBuffer) {
|
||||
guard let byte = buffer.getBytes(at: 0, length: 1) else {
|
||||
print("No bytes received from device")
|
||||
return nil
|
||||
}
|
||||
guard let event = MessageResult(rawValue: byte[0]) else {
|
||||
print("Unknown response \(byte[0]) received from device")
|
||||
return nil
|
||||
}
|
||||
self.event = event
|
||||
guard let data = buffer.getSlice(at: 1, length: Message.length) else {
|
||||
self.response = nil
|
||||
return
|
||||
}
|
||||
self.response = Message(decodeFrom: data)
|
||||
}
|
||||
|
||||
/**
|
||||
Create a response from an event without a message from the device.
|
||||
- Parameter event: The response from the device.
|
||||
*/
|
||||
init(event: MessageResult) {
|
||||
self.event = event
|
||||
self.response = nil
|
||||
}
|
||||
|
||||
/// Get the reponse encoded in bytes.
|
||||
var encoded: Data {
|
||||
guard let message = response else {
|
||||
return event.encoded
|
||||
}
|
||||
return event.encoded + message.encoded
|
||||
}
|
||||
}
|
17
Sesame/API/Extensions/Data+Coding.swift
Normal file
17
Sesame/API/Extensions/Data+Coding.swift
Normal file
@ -0,0 +1,17 @@
|
||||
import Foundation
|
||||
|
||||
extension Data {
|
||||
|
||||
func convert<T>(into value: T) -> T {
|
||||
withUnsafeBytes {
|
||||
$0.baseAddress!.load(as: T.self)
|
||||
}
|
||||
}
|
||||
|
||||
init<T>(from value: T) {
|
||||
var target = value
|
||||
self = Swift.withUnsafeBytes(of: &target) {
|
||||
Data($0)
|
||||
}
|
||||
}
|
||||
}
|
@ -40,20 +40,3 @@ extension Data {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
extension Data {
|
||||
|
||||
|
||||
func convert<T>(into value: T) -> T {
|
||||
withUnsafeBytes {
|
||||
$0.baseAddress!.load(as: T.self)
|
||||
}
|
||||
}
|
||||
|
||||
init<T>(from value: T) {
|
||||
var target = value
|
||||
self = Swift.withUnsafeBytes(of: &target) {
|
||||
Data($0)
|
||||
}
|
||||
}
|
||||
}
|
@ -15,4 +15,7 @@ extension UInt32 {
|
||||
var encoded: Data {
|
||||
Data(from: CFSwapInt32HostToLittle(self))
|
||||
}
|
||||
|
||||
/// The size of a `UInt32` when converted to data
|
||||
static let byteSize = MemoryLayout<UInt32>.size
|
||||
}
|
8
Sesame/API/Message+Size.swift
Normal file
8
Sesame/API/Message+Size.swift
Normal file
@ -0,0 +1,8 @@
|
||||
import Foundation
|
||||
|
||||
extension Message {
|
||||
|
||||
/// The byte length of an encoded message content
|
||||
static let size: Int = 2 + 2 * UInt32.byteSize
|
||||
|
||||
}
|
@ -1,179 +0,0 @@
|
||||
import Foundation
|
||||
import NIOCore
|
||||
|
||||
#if canImport(CryptoKit)
|
||||
import CryptoKit
|
||||
#else
|
||||
import Crypto
|
||||
#endif
|
||||
|
||||
/**
|
||||
An authenticated message to or from the device.
|
||||
*/
|
||||
struct Message: Equatable, Hashable {
|
||||
|
||||
/// The message authentication code for the message (32 bytes)
|
||||
let mac: Data
|
||||
|
||||
/// The message content
|
||||
let content: Content
|
||||
|
||||
/**
|
||||
Create an authenticated message
|
||||
- Parameter mac: The message authentication code
|
||||
- Parameter content: The message content
|
||||
*/
|
||||
init(mac: Data, content: Content) {
|
||||
self.mac = mac
|
||||
self.content = content
|
||||
}
|
||||
}
|
||||
|
||||
extension Message: Codable {
|
||||
|
||||
enum CodingKeys: Int, CodingKey {
|
||||
case mac = 1
|
||||
case content = 2
|
||||
}
|
||||
}
|
||||
|
||||
extension Message {
|
||||
|
||||
/**
|
||||
The message content without authentication.
|
||||
*/
|
||||
struct Content: Equatable, Hashable {
|
||||
|
||||
/// The time of message creation, in UNIX time (seconds since 1970)
|
||||
let time: UInt32
|
||||
|
||||
/// The counter of the message (for freshness)
|
||||
let id: UInt32
|
||||
|
||||
let deviceId: UInt8?
|
||||
|
||||
/**
|
||||
Create new message content.
|
||||
- Parameter time: The time of message creation,
|
||||
- Parameter id: The counter of the message
|
||||
*/
|
||||
init(time: UInt32, id: UInt32, device: UInt8) {
|
||||
self.time = time
|
||||
self.id = id
|
||||
self.deviceId = device
|
||||
}
|
||||
|
||||
/**
|
||||
Decode message content from data.
|
||||
|
||||
The data consists of two `UInt32` encoded in little endian format
|
||||
- Warning: The sequence must contain at least 8 bytes, or the function will crash.
|
||||
- Parameter data: The sequence containing the bytes.
|
||||
*/
|
||||
init<T: Sequence>(decodeFrom data: T) where T.Element == UInt8 {
|
||||
self.time = UInt32(data: Data(data.prefix(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
|
||||
static var length: Int {
|
||||
MemoryLayout<UInt32>.size * 2 + 1
|
||||
}
|
||||
|
||||
/// The message content encoded to data
|
||||
var encoded: Data {
|
||||
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 {
|
||||
|
||||
/// The length of a message in bytes
|
||||
static var length: Int {
|
||||
SHA256.byteCount + Content.length
|
||||
}
|
||||
|
||||
/**
|
||||
Decode a message from a byte buffer.
|
||||
The buffer must contain at least `Message.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: Message.length) else {
|
||||
return nil
|
||||
}
|
||||
self.init(decodeFrom: data)
|
||||
}
|
||||
|
||||
init?(decodeFrom data: Data, index: inout Int) {
|
||||
guard index + Message.length <= data.count else {
|
||||
return nil
|
||||
}
|
||||
self.init(decodeFrom: data.advanced(by: index))
|
||||
index += Message.length
|
||||
}
|
||||
|
||||
/// The message encoded to data
|
||||
var encoded: Data {
|
||||
mac + content.encoded
|
||||
}
|
||||
|
||||
var bytes: [UInt8] {
|
||||
Array(encoded)
|
||||
}
|
||||
|
||||
/**
|
||||
Create a message from received bytes.
|
||||
- Parameter data: The sequence of bytes
|
||||
- Note: The sequence must contain at least `Message.length` bytes, or the function will crash.
|
||||
*/
|
||||
init<T: Sequence>(decodeFrom data: T) where T.Element == UInt8 {
|
||||
let count = SHA256.byteCount
|
||||
self.mac = Data(data.prefix(count))
|
||||
self.content = .init(decodeFrom: Array(data.dropFirst(count)))
|
||||
}
|
||||
|
||||
/**
|
||||
Check if the message contains a valid authentication code
|
||||
- Parameter key: The key used to sign the message.
|
||||
- Returns: `true`, if the message is valid.
|
||||
*/
|
||||
func isValid(using key: SymmetricKey) -> Bool {
|
||||
HMAC<SHA256>.isValidAuthenticationCode(mac, authenticating: content.encoded, using: key)
|
||||
}
|
||||
}
|
||||
|
||||
extension Message.Content {
|
||||
|
||||
/**
|
||||
Calculate an authentication code for the message content.
|
||||
- Parameter key: The key to use to sign the content.
|
||||
- Returns: The new message signed with the key.
|
||||
*/
|
||||
func authenticate(using key: SymmetricKey) -> Message {
|
||||
let mac = HMAC<SHA256>.authenticationCode(for: encoded, using: key)
|
||||
return .init(mac: Data(mac.map { $0 }), content: self)
|
||||
}
|
||||
|
||||
/**
|
||||
Calculate an authentication code for the message content and convert everything to data.
|
||||
- Parameter key: The key to use to sign the content.
|
||||
- Returns: The new message signed with the key, serialized to bytes.
|
||||
*/
|
||||
func authenticateAndSerialize(using key: SymmetricKey) -> Data {
|
||||
let encoded = self.encoded
|
||||
let mac = HMAC<SHA256>.authenticationCode(for: encoded, using: key)
|
||||
return Data(mac.map { $0 }) + encoded
|
||||
}
|
||||
}
|
@ -5,92 +5,231 @@ import Foundation
|
||||
*/
|
||||
enum MessageResult: UInt8 {
|
||||
|
||||
/// Text content was received, although binary data was expected
|
||||
// MARK: Device status
|
||||
|
||||
/// The message was accepted.
|
||||
case messageAccepted = 0
|
||||
|
||||
/// The web socket received text while waiting for binary data.
|
||||
case textReceived = 1
|
||||
|
||||
/// A socket event on the device was unexpected (not binary data)
|
||||
/// An unexpected socket event occured while performing the exchange.
|
||||
case unexpectedSocketEvent = 2
|
||||
|
||||
/// The size of the payload (i.e. message) was invalid
|
||||
case invalidMessageSize = 3
|
||||
/// The received message size is invalid.
|
||||
case invalidMessageSizeFromRemote = 3
|
||||
|
||||
/// The transmitted message could not be authenticated using the key
|
||||
case messageAuthenticationFailed = 4
|
||||
/// The message signature was incorrect.
|
||||
case invalidSignatureFromRemote = 4
|
||||
|
||||
/// The message time was not within the acceptable bounds
|
||||
case messageTimeMismatch = 5
|
||||
/// The server challenge of the message did not match previous messages
|
||||
case invalidServerChallengeFromRemote = 5
|
||||
|
||||
/// A later key has been used, invalidating this key (to prevent replay attacks after blocked communication)
|
||||
case messageCounterInvalid = 6
|
||||
/// The client challenge of the message did not match previous messages
|
||||
case invalidClientChallengeFromRemote = 6
|
||||
|
||||
/// The key was accepted by the device, and the door will be opened
|
||||
case messageAccepted = 7
|
||||
/// An unexpected or unsupported message type was received
|
||||
case invalidMessageTypeFromRemote = 7
|
||||
|
||||
/// The device id is invalid
|
||||
case messageDeviceInvalid = 8
|
||||
/// A message is already being processed
|
||||
case tooManyRequests = 8
|
||||
|
||||
/// The received message result was not ``messageAccepted``
|
||||
case invalidMessageResultFromRemote = 9
|
||||
|
||||
/// The request did not contain body data with the key
|
||||
case noBodyData = 10
|
||||
/// An invalid Url parameter was set sending a message to the device over a local connection
|
||||
case invalidUrlParameter = 10
|
||||
|
||||
/// The device is not connected
|
||||
case deviceNotConnected = 12
|
||||
// MARK: Server status
|
||||
|
||||
/// The device did not respond within the timeout
|
||||
case deviceTimedOut = 13
|
||||
/// The body data posting a message was missing or of wrong length
|
||||
case noOrInvalidBodyDataFromRemote = 21
|
||||
|
||||
/// Another message is being processed by the device
|
||||
case operationInProgress = 14
|
||||
/// The authentication token for the server was invalid
|
||||
case invalidServerAuthenticationFromRemote = 22
|
||||
|
||||
/// The device is connected
|
||||
case deviceConnected = 15
|
||||
/// The request took too long to complete
|
||||
case deviceTimedOut = 23
|
||||
|
||||
case invalidUrlParameter = 20
|
||||
/// The device is not connected to the server via web socket
|
||||
case deviceNotConnected = 24
|
||||
|
||||
/// The device sent a response of invalid size
|
||||
case invalidMessageSizeFromDevice = 25
|
||||
|
||||
/// The header with the authentication token was missing or invalid (not a hex string) from a server request.
|
||||
case missingOrInvalidAuthenticationHeaderFromRemote = 26
|
||||
|
||||
/// The server produced an internal error (500)
|
||||
case internalServerError = 27
|
||||
|
||||
// MARK: Remote status
|
||||
|
||||
/// The initial state without information about the connection
|
||||
case notChecked = 30
|
||||
|
||||
/// The url string is not a valid url
|
||||
case serverUrlInvalid = 31
|
||||
|
||||
/// The device key or auth token is missing for a request.
|
||||
case noKeyAvailable = 32
|
||||
|
||||
/// The Sesame server behind the proxy could not be found (502)
|
||||
case serviceBehindProxyUnavailable = 33
|
||||
|
||||
/// The server url could not be found (404)
|
||||
case pathOnServerNotFound = 34
|
||||
|
||||
/// The url session request returned an unknown response
|
||||
case unexpectedUrlResponseType = 35
|
||||
|
||||
/// The request to the server returned an unhandled HTTP code
|
||||
case unexpectedServerResponseCode = 36
|
||||
|
||||
/// A valid server challenge was received
|
||||
case deviceAvailable = 37
|
||||
|
||||
case invalidSignatureFromDevice = 38
|
||||
|
||||
case invalidMessageTypeFromDevice = 39
|
||||
|
||||
case unknownMessageResultFromDevice = 40
|
||||
|
||||
/// The device sent a message with an invalid client challenge
|
||||
case invalidClientChallengeFromDevice = 41
|
||||
|
||||
/// The device used an invalid server challenge in a response
|
||||
case invalidServerChallengeFromDevice = 42
|
||||
|
||||
/// The unlock process was successfully completed
|
||||
case unlocked = 43
|
||||
}
|
||||
|
||||
extension MessageResult: Error {
|
||||
|
||||
case invalidResponseAuthentication = 21
|
||||
}
|
||||
|
||||
extension MessageResult: CustomStringConvertible {
|
||||
|
||||
var description: String {
|
||||
switch self {
|
||||
case .messageAccepted:
|
||||
return "Message accepted"
|
||||
case .textReceived:
|
||||
return "The device received unexpected text"
|
||||
case .unexpectedSocketEvent:
|
||||
return "Unexpected socket event for the device"
|
||||
case .invalidMessageSize:
|
||||
return "Invalid message data"
|
||||
case .messageAuthenticationFailed:
|
||||
case .invalidMessageSizeFromRemote:
|
||||
return "Invalid message data from remote"
|
||||
case .invalidSignatureFromRemote:
|
||||
return "Message authentication failed"
|
||||
case .messageTimeMismatch:
|
||||
return "Message time invalid"
|
||||
case .messageCounterInvalid:
|
||||
return "Message counter invalid"
|
||||
case .messageAccepted:
|
||||
return "Message accepted"
|
||||
case .messageDeviceInvalid:
|
||||
return "Invalid device ID"
|
||||
case .noBodyData:
|
||||
return "No body data included in the request"
|
||||
case .deviceNotConnected:
|
||||
return "Device not connected"
|
||||
case .deviceTimedOut:
|
||||
return "The device did not respond"
|
||||
case .operationInProgress:
|
||||
return "Another operation is in progress"
|
||||
case .deviceConnected:
|
||||
return "The device is connected"
|
||||
case .invalidServerChallengeFromRemote:
|
||||
return "Remote used wrong server challenge"
|
||||
case .invalidClientChallengeFromRemote:
|
||||
return "Wrong client challenge sent"
|
||||
case .invalidMessageTypeFromRemote:
|
||||
return "Message type from remote invalid"
|
||||
case .tooManyRequests:
|
||||
return "Device busy"
|
||||
case .invalidMessageResultFromRemote:
|
||||
return "Invalid message result"
|
||||
case .invalidUrlParameter:
|
||||
return "The url parameter could not be found"
|
||||
case .invalidResponseAuthentication:
|
||||
return "The response could not be authenticated"
|
||||
|
||||
case .noOrInvalidBodyDataFromRemote:
|
||||
return "Invalid body data in server request"
|
||||
case .invalidServerAuthenticationFromRemote:
|
||||
return "Invalid server token"
|
||||
case .deviceTimedOut:
|
||||
return "The device did not respond"
|
||||
case .deviceNotConnected:
|
||||
return "Device not connected to server"
|
||||
case .invalidMessageSizeFromDevice:
|
||||
return "Invalid device message size"
|
||||
case .missingOrInvalidAuthenticationHeaderFromRemote:
|
||||
return "Invalid server token format"
|
||||
case .internalServerError:
|
||||
return "Internal server error"
|
||||
|
||||
case .notChecked:
|
||||
return "Not checked"
|
||||
case .serverUrlInvalid:
|
||||
return "Invalid server url"
|
||||
case .noKeyAvailable:
|
||||
return "No key available"
|
||||
case .serviceBehindProxyUnavailable:
|
||||
return "Service behind proxy not found"
|
||||
case .pathOnServerNotFound:
|
||||
return "Invalid server path"
|
||||
case .unexpectedUrlResponseType:
|
||||
return "Unexpected URL response"
|
||||
case .unexpectedServerResponseCode:
|
||||
return "Unexpected server response code"
|
||||
case .deviceAvailable:
|
||||
return "Device available"
|
||||
case .invalidSignatureFromDevice:
|
||||
return "Invalid device signature"
|
||||
case .invalidMessageTypeFromDevice:
|
||||
return "Message type from device invalid"
|
||||
case .unknownMessageResultFromDevice:
|
||||
return "Unknown message result"
|
||||
case .invalidClientChallengeFromDevice:
|
||||
return "Device used wrong client challenge"
|
||||
case .invalidServerChallengeFromDevice:
|
||||
return "Invalid"
|
||||
case .unlocked:
|
||||
return "Unlocked"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
extension MessageResult: Codable {
|
||||
|
||||
}
|
||||
|
||||
extension MessageResult {
|
||||
|
||||
var encoded: Data {
|
||||
Data([rawValue])
|
||||
}
|
||||
}
|
||||
|
||||
extension MessageResult {
|
||||
|
||||
init(httpCode: Int) {
|
||||
switch httpCode {
|
||||
case 200: self = .messageAccepted
|
||||
case 204: self = .noOrInvalidBodyDataFromRemote
|
||||
case 403: self = .invalidServerAuthenticationFromRemote
|
||||
case 404: self = .pathOnServerNotFound
|
||||
case 408: self = .deviceTimedOut
|
||||
case 412: self = .deviceNotConnected
|
||||
case 413: self = .invalidMessageSizeFromDevice
|
||||
case 422: self = .missingOrInvalidAuthenticationHeaderFromRemote
|
||||
case 429: self = .tooManyRequests
|
||||
case 500: self = .internalServerError
|
||||
case 501: self = .unexpectedServerResponseCode
|
||||
case 502: self = .serviceBehindProxyUnavailable
|
||||
default: self = .unexpectedServerResponseCode
|
||||
}
|
||||
}
|
||||
|
||||
var statusCode: Int {
|
||||
switch self {
|
||||
case .messageAccepted: return 200 // ok
|
||||
case .noOrInvalidBodyDataFromRemote: return 204 // noContent
|
||||
case .invalidServerAuthenticationFromRemote: return 403 // forbidden
|
||||
case .pathOnServerNotFound: return 404 // notFound
|
||||
case .deviceTimedOut: return 408 // requestTimeout
|
||||
case .invalidMessageSizeFromRemote: return 411 // lengthRequired
|
||||
case .deviceNotConnected: return 412 // preconditionFailed
|
||||
case .invalidMessageSizeFromDevice: return 413 // payloadTooLarge
|
||||
case .missingOrInvalidAuthenticationHeaderFromRemote: return 422 // unprocessableEntity
|
||||
case .tooManyRequests: return 429 // tooManyRequests
|
||||
case .internalServerError: return 500 // internalServerError
|
||||
case .unexpectedServerResponseCode: return 501 // notImplemented
|
||||
case .serviceBehindProxyUnavailable: return 502 // badGateway
|
||||
default: return 501 // == unexpectedServerResponseCode
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -1,26 +0,0 @@
|
||||
import Foundation
|
||||
import NIOCore
|
||||
|
||||
#if canImport(CryptoKit)
|
||||
import CryptoKit
|
||||
#else
|
||||
import Crypto
|
||||
#endif
|
||||
|
||||
struct ServerMessage {
|
||||
|
||||
static let authTokenSize = SHA256.byteCount
|
||||
|
||||
let authToken: Data
|
||||
|
||||
let message: Message
|
||||
|
||||
init(authToken: Data, message: Message) {
|
||||
self.authToken = authToken
|
||||
self.message = message
|
||||
}
|
||||
|
||||
var encoded: Data {
|
||||
authToken + message.encoded
|
||||
}
|
||||
}
|
14
Sesame/API/SesameHeader.swift
Normal file
14
Sesame/API/SesameHeader.swift
Normal file
@ -0,0 +1,14 @@
|
||||
import Foundation
|
||||
#if canImport(CryptoKit)
|
||||
import CryptoKit
|
||||
#else
|
||||
import Crypto
|
||||
#endif
|
||||
|
||||
enum SesameHeader {
|
||||
|
||||
static let authenticationHeader = "Authorization"
|
||||
|
||||
static let serverAuthenticationTokenSize = SHA256.byteCount
|
||||
|
||||
}
|
@ -3,10 +3,7 @@ import Foundation
|
||||
/**
|
||||
The active urls on the server, for the device and the remote to connect
|
||||
*/
|
||||
enum RouteAPI: String {
|
||||
|
||||
/// Check the device status
|
||||
case getDeviceStatus = "status"
|
||||
enum SesameRoute: String {
|
||||
|
||||
/// Send a message to the server, to relay to the device
|
||||
case postMessage = "message"
|
15
Sesame/API/SignedMessage+Size.swift
Normal file
15
Sesame/API/SignedMessage+Size.swift
Normal file
@ -0,0 +1,15 @@
|
||||
import Foundation
|
||||
|
||||
#if canImport(CryptoKit)
|
||||
import CryptoKit
|
||||
#else
|
||||
import Crypto
|
||||
#endif
|
||||
|
||||
extension SignedMessage {
|
||||
|
||||
/// The length of a message in bytes
|
||||
static var size: Int {
|
||||
SHA256.byteCount + Message.size
|
||||
}
|
||||
}
|
18
Sesame/Common/ActiveRequestType.swift
Normal file
18
Sesame/Common/ActiveRequestType.swift
Normal file
@ -0,0 +1,18 @@
|
||||
import Foundation
|
||||
|
||||
enum RequestType {
|
||||
case challenge
|
||||
case unlock
|
||||
}
|
||||
|
||||
extension RequestType: CustomStringConvertible {
|
||||
|
||||
var description: String {
|
||||
switch self {
|
||||
case .challenge:
|
||||
return "Challenge"
|
||||
case .unlock:
|
||||
return "Unlock"
|
||||
}
|
||||
}
|
||||
}
|
@ -3,91 +3,100 @@ import CryptoKit
|
||||
|
||||
final class Client {
|
||||
|
||||
// TODO: Use or delete
|
||||
private let delegate = NeverCacheDelegate()
|
||||
private let localRequestRoute = "message"
|
||||
|
||||
private let urlMessageParameter = "m"
|
||||
|
||||
init() {}
|
||||
|
||||
func deviceStatus(authToken: Data, server: String) async -> ClientState {
|
||||
await send(path: .getDeviceStatus, server: server, data: authToken).state
|
||||
func send(_ message: Message, to url: String, through route: TransmissionType, using keys: KeySet) async -> ServerResponse {
|
||||
let sentTime = Date.now
|
||||
let signedMessage = message.authenticate(using: keys.remote)
|
||||
let response: Message
|
||||
switch route {
|
||||
case .throughServer:
|
||||
response = await send(signedMessage, toServerUrl: url, authenticateWith: keys.server, verifyUsing: keys.device)
|
||||
|
||||
case .overLocalWifi:
|
||||
response = await send(signedMessage, toLocalDeviceUrl: url, verifyUsing: keys.device)
|
||||
}
|
||||
let receivedTime = Date.now
|
||||
// Create best guess for creation of challenge.
|
||||
let roundTripTime = receivedTime.timeIntervalSince(sentTime)
|
||||
let serverChallenge = ServerChallenge(
|
||||
creationDate: sentTime.addingTimeInterval(roundTripTime / 2),
|
||||
message: response)
|
||||
|
||||
// Validate message content
|
||||
guard response.result == .messageAccepted else {
|
||||
print("Failure: \(response)")
|
||||
return (response, nil)
|
||||
}
|
||||
|
||||
func sendMessageOverLocalNetwork(_ message: Message, server: String) async -> (state: ClientState, response: Message?) {
|
||||
guard response.clientChallenge == message.clientChallenge else {
|
||||
print("Invalid client challenge: \(response)")
|
||||
return (response.with(result: .invalidClientChallengeFromDevice), nil)
|
||||
}
|
||||
return (response, serverChallenge)
|
||||
}
|
||||
|
||||
|
||||
private func send(_ message: SignedMessage, toLocalDeviceUrl server: String, verifyUsing deviceKey: SymmetricKey) async -> Message {
|
||||
let data = message.encoded.hexEncoded
|
||||
guard let url = URL(string: server + "message?m=\(data)") else {
|
||||
return (.internalError("Invalid server url"), nil)
|
||||
guard let url = URL(string: server)?.appendingPathComponent("\(localRequestRoute)?\(urlMessageParameter)=\(data)") else {
|
||||
return message.message.with(result: .serverUrlInvalid)
|
||||
}
|
||||
|
||||
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)
|
||||
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(to url: URL, data: Data) async -> (state: ClientState, response: Message?) {
|
||||
var request = URLRequest(url: url)
|
||||
request.httpBody = data
|
||||
request.httpMethod = "POST"
|
||||
request.timeoutInterval = 10
|
||||
return await requestAndDecode(request)
|
||||
return await perform(request, inResponseTo: message.message, verifyUsing: deviceKey)
|
||||
}
|
||||
|
||||
private func requestAndDecode(_ request: URLRequest) async -> (state: ClientState, response: Message?) {
|
||||
guard let data = await fulfill(request) else {
|
||||
return (.deviceNotAvailable(.serverNotReached), nil)
|
||||
}
|
||||
guard let byte = data.first else {
|
||||
return (.internalError("Empty response"), nil)
|
||||
}
|
||||
guard let status = MessageResult(rawValue: byte) else {
|
||||
return (.internalError("Invalid message response: \(byte)"), nil)
|
||||
}
|
||||
let result = ClientState(keyResult: status)
|
||||
guard data.count == Message.length + 1 else {
|
||||
if data.count != 1 {
|
||||
print("Device response with only \(data.count) bytes")
|
||||
}
|
||||
return (result, nil)
|
||||
}
|
||||
let messageData = Array(data.advanced(by: 1))
|
||||
let message = Message(decodeFrom: messageData)
|
||||
return (result, message)
|
||||
private func send(_ message: SignedMessage, toServerUrl server: String, authenticateWith authToken: Data, verifyUsing deviceKey: SymmetricKey) async -> Message {
|
||||
guard let url = URL(string: server)?.appendingPathComponent(SesameRoute.postMessage.rawValue) else {
|
||||
return message.message.with(result: .serverUrlInvalid)
|
||||
}
|
||||
|
||||
private func fulfill(_ request: URLRequest) async -> Data? {
|
||||
var request = URLRequest(url: url)
|
||||
request.httpBody = message.encoded
|
||||
request.httpMethod = "POST"
|
||||
request.timeoutInterval = 10
|
||||
request.addValue(authToken.hexEncoded, forHTTPHeaderField: SesameHeader.authenticationHeader)
|
||||
return await perform(request, inResponseTo: message.message, verifyUsing: deviceKey)
|
||||
}
|
||||
|
||||
private func perform(_ request: URLRequest, inResponseTo message: Message, verifyUsing deviceKey: SymmetricKey) async -> Message {
|
||||
let (response, responseData) = await fulfill(request)
|
||||
guard response == .messageAccepted, let data = responseData else {
|
||||
return message.with(result: response)
|
||||
}
|
||||
guard data.count == SignedMessage.size else {
|
||||
print("[WARN] Received message with \(data.count) bytes (\(Array(data)))")
|
||||
return message.with(result: .invalidMessageSizeFromDevice)
|
||||
}
|
||||
let decodedMessage: SignedMessage
|
||||
do {
|
||||
decodedMessage = try SignedMessage(decodeFrom: data)
|
||||
} catch {
|
||||
return message.with(result: error as! MessageResult)
|
||||
}
|
||||
guard decodedMessage.isValid(using: deviceKey) else {
|
||||
return message.with(result: .invalidSignatureFromDevice)
|
||||
}
|
||||
return decodedMessage.message
|
||||
}
|
||||
|
||||
private func fulfill(_ request: URLRequest) async -> (response: MessageResult, data: Data?) {
|
||||
do {
|
||||
let (data, response) = try await URLSession.shared.data(for: request)
|
||||
guard let code = (response as? HTTPURLResponse)?.statusCode else {
|
||||
print("No response from server")
|
||||
return nil
|
||||
return (.unexpectedUrlResponseType, nil)
|
||||
}
|
||||
guard code == 200 else {
|
||||
print("Invalid server response \(code)")
|
||||
return nil
|
||||
}
|
||||
return data
|
||||
return (.init(httpCode: code), data)
|
||||
} catch {
|
||||
print("Request failed: \(error)")
|
||||
return nil
|
||||
return (.deviceTimedOut, nil)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
class NeverCacheDelegate: NSObject, NSURLConnectionDataDelegate {
|
||||
|
||||
func connection(_ connection: NSURLConnection, willCacheResponse cachedResponse: CachedURLResponse) -> CachedURLResponse? {
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
@ -1,371 +0,0 @@
|
||||
import Foundation
|
||||
import SwiftUI
|
||||
import SFSafeSymbols
|
||||
|
||||
enum ConnectionError {
|
||||
case serverNotReached
|
||||
case deviceDisconnected
|
||||
}
|
||||
|
||||
extension ConnectionError: CustomStringConvertible {
|
||||
|
||||
var description: String {
|
||||
switch self {
|
||||
case .serverNotReached:
|
||||
return "Server unavailable"
|
||||
case .deviceDisconnected:
|
||||
return "Device disconnected"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
enum RejectionCause {
|
||||
case invalidDeviceId
|
||||
case invalidCounter
|
||||
case invalidTime
|
||||
case invalidAuthentication
|
||||
case timeout
|
||||
case missingKey
|
||||
}
|
||||
|
||||
extension RejectionCause: CustomStringConvertible {
|
||||
|
||||
var description: String {
|
||||
switch self {
|
||||
case .invalidDeviceId:
|
||||
return "Invalid device ID"
|
||||
case .invalidCounter:
|
||||
return "Invalid counter"
|
||||
case .invalidTime:
|
||||
return "Invalid time"
|
||||
case .invalidAuthentication:
|
||||
return "Invalid authentication"
|
||||
case .timeout:
|
||||
return "Device not responding"
|
||||
case .missingKey:
|
||||
return "No key to verify message"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
enum ClientState {
|
||||
|
||||
/// There is no key stored locally on the client. A new key must be generated before use.
|
||||
case noKeyAvailable
|
||||
|
||||
/// The device status is being requested
|
||||
case requestingStatus
|
||||
|
||||
/// The remote device is not connected (no socket opened)
|
||||
case deviceNotAvailable(ConnectionError)
|
||||
|
||||
/// The device is connected and ready to receive a message
|
||||
case ready
|
||||
|
||||
/// The message is being transmitted and a response is awaited
|
||||
case waitingForResponse
|
||||
|
||||
/// The transmitted message was rejected (multiple possible reasons)
|
||||
case messageRejected(RejectionCause)
|
||||
|
||||
case responseRejected(RejectionCause)
|
||||
|
||||
/// The device responded that the opening action was started
|
||||
case openSesame
|
||||
|
||||
case internalError(String)
|
||||
|
||||
var canSendKey: Bool {
|
||||
switch self {
|
||||
case .ready, .openSesame, .messageRejected:
|
||||
return true
|
||||
default:
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
init(keyResult: MessageResult) {
|
||||
switch keyResult {
|
||||
case .messageAuthenticationFailed:
|
||||
self = .messageRejected(.invalidAuthentication)
|
||||
case .messageTimeMismatch:
|
||||
self = .messageRejected(.invalidTime)
|
||||
case .messageCounterInvalid:
|
||||
self = .messageRejected(.invalidCounter)
|
||||
case .deviceTimedOut:
|
||||
self = .messageRejected(.timeout)
|
||||
case .messageAccepted:
|
||||
self = .openSesame
|
||||
case .messageDeviceInvalid:
|
||||
self = .messageRejected(.invalidDeviceId)
|
||||
case .noBodyData, .invalidMessageSize, .textReceived, .unexpectedSocketEvent, .invalidUrlParameter, .invalidResponseAuthentication:
|
||||
print("Unexpected internal error: \(keyResult)")
|
||||
self = .internalError(keyResult.description)
|
||||
case .deviceNotConnected:
|
||||
self = .deviceNotAvailable(.deviceDisconnected)
|
||||
case .operationInProgress:
|
||||
self = .waitingForResponse
|
||||
case .deviceConnected:
|
||||
self = .ready
|
||||
}
|
||||
}
|
||||
|
||||
var actionText: String {
|
||||
switch self {
|
||||
case .noKeyAvailable:
|
||||
return "No key"
|
||||
case .requestingStatus:
|
||||
return "Checking..."
|
||||
case .deviceNotAvailable(let connectionError):
|
||||
switch connectionError {
|
||||
case .serverNotReached:
|
||||
return "Server not found"
|
||||
case .deviceDisconnected:
|
||||
return "Device disconnected"
|
||||
}
|
||||
case .ready:
|
||||
return "Unlock"
|
||||
case .waitingForResponse:
|
||||
return "Unlocking..."
|
||||
case .messageRejected(let rejectionCause):
|
||||
switch rejectionCause {
|
||||
case .invalidDeviceId:
|
||||
return "Invalid device ID"
|
||||
case .invalidCounter:
|
||||
return "Invalid counter"
|
||||
case .invalidTime:
|
||||
return "Invalid timestamp"
|
||||
case .invalidAuthentication:
|
||||
return "Invalid signature"
|
||||
case .timeout:
|
||||
return "Device not responding"
|
||||
case .missingKey:
|
||||
return "Device key missing"
|
||||
}
|
||||
case .responseRejected(let rejectionCause):
|
||||
switch rejectionCause {
|
||||
case .invalidDeviceId:
|
||||
return "Invalid device id (response)"
|
||||
case .invalidCounter:
|
||||
return "Invalid counter (response)"
|
||||
case .invalidTime:
|
||||
return "Invalid time (response)"
|
||||
case .invalidAuthentication:
|
||||
return "Invalid signature (response)"
|
||||
case .timeout:
|
||||
return "Timed out (response)"
|
||||
case .missingKey:
|
||||
return "Missing key (response)"
|
||||
}
|
||||
case .openSesame:
|
||||
return "Unlocked"
|
||||
case .internalError(let string):
|
||||
return string
|
||||
}
|
||||
}
|
||||
|
||||
var requiresDescription: Bool {
|
||||
switch self {
|
||||
case .deviceNotAvailable, .messageRejected, .internalError, .responseRejected:
|
||||
return true
|
||||
default:
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
var color: Color {
|
||||
switch self {
|
||||
case .noKeyAvailable:
|
||||
return Color(red: 50/255, green: 50/255, blue: 50/255)
|
||||
case .deviceNotAvailable:
|
||||
return Color(red: 150/255, green: 90/255, blue: 90/255)
|
||||
case .messageRejected, .responseRejected:
|
||||
return Color(red: 160/255, green: 30/255, blue: 30/255)
|
||||
case .internalError:
|
||||
return Color(red: 100/255, green: 0/255, blue: 0/255)
|
||||
case .ready:
|
||||
return Color(red: 115/255, green: 140/255, blue: 90/255)
|
||||
case .requestingStatus, .waitingForResponse:
|
||||
return Color(red: 160/255, green: 170/255, blue: 110/255)
|
||||
case .openSesame:
|
||||
return Color(red: 65/255, green: 110/255, blue: 60/255)
|
||||
}
|
||||
}
|
||||
|
||||
var allowsAction: Bool {
|
||||
switch self {
|
||||
case .noKeyAvailable, .waitingForResponse:
|
||||
return false
|
||||
default:
|
||||
return true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
extension ClientState: Equatable {
|
||||
|
||||
}
|
||||
|
||||
extension ClientState: CustomStringConvertible {
|
||||
|
||||
var description: String {
|
||||
switch self {
|
||||
case .noKeyAvailable:
|
||||
return "No key set."
|
||||
case .requestingStatus:
|
||||
return "Checking device status"
|
||||
case .deviceNotAvailable(let status):
|
||||
return status.description
|
||||
case .ready:
|
||||
return "Ready"
|
||||
case .waitingForResponse:
|
||||
return "Unlocking..."
|
||||
case .messageRejected(let cause):
|
||||
return cause.description
|
||||
case .openSesame:
|
||||
return "Unlocked"
|
||||
case .internalError(let e):
|
||||
return "Error: \(e)"
|
||||
case .responseRejected(let cause):
|
||||
switch cause {
|
||||
case .invalidAuthentication:
|
||||
return "Device message not authenticated"
|
||||
default:
|
||||
return cause.description
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: Coding
|
||||
|
||||
extension ClientState {
|
||||
|
||||
var encoded: Data {
|
||||
Data([code])
|
||||
}
|
||||
|
||||
var code: UInt8 {
|
||||
switch self {
|
||||
case .noKeyAvailable:
|
||||
return 1
|
||||
case .requestingStatus:
|
||||
return 2
|
||||
case .deviceNotAvailable(let connectionError):
|
||||
switch connectionError {
|
||||
case .serverNotReached:
|
||||
return 3
|
||||
case .deviceDisconnected:
|
||||
return 4
|
||||
}
|
||||
case .ready:
|
||||
return 5
|
||||
case .waitingForResponse:
|
||||
return 6
|
||||
case .messageRejected(let rejectionCause):
|
||||
switch rejectionCause {
|
||||
case .invalidDeviceId:
|
||||
return 19
|
||||
case .invalidCounter:
|
||||
return 7
|
||||
case .invalidTime:
|
||||
return 8
|
||||
case .invalidAuthentication:
|
||||
return 9
|
||||
case .timeout:
|
||||
return 10
|
||||
case .missingKey:
|
||||
return 11
|
||||
}
|
||||
case .responseRejected(let rejectionCause):
|
||||
switch rejectionCause {
|
||||
case .invalidCounter:
|
||||
return 12
|
||||
case .invalidTime:
|
||||
return 13
|
||||
case .invalidAuthentication:
|
||||
return 14
|
||||
case .timeout:
|
||||
return 15
|
||||
case .missingKey:
|
||||
return 16
|
||||
case .invalidDeviceId:
|
||||
return 20
|
||||
}
|
||||
case .openSesame:
|
||||
return 17
|
||||
case .internalError:
|
||||
return 18
|
||||
}
|
||||
}
|
||||
|
||||
init(code: UInt8) {
|
||||
switch code {
|
||||
case 1:
|
||||
self = .noKeyAvailable
|
||||
case 2:
|
||||
self = .requestingStatus
|
||||
case 3:
|
||||
self = .deviceNotAvailable(.serverNotReached)
|
||||
case 4:
|
||||
self = .deviceNotAvailable(.deviceDisconnected)
|
||||
case 5:
|
||||
self = .ready
|
||||
case 6:
|
||||
self = .waitingForResponse
|
||||
case 7:
|
||||
self = .messageRejected(.invalidCounter)
|
||||
case 8:
|
||||
self = .messageRejected(.invalidTime)
|
||||
case 9:
|
||||
self = .messageRejected(.invalidAuthentication)
|
||||
case 10:
|
||||
self = .messageRejected(.timeout)
|
||||
case 11:
|
||||
self = .messageRejected(.missingKey)
|
||||
case 12:
|
||||
self = .responseRejected(.invalidCounter)
|
||||
case 13:
|
||||
self = .responseRejected(.invalidTime)
|
||||
case 14:
|
||||
self = .responseRejected(.invalidAuthentication)
|
||||
case 15:
|
||||
self = .responseRejected(.timeout)
|
||||
case 16:
|
||||
self = .responseRejected(.missingKey)
|
||||
case 17:
|
||||
self = .openSesame
|
||||
case 18:
|
||||
self = .internalError("")
|
||||
case 19:
|
||||
self = .messageRejected(.invalidDeviceId)
|
||||
case 20:
|
||||
self = .responseRejected(.invalidDeviceId)
|
||||
default:
|
||||
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,10 +1,27 @@
|
||||
import Foundation
|
||||
import CryptoKit
|
||||
|
||||
enum ConnectionStrategy: String, CaseIterable, Identifiable {
|
||||
case local = "Local"
|
||||
case localFirst = "Local first"
|
||||
case remote = "Remote"
|
||||
case remoteFirst = "Remote first"
|
||||
enum ConnectionStrategy: Int, CaseIterable, Identifiable {
|
||||
case local = 0
|
||||
case remote = 1
|
||||
case localFirst = 2
|
||||
case remoteFirst = 3
|
||||
|
||||
var id: Self { self }
|
||||
var id: Int { rawValue }
|
||||
|
||||
var transmissionTypes: [TransmissionType] {
|
||||
switch self {
|
||||
case .local: return [.overLocalWifi]
|
||||
case .localFirst: return [.overLocalWifi, .throughServer]
|
||||
case .remote: return [.throughServer]
|
||||
case .remoteFirst: return [.throughServer, .overLocalWifi]
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
extension ConnectionStrategy: CustomStringConvertible {
|
||||
|
||||
var description: String {
|
||||
transmissionTypes.map { $0.displayName }.joined(separator: "+")
|
||||
}
|
||||
}
|
||||
|
9
Sesame/Common/Extensions/Text+Extensions.swift
Normal file
9
Sesame/Common/Extensions/Text+Extensions.swift
Normal file
@ -0,0 +1,9 @@
|
||||
import Foundation
|
||||
import SwiftUI
|
||||
|
||||
extension Text {
|
||||
|
||||
init(display: CustomStringConvertible) {
|
||||
self.init(display.description)
|
||||
}
|
||||
}
|
8
Sesame/Common/Extensions/UInt32+Random.swift
Normal file
8
Sesame/Common/Extensions/UInt32+Random.swift
Normal file
@ -0,0 +1,8 @@
|
||||
import Foundation
|
||||
|
||||
extension UInt32 {
|
||||
|
||||
static func random() -> UInt32 {
|
||||
random(in: UInt32.min...UInt32.max)
|
||||
}
|
||||
}
|
@ -1,6 +1,7 @@
|
||||
import Foundation
|
||||
import CBORCoding
|
||||
|
||||
/*
|
||||
class HistoryManagerBase: ObservableObject {
|
||||
|
||||
@Published
|
||||
@ -141,3 +142,4 @@ final class HistoryManagerMock: HistoryManagerBase, HistoryManagerProtocol {
|
||||
return true
|
||||
}
|
||||
}
|
||||
*/
|
||||
|
@ -129,9 +129,8 @@ final class KeyManagement: ObservableObject {
|
||||
@Published
|
||||
private(set) var hasAuthToken = false
|
||||
|
||||
var hasAllKeys: Bool {
|
||||
hasRemoteKey && hasDeviceKey && hasAuthToken
|
||||
}
|
||||
@Published
|
||||
private(set) var hasAllKeys = false
|
||||
|
||||
init() {
|
||||
self.keyChain = KeyChain(domain: "christophhagen.de")
|
||||
@ -189,5 +188,6 @@ final class KeyManagement: ObservableObject {
|
||||
self.hasRemoteKey = keyChain.has(.remoteKey)
|
||||
self.hasDeviceKey = keyChain.has(.deviceKey)
|
||||
self.hasAuthToken = keyChain.has(.authToken)
|
||||
self.hasAllKeys = hasRemoteKey && hasDeviceKey && hasAuthToken
|
||||
}
|
||||
}
|
||||
|
12
Sesame/Common/PendingOperation.swift
Normal file
12
Sesame/Common/PendingOperation.swift
Normal file
@ -0,0 +1,12 @@
|
||||
import Foundation
|
||||
|
||||
struct PendingOperation {
|
||||
|
||||
let route: TransmissionType
|
||||
|
||||
let operation: RequestType
|
||||
}
|
||||
|
||||
extension PendingOperation: Equatable {
|
||||
|
||||
}
|
229
Sesame/Common/RequestCoordinator.swift
Normal file
229
Sesame/Common/RequestCoordinator.swift
Normal file
@ -0,0 +1,229 @@
|
||||
import Foundation
|
||||
import SwiftUI
|
||||
import SwiftData
|
||||
|
||||
final class RequestCoordinator: ObservableObject {
|
||||
|
||||
@Published
|
||||
var serverChallenge: ServerChallenge? = nil
|
||||
|
||||
@Published
|
||||
var state: MessageResult = .noKeyAvailable
|
||||
|
||||
@Published
|
||||
private var timer: Timer?
|
||||
|
||||
@Published
|
||||
var pendingRequests: [PendingOperation] = []
|
||||
|
||||
@Published
|
||||
var activeRequest: PendingOperation?
|
||||
|
||||
@Published
|
||||
var keyManager = KeyManagement()
|
||||
|
||||
@AppStorage("server")
|
||||
var serverPath: String = "https://christophhagen.de/sesame/"
|
||||
|
||||
@AppStorage("localIP")
|
||||
var localAddress: String = "192.168.178.104/"
|
||||
|
||||
@AppStorage("connectionType")
|
||||
var connectionType: ConnectionStrategy = .remoteFirst
|
||||
|
||||
let modelContext: ModelContext
|
||||
|
||||
init(modelContext: ModelContext) {
|
||||
self.modelContext = modelContext
|
||||
if keyManager.hasAllKeys {
|
||||
self.state = .notChecked
|
||||
}
|
||||
}
|
||||
|
||||
let client = Client()
|
||||
|
||||
var needsNewServerChallenge: Bool {
|
||||
serverChallenge?.isExpired ?? true
|
||||
}
|
||||
|
||||
@Published
|
||||
var isPerformingRequest: Bool = false
|
||||
|
||||
func startUnlock() {
|
||||
addOperations(.challenge, .unlock)
|
||||
}
|
||||
|
||||
func startChallenge() {
|
||||
addOperations(.challenge)
|
||||
}
|
||||
|
||||
private func addOperations(_ operations: RequestType...) {
|
||||
#warning("Only perform challenge when doing unlock? Remove code complexity")
|
||||
// Just add all operations for an unlock
|
||||
// For every completed operation, the unnecessary ones will be removed without executing them
|
||||
let operations = connectionType.transmissionTypes.map { route in
|
||||
operations.map { PendingOperation(route: route, operation: $0) }
|
||||
}.joined()
|
||||
|
||||
pendingRequests.append(contentsOf: operations)
|
||||
continueRequests()
|
||||
}
|
||||
|
||||
private func continueRequests() {
|
||||
guard activeRequest == nil else {
|
||||
return
|
||||
}
|
||||
guard !pendingRequests.isEmpty else {
|
||||
self.isPerformingRequest = false
|
||||
return
|
||||
}
|
||||
let activeRequest = pendingRequests.removeFirst()
|
||||
self.activeRequest = activeRequest
|
||||
self.isPerformingRequest = true
|
||||
Task {
|
||||
await process(request: activeRequest)
|
||||
}
|
||||
}
|
||||
|
||||
private func process(request: PendingOperation) async {
|
||||
let startTime = Date.now
|
||||
let (success, response, challenge) = await self.start(request)
|
||||
let endTime = Date.now
|
||||
let roundTripTime = endTime.timeIntervalSince(startTime)
|
||||
|
||||
if let s = challenge?.message {
|
||||
print("\(s) took \(Int(roundTripTime * 1000)) ms")
|
||||
} else {
|
||||
print("\(request.operation.description) took \(Int(roundTripTime * 1000)) ms")
|
||||
}
|
||||
|
||||
if request.operation == .unlock, let response {
|
||||
print("Saving history item")
|
||||
let item = HistoryItem(message: response, startDate: startTime, route: request.route, finishDate: endTime)
|
||||
modelContext.insert(item)
|
||||
}
|
||||
|
||||
DispatchQueue.main.async {
|
||||
self.filterPendingRequests(after: request, success: success, hasChallenge: challenge != nil)
|
||||
if let response {
|
||||
self.state = response.result
|
||||
}
|
||||
if let challenge {
|
||||
self.serverChallenge = challenge
|
||||
}
|
||||
self.activeRequest = nil
|
||||
self.continueRequests()
|
||||
}
|
||||
}
|
||||
|
||||
private func filterPendingRequests(after operation: PendingOperation, success: Bool, hasChallenge: Bool) {
|
||||
if success {
|
||||
// Filter all unlocks
|
||||
if operation.operation == .unlock {
|
||||
// Successful unlock means no need for next challenge or unlocks, so remove all
|
||||
self.pendingRequests = []
|
||||
} else {
|
||||
// Successful challenge means no need for additional challenges, but keep unlocks
|
||||
self.pendingRequests = pendingRequests.filter { $0.operation != .challenge }
|
||||
}
|
||||
} else {
|
||||
// Filter all operations with the same route for connection errors
|
||||
// And with type, depending on error?
|
||||
}
|
||||
}
|
||||
|
||||
private func start(_ operation: PendingOperation) async -> OptionalServerResponse {
|
||||
switch operation.operation {
|
||||
case .challenge:
|
||||
if let serverChallenge, !serverChallenge.isExpired {
|
||||
return (true, serverChallenge.message, serverChallenge)
|
||||
}
|
||||
return await performChallenge(route: operation.route)
|
||||
|
||||
case .unlock:
|
||||
guard let serverChallenge, !serverChallenge.isExpired else {
|
||||
return (false, nil, nil)
|
||||
}
|
||||
return await performUnlock(with: serverChallenge.message, route: operation.route)
|
||||
}
|
||||
}
|
||||
|
||||
private func performChallenge(route: TransmissionType) async -> OptionalServerResponse {
|
||||
let initialMessage = Message.initial()
|
||||
let (result, challenge) = await send(initialMessage, route: route)
|
||||
guard let message = challenge?.message else {
|
||||
return (false, result, nil)
|
||||
}
|
||||
// Can't get here without the message being accepted
|
||||
guard message.messageType == .challenge else {
|
||||
print("Invalid message type for challenge: \(message)")
|
||||
return (false, result.with(result: .invalidMessageTypeFromDevice), nil)
|
||||
}
|
||||
return (true, result.with(result: .deviceAvailable), challenge)
|
||||
}
|
||||
|
||||
private func performUnlock(with challenge: Message, route: TransmissionType) async -> OptionalServerResponse {
|
||||
let request = challenge.requestMessage()
|
||||
let (unlockState, responseData) = await send(request, route: route)
|
||||
|
||||
guard let response = responseData?.message else {
|
||||
return (false, unlockState, nil)
|
||||
}
|
||||
switch response.messageType {
|
||||
case .initial, .request:
|
||||
print("Invalid message type for response: \(response)")
|
||||
return (false, response.with(result: .invalidMessageTypeFromDevice), nil)
|
||||
case .challenge:
|
||||
// New challenge received, challenge was expired
|
||||
return (true, unlockState, responseData)
|
||||
case .response:
|
||||
break
|
||||
}
|
||||
|
||||
guard response.serverChallenge == request.serverChallenge else {
|
||||
print("Invalid server challenge for unlock: \(response)")
|
||||
return (false, response.with(result: .invalidServerChallengeFromDevice), nil)
|
||||
}
|
||||
return (true, response.with(result: .unlocked), nil)
|
||||
}
|
||||
|
||||
private func url(for route: TransmissionType) -> String {
|
||||
switch route {
|
||||
case .throughServer:
|
||||
return serverPath
|
||||
case .overLocalWifi:
|
||||
return localAddress
|
||||
}
|
||||
}
|
||||
|
||||
private func send(_ message: Message, route: TransmissionType) async -> ServerResponse {
|
||||
guard let keys = keyManager.getAllKeys() else {
|
||||
return (message.with(result: .noKeyAvailable), nil)
|
||||
}
|
||||
let url = url(for: route)
|
||||
return await client.send(message, to: url, through: route, using: keys)
|
||||
}
|
||||
|
||||
func startUpdatingServerChallenge() {
|
||||
guard timer == nil else {
|
||||
return
|
||||
}
|
||||
DispatchQueue.main.async {
|
||||
self.timer = Timer.scheduledTimer(withTimeInterval: 3, repeats: true) { [weak self] timer in
|
||||
guard let self else {
|
||||
timer.invalidate()
|
||||
return
|
||||
}
|
||||
DispatchQueue.main.async {
|
||||
self.startChallenge()
|
||||
}
|
||||
}
|
||||
self.timer!.fire()
|
||||
}
|
||||
}
|
||||
|
||||
func endUpdatingServerChallenge() {
|
||||
timer?.invalidate()
|
||||
timer = nil
|
||||
}
|
||||
}
|
17
Sesame/Common/ServerChallenge.swift
Normal file
17
Sesame/Common/ServerChallenge.swift
Normal file
@ -0,0 +1,17 @@
|
||||
import Foundation
|
||||
|
||||
struct ServerChallenge {
|
||||
|
||||
private static let challengeExpiryTime: TimeInterval = 25.0
|
||||
|
||||
let creationDate: Date
|
||||
|
||||
let message: Message
|
||||
|
||||
var isExpired: Bool {
|
||||
creationDate.addingTimeInterval(ServerChallenge.challengeExpiryTime) < Date.now
|
||||
}
|
||||
}
|
||||
|
||||
typealias ServerResponse = (result: Message, challenge: ServerChallenge?)
|
||||
typealias OptionalServerResponse = (success: Bool, result: Message?, challenge: ServerChallenge?)
|
42
Sesame/Common/TransmissionType.swift
Normal file
42
Sesame/Common/TransmissionType.swift
Normal file
@ -0,0 +1,42 @@
|
||||
import Foundation
|
||||
import SFSafeSymbols
|
||||
|
||||
enum TransmissionType: Int {
|
||||
case throughServer = 0
|
||||
case overLocalWifi = 1
|
||||
}
|
||||
|
||||
extension TransmissionType: Codable {
|
||||
|
||||
}
|
||||
|
||||
extension TransmissionType {
|
||||
|
||||
var symbol: SFSymbol {
|
||||
switch self {
|
||||
case .throughServer: return .network
|
||||
case .overLocalWifi: return .wifi
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
extension TransmissionType: CaseIterable {
|
||||
|
||||
}
|
||||
|
||||
extension TransmissionType: CustomStringConvertible {
|
||||
|
||||
var description: String {
|
||||
displayName
|
||||
}
|
||||
}
|
||||
|
||||
extension TransmissionType {
|
||||
|
||||
var displayName: String {
|
||||
switch self {
|
||||
case .throughServer: return "Mobile"
|
||||
case .overLocalWifi: return "WiFi"
|
||||
}
|
||||
}
|
||||
}
|
@ -1,288 +1,98 @@
|
||||
import SwiftUI
|
||||
import SwiftData
|
||||
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
|
||||
private let unlockButtonSize: CGFloat = 250
|
||||
private let smallButtonSize: CGFloat = 50
|
||||
private let buttonBackground: Color = .white.opacity(0.2)
|
||||
private let buttonColor: Color = .white
|
||||
|
||||
@ObservedObject
|
||||
var keyManager = KeyManagement()
|
||||
var coordinator: RequestCoordinator
|
||||
|
||||
let history = HistoryManager()
|
||||
@State private var showSettingsSheet = false
|
||||
@State private var showHistorySheet = false
|
||||
@State private var didShowKeySheetOnce = false
|
||||
|
||||
@State
|
||||
var state: ClientState = .noKeyAvailable
|
||||
|
||||
@State
|
||||
private var timer: Timer?
|
||||
|
||||
@State
|
||||
private var hasActiveRequest = false
|
||||
|
||||
@State
|
||||
private var responseTime: Date? = nil
|
||||
|
||||
@State
|
||||
private var showSettingsSheet = false
|
||||
|
||||
@State
|
||||
private var showHistorySheet = false
|
||||
|
||||
@State
|
||||
private var didShowKeySheetOnce = false
|
||||
|
||||
let server = Client()
|
||||
|
||||
var compensationTime: UInt32 {
|
||||
isCompensatingDaylightTime ? 3600 : 0
|
||||
init(modelContext: ModelContext) {
|
||||
self.coordinator = .init(modelContext: modelContext)
|
||||
}
|
||||
|
||||
var isPerformingRequests: Bool {
|
||||
hasActiveRequest ||
|
||||
state == .waitingForResponse
|
||||
}
|
||||
|
||||
var buttonBackground: Color {
|
||||
state.allowsAction ?
|
||||
.white.opacity(0.2) :
|
||||
.black.opacity(0.2)
|
||||
}
|
||||
|
||||
let buttonBorderWidth: CGFloat = 3
|
||||
|
||||
var buttonColor: Color {
|
||||
state.allowsAction ? .white : .gray
|
||||
}
|
||||
|
||||
private let buttonWidth: CGFloat = 250
|
||||
|
||||
private let smallButtonHeight: CGFloat = 50
|
||||
|
||||
private let smallButtonWidth: CGFloat = 120
|
||||
|
||||
private let smallButtonBorderWidth: CGFloat = 1
|
||||
|
||||
var body: some View {
|
||||
GeometryReader { geo in
|
||||
VStack(spacing: 20) {
|
||||
HStack {
|
||||
Button("History", action: { showHistorySheet = true })
|
||||
.frame(width: smallButtonWidth,
|
||||
height: smallButtonHeight)
|
||||
.background(.white.opacity(0.2))
|
||||
.cornerRadius(smallButtonHeight / 2)
|
||||
.overlay(RoundedRectangle(cornerRadius: smallButtonHeight / 2).stroke(lineWidth: smallButtonBorderWidth).foregroundColor(.white))
|
||||
.foregroundColor(.white)
|
||||
.font(.title2)
|
||||
.padding()
|
||||
Spacer()
|
||||
Button("Settings", action: { showSettingsSheet = true })
|
||||
.frame(width: smallButtonWidth,
|
||||
height: smallButtonHeight)
|
||||
.background(.white.opacity(0.2))
|
||||
.cornerRadius(smallButtonHeight / 2)
|
||||
.overlay(RoundedRectangle(cornerRadius: smallButtonHeight / 2).stroke(lineWidth: smallButtonBorderWidth).foregroundColor(.white))
|
||||
.foregroundColor(.white)
|
||||
.font(.title2)
|
||||
.padding()
|
||||
}
|
||||
Text("Sesame")
|
||||
.font(.title)
|
||||
Spacer()
|
||||
if state.requiresDescription {
|
||||
Text(state.description)
|
||||
.padding()
|
||||
}
|
||||
Button(state.actionText, action: mainButtonPressed)
|
||||
.frame(width: buttonWidth,
|
||||
height: buttonWidth)
|
||||
Text(coordinator.state.description)
|
||||
Spacer()
|
||||
HStack(alignment: .bottom, spacing: 0) {
|
||||
Button(action: { showHistorySheet = true }) {
|
||||
Image(systemSymbol: .clockArrowCirclepath)
|
||||
.foregroundColor(.white)
|
||||
.frame(width: smallButtonSize, height: smallButtonSize)
|
||||
.background(.white.opacity(0.2))
|
||||
.cornerRadius(smallButtonSize / 2)
|
||||
.font(.title2)
|
||||
|
||||
}
|
||||
Button("Unlock", action: coordinator.startUnlock)
|
||||
.frame(width: unlockButtonSize, height: unlockButtonSize)
|
||||
.background(buttonBackground)
|
||||
.cornerRadius(buttonWidth / 2)
|
||||
.overlay(RoundedRectangle(cornerRadius: buttonWidth / 2)
|
||||
.stroke(lineWidth: buttonBorderWidth).foregroundColor(buttonColor))
|
||||
.cornerRadius(unlockButtonSize / 2)
|
||||
.foregroundColor(buttonColor)
|
||||
.font(.title)
|
||||
.disabled(!state.allowsAction)
|
||||
.padding(.bottom, (geo.size.width-buttonWidth) / 2)
|
||||
Button(action: { showSettingsSheet = true }) {
|
||||
Image(systemSymbol: .gearshape)
|
||||
.foregroundColor(.white)
|
||||
.frame(width: smallButtonSize, height: smallButtonSize)
|
||||
.background(.white.opacity(0.2))
|
||||
.cornerRadius(smallButtonSize / 2)
|
||||
.font(.title2)
|
||||
}
|
||||
.background(state.color)
|
||||
.onAppear {
|
||||
if keyManager.hasAllKeys {
|
||||
state = .requestingStatus
|
||||
}
|
||||
startRegularStatusUpdates()
|
||||
|
||||
Picker("Connection type", selection: $coordinator.connectionType) {
|
||||
ForEach(ConnectionStrategy.allCases, id: \.rawValue) { connection in
|
||||
Text(connection.description).tag(connection)
|
||||
}
|
||||
.onDisappear {
|
||||
endRegularStatusUpdates()
|
||||
}
|
||||
.frame(width: geo.size.width, height: geo.size.height)
|
||||
.animation(.easeInOut, value: state.color)
|
||||
.pickerStyle(.segmented)
|
||||
.padding(.horizontal, 30)
|
||||
}
|
||||
.background(coordinator.state.color)
|
||||
.onAppear(perform: coordinator.startUpdatingServerChallenge)
|
||||
.onDisappear(perform: coordinator.endUpdatingServerChallenge)
|
||||
.animation(.easeInOut, value: coordinator.state.color)
|
||||
.sheet(isPresented: $showSettingsSheet) {
|
||||
SettingsView(
|
||||
keyManager: keyManager,
|
||||
serverAddress: $serverPath,
|
||||
localAddress: $localAddress,
|
||||
deviceID: $deviceID,
|
||||
nextMessageCounter: $nextMessageCounter,
|
||||
isCompensatingDaylightTime: $isCompensatingDaylightTime,
|
||||
useLocalConnection: $useLocalConnection)
|
||||
}
|
||||
.sheet(isPresented: $showHistorySheet) {
|
||||
HistoryView(history: history)
|
||||
}
|
||||
keyManager: coordinator.keyManager,
|
||||
serverAddress: $coordinator.serverPath,
|
||||
localAddress: $coordinator.localAddress)
|
||||
}
|
||||
.sheet(isPresented: $showHistorySheet) { HistoryView() }
|
||||
.preferredColorScheme(.dark)
|
||||
}
|
||||
|
||||
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 content = Message.Content(
|
||||
time: sentTime.timestamp + compensationTime,
|
||||
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) {
|
||||
#Preview {
|
||||
do {
|
||||
try history.save(item: historyItem)
|
||||
let config = ModelConfiguration(isStoredInMemoryOnly: true)
|
||||
let container = try ModelContainer(for: HistoryItem.self, configurations: config)
|
||||
|
||||
let item = HistoryItem.mock
|
||||
container.mainContext.insert(item)
|
||||
try container.mainContext.save()
|
||||
return ContentView(modelContext: container.mainContext)
|
||||
.modelContainer(container)
|
||||
} catch {
|
||||
print("Failed to save item: \(error)")
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
private func startRegularStatusUpdates() {
|
||||
guard timer == nil else {
|
||||
return
|
||||
}
|
||||
DispatchQueue.main.async {
|
||||
timer = Timer.scheduledTimer(withTimeInterval: 5, repeats: true, block: checkDeviceStatus)
|
||||
timer!.fire()
|
||||
}
|
||||
}
|
||||
|
||||
private func endRegularStatusUpdates() {
|
||||
timer?.invalidate()
|
||||
timer = nil
|
||||
}
|
||||
|
||||
func checkDeviceStatus(_ timer: Timer) {
|
||||
guard !useLocalConnection else {
|
||||
return
|
||||
}
|
||||
guard let authToken = keyManager.get(.authToken) else {
|
||||
if !didShowKeySheetOnce {
|
||||
didShowKeySheetOnce = true
|
||||
//showSettingsSheet = true
|
||||
}
|
||||
return
|
||||
}
|
||||
guard !hasActiveRequest else {
|
||||
return
|
||||
}
|
||||
hasActiveRequest = true
|
||||
Task {
|
||||
let newState = await server.deviceStatus(authToken: authToken.data, server: serverPath)
|
||||
hasActiveRequest = false
|
||||
switch state {
|
||||
case .noKeyAvailable:
|
||||
return
|
||||
case .requestingStatus, .deviceNotAvailable, .ready:
|
||||
state = newState
|
||||
case .waitingForResponse:
|
||||
return
|
||||
case .messageRejected, .openSesame, .internalError, .responseRejected:
|
||||
guard let time = responseTime else {
|
||||
state = newState
|
||||
return
|
||||
}
|
||||
responseTime = nil
|
||||
// Wait at least 5 seconds after these states have been reached before changing the
|
||||
// interface to allow sufficient time to see the result
|
||||
let elapsed = Date.now.timeIntervalSince(time)
|
||||
guard elapsed < 5 else {
|
||||
state = newState
|
||||
return
|
||||
}
|
||||
let secondsToWait = Int(elapsed.rounded(.up))
|
||||
DispatchQueue.main.asyncAfter(deadline: .now() + .seconds(secondsToWait)) {
|
||||
state = newState
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
struct ContentView_Previews: PreviewProvider {
|
||||
static var previews: some View {
|
||||
ContentView()
|
||||
.previewDevice("iPhone 8")
|
||||
}
|
||||
}
|
||||
|
||||
extension Date {
|
||||
|
||||
var timestamp: UInt32 {
|
||||
UInt32(timeIntervalSince1970.rounded())
|
||||
}
|
||||
|
||||
init(timestamp: UInt32) {
|
||||
self.init(timeIntervalSince1970: TimeInterval(timestamp))
|
||||
fatalError("Failed to create model container.")
|
||||
}
|
||||
}
|
||||
|
12
Sesame/Extensions/Date+Timestamp.swift
Normal file
12
Sesame/Extensions/Date+Timestamp.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))
|
||||
}
|
||||
}
|
@ -1,114 +1,55 @@
|
||||
import Foundation
|
||||
import SwiftData
|
||||
|
||||
@Model
|
||||
final class HistoryItem {
|
||||
|
||||
struct HistoryItem {
|
||||
let startDate: Date
|
||||
|
||||
/// The sent/received date (local time, not including compensation offset)
|
||||
let requestDate: Date
|
||||
let message: Message
|
||||
|
||||
let request: Message.Content
|
||||
let route: TransmissionType
|
||||
|
||||
let usedLocalConnection: Bool
|
||||
let finishDate: Date
|
||||
|
||||
var response: ClientState
|
||||
|
||||
let responseMessage: Message.Content?
|
||||
|
||||
let responseDate: Date
|
||||
|
||||
init(sent message: Message.Content, sentDate: Date, local: Bool, response: ClientState, responseDate: Date, responseMessage: Message.Content?) {
|
||||
self.requestDate = sentDate
|
||||
self.request = message
|
||||
self.responseMessage = responseMessage
|
||||
self.response = response
|
||||
self.responseDate = responseDate
|
||||
self.usedLocalConnection = local
|
||||
init(message: Message, startDate: Date, route: TransmissionType, finishDate: Date) {
|
||||
self.startDate = startDate
|
||||
self.message = message
|
||||
self.finishDate = finishDate
|
||||
self.route = route
|
||||
}
|
||||
|
||||
// MARK: Statistics
|
||||
|
||||
var roundTripTime: TimeInterval {
|
||||
responseDate.timeIntervalSince(requestDate)
|
||||
finishDate.timeIntervalSince(startDate)
|
||||
}
|
||||
|
||||
var deviceTime: Date? {
|
||||
guard let timestamp = responseMessage?.time else {
|
||||
return nil
|
||||
}
|
||||
return Date(timestamp: timestamp)
|
||||
}
|
||||
|
||||
var requestLatency: TimeInterval? {
|
||||
deviceTime?.timeIntervalSince(requestDate)
|
||||
}
|
||||
|
||||
var responseLatency: TimeInterval? {
|
||||
guard let deviceTime = deviceTime else {
|
||||
return nil
|
||||
}
|
||||
return responseDate.timeIntervalSince(deviceTime)
|
||||
}
|
||||
|
||||
var clockOffset: Int? {
|
||||
guard let deviceTime = deviceTime else {
|
||||
return nil
|
||||
}
|
||||
let estimatedArrival = requestDate.advanced(by: roundTripTime / 2)
|
||||
return Int(deviceTime.timeIntervalSince(estimatedArrival))
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
extension HistoryItem: Codable {
|
||||
|
||||
enum CodingKeys: Int, CodingKey {
|
||||
case requestDate = 1
|
||||
case request = 2
|
||||
case usedLocalConnection = 3
|
||||
case response = 4
|
||||
case responseMessage = 5
|
||||
case responseDate = 6
|
||||
}
|
||||
}
|
||||
|
||||
extension ClientState: Codable {
|
||||
|
||||
init(from decoder: Decoder) throws {
|
||||
let code = try decoder.singleValueContainer().decode(UInt8.self)
|
||||
self.init(code: code)
|
||||
}
|
||||
|
||||
func encode(to encoder: Encoder) throws {
|
||||
var container = encoder.singleValueContainer()
|
||||
try container.encode(code)
|
||||
var response: MessageResult {
|
||||
message.result
|
||||
}
|
||||
}
|
||||
|
||||
extension HistoryItem: Identifiable {
|
||||
|
||||
var id: UInt32 {
|
||||
requestDate.timestamp
|
||||
var id: Double {
|
||||
startDate.timeIntervalSince1970
|
||||
}
|
||||
}
|
||||
|
||||
extension HistoryItem: Comparable {
|
||||
|
||||
static func < (lhs: HistoryItem, rhs: HistoryItem) -> Bool {
|
||||
lhs.requestDate < rhs.requestDate
|
||||
lhs.startDate < rhs.startDate
|
||||
}
|
||||
}
|
||||
|
||||
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)
|
||||
let message = Message(messageType: .request, clientChallenge: 123, serverChallenge: 234, result: .unlocked)
|
||||
return .init(
|
||||
sent: content,
|
||||
sentDate: .now,
|
||||
local: false,
|
||||
response: .openSesame,
|
||||
responseDate: .now + 2,
|
||||
responseMessage: content2)
|
||||
message: message,
|
||||
startDate: Date.now.addingTimeInterval(-5),
|
||||
route: .throughServer,
|
||||
finishDate: Date.now)
|
||||
}
|
||||
}
|
||||
|
@ -1,6 +1,8 @@
|
||||
import SwiftUI
|
||||
import SwiftData
|
||||
import SFSafeSymbols
|
||||
|
||||
|
||||
private let df: DateFormatter = {
|
||||
let df = DateFormatter()
|
||||
df.dateStyle = .short
|
||||
@ -13,60 +15,55 @@ struct HistoryListItem: View {
|
||||
let entry: HistoryItem
|
||||
|
||||
var entryTime: String {
|
||||
df.string(from: entry.requestDate)
|
||||
df.string(from: entry.startDate)
|
||||
}
|
||||
|
||||
var roundTripText: String {
|
||||
"\(Int(entry.roundTripTime * 1000)) ms"
|
||||
}
|
||||
|
||||
var counterText: String {
|
||||
let sentCounter = entry.request.id
|
||||
let startText = "\(sentCounter)"
|
||||
guard let rCounter = entry.responseMessage?.id else {
|
||||
return startText
|
||||
}
|
||||
let diff = Int(rCounter) - Int(sentCounter)
|
||||
guard diff != 1 && diff != 0 else {
|
||||
return startText
|
||||
}
|
||||
return startText + " (\(diff))"
|
||||
var clientNonceText: String {
|
||||
"\(entry.message.clientChallenge)"
|
||||
}
|
||||
|
||||
var timeOffsetText: String? {
|
||||
guard let offset = entry.clockOffset else {
|
||||
return nil
|
||||
}
|
||||
return "\(offset) s"
|
||||
var serverNonceText: String {
|
||||
"\(entry.message.serverChallenge)"
|
||||
}
|
||||
|
||||
var body: some View {
|
||||
VStack(alignment: .leading) {
|
||||
HStack {
|
||||
Image(systemSymbol: entry.route.symbol)
|
||||
Text(entry.response.description)
|
||||
.font(.headline)
|
||||
Spacer()
|
||||
Text(entryTime)
|
||||
}.padding(.bottom, 1)
|
||||
HStack {
|
||||
Image(systemSymbol: entry.usedLocalConnection ? .wifi : .network)
|
||||
Text(roundTripText)
|
||||
.font(.subheadline)
|
||||
Image(systemSymbol: .personalhotspot)
|
||||
Text(counterText)
|
||||
.font(.subheadline)
|
||||
if let timeOffsetText {
|
||||
Image(systemSymbol: .stopwatch)
|
||||
Text(timeOffsetText)
|
||||
.font(.subheadline)
|
||||
}
|
||||
}.foregroundColor(.secondary)
|
||||
HStack {
|
||||
Image(systemSymbol: .arrowUpArrowDownCircle)
|
||||
Text(roundTripText).padding(.trailing)
|
||||
Image(systemSymbol: .lockIphone)
|
||||
Text(clientNonceText).padding(.trailing)
|
||||
Image(systemSymbol: .doorRightHandClosed)
|
||||
Text(serverNonceText).padding(.trailing)
|
||||
}
|
||||
.foregroundColor(.secondary)
|
||||
.font(.footnote)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
struct HistoryListItem_Previews: PreviewProvider {
|
||||
static var previews: some View {
|
||||
HistoryListItem(entry: .mock)
|
||||
#Preview {
|
||||
do {
|
||||
let config = ModelConfiguration(isStoredInMemoryOnly: true)
|
||||
let container = try ModelContainer(for: HistoryItem.self, configurations: config)
|
||||
|
||||
let item = HistoryItem.mock
|
||||
container.mainContext.insert(item)
|
||||
try container.mainContext.save()
|
||||
return HistoryListItem(entry: item)
|
||||
.modelContainer(container)
|
||||
} catch {
|
||||
fatalError("Failed to create model container.")
|
||||
}
|
||||
}
|
||||
|
@ -1,14 +1,14 @@
|
||||
import SwiftUI
|
||||
import SwiftData
|
||||
|
||||
struct HistoryView: View {
|
||||
|
||||
let history: HistoryManagerProtocol
|
||||
|
||||
@State
|
||||
@Query
|
||||
private var items: [HistoryItem] = []
|
||||
|
||||
@State
|
||||
private var unlockCount = 0
|
||||
private var unlockCount: Int {
|
||||
items.count { $0.response == .unlocked }
|
||||
}
|
||||
|
||||
private var percentage: Double {
|
||||
guard items.count > 0 else {
|
||||
@ -17,11 +17,18 @@ struct HistoryView: View {
|
||||
return Double(unlockCount * 100) / Double(items.count)
|
||||
}
|
||||
|
||||
private var requestNumberText: String {
|
||||
guard items.count != 1 else {
|
||||
return "1 Request"
|
||||
}
|
||||
return "\(items.count) Requests"
|
||||
}
|
||||
|
||||
var body: some View {
|
||||
NavigationView {
|
||||
List {
|
||||
HStack {
|
||||
Text("\(items.count) requests")
|
||||
Text(requestNumberText)
|
||||
.foregroundColor(.primary)
|
||||
.font(.body)
|
||||
Spacer()
|
||||
@ -35,26 +42,20 @@ struct HistoryView: View {
|
||||
}
|
||||
.navigationTitle("History")
|
||||
}
|
||||
.onAppear {
|
||||
load()
|
||||
}
|
||||
}
|
||||
|
||||
private func load() {
|
||||
Task {
|
||||
let entries = history.loadEntries()
|
||||
DispatchQueue.main.async {
|
||||
items = entries
|
||||
unlockCount = items.count {
|
||||
$0.response == .openSesame
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
#Preview {
|
||||
do {
|
||||
let config = ModelConfiguration(isStoredInMemoryOnly: true)
|
||||
let container = try ModelContainer(for: HistoryItem.self, configurations: config)
|
||||
|
||||
struct HistoryView_Previews: PreviewProvider {
|
||||
static var previews: some View {
|
||||
HistoryView(history: HistoryManagerMock())
|
||||
let item = HistoryItem.mock
|
||||
container.mainContext.insert(item)
|
||||
try container.mainContext.save()
|
||||
return HistoryView()
|
||||
.modelContainer(container)
|
||||
} catch {
|
||||
fatalError("Failed to create model container.")
|
||||
}
|
||||
}
|
@ -1,10 +1,24 @@
|
||||
import SwiftUI
|
||||
import SwiftData
|
||||
|
||||
@main
|
||||
struct SesameApp: App {
|
||||
|
||||
@State
|
||||
var modelContainer: ModelContainer
|
||||
|
||||
init() {
|
||||
do {
|
||||
self.modelContainer = try ModelContainer(for: HistoryItem.self)
|
||||
} catch {
|
||||
fatalError("Failed to create model container: \(error)")
|
||||
}
|
||||
}
|
||||
|
||||
var body: some Scene {
|
||||
WindowGroup {
|
||||
ContentView()
|
||||
}
|
||||
ContentView(modelContext: modelContainer.mainContext)
|
||||
}
|
||||
.modelContainer(modelContainer)
|
||||
}
|
||||
}
|
||||
|
@ -10,31 +10,6 @@ struct SettingsView: View {
|
||||
@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 {
|
||||
@ -53,49 +28,11 @@ struct SettingsView: View {
|
||||
.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("/") {
|
||||
@ -103,54 +40,8 @@ struct SettingsView: View {
|
||||
}
|
||||
}
|
||||
.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 {
|
||||
@ -158,10 +49,6 @@ struct SettingsView_Previews: PreviewProvider {
|
||||
SettingsView(
|
||||
keyManager: KeyManagement(),
|
||||
serverAddress: .constant("https://example.com"),
|
||||
localAddress: .constant("192.168.178.42"),
|
||||
deviceID: .constant(0),
|
||||
nextMessageCounter: .constant(12345678),
|
||||
isCompensatingDaylightTime: .constant(true),
|
||||
useLocalConnection: .constant(false))
|
||||
localAddress: .constant("192.168.178.42"))
|
||||
}
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user