Compare commits
24 Commits
Author | SHA1 | Date | |
---|---|---|---|
|
3591842861 | ||
|
91af68a44b | ||
|
877bba56b4 | ||
|
c049f9c9d6 | ||
|
03735d9e72 | ||
|
e4f93d94a9 | ||
|
973f0cb1c1 | ||
|
e9d870bd12 | ||
|
9086c6a916 | ||
|
f284696e21 | ||
|
f8fc37d7e0 | ||
|
33d84b40db | ||
|
ad94588b3c | ||
|
b749a80f5d | ||
|
941aebd9ca | ||
|
7a443d51b3 | ||
|
95ece1ddcc | ||
|
5f9af35542 | ||
|
e5ea8c4951 | ||
|
f451715a11 | ||
|
32b4c8c81a | ||
|
f599cb790b | ||
|
9b14f442b0 | ||
|
8a17eef19b |
BIN
Banner.key
BIN
Banner.key
Binary file not shown.
@ -0,0 +1,11 @@
|
|||||||
|
{
|
||||||
|
"colors" : [
|
||||||
|
{
|
||||||
|
"idiom" : "universal"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"info" : {
|
||||||
|
"author" : "xcode",
|
||||||
|
"version" : 1
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,14 @@
|
|||||||
|
{
|
||||||
|
"images" : [
|
||||||
|
{
|
||||||
|
"filename" : "sesame.png",
|
||||||
|
"idiom" : "universal",
|
||||||
|
"platform" : "watchos",
|
||||||
|
"size" : "1024x1024"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"info" : {
|
||||||
|
"author" : "xcode",
|
||||||
|
"version" : 1
|
||||||
|
}
|
||||||
|
}
|
Binary file not shown.
After Width: | Height: | Size: 113 KiB |
@ -0,0 +1,25 @@
|
|||||||
|
{
|
||||||
|
"images" : [
|
||||||
|
{
|
||||||
|
"idiom" : "watch",
|
||||||
|
"scale" : "2x"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"idiom" : "watch",
|
||||||
|
"scale" : "2x",
|
||||||
|
"screen-width" : "<=145"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"idiom" : "watch",
|
||||||
|
"scale" : "2x",
|
||||||
|
"screen-width" : ">183"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"info" : {
|
||||||
|
"author" : "xcode",
|
||||||
|
"version" : 1
|
||||||
|
},
|
||||||
|
"properties" : {
|
||||||
|
"auto-scaling" : "auto"
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,53 @@
|
|||||||
|
{
|
||||||
|
"assets" : [
|
||||||
|
{
|
||||||
|
"filename" : "Circular.imageset",
|
||||||
|
"idiom" : "watch",
|
||||||
|
"role" : "circular"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"filename" : "Extra Large.imageset",
|
||||||
|
"idiom" : "watch",
|
||||||
|
"role" : "extra-large"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"filename" : "Graphic Bezel.imageset",
|
||||||
|
"idiom" : "watch",
|
||||||
|
"role" : "graphic-bezel"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"filename" : "Graphic Circular.imageset",
|
||||||
|
"idiom" : "watch",
|
||||||
|
"role" : "graphic-circular"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"filename" : "Graphic Corner.imageset",
|
||||||
|
"idiom" : "watch",
|
||||||
|
"role" : "graphic-corner"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"filename" : "Graphic Extra Large.imageset",
|
||||||
|
"idiom" : "watch",
|
||||||
|
"role" : "graphic-extra-large"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"filename" : "Graphic Large Rectangular.imageset",
|
||||||
|
"idiom" : "watch",
|
||||||
|
"role" : "graphic-large-rectangular"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"filename" : "Modular.imageset",
|
||||||
|
"idiom" : "watch",
|
||||||
|
"role" : "modular"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"filename" : "Utilitarian.imageset",
|
||||||
|
"idiom" : "watch",
|
||||||
|
"role" : "utilitarian"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"info" : {
|
||||||
|
"author" : "xcode",
|
||||||
|
"version" : 1
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,25 @@
|
|||||||
|
{
|
||||||
|
"images" : [
|
||||||
|
{
|
||||||
|
"idiom" : "watch",
|
||||||
|
"scale" : "2x"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"idiom" : "watch",
|
||||||
|
"scale" : "2x",
|
||||||
|
"screen-width" : "<=145"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"idiom" : "watch",
|
||||||
|
"scale" : "2x",
|
||||||
|
"screen-width" : ">183"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"info" : {
|
||||||
|
"author" : "xcode",
|
||||||
|
"version" : 1
|
||||||
|
},
|
||||||
|
"properties" : {
|
||||||
|
"auto-scaling" : "auto"
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,20 @@
|
|||||||
|
{
|
||||||
|
"images" : [
|
||||||
|
{
|
||||||
|
"idiom" : "watch",
|
||||||
|
"scale" : "2x"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"idiom" : "watch",
|
||||||
|
"scale" : "2x",
|
||||||
|
"screen-width" : ">183"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"info" : {
|
||||||
|
"author" : "xcode",
|
||||||
|
"version" : 1
|
||||||
|
},
|
||||||
|
"properties" : {
|
||||||
|
"auto-scaling" : "auto"
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,20 @@
|
|||||||
|
{
|
||||||
|
"images" : [
|
||||||
|
{
|
||||||
|
"idiom" : "watch",
|
||||||
|
"scale" : "2x"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"idiom" : "watch",
|
||||||
|
"scale" : "2x",
|
||||||
|
"screen-width" : ">183"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"info" : {
|
||||||
|
"author" : "xcode",
|
||||||
|
"version" : 1
|
||||||
|
},
|
||||||
|
"properties" : {
|
||||||
|
"auto-scaling" : "auto"
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,20 @@
|
|||||||
|
{
|
||||||
|
"images" : [
|
||||||
|
{
|
||||||
|
"idiom" : "watch",
|
||||||
|
"scale" : "2x"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"idiom" : "watch",
|
||||||
|
"scale" : "2x",
|
||||||
|
"screen-width" : ">183"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"info" : {
|
||||||
|
"author" : "xcode",
|
||||||
|
"version" : 1
|
||||||
|
},
|
||||||
|
"properties" : {
|
||||||
|
"auto-scaling" : "auto"
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,25 @@
|
|||||||
|
{
|
||||||
|
"images" : [
|
||||||
|
{
|
||||||
|
"idiom" : "watch",
|
||||||
|
"scale" : "2x"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"idiom" : "watch",
|
||||||
|
"scale" : "2x",
|
||||||
|
"screen-width" : "<=145"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"idiom" : "watch",
|
||||||
|
"scale" : "2x",
|
||||||
|
"screen-width" : ">183"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"info" : {
|
||||||
|
"author" : "xcode",
|
||||||
|
"version" : 1
|
||||||
|
},
|
||||||
|
"properties" : {
|
||||||
|
"auto-scaling" : "auto"
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,20 @@
|
|||||||
|
{
|
||||||
|
"images" : [
|
||||||
|
{
|
||||||
|
"idiom" : "watch",
|
||||||
|
"scale" : "2x"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"idiom" : "watch",
|
||||||
|
"scale" : "2x",
|
||||||
|
"screen-width" : ">183"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"info" : {
|
||||||
|
"author" : "xcode",
|
||||||
|
"version" : 1
|
||||||
|
},
|
||||||
|
"properties" : {
|
||||||
|
"auto-scaling" : "auto"
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,25 @@
|
|||||||
|
{
|
||||||
|
"images" : [
|
||||||
|
{
|
||||||
|
"idiom" : "watch",
|
||||||
|
"scale" : "2x"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"idiom" : "watch",
|
||||||
|
"scale" : "2x",
|
||||||
|
"screen-width" : "<=145"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"idiom" : "watch",
|
||||||
|
"scale" : "2x",
|
||||||
|
"screen-width" : ">183"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"info" : {
|
||||||
|
"author" : "xcode",
|
||||||
|
"version" : 1
|
||||||
|
},
|
||||||
|
"properties" : {
|
||||||
|
"auto-scaling" : "auto"
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,25 @@
|
|||||||
|
{
|
||||||
|
"images" : [
|
||||||
|
{
|
||||||
|
"idiom" : "watch",
|
||||||
|
"scale" : "2x"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"idiom" : "watch",
|
||||||
|
"scale" : "2x",
|
||||||
|
"screen-width" : "<=145"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"idiom" : "watch",
|
||||||
|
"scale" : "2x",
|
||||||
|
"screen-width" : ">183"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"info" : {
|
||||||
|
"author" : "xcode",
|
||||||
|
"version" : 1
|
||||||
|
},
|
||||||
|
"properties" : {
|
||||||
|
"auto-scaling" : "auto"
|
||||||
|
}
|
||||||
|
}
|
12
Sesame-Watch Watch App/Date+Extensions.swift
Normal file
12
Sesame-Watch Watch App/Date+Extensions.swift
Normal file
@ -0,0 +1,12 @@
|
|||||||
|
import Foundation
|
||||||
|
|
||||||
|
extension Date {
|
||||||
|
|
||||||
|
var timestamp: UInt32 {
|
||||||
|
UInt32(timeIntervalSince1970.rounded())
|
||||||
|
}
|
||||||
|
|
||||||
|
init(timestamp: UInt32) {
|
||||||
|
self.init(timeIntervalSince1970: TimeInterval(timestamp))
|
||||||
|
}
|
||||||
|
}
|
82
Sesame-Watch Watch App/HistoryItemDetail.swift
Normal file
82
Sesame-Watch Watch App/HistoryItemDetail.swift
Normal file
@ -0,0 +1,82 @@
|
|||||||
|
import SwiftUI
|
||||||
|
import SwiftData
|
||||||
|
import SFSafeSymbols
|
||||||
|
|
||||||
|
private let df: DateFormatter = {
|
||||||
|
let df = DateFormatter()
|
||||||
|
df.dateStyle = .short
|
||||||
|
df.timeStyle = .short
|
||||||
|
df.doesRelativeDateFormatting = true
|
||||||
|
return df
|
||||||
|
}()
|
||||||
|
|
||||||
|
struct HistoryItemDetail: View {
|
||||||
|
|
||||||
|
@Environment(\.modelContext)
|
||||||
|
private var modelContext
|
||||||
|
|
||||||
|
let item: HistoryItem
|
||||||
|
|
||||||
|
@Environment(\.dismiss) private var dismiss
|
||||||
|
|
||||||
|
private var entryTime: String {
|
||||||
|
df.string(from: item.startDate)
|
||||||
|
}
|
||||||
|
|
||||||
|
var body: some View {
|
||||||
|
List {
|
||||||
|
SettingsListTextItem(
|
||||||
|
title: "Status",
|
||||||
|
value: item.response.description)
|
||||||
|
SettingsListTextItem(
|
||||||
|
title: "Date",
|
||||||
|
value: entryTime)
|
||||||
|
SettingsListTextItem(
|
||||||
|
title: "Connection",
|
||||||
|
value: item.route.displayName)
|
||||||
|
SettingsListTextItem(
|
||||||
|
title: "Round Trip Time",
|
||||||
|
value: "\(Int(item.roundTripTime * 1000)) ms")
|
||||||
|
SettingsListTextItem(
|
||||||
|
title: "Client challenge",
|
||||||
|
value: "\(item.message.clientChallenge)")
|
||||||
|
SettingsListTextItem(
|
||||||
|
title: "Server challenge",
|
||||||
|
value: "\(item.message.serverChallenge)")
|
||||||
|
Button {
|
||||||
|
delete(item: item)
|
||||||
|
} label: {
|
||||||
|
HStack {
|
||||||
|
Spacer()
|
||||||
|
Label("Delete", systemSymbol: .trash)
|
||||||
|
Spacer()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.listRowBackground(
|
||||||
|
RoundedRectangle(cornerSize: CGSize(width: 8, height: 8))
|
||||||
|
.fill(.red)
|
||||||
|
)
|
||||||
|
.foregroundColor(.white)
|
||||||
|
}.navigationTitle("Details")
|
||||||
|
}
|
||||||
|
|
||||||
|
private func delete(item: HistoryItem) {
|
||||||
|
modelContext.delete(item)
|
||||||
|
dismiss()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#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.")
|
||||||
|
}
|
||||||
|
}
|
42
Sesame-Watch Watch App/HistoryListRow.swift
Normal file
42
Sesame-Watch Watch App/HistoryListRow.swift
Normal file
@ -0,0 +1,42 @@
|
|||||||
|
import SwiftUI
|
||||||
|
import SFSafeSymbols
|
||||||
|
|
||||||
|
private let df: DateFormatter = {
|
||||||
|
let df = DateFormatter()
|
||||||
|
df.dateStyle = .short
|
||||||
|
df.timeStyle = .short
|
||||||
|
df.doesRelativeDateFormatting = true
|
||||||
|
return df
|
||||||
|
}()
|
||||||
|
|
||||||
|
struct HistoryListRow: View {
|
||||||
|
|
||||||
|
let item: HistoryItem
|
||||||
|
|
||||||
|
private var entryTime: String {
|
||||||
|
df.string(from: item.startDate)
|
||||||
|
}
|
||||||
|
|
||||||
|
var body: some View {
|
||||||
|
VStack(alignment: .leading) {
|
||||||
|
HStack {
|
||||||
|
Image(systemSymbol: item.response.symbol)
|
||||||
|
Text(item.response.description)
|
||||||
|
.font(.headline)
|
||||||
|
.foregroundColor(.primary)
|
||||||
|
Spacer()
|
||||||
|
}
|
||||||
|
Text(entryTime)
|
||||||
|
.font(.footnote)
|
||||||
|
.foregroundColor(.accentColor)
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
struct HistoryListRow_Previews: PreviewProvider {
|
||||||
|
static var previews: some View {
|
||||||
|
HistoryListRow(item: .mock)
|
||||||
|
}
|
||||||
|
}
|
76
Sesame-Watch Watch App/HistoryView.swift
Normal file
76
Sesame-Watch Watch App/HistoryView.swift
Normal file
@ -0,0 +1,76 @@
|
|||||||
|
import SwiftUI
|
||||||
|
import SwiftData
|
||||||
|
|
||||||
|
struct HistoryView: View {
|
||||||
|
|
||||||
|
@Environment(\.modelContext)
|
||||||
|
private var modelContext
|
||||||
|
|
||||||
|
@Query(sort: \HistoryItem.startDate, order: .reverse)
|
||||||
|
var history: [HistoryItem] = []
|
||||||
|
|
||||||
|
private var unlockCount: Int {
|
||||||
|
history.count { $0.response == .unlocked }
|
||||||
|
}
|
||||||
|
|
||||||
|
private var percentage: Double {
|
||||||
|
guard history.count > 0 else {
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
return Double(unlockCount * 100) / Double(history.count)
|
||||||
|
}
|
||||||
|
|
||||||
|
var body: some View {
|
||||||
|
NavigationView {
|
||||||
|
List {
|
||||||
|
HStack {
|
||||||
|
VStack(alignment: .leading) {
|
||||||
|
Text("\(history.count) requests")
|
||||||
|
.foregroundColor(.primary)
|
||||||
|
.font(.body)
|
||||||
|
Text(String(format: "%.1f %% success", percentage))
|
||||||
|
.foregroundColor(.secondary)
|
||||||
|
.font(.footnote)
|
||||||
|
}
|
||||||
|
Spacer()
|
||||||
|
}
|
||||||
|
.listRowBackground(Color.clear)
|
||||||
|
|
||||||
|
ForEach(history) { item in
|
||||||
|
NavigationLink {
|
||||||
|
HistoryItemDetail(item: item)
|
||||||
|
} label: {
|
||||||
|
HistoryListRow(item: item)
|
||||||
|
}
|
||||||
|
.swipeActions(edge: .trailing) {
|
||||||
|
Button {
|
||||||
|
delete(item: item)
|
||||||
|
} label: {
|
||||||
|
Label("Delete", systemSymbol: .trash)
|
||||||
|
}.tint(.red)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.navigationTitle("History")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private func delete(item: HistoryItem) {
|
||||||
|
modelContext.delete(item)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#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.")
|
||||||
|
}
|
||||||
|
}
|
52
Sesame-Watch Watch App/Sesame_WatchApp.swift
Normal file
52
Sesame-Watch Watch App/Sesame_WatchApp.swift
Normal file
@ -0,0 +1,52 @@
|
|||||||
|
import SwiftUI
|
||||||
|
import SwiftData
|
||||||
|
|
||||||
|
private enum MainScreenSelection: Int {
|
||||||
|
case unlock = 0
|
||||||
|
case settings = 1
|
||||||
|
case history = 2
|
||||||
|
}
|
||||||
|
|
||||||
|
@main
|
||||||
|
struct SesameWatchApp: App {
|
||||||
|
|
||||||
|
@State
|
||||||
|
var modelContainer: ModelContainer
|
||||||
|
|
||||||
|
@ObservedObject
|
||||||
|
var coordinator: RequestCoordinator
|
||||||
|
|
||||||
|
let keyManagement = KeyManagement()
|
||||||
|
|
||||||
|
@State
|
||||||
|
private var selectedScreen: MainScreenSelection = .unlock
|
||||||
|
|
||||||
|
@State
|
||||||
|
var didLaunchFromComplication = false
|
||||||
|
|
||||||
|
init() {
|
||||||
|
let modelContainer = SesameWatchApp.loadModelContainer()
|
||||||
|
self.modelContainer = modelContainer
|
||||||
|
self.coordinator = .init(modelContext: modelContainer.mainContext)
|
||||||
|
}
|
||||||
|
|
||||||
|
var body: some Scene {
|
||||||
|
WindowGroup {
|
||||||
|
TabView(selection: $selectedScreen) {
|
||||||
|
ContentView(coordinator: coordinator, didLaunchFromComplication: $didLaunchFromComplication)
|
||||||
|
.tag(MainScreenSelection.unlock)
|
||||||
|
SettingsView()
|
||||||
|
.environmentObject(keyManagement)
|
||||||
|
.tag(MainScreenSelection.settings)
|
||||||
|
HistoryView()
|
||||||
|
.tag(MainScreenSelection.history)
|
||||||
|
}
|
||||||
|
.tabViewStyle(PageTabViewStyle())
|
||||||
|
.onOpenURL { url in
|
||||||
|
didLaunchFromComplication = true
|
||||||
|
selectedScreen = .unlock
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.modelContainer(modelContainer)
|
||||||
|
}
|
||||||
|
}
|
77
Sesame-Watch Watch App/Settings/SettingsKeyInputView.swift
Normal file
77
Sesame-Watch Watch App/Settings/SettingsKeyInputView.swift
Normal file
@ -0,0 +1,77 @@
|
|||||||
|
import SwiftUI
|
||||||
|
import CryptoKit
|
||||||
|
|
||||||
|
struct SettingsKeyInputView: View {
|
||||||
|
|
||||||
|
let type: KeyManagement.KeyType
|
||||||
|
|
||||||
|
@State
|
||||||
|
private var text: String = ""
|
||||||
|
|
||||||
|
let footnote: String
|
||||||
|
|
||||||
|
@EnvironmentObject
|
||||||
|
private var keys: KeyManagement
|
||||||
|
|
||||||
|
private var hasKey: Bool {
|
||||||
|
keys.has(type)
|
||||||
|
}
|
||||||
|
|
||||||
|
private var displayText: String {
|
||||||
|
keys.get(type)?.displayString ?? "-"
|
||||||
|
}
|
||||||
|
|
||||||
|
private var copyText: String {
|
||||||
|
guard let key = keys.get(type)?.data else {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
guard type.usesHashing else {
|
||||||
|
return key.hexEncoded
|
||||||
|
}
|
||||||
|
return SHA256.hash(data: key).hexEncoded
|
||||||
|
}
|
||||||
|
|
||||||
|
var body: some View {
|
||||||
|
ScrollView {
|
||||||
|
VStack(alignment: .leading) {
|
||||||
|
TextField(type.displayName, text: $text)
|
||||||
|
.onSubmit(validateText)
|
||||||
|
.foregroundColor(.accentColor)
|
||||||
|
Text(footnote)
|
||||||
|
.font(.footnote)
|
||||||
|
.foregroundColor(.secondary)
|
||||||
|
}
|
||||||
|
.navigationTitle(type.displayName)
|
||||||
|
.onAppear {
|
||||||
|
if text == "" {
|
||||||
|
text = displayText
|
||||||
|
print("Text inserted")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private func validateText() {
|
||||||
|
let cleanText = text.replacingOccurrences(of: " ", with: "").trimmingCharacters(in: .whitespacesAndNewlines).lowercased()
|
||||||
|
guard let keyData = Data(fromHexEncodedString: cleanText) else {
|
||||||
|
print("Invalid key string")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
let keyLength = type.keyLength.bitCount
|
||||||
|
guard keyData.count * 8 == keyLength else {
|
||||||
|
print("Invalid key length \(keyData.count * 8) bits, expected \(keyLength) (Input: '\(text)')")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
keys.save(type, data: keyData)
|
||||||
|
print("Key \(type) saved")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
struct SettingsKeyInputView_Previews: PreviewProvider {
|
||||||
|
static var previews: some View {
|
||||||
|
SettingsKeyInputView(
|
||||||
|
type: .remoteKey,
|
||||||
|
footnote: "Some text describing the purpose of the key.")
|
||||||
|
.environmentObject(KeyManagement())
|
||||||
|
}
|
||||||
|
}
|
47
Sesame-Watch Watch App/Settings/SettingsKeyItemLink.swift
Normal file
47
Sesame-Watch Watch App/Settings/SettingsKeyItemLink.swift
Normal file
@ -0,0 +1,47 @@
|
|||||||
|
import SwiftUI
|
||||||
|
|
||||||
|
struct SettingsKeyItemLink: View {
|
||||||
|
|
||||||
|
let type: KeyManagement.KeyType
|
||||||
|
|
||||||
|
let footnote: String
|
||||||
|
|
||||||
|
@EnvironmentObject
|
||||||
|
private var keys: KeyManagement
|
||||||
|
|
||||||
|
@State
|
||||||
|
private var keyText = "..."
|
||||||
|
|
||||||
|
var body: some View {
|
||||||
|
NavigationLink {
|
||||||
|
SettingsKeyInputView(
|
||||||
|
type: type,
|
||||||
|
footnote: footnote)
|
||||||
|
.environmentObject(keys)
|
||||||
|
} label: {
|
||||||
|
SettingsListTextItem(
|
||||||
|
title: type.displayName,
|
||||||
|
value: keyText)
|
||||||
|
.onAppear(perform: updateKeyText)
|
||||||
|
}
|
||||||
|
.buttonStyle(PlainButtonStyle())
|
||||||
|
}
|
||||||
|
|
||||||
|
private func updateKeyText() {
|
||||||
|
Task {
|
||||||
|
let key = keys.get(type)?.displayString ?? "Not set"
|
||||||
|
DispatchQueue.main.async {
|
||||||
|
keyText = key
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
struct SettingsKeyItemLink_Previews: PreviewProvider {
|
||||||
|
static var previews: some View {
|
||||||
|
SettingsKeyItemLink(
|
||||||
|
type: .deviceKey,
|
||||||
|
footnote: "Some text describing the purpose of the key.")
|
||||||
|
.environmentObject(KeyManagement())
|
||||||
|
}
|
||||||
|
}
|
31
Sesame-Watch Watch App/Settings/SettingsListTextItem.swift
Normal file
31
Sesame-Watch Watch App/Settings/SettingsListTextItem.swift
Normal file
@ -0,0 +1,31 @@
|
|||||||
|
import SwiftUI
|
||||||
|
|
||||||
|
struct SettingsListTextItem: View {
|
||||||
|
|
||||||
|
let title: String
|
||||||
|
|
||||||
|
let value: String
|
||||||
|
|
||||||
|
var body: some View {
|
||||||
|
VStack(alignment: .leading) {
|
||||||
|
HStack {
|
||||||
|
Text(title)
|
||||||
|
.foregroundColor(.primary)
|
||||||
|
Spacer()
|
||||||
|
}
|
||||||
|
Text(value)
|
||||||
|
.font(.footnote)
|
||||||
|
.foregroundColor(.secondary)
|
||||||
|
.lineLimit(1)
|
||||||
|
}
|
||||||
|
.padding()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
struct SettingsListTextItem_Previews: PreviewProvider {
|
||||||
|
static var previews: some View {
|
||||||
|
SettingsListTextItem(
|
||||||
|
title: "Title",
|
||||||
|
value: "Some longer text")
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,45 @@
|
|||||||
|
import SwiftUI
|
||||||
|
|
||||||
|
struct SettingsNumberInputView<Value>: View where Value: FixedWidthInteger {
|
||||||
|
|
||||||
|
let title: String
|
||||||
|
|
||||||
|
@Binding
|
||||||
|
var value: Value
|
||||||
|
|
||||||
|
@State
|
||||||
|
private var text: String = ""
|
||||||
|
|
||||||
|
let footnote: String
|
||||||
|
|
||||||
|
var body: some View {
|
||||||
|
VStack(alignment: .leading) {
|
||||||
|
TextField(title, text: $text)
|
||||||
|
.onSubmit {
|
||||||
|
guard let newValue = Value(text) else {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
value = newValue
|
||||||
|
}
|
||||||
|
.foregroundColor(.accentColor)
|
||||||
|
Text(footnote)
|
||||||
|
.font(.footnote)
|
||||||
|
.foregroundColor(.secondary)
|
||||||
|
Spacer()
|
||||||
|
}
|
||||||
|
.navigationTitle(title)
|
||||||
|
.navigationBarBackButtonHidden(false)
|
||||||
|
.onAppear {
|
||||||
|
text = "\(value)"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
struct SettingsNumberInputView_Previews: PreviewProvider {
|
||||||
|
static var previews: some View {
|
||||||
|
SettingsNumberInputView(
|
||||||
|
title: "Title",
|
||||||
|
value: .constant(0),
|
||||||
|
footnote: "Some more text explaining the purpose of the text field.")
|
||||||
|
}
|
||||||
|
}
|
30
Sesame-Watch Watch App/Settings/SettingsNumberItemLink.swift
Normal file
30
Sesame-Watch Watch App/Settings/SettingsNumberItemLink.swift
Normal file
@ -0,0 +1,30 @@
|
|||||||
|
import SwiftUI
|
||||||
|
|
||||||
|
struct SettingsNumberItemLink<Value>: View where Value: FixedWidthInteger {
|
||||||
|
|
||||||
|
let title: String
|
||||||
|
|
||||||
|
@Binding
|
||||||
|
var value: Value
|
||||||
|
|
||||||
|
let footnote: String
|
||||||
|
|
||||||
|
var body: some View {
|
||||||
|
NavigationLink {
|
||||||
|
SettingsNumberInputView(
|
||||||
|
title: title,
|
||||||
|
value: $value,
|
||||||
|
footnote: footnote
|
||||||
|
)
|
||||||
|
} label: {
|
||||||
|
SettingsListTextItem(title: title, value: "\(value)")
|
||||||
|
}
|
||||||
|
.buttonStyle(PlainButtonStyle())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
struct SettingsNumberItemLink_Previews: PreviewProvider {
|
||||||
|
static var previews: some View {
|
||||||
|
SettingsNumberItemLink(title: "Title", value: .constant(0), footnote: "Some more text explaining the purpose of the text field.")
|
||||||
|
}
|
||||||
|
}
|
33
Sesame-Watch Watch App/Settings/SettingsTextInputView.swift
Normal file
33
Sesame-Watch Watch App/Settings/SettingsTextInputView.swift
Normal file
@ -0,0 +1,33 @@
|
|||||||
|
import SwiftUI
|
||||||
|
|
||||||
|
struct SettingsTextInputView: View {
|
||||||
|
|
||||||
|
let title: String
|
||||||
|
|
||||||
|
@Binding
|
||||||
|
var text: String
|
||||||
|
|
||||||
|
let footnote: String
|
||||||
|
|
||||||
|
var body: some View {
|
||||||
|
VStack(alignment: .leading) {
|
||||||
|
TextField(title, text: $text)
|
||||||
|
.foregroundColor(.accentColor)
|
||||||
|
Text(footnote)
|
||||||
|
.font(.footnote)
|
||||||
|
.foregroundColor(.secondary)
|
||||||
|
Spacer()
|
||||||
|
}
|
||||||
|
.navigationTitle(title)
|
||||||
|
.navigationBarBackButtonHidden(false)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
struct SettingsTextInputView_Previews: PreviewProvider {
|
||||||
|
static var previews: some View {
|
||||||
|
SettingsTextInputView(
|
||||||
|
title: "Title",
|
||||||
|
text: .constant("Text"),
|
||||||
|
footnote: "Some more text explaining the purpose of the text field.")
|
||||||
|
}
|
||||||
|
}
|
31
Sesame-Watch Watch App/Settings/SettingsTextItemLink.swift
Normal file
31
Sesame-Watch Watch App/Settings/SettingsTextItemLink.swift
Normal file
@ -0,0 +1,31 @@
|
|||||||
|
import SwiftUI
|
||||||
|
|
||||||
|
struct SettingsTextItemLink: View {
|
||||||
|
|
||||||
|
let title: String
|
||||||
|
|
||||||
|
@Binding
|
||||||
|
var value: String
|
||||||
|
|
||||||
|
let footnote: String
|
||||||
|
|
||||||
|
var body: some View {
|
||||||
|
NavigationLink {
|
||||||
|
SettingsTextInputView(
|
||||||
|
title: title,
|
||||||
|
text: $value,
|
||||||
|
footnote: footnote
|
||||||
|
)
|
||||||
|
} label: {
|
||||||
|
SettingsListTextItem(title: title, value: value)
|
||||||
|
}
|
||||||
|
.buttonStyle(PlainButtonStyle())
|
||||||
|
.padding(0)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
struct SettingsTextItemLink_Previews: PreviewProvider {
|
||||||
|
static var previews: some View {
|
||||||
|
SettingsTextItemLink(title: "Title", value: .constant("Some value"), footnote: "Some more text explaining the purpose of the text field.")
|
||||||
|
}
|
||||||
|
}
|
72
Sesame-Watch Watch App/SettingsView.swift
Normal file
72
Sesame-Watch Watch App/SettingsView.swift
Normal file
@ -0,0 +1,72 @@
|
|||||||
|
import SwiftUI
|
||||||
|
|
||||||
|
struct SettingsView: View {
|
||||||
|
|
||||||
|
@AppStorage("connectionType")
|
||||||
|
var connectionType: ConnectionStrategy = .remoteFirst
|
||||||
|
|
||||||
|
@AppStorage("server")
|
||||||
|
var serverPath: String = "https://christophhagen.de/sesame/"
|
||||||
|
|
||||||
|
@AppStorage("localIP")
|
||||||
|
var localAddress: String = "192.168.178.104/"
|
||||||
|
|
||||||
|
@AppStorage("localPort")
|
||||||
|
var localPort: UInt16 = 8888
|
||||||
|
|
||||||
|
@EnvironmentObject
|
||||||
|
var keys: KeyManagement
|
||||||
|
|
||||||
|
var some: String { "some" }
|
||||||
|
|
||||||
|
var body: some View {
|
||||||
|
NavigationStack {
|
||||||
|
List {
|
||||||
|
Picker("Connection", selection: $connectionType) {
|
||||||
|
Text(display: ConnectionStrategy.local)
|
||||||
|
.tag(ConnectionStrategy.local)
|
||||||
|
Text(display: ConnectionStrategy.localFirst)
|
||||||
|
.tag(ConnectionStrategy.localFirst)
|
||||||
|
Text(display: ConnectionStrategy.remote)
|
||||||
|
.tag(ConnectionStrategy.remote)
|
||||||
|
Text(display: ConnectionStrategy.remoteFirst)
|
||||||
|
.tag(ConnectionStrategy.remoteFirst)
|
||||||
|
}
|
||||||
|
.padding(.leading)
|
||||||
|
SettingsTextItemLink(
|
||||||
|
title: "Server url",
|
||||||
|
value: $serverPath,
|
||||||
|
footnote: "The url where the sesame server listens for incoming messages.")
|
||||||
|
SettingsTextItemLink(
|
||||||
|
title: "Local url",
|
||||||
|
value: $localAddress,
|
||||||
|
footnote: "The url where the device can be reached directly on the local WiFi network.")
|
||||||
|
SettingsNumberItemLink(
|
||||||
|
title: "Local port",
|
||||||
|
value: $localPort,
|
||||||
|
footnote: "The port for the local connection")
|
||||||
|
SettingsKeyItemLink(
|
||||||
|
type: .deviceKey,
|
||||||
|
footnote: "The key used by the device for responses")
|
||||||
|
.environmentObject(keys)
|
||||||
|
SettingsKeyItemLink(
|
||||||
|
type: .remoteKey,
|
||||||
|
footnote: "The key used by the remote for requests.")
|
||||||
|
.environmentObject(keys)
|
||||||
|
SettingsKeyItemLink(
|
||||||
|
type: .authToken,
|
||||||
|
footnote: "The authentication token of the remote for the server.")
|
||||||
|
.environmentObject(keys)
|
||||||
|
}
|
||||||
|
.navigationTitle("Settings")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
struct SettingsView_Previews: PreviewProvider {
|
||||||
|
static var previews: some View {
|
||||||
|
SettingsView()
|
||||||
|
.previewDevice("Apple Watch Series 7 - 41mm")
|
||||||
|
.environmentObject(KeyManagement())
|
||||||
|
}
|
||||||
|
}
|
83
Sesame-Watch Watch App/UnlockView.swift
Normal file
83
Sesame-Watch Watch App/UnlockView.swift
Normal file
@ -0,0 +1,83 @@
|
|||||||
|
import SwiftUI
|
||||||
|
import SwiftData
|
||||||
|
import SFSafeSymbols
|
||||||
|
import CryptoKit
|
||||||
|
import UIKit
|
||||||
|
|
||||||
|
struct ContentView: View {
|
||||||
|
|
||||||
|
@Binding
|
||||||
|
var didLaunchFromComplication: Bool
|
||||||
|
|
||||||
|
@ObservedObject
|
||||||
|
var coordinator: RequestCoordinator
|
||||||
|
|
||||||
|
init(coordinator: RequestCoordinator, didLaunchFromComplication: Binding<Bool>) {
|
||||||
|
self._didLaunchFromComplication = didLaunchFromComplication
|
||||||
|
self.coordinator = coordinator
|
||||||
|
}
|
||||||
|
|
||||||
|
var buttonBackground: Color {
|
||||||
|
.white.opacity(0.2)
|
||||||
|
}
|
||||||
|
|
||||||
|
var buttonColor: Color {
|
||||||
|
.white
|
||||||
|
}
|
||||||
|
|
||||||
|
private var stateText: String {
|
||||||
|
if coordinator.state == .notChecked {
|
||||||
|
return "Unlock"
|
||||||
|
}
|
||||||
|
return coordinator.state.description
|
||||||
|
}
|
||||||
|
|
||||||
|
var body: some View {
|
||||||
|
HStack {
|
||||||
|
Spacer()
|
||||||
|
VStack(alignment: .center) {
|
||||||
|
Image(systemSymbol: .lock)
|
||||||
|
.resizable()
|
||||||
|
.aspectRatio(contentMode: .fit)
|
||||||
|
.fontWeight(.ultraLight)
|
||||||
|
.padding()
|
||||||
|
.onTapGesture { coordinator.startUnlock() }
|
||||||
|
if coordinator.isPerformingRequest {
|
||||||
|
ProgressView()
|
||||||
|
.progressViewStyle(CircularProgressViewStyle())
|
||||||
|
.frame(width: 20, height: 20)
|
||||||
|
} else {
|
||||||
|
Text(stateText)
|
||||||
|
.font(.subheadline)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Spacer()
|
||||||
|
}
|
||||||
|
.background(coordinator.state.color)
|
||||||
|
.animation(.easeInOut, value: coordinator.state.color)
|
||||||
|
.onChange(of: didLaunchFromComplication) { _, launched in
|
||||||
|
guard launched else {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
coordinator.startUnlock(quitAfterSuccess: true)
|
||||||
|
didLaunchFromComplication = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#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()
|
||||||
|
let coordinator = RequestCoordinator(modelContext: container.mainContext)
|
||||||
|
return ContentView(coordinator: coordinator, didLaunchFromComplication: .constant(false))
|
||||||
|
.modelContainer(container)
|
||||||
|
} catch {
|
||||||
|
fatalError("Failed to create model container.")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,11 @@
|
|||||||
|
{
|
||||||
|
"colors" : [
|
||||||
|
{
|
||||||
|
"idiom" : "universal"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"info" : {
|
||||||
|
"author" : "xcode",
|
||||||
|
"version" : 1
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,13 @@
|
|||||||
|
{
|
||||||
|
"images" : [
|
||||||
|
{
|
||||||
|
"idiom" : "universal",
|
||||||
|
"platform" : "watchos",
|
||||||
|
"size" : "1024x1024"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"info" : {
|
||||||
|
"author" : "xcode",
|
||||||
|
"version" : 1
|
||||||
|
}
|
||||||
|
}
|
6
Sesame-Widget/Assets.xcassets/Contents.json
Normal file
6
Sesame-Widget/Assets.xcassets/Contents.json
Normal file
@ -0,0 +1,6 @@
|
|||||||
|
{
|
||||||
|
"info" : {
|
||||||
|
"author" : "xcode",
|
||||||
|
"version" : 1
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,11 @@
|
|||||||
|
{
|
||||||
|
"colors" : [
|
||||||
|
{
|
||||||
|
"idiom" : "universal"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"info" : {
|
||||||
|
"author" : "xcode",
|
||||||
|
"version" : 1
|
||||||
|
}
|
||||||
|
}
|
11
Sesame-Widget/Info.plist
Normal file
11
Sesame-Widget/Info.plist
Normal file
@ -0,0 +1,11 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||||
|
<plist version="1.0">
|
||||||
|
<dict>
|
||||||
|
<key>NSExtension</key>
|
||||||
|
<dict>
|
||||||
|
<key>NSExtensionPointIdentifier</key>
|
||||||
|
<string>com.apple.widgetkit-extension</string>
|
||||||
|
</dict>
|
||||||
|
</dict>
|
||||||
|
</plist>
|
73
Sesame-Widget/Sesame_Widget.swift
Normal file
73
Sesame-Widget/Sesame_Widget.swift
Normal file
@ -0,0 +1,73 @@
|
|||||||
|
import WidgetKit
|
||||||
|
import SwiftUI
|
||||||
|
import SFSafeSymbols
|
||||||
|
|
||||||
|
struct Provider: TimelineProvider {
|
||||||
|
|
||||||
|
func placeholder(in context: Context) -> SimpleEntry {
|
||||||
|
SimpleEntry(date: Date())
|
||||||
|
}
|
||||||
|
|
||||||
|
func getSnapshot(in context: Context, completion: @escaping (SimpleEntry) -> ()) {
|
||||||
|
let entry = SimpleEntry(date: Date())
|
||||||
|
completion(entry)
|
||||||
|
}
|
||||||
|
|
||||||
|
func getTimeline(in context: Context, completion: @escaping (Timeline<Entry>) -> ()) {
|
||||||
|
let entries = [Entry(date: .now), Entry(date: .distantFuture)]
|
||||||
|
completion(Timeline(entries: entries, policy: .atEnd))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
struct SimpleEntry: TimelineEntry {
|
||||||
|
let date: Date
|
||||||
|
}
|
||||||
|
|
||||||
|
struct Sesame_WidgetEntryView : View {
|
||||||
|
|
||||||
|
@Environment(\.widgetRenderingMode)
|
||||||
|
var widgetRenderingMode
|
||||||
|
|
||||||
|
@Environment(\.widgetFamily)
|
||||||
|
var widgetFamily
|
||||||
|
|
||||||
|
var entry: Provider.Entry
|
||||||
|
|
||||||
|
var body: some View {
|
||||||
|
switch widgetRenderingMode {
|
||||||
|
default:
|
||||||
|
image.unredacted()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private var image: some View {
|
||||||
|
Image(systemSymbol: .lock)
|
||||||
|
.resizable()
|
||||||
|
.aspectRatio(contentMode: .fit)
|
||||||
|
.padding()
|
||||||
|
.fontWeight(.thin)
|
||||||
|
.widgetURL(URL(string: "sesame:///open")!)
|
||||||
|
.containerBackground(.green, for: .widget)
|
||||||
|
.widgetAccentable()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@main
|
||||||
|
struct Sesame_Widget: Widget {
|
||||||
|
let kind: String = "SesameOpen"
|
||||||
|
|
||||||
|
var body: some WidgetConfiguration {
|
||||||
|
StaticConfiguration(kind: kind, provider: Provider()) { entry in
|
||||||
|
Sesame_WidgetEntryView(entry: entry)
|
||||||
|
}
|
||||||
|
.configurationDisplayName("Open")
|
||||||
|
.description("This widget can be used to unlock the door with a single tap.")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
struct Sesame_Widget_Previews: PreviewProvider {
|
||||||
|
static var previews: some View {
|
||||||
|
Sesame_WidgetEntryView(entry: SimpleEntry(date: Date()))
|
||||||
|
.previewContext(WidgetPreviewContext(family: .accessoryRectangular))
|
||||||
|
}
|
||||||
|
}
|
@ -8,53 +8,192 @@
|
|||||||
|
|
||||||
/* Begin PBXBuildFile section */
|
/* Begin PBXBuildFile section */
|
||||||
884A45B7279F48C100D6E650 /* SesameApp.swift in Sources */ = {isa = PBXBuildFile; fileRef = 884A45B6279F48C100D6E650 /* SesameApp.swift */; };
|
884A45B7279F48C100D6E650 /* SesameApp.swift in Sources */ = {isa = PBXBuildFile; fileRef = 884A45B6279F48C100D6E650 /* SesameApp.swift */; };
|
||||||
884A45B9279F48C100D6E650 /* ContentView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 884A45B8279F48C100D6E650 /* ContentView.swift */; };
|
884A45B9279F48C100D6E650 /* MainView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 884A45B8279F48C100D6E650 /* MainView.swift */; };
|
||||||
884A45BB279F48C300D6E650 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 884A45BA279F48C300D6E650 /* Assets.xcassets */; };
|
884A45BB279F48C300D6E650 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 884A45BA279F48C300D6E650 /* Assets.xcassets */; };
|
||||||
884A45BE279F48C300D6E650 /* Preview Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 884A45BD279F48C300D6E650 /* Preview Assets.xcassets */; };
|
|
||||||
884A45C5279F4BBE00D6E650 /* KeyManagement.swift in Sources */ = {isa = PBXBuildFile; fileRef = 884A45C4279F4BBE00D6E650 /* KeyManagement.swift */; };
|
884A45C5279F4BBE00D6E650 /* KeyManagement.swift in Sources */ = {isa = PBXBuildFile; fileRef = 884A45C4279F4BBE00D6E650 /* KeyManagement.swift */; };
|
||||||
884A45C927A43D7900D6E650 /* ClientState.swift in Sources */ = {isa = PBXBuildFile; fileRef = 884A45C827A43D7900D6E650 /* ClientState.swift */; };
|
|
||||||
884A45CB27A464C000D6E650 /* SymmetricKey+Extensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 884A45CA27A464C000D6E650 /* SymmetricKey+Extensions.swift */; };
|
884A45CB27A464C000D6E650 /* SymmetricKey+Extensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 884A45CA27A464C000D6E650 /* SymmetricKey+Extensions.swift */; };
|
||||||
884A45CD27A465F500D6E650 /* Client.swift in Sources */ = {isa = PBXBuildFile; fileRef = 884A45CC27A465F500D6E650 /* Client.swift */; };
|
|
||||||
884A45CF27A5402D00D6E650 /* MessageResult.swift in Sources */ = {isa = PBXBuildFile; fileRef = 884A45CE27A5402D00D6E650 /* MessageResult.swift */; };
|
884A45CF27A5402D00D6E650 /* MessageResult.swift in Sources */ = {isa = PBXBuildFile; fileRef = 884A45CE27A5402D00D6E650 /* MessageResult.swift */; };
|
||||||
E24EE77227FDCCC00011CFD2 /* Data+Extensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = E24EE77127FDCCC00011CFD2 /* Data+Extensions.swift */; };
|
8860D7432B22858600849FAC /* Date+Timestamp.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8860D7422B22858600849FAC /* Date+Timestamp.swift */; };
|
||||||
E24EE77427FF95920011CFD2 /* DeviceResponse.swift in Sources */ = {isa = PBXBuildFile; fileRef = E24EE77327FF95920011CFD2 /* DeviceResponse.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 */; };
|
||||||
|
888A11332B32DBBB0099A12B /* UInt8+Extensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 888A11322B32DBBB0099A12B /* UInt8+Extensions.swift */; };
|
||||||
|
888A11342B32DC3C0099A12B /* UInt8+Extensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 888A11322B32DBBB0099A12B /* UInt8+Extensions.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 */; };
|
||||||
|
88BA7DD32BD41B8A008F2A3C /* UDPClient.swift in Sources */ = {isa = PBXBuildFile; fileRef = 88BA7DD22BD41B8A008F2A3C /* UDPClient.swift */; };
|
||||||
|
88E197B229EDC9BC00BF1D19 /* Sesame_WatchApp.swift in Sources */ = {isa = PBXBuildFile; fileRef = 88E197B129EDC9BC00BF1D19 /* Sesame_WatchApp.swift */; };
|
||||||
|
88E197B429EDC9BC00BF1D19 /* UnlockView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 88E197B329EDC9BC00BF1D19 /* UnlockView.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 */; };
|
||||||
|
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+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+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 */; };
|
||||||
|
88E35EF52B3B0A9800485A66 /* App+Extensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 88E35EF42B3B0A9800485A66 /* App+Extensions.swift */; };
|
||||||
|
88E35EF62B3B0A9800485A66 /* App+Extensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 88E35EF42B3B0A9800485A66 /* App+Extensions.swift */; };
|
||||||
|
E240654B2A8153C6009C1AD8 /* SettingsListTextItem.swift in Sources */ = {isa = PBXBuildFile; fileRef = E240654A2A8153C6009C1AD8 /* SettingsListTextItem.swift */; };
|
||||||
|
E240654F2A8159B7009C1AD8 /* SettingsTextInputView.swift in Sources */ = {isa = PBXBuildFile; fileRef = E240654E2A8159B7009C1AD8 /* SettingsTextInputView.swift */; };
|
||||||
|
E24065512A819066009C1AD8 /* SettingsTextItemLink.swift in Sources */ = {isa = PBXBuildFile; fileRef = E24065502A819066009C1AD8 /* SettingsTextItemLink.swift */; };
|
||||||
|
E24065532A819614009C1AD8 /* SettingsNumberItemLink.swift in Sources */ = {isa = PBXBuildFile; fileRef = E24065522A819614009C1AD8 /* SettingsNumberItemLink.swift */; };
|
||||||
|
E24065552A819663009C1AD8 /* SettingsNumberInputView.swift in Sources */ = {isa = PBXBuildFile; fileRef = E24065542A819663009C1AD8 /* SettingsNumberInputView.swift */; };
|
||||||
|
E24065582A819AE3009C1AD8 /* SettingsKeyItemLink.swift in Sources */ = {isa = PBXBuildFile; fileRef = E24065572A819AE3009C1AD8 /* SettingsKeyItemLink.swift */; };
|
||||||
|
E240655A2A82218D009C1AD8 /* SettingsKeyInputView.swift in Sources */ = {isa = PBXBuildFile; fileRef = E24065592A82218D009C1AD8 /* SettingsKeyInputView.swift */; };
|
||||||
|
E240655B2A822397009C1AD8 /* KeyManagement.swift in Sources */ = {isa = PBXBuildFile; fileRef = 884A45C4279F4BBE00D6E650 /* KeyManagement.swift */; };
|
||||||
|
E240655C2A822C8E009C1AD8 /* HistoryManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = E28DED36281EC7FB00259690 /* HistoryManager.swift */; };
|
||||||
|
E240655E2A822E97009C1AD8 /* HistoryListRow.swift in Sources */ = {isa = PBXBuildFile; fileRef = E240655D2A822E97009C1AD8 /* HistoryListRow.swift */; };
|
||||||
|
E24065602A822ED9009C1AD8 /* HistoryItemDetail.swift in Sources */ = {isa = PBXBuildFile; fileRef = E240655F2A822ED9009C1AD8 /* HistoryItemDetail.swift */; };
|
||||||
|
E24EE77227FDCCC00011CFD2 /* Data+Hex.swift in Sources */ = {isa = PBXBuildFile; fileRef = E24EE77127FDCCC00011CFD2 /* Data+Hex.swift */; };
|
||||||
E24EE77727FF95C00011CFD2 /* NIOCore in Frameworks */ = {isa = PBXBuildFile; productRef = E24EE77627FF95C00011CFD2 /* NIOCore */; };
|
E24EE77727FF95C00011CFD2 /* NIOCore in Frameworks */ = {isa = PBXBuildFile; productRef = E24EE77627FF95C00011CFD2 /* NIOCore */; };
|
||||||
E24EE77927FF95E00011CFD2 /* Message.swift in Sources */ = {isa = PBXBuildFile; fileRef = E24EE77827FF95E00011CFD2 /* Message.swift */; };
|
E24EE77927FF95E00011CFD2 /* Message.swift in Sources */ = {isa = PBXBuildFile; fileRef = E24EE77827FF95E00011CFD2 /* Message.swift */; };
|
||||||
E28DED2D281E840B00259690 /* KeyView.swift in Sources */ = {isa = PBXBuildFile; fileRef = E28DED2C281E840B00259690 /* KeyView.swift */; };
|
E24F6C6E2A89749A0040F8C4 /* ConnectionStrategy.swift in Sources */ = {isa = PBXBuildFile; fileRef = E24F6C6D2A89749A0040F8C4 /* ConnectionStrategy.swift */; };
|
||||||
|
E24F6C6F2A8974C60040F8C4 /* ConnectionStrategy.swift in Sources */ = {isa = PBXBuildFile; fileRef = E24F6C6D2A89749A0040F8C4 /* ConnectionStrategy.swift */; };
|
||||||
|
E25231782C0227B500FFE373 /* UDPClient.swift in Sources */ = {isa = PBXBuildFile; fileRef = 88BA7DD22BD41B8A008F2A3C /* UDPClient.swift */; };
|
||||||
|
E25317562A8A1ABF005A537D /* HistoryItem.swift in Sources */ = {isa = PBXBuildFile; fileRef = E28DED34281EB17600259690 /* HistoryItem.swift */; };
|
||||||
|
E268E0822A85302000185913 /* WidgetKit.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = E268E0532A852F8E00185913 /* WidgetKit.framework */; };
|
||||||
|
E268E0832A85302000185913 /* SwiftUI.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = E268E0552A852F8E00185913 /* SwiftUI.framework */; };
|
||||||
|
E268E0862A85302000185913 /* Sesame_Widget.swift in Sources */ = {isa = PBXBuildFile; fileRef = E268E0852A85302000185913 /* Sesame_Widget.swift */; };
|
||||||
|
E268E0882A85302000185913 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = E268E0872A85302000185913 /* Assets.xcassets */; };
|
||||||
|
E268E08C2A85302000185913 /* Sesame-WidgetExtension.appex in Embed Foundation Extensions */ = {isa = PBXBuildFile; fileRef = E268E0812A85302000185913 /* Sesame-WidgetExtension.appex */; settings = {ATTRIBUTES = (RemoveHeadersOnCopy, ); }; };
|
||||||
|
E268E0912A85318500185913 /* SFSafeSymbols in Frameworks */ = {isa = PBXBuildFile; productRef = E268E0902A85318500185913 /* SFSafeSymbols */; };
|
||||||
|
E28DED2D281E840B00259690 /* SettingsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = E28DED2C281E840B00259690 /* SettingsView.swift */; };
|
||||||
E28DED2F281E8A0500259690 /* SingleKeyView.swift in Sources */ = {isa = PBXBuildFile; fileRef = E28DED2E281E8A0500259690 /* SingleKeyView.swift */; };
|
E28DED2F281E8A0500259690 /* SingleKeyView.swift in Sources */ = {isa = PBXBuildFile; fileRef = E28DED2E281E8A0500259690 /* SingleKeyView.swift */; };
|
||||||
E28DED31281EAE9100259690 /* HistoryView.swift in Sources */ = {isa = PBXBuildFile; fileRef = E28DED30281EAE9100259690 /* HistoryView.swift */; };
|
E28DED31281EAE9100259690 /* HistoryView.swift in Sources */ = {isa = PBXBuildFile; fileRef = E28DED30281EAE9100259690 /* HistoryView.swift */; };
|
||||||
E28DED33281EB15B00259690 /* HistoryListItem.swift in Sources */ = {isa = PBXBuildFile; fileRef = E28DED32281EB15B00259690 /* HistoryListItem.swift */; };
|
E28DED33281EB15B00259690 /* HistoryListItem.swift in Sources */ = {isa = PBXBuildFile; fileRef = E28DED32281EB15B00259690 /* HistoryListItem.swift */; };
|
||||||
E28DED35281EB17600259690 /* HistoryItem.swift in Sources */ = {isa = PBXBuildFile; fileRef = E28DED34281EB17600259690 /* HistoryItem.swift */; };
|
E28DED35281EB17600259690 /* HistoryItem.swift in Sources */ = {isa = PBXBuildFile; fileRef = E28DED34281EB17600259690 /* HistoryItem.swift */; };
|
||||||
E28DED37281EC7FB00259690 /* HistoryManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = E28DED36281EC7FB00259690 /* HistoryManager.swift */; };
|
E28DED37281EC7FB00259690 /* HistoryManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = E28DED36281EC7FB00259690 /* HistoryManager.swift */; };
|
||||||
E2C5C1DB2806FE8900769EF6 /* RouteAPI.swift in Sources */ = {isa = PBXBuildFile; fileRef = E2C5C1DA2806FE8900769EF6 /* RouteAPI.swift */; };
|
E2C5C1DB2806FE8900769EF6 /* SesameRoute.swift in Sources */ = {isa = PBXBuildFile; fileRef = E2C5C1DA2806FE8900769EF6 /* SesameRoute.swift */; };
|
||||||
E2C5C1DD281B3AC400769EF6 /* UInt32+Extensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = E2C5C1DC281B3AC400769EF6 /* UInt32+Extensions.swift */; };
|
E2C5C1DD281B3AC400769EF6 /* UInt32+Coding.swift in Sources */ = {isa = PBXBuildFile; fileRef = E2C5C1DC281B3AC400769EF6 /* UInt32+Coding.swift */; };
|
||||||
E2C5C1F8281E769F00769EF6 /* ServerMessage.swift in Sources */ = {isa = PBXBuildFile; fileRef = E2C5C1F7281E769F00769EF6 /* ServerMessage.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 */
|
/* End PBXBuildFile section */
|
||||||
|
|
||||||
|
/* Begin PBXContainerItemProxy section */
|
||||||
|
E268E08A2A85302000185913 /* PBXContainerItemProxy */ = {
|
||||||
|
isa = PBXContainerItemProxy;
|
||||||
|
containerPortal = 884A45AB279F48C100D6E650 /* Project object */;
|
||||||
|
proxyType = 1;
|
||||||
|
remoteGlobalIDString = E268E0802A85302000185913;
|
||||||
|
remoteInfo = "Sesame-WidgetExtension";
|
||||||
|
};
|
||||||
|
/* End PBXContainerItemProxy section */
|
||||||
|
|
||||||
|
/* Begin PBXCopyFilesBuildPhase section */
|
||||||
|
E268E0662A852F8E00185913 /* Embed Foundation Extensions */ = {
|
||||||
|
isa = PBXCopyFilesBuildPhase;
|
||||||
|
buildActionMask = 2147483647;
|
||||||
|
dstPath = "";
|
||||||
|
dstSubfolderSpec = 13;
|
||||||
|
files = (
|
||||||
|
E268E08C2A85302000185913 /* Sesame-WidgetExtension.appex in Embed Foundation Extensions */,
|
||||||
|
);
|
||||||
|
name = "Embed Foundation Extensions";
|
||||||
|
runOnlyForDeploymentPostprocessing = 0;
|
||||||
|
};
|
||||||
|
/* End PBXCopyFilesBuildPhase section */
|
||||||
|
|
||||||
/* Begin PBXFileReference section */
|
/* Begin PBXFileReference section */
|
||||||
884A45B3279F48C100D6E650 /* Sesame.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = Sesame.app; sourceTree = BUILT_PRODUCTS_DIR; };
|
884A45B3279F48C100D6E650 /* Sesame.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = Sesame.app; sourceTree = BUILT_PRODUCTS_DIR; };
|
||||||
884A45B6279F48C100D6E650 /* SesameApp.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SesameApp.swift; sourceTree = "<group>"; };
|
884A45B6279F48C100D6E650 /* SesameApp.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SesameApp.swift; sourceTree = "<group>"; };
|
||||||
884A45B8279F48C100D6E650 /* ContentView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ContentView.swift; sourceTree = "<group>"; };
|
884A45B8279F48C100D6E650 /* MainView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MainView.swift; sourceTree = "<group>"; };
|
||||||
884A45BA279F48C300D6E650 /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = "<group>"; };
|
884A45BA279F48C300D6E650 /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = "<group>"; };
|
||||||
884A45BD279F48C300D6E650 /* Preview Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = "Preview Assets.xcassets"; sourceTree = "<group>"; };
|
|
||||||
884A45C4279F4BBE00D6E650 /* KeyManagement.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = KeyManagement.swift; sourceTree = "<group>"; };
|
884A45C4279F4BBE00D6E650 /* KeyManagement.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = KeyManagement.swift; sourceTree = "<group>"; };
|
||||||
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>"; };
|
884A45CA27A464C000D6E650 /* SymmetricKey+Extensions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "SymmetricKey+Extensions.swift"; sourceTree = "<group>"; };
|
||||||
884A45CC27A465F500D6E650 /* Client.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Client.swift; sourceTree = "<group>"; };
|
884A45CC27A465F500D6E650 /* Client.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Client.swift; sourceTree = "<group>"; };
|
||||||
884A45CE27A5402D00D6E650 /* MessageResult.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MessageResult.swift; sourceTree = "<group>"; };
|
884A45CE27A5402D00D6E650 /* MessageResult.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MessageResult.swift; sourceTree = "<group>"; };
|
||||||
E24EE77127FDCCC00011CFD2 /* Data+Extensions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Data+Extensions.swift"; sourceTree = "<group>"; };
|
8860D7422B22858600849FAC /* Date+Timestamp.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Date+Timestamp.swift"; sourceTree = "<group>"; };
|
||||||
E24EE77327FF95920011CFD2 /* DeviceResponse.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DeviceResponse.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>"; };
|
||||||
|
888A11322B32DBBB0099A12B /* UInt8+Extensions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "UInt8+Extensions.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>"; };
|
||||||
|
88BA7DD22BD41B8A008F2A3C /* UDPClient.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UDPClient.swift; sourceTree = "<group>"; };
|
||||||
|
88E197AC29EDC9BC00BF1D19 /* Sesame-Watch Watch App.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = "Sesame-Watch Watch App.app"; sourceTree = BUILT_PRODUCTS_DIR; };
|
||||||
|
88E197B129EDC9BC00BF1D19 /* Sesame_WatchApp.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Sesame_WatchApp.swift; sourceTree = "<group>"; };
|
||||||
|
88E197B329EDC9BC00BF1D19 /* UnlockView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UnlockView.swift; sourceTree = "<group>"; };
|
||||||
|
88E197B529EDC9BD00BF1D19 /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = "<group>"; };
|
||||||
|
88E197D629EDCFE800BF1D19 /* Date+Extensions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Date+Extensions.swift"; sourceTree = "<group>"; };
|
||||||
|
88E35EF42B3B0A9800485A66 /* App+Extensions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "App+Extensions.swift"; sourceTree = "<group>"; };
|
||||||
|
E240654A2A8153C6009C1AD8 /* SettingsListTextItem.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SettingsListTextItem.swift; sourceTree = "<group>"; };
|
||||||
|
E240654E2A8159B7009C1AD8 /* SettingsTextInputView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SettingsTextInputView.swift; sourceTree = "<group>"; };
|
||||||
|
E24065502A819066009C1AD8 /* SettingsTextItemLink.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SettingsTextItemLink.swift; sourceTree = "<group>"; };
|
||||||
|
E24065522A819614009C1AD8 /* SettingsNumberItemLink.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SettingsNumberItemLink.swift; sourceTree = "<group>"; };
|
||||||
|
E24065542A819663009C1AD8 /* SettingsNumberInputView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SettingsNumberInputView.swift; sourceTree = "<group>"; };
|
||||||
|
E24065572A819AE3009C1AD8 /* SettingsKeyItemLink.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SettingsKeyItemLink.swift; sourceTree = "<group>"; };
|
||||||
|
E24065592A82218D009C1AD8 /* SettingsKeyInputView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SettingsKeyInputView.swift; sourceTree = "<group>"; };
|
||||||
|
E240655D2A822E97009C1AD8 /* HistoryListRow.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HistoryListRow.swift; sourceTree = "<group>"; };
|
||||||
|
E240655F2A822ED9009C1AD8 /* HistoryItemDetail.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HistoryItemDetail.swift; sourceTree = "<group>"; };
|
||||||
|
E24EE77127FDCCC00011CFD2 /* Data+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>"; };
|
E24EE77827FF95E00011CFD2 /* Message.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Message.swift; sourceTree = "<group>"; };
|
||||||
E28DED2C281E840B00259690 /* KeyView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = KeyView.swift; sourceTree = "<group>"; };
|
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; };
|
||||||
|
E268E0552A852F8E00185913 /* SwiftUI.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = SwiftUI.framework; path = System/Library/Frameworks/SwiftUI.framework; sourceTree = SDKROOT; };
|
||||||
|
E268E0812A85302000185913 /* Sesame-WidgetExtension.appex */ = {isa = PBXFileReference; explicitFileType = "wrapper.app-extension"; includeInIndex = 0; path = "Sesame-WidgetExtension.appex"; sourceTree = BUILT_PRODUCTS_DIR; };
|
||||||
|
E268E0852A85302000185913 /* Sesame_Widget.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Sesame_Widget.swift; sourceTree = "<group>"; };
|
||||||
|
E268E0872A85302000185913 /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = "<group>"; };
|
||||||
|
E268E0892A85302000185913 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = "<group>"; };
|
||||||
|
E28DED2C281E840B00259690 /* SettingsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SettingsView.swift; sourceTree = "<group>"; };
|
||||||
E28DED2E281E8A0500259690 /* SingleKeyView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SingleKeyView.swift; sourceTree = "<group>"; };
|
E28DED2E281E8A0500259690 /* SingleKeyView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SingleKeyView.swift; sourceTree = "<group>"; };
|
||||||
E28DED30281EAE9100259690 /* HistoryView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HistoryView.swift; sourceTree = "<group>"; };
|
E28DED30281EAE9100259690 /* HistoryView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HistoryView.swift; sourceTree = "<group>"; };
|
||||||
E28DED32281EB15B00259690 /* HistoryListItem.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HistoryListItem.swift; sourceTree = "<group>"; };
|
E28DED32281EB15B00259690 /* HistoryListItem.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HistoryListItem.swift; sourceTree = "<group>"; };
|
||||||
E28DED34281EB17600259690 /* HistoryItem.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HistoryItem.swift; sourceTree = "<group>"; };
|
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>"; };
|
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>"; };
|
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>"; };
|
E2C5C1DA2806FE8900769EF6 /* SesameRoute.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SesameRoute.swift; sourceTree = "<group>"; };
|
||||||
E2C5C1DC281B3AC400769EF6 /* UInt32+Extensions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "UInt32+Extensions.swift"; sourceTree = "<group>"; };
|
E2C5C1DC281B3AC400769EF6 /* UInt32+Coding.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "UInt32+Coding.swift"; sourceTree = "<group>"; };
|
||||||
E2C5C1F7281E769F00769EF6 /* ServerMessage.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ServerMessage.swift; sourceTree = "<group>"; };
|
E2F5DCC92A88E913002858B9 /* Array+Extensions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Array+Extensions.swift"; sourceTree = "<group>"; };
|
||||||
/* End PBXFileReference section */
|
/* End PBXFileReference section */
|
||||||
|
|
||||||
/* Begin PBXFrameworksBuildPhase section */
|
/* Begin PBXFrameworksBuildPhase section */
|
||||||
@ -62,10 +201,32 @@
|
|||||||
isa = PBXFrameworksBuildPhase;
|
isa = PBXFrameworksBuildPhase;
|
||||||
buildActionMask = 2147483647;
|
buildActionMask = 2147483647;
|
||||||
files = (
|
files = (
|
||||||
|
8864665229E5939C004FE2BE /* SFSafeSymbols in Frameworks */,
|
||||||
|
8864664F29E5684C004FE2BE /* CBORCoding in Frameworks */,
|
||||||
E24EE77727FF95C00011CFD2 /* NIOCore in Frameworks */,
|
E24EE77727FF95C00011CFD2 /* NIOCore in Frameworks */,
|
||||||
);
|
);
|
||||||
runOnlyForDeploymentPostprocessing = 0;
|
runOnlyForDeploymentPostprocessing = 0;
|
||||||
};
|
};
|
||||||
|
88E197A929EDC9BC00BF1D19 /* Frameworks */ = {
|
||||||
|
isa = PBXFrameworksBuildPhase;
|
||||||
|
buildActionMask = 2147483647;
|
||||||
|
files = (
|
||||||
|
88E197D029EDCD7D00BF1D19 /* SFSafeSymbols in Frameworks */,
|
||||||
|
88E197CE29EDCD7500BF1D19 /* CBORCoding in Frameworks */,
|
||||||
|
88E197CC29EDCD4900BF1D19 /* NIOCore in Frameworks */,
|
||||||
|
);
|
||||||
|
runOnlyForDeploymentPostprocessing = 0;
|
||||||
|
};
|
||||||
|
E268E07E2A85302000185913 /* Frameworks */ = {
|
||||||
|
isa = PBXFrameworksBuildPhase;
|
||||||
|
buildActionMask = 2147483647;
|
||||||
|
files = (
|
||||||
|
E268E0912A85318500185913 /* SFSafeSymbols in Frameworks */,
|
||||||
|
E268E0832A85302000185913 /* SwiftUI.framework in Frameworks */,
|
||||||
|
E268E0822A85302000185913 /* WidgetKit.framework in Frameworks */,
|
||||||
|
);
|
||||||
|
runOnlyForDeploymentPostprocessing = 0;
|
||||||
|
};
|
||||||
/* End PBXFrameworksBuildPhase section */
|
/* End PBXFrameworksBuildPhase section */
|
||||||
|
|
||||||
/* Begin PBXGroup section */
|
/* Begin PBXGroup section */
|
||||||
@ -73,7 +234,10 @@
|
|||||||
isa = PBXGroup;
|
isa = PBXGroup;
|
||||||
children = (
|
children = (
|
||||||
884A45B5279F48C100D6E650 /* Sesame */,
|
884A45B5279F48C100D6E650 /* Sesame */,
|
||||||
|
88E197B029EDC9BC00BF1D19 /* Sesame-Watch Watch App */,
|
||||||
|
E268E0842A85302000185913 /* Sesame-Widget */,
|
||||||
884A45B4279F48C100D6E650 /* Products */,
|
884A45B4279F48C100D6E650 /* Products */,
|
||||||
|
88E197CA29EDCD4900BF1D19 /* Frameworks */,
|
||||||
);
|
);
|
||||||
sourceTree = "<group>";
|
sourceTree = "<group>";
|
||||||
};
|
};
|
||||||
@ -81,6 +245,8 @@
|
|||||||
isa = PBXGroup;
|
isa = PBXGroup;
|
||||||
children = (
|
children = (
|
||||||
884A45B3279F48C100D6E650 /* Sesame.app */,
|
884A45B3279F48C100D6E650 /* Sesame.app */,
|
||||||
|
88E197AC29EDC9BC00BF1D19 /* Sesame-Watch Watch App.app */,
|
||||||
|
E268E0812A85302000185913 /* Sesame-WidgetExtension.appex */,
|
||||||
);
|
);
|
||||||
name = Products;
|
name = Products;
|
||||||
sourceTree = "<group>";
|
sourceTree = "<group>";
|
||||||
@ -89,43 +255,150 @@
|
|||||||
isa = PBXGroup;
|
isa = PBXGroup;
|
||||||
children = (
|
children = (
|
||||||
E28DED38281EE9CF00259690 /* Info.plist */,
|
E28DED38281EE9CF00259690 /* Info.plist */,
|
||||||
E2C5C1D92806FE4A00769EF6 /* API */,
|
|
||||||
884A45B6279F48C100D6E650 /* SesameApp.swift */,
|
|
||||||
884A45B8279F48C100D6E650 /* ContentView.swift */,
|
|
||||||
E28DED30281EAE9100259690 /* HistoryView.swift */,
|
|
||||||
E28DED32281EB15B00259690 /* HistoryListItem.swift */,
|
|
||||||
E28DED34281EB17600259690 /* HistoryItem.swift */,
|
|
||||||
E28DED36281EC7FB00259690 /* HistoryManager.swift */,
|
|
||||||
E28DED2C281E840B00259690 /* KeyView.swift */,
|
|
||||||
E28DED2E281E8A0500259690 /* SingleKeyView.swift */,
|
|
||||||
884A45CC27A465F500D6E650 /* Client.swift */,
|
|
||||||
884A45C827A43D7900D6E650 /* ClientState.swift */,
|
|
||||||
884A45C4279F4BBE00D6E650 /* KeyManagement.swift */,
|
|
||||||
884A45CA27A464C000D6E650 /* SymmetricKey+Extensions.swift */,
|
|
||||||
884A45BA279F48C300D6E650 /* Assets.xcassets */,
|
884A45BA279F48C300D6E650 /* Assets.xcassets */,
|
||||||
884A45BC279F48C300D6E650 /* Preview Content */,
|
E24F6C6C2A89748B0040F8C4 /* Common */,
|
||||||
|
E2C5C1D92806FE4A00769EF6 /* API */,
|
||||||
|
8860D7442B2328B800849FAC /* API Extensions */,
|
||||||
|
884A45B6279F48C100D6E650 /* SesameApp.swift */,
|
||||||
|
884A45B8279F48C100D6E650 /* MainView.swift */,
|
||||||
|
E28DED2C281E840B00259690 /* SettingsView.swift */,
|
||||||
|
E28DED2E281E8A0500259690 /* SingleKeyView.swift */,
|
||||||
|
E25317542A8A1A07005A537D /* History */,
|
||||||
|
E25317552A8A1A32005A537D /* Extensions */,
|
||||||
);
|
);
|
||||||
path = Sesame;
|
path = Sesame;
|
||||||
sourceTree = "<group>";
|
sourceTree = "<group>";
|
||||||
};
|
};
|
||||||
884A45BC279F48C300D6E650 /* Preview Content */ = {
|
8860D7442B2328B800849FAC /* API Extensions */ = {
|
||||||
isa = PBXGroup;
|
isa = PBXGroup;
|
||||||
children = (
|
children = (
|
||||||
884A45BD279F48C300D6E650 /* Preview Assets.xcassets */,
|
88AEE3872B226FED0034EDA9 /* MessageResult+UI.swift */,
|
||||||
|
88AEE37E2B2217050034EDA9 /* MessageType.swift */,
|
||||||
|
E24EE77827FF95E00011CFD2 /* Message.swift */,
|
||||||
|
88AEE3852B22376D0034EDA9 /* Message+Crypto.swift */,
|
||||||
|
88AEE3832B2236DC0034EDA9 /* SignedMessage.swift */,
|
||||||
|
8860D7472B23294600849FAC /* SignedMessage+Crypto.swift */,
|
||||||
);
|
);
|
||||||
path = "Preview Content";
|
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 */,
|
||||||
|
88E35EF42B3B0A9800485A66 /* App+Extensions.swift */,
|
||||||
|
);
|
||||||
|
path = Extensions;
|
||||||
|
sourceTree = "<group>";
|
||||||
|
};
|
||||||
|
88AEE3822B22331E0034EDA9 /* Extensions */ = {
|
||||||
|
isa = PBXGroup;
|
||||||
|
children = (
|
||||||
|
E24EE77127FDCCC00011CFD2 /* Data+Hex.swift */,
|
||||||
|
8860D74D2B232AED00849FAC /* Data+Coding.swift */,
|
||||||
|
E2C5C1DC281B3AC400769EF6 /* UInt32+Coding.swift */,
|
||||||
|
888A11322B32DBBB0099A12B /* UInt8+Extensions.swift */,
|
||||||
|
);
|
||||||
|
path = Extensions;
|
||||||
|
sourceTree = "<group>";
|
||||||
|
};
|
||||||
|
88E197B029EDC9BC00BF1D19 /* Sesame-Watch Watch App */ = {
|
||||||
|
isa = PBXGroup;
|
||||||
|
children = (
|
||||||
|
88E197B529EDC9BD00BF1D19 /* Assets.xcassets */,
|
||||||
|
E24065562A819AAD009C1AD8 /* Settings */,
|
||||||
|
88E197B129EDC9BC00BF1D19 /* Sesame_WatchApp.swift */,
|
||||||
|
88E197B329EDC9BC00BF1D19 /* UnlockView.swift */,
|
||||||
|
888362332A80F3F90032BBB2 /* SettingsView.swift */,
|
||||||
|
888362352A80F4420032BBB2 /* HistoryView.swift */,
|
||||||
|
E240655D2A822E97009C1AD8 /* HistoryListRow.swift */,
|
||||||
|
E240655F2A822ED9009C1AD8 /* HistoryItemDetail.swift */,
|
||||||
|
88E197D629EDCFE800BF1D19 /* Date+Extensions.swift */,
|
||||||
|
);
|
||||||
|
path = "Sesame-Watch Watch App";
|
||||||
|
sourceTree = "<group>";
|
||||||
|
};
|
||||||
|
88E197CA29EDCD4900BF1D19 /* Frameworks */ = {
|
||||||
|
isa = PBXGroup;
|
||||||
|
children = (
|
||||||
|
E268E0532A852F8E00185913 /* WidgetKit.framework */,
|
||||||
|
E268E0552A852F8E00185913 /* SwiftUI.framework */,
|
||||||
|
);
|
||||||
|
name = Frameworks;
|
||||||
|
sourceTree = "<group>";
|
||||||
|
};
|
||||||
|
E24065562A819AAD009C1AD8 /* Settings */ = {
|
||||||
|
isa = PBXGroup;
|
||||||
|
children = (
|
||||||
|
E24065502A819066009C1AD8 /* SettingsTextItemLink.swift */,
|
||||||
|
E24065522A819614009C1AD8 /* SettingsNumberItemLink.swift */,
|
||||||
|
E24065542A819663009C1AD8 /* SettingsNumberInputView.swift */,
|
||||||
|
E240654E2A8159B7009C1AD8 /* SettingsTextInputView.swift */,
|
||||||
|
E240654A2A8153C6009C1AD8 /* SettingsListTextItem.swift */,
|
||||||
|
E24065572A819AE3009C1AD8 /* SettingsKeyItemLink.swift */,
|
||||||
|
E24065592A82218D009C1AD8 /* SettingsKeyInputView.swift */,
|
||||||
|
);
|
||||||
|
path = Settings;
|
||||||
|
sourceTree = "<group>";
|
||||||
|
};
|
||||||
|
E24F6C6C2A89748B0040F8C4 /* Common */ = {
|
||||||
|
isa = PBXGroup;
|
||||||
|
children = (
|
||||||
|
8860D76B2B246F5600849FAC /* Extensions */,
|
||||||
|
884A45CC27A465F500D6E650 /* Client.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 */,
|
||||||
|
88BA7DD22BD41B8A008F2A3C /* UDPClient.swift */,
|
||||||
|
);
|
||||||
|
path = Common;
|
||||||
|
sourceTree = "<group>";
|
||||||
|
};
|
||||||
|
E25317542A8A1A07005A537D /* History */ = {
|
||||||
|
isa = PBXGroup;
|
||||||
|
children = (
|
||||||
|
E28DED30281EAE9100259690 /* HistoryView.swift */,
|
||||||
|
E28DED32281EB15B00259690 /* HistoryListItem.swift */,
|
||||||
|
E28DED34281EB17600259690 /* HistoryItem.swift */,
|
||||||
|
);
|
||||||
|
path = History;
|
||||||
|
sourceTree = "<group>";
|
||||||
|
};
|
||||||
|
E25317552A8A1A32005A537D /* Extensions */ = {
|
||||||
|
isa = PBXGroup;
|
||||||
|
children = (
|
||||||
|
8860D7422B22858600849FAC /* Date+Timestamp.swift */,
|
||||||
|
);
|
||||||
|
path = Extensions;
|
||||||
|
sourceTree = "<group>";
|
||||||
|
};
|
||||||
|
E268E0842A85302000185913 /* Sesame-Widget */ = {
|
||||||
|
isa = PBXGroup;
|
||||||
|
children = (
|
||||||
|
E268E0892A85302000185913 /* Info.plist */,
|
||||||
|
E268E0872A85302000185913 /* Assets.xcassets */,
|
||||||
|
E268E0852A85302000185913 /* Sesame_Widget.swift */,
|
||||||
|
);
|
||||||
|
path = "Sesame-Widget";
|
||||||
sourceTree = "<group>";
|
sourceTree = "<group>";
|
||||||
};
|
};
|
||||||
E2C5C1D92806FE4A00769EF6 /* API */ = {
|
E2C5C1D92806FE4A00769EF6 /* API */ = {
|
||||||
isa = PBXGroup;
|
isa = PBXGroup;
|
||||||
children = (
|
children = (
|
||||||
E24EE77127FDCCC00011CFD2 /* Data+Extensions.swift */,
|
88AEE3822B22331E0034EDA9 /* Extensions */,
|
||||||
E24EE77327FF95920011CFD2 /* DeviceResponse.swift */,
|
|
||||||
E24EE77827FF95E00011CFD2 /* Message.swift */,
|
|
||||||
884A45CE27A5402D00D6E650 /* MessageResult.swift */,
|
884A45CE27A5402D00D6E650 /* MessageResult.swift */,
|
||||||
E2C5C1DA2806FE8900769EF6 /* RouteAPI.swift */,
|
E2C5C1DA2806FE8900769EF6 /* SesameRoute.swift */,
|
||||||
E2C5C1F7281E769F00769EF6 /* ServerMessage.swift */,
|
8860D7452B2328EC00849FAC /* Message+Size.swift */,
|
||||||
E2C5C1DC281B3AC400769EF6 /* UInt32+Extensions.swift */,
|
8860D7492B2329CE00849FAC /* SignedMessage+Size.swift */,
|
||||||
|
8860D74B2B232A7700849FAC /* SesameHeader.swift */,
|
||||||
);
|
);
|
||||||
path = API;
|
path = API;
|
||||||
sourceTree = "<group>";
|
sourceTree = "<group>";
|
||||||
@ -148,11 +421,57 @@
|
|||||||
name = Sesame;
|
name = Sesame;
|
||||||
packageProductDependencies = (
|
packageProductDependencies = (
|
||||||
E24EE77627FF95C00011CFD2 /* NIOCore */,
|
E24EE77627FF95C00011CFD2 /* NIOCore */,
|
||||||
|
8864664E29E5684C004FE2BE /* CBORCoding */,
|
||||||
|
8864665129E5939C004FE2BE /* SFSafeSymbols */,
|
||||||
);
|
);
|
||||||
productName = Sesame;
|
productName = Sesame;
|
||||||
productReference = 884A45B3279F48C100D6E650 /* Sesame.app */;
|
productReference = 884A45B3279F48C100D6E650 /* Sesame.app */;
|
||||||
productType = "com.apple.product-type.application";
|
productType = "com.apple.product-type.application";
|
||||||
};
|
};
|
||||||
|
88E197AB29EDC9BC00BF1D19 /* Sesame-Watch Watch App */ = {
|
||||||
|
isa = PBXNativeTarget;
|
||||||
|
buildConfigurationList = 88E197BF29EDC9BD00BF1D19 /* Build configuration list for PBXNativeTarget "Sesame-Watch Watch App" */;
|
||||||
|
buildPhases = (
|
||||||
|
88E197A829EDC9BC00BF1D19 /* Sources */,
|
||||||
|
88E197A929EDC9BC00BF1D19 /* Frameworks */,
|
||||||
|
88E197AA29EDC9BC00BF1D19 /* Resources */,
|
||||||
|
E268E0662A852F8E00185913 /* Embed Foundation Extensions */,
|
||||||
|
);
|
||||||
|
buildRules = (
|
||||||
|
);
|
||||||
|
dependencies = (
|
||||||
|
E268E08B2A85302000185913 /* PBXTargetDependency */,
|
||||||
|
);
|
||||||
|
name = "Sesame-Watch Watch App";
|
||||||
|
packageProductDependencies = (
|
||||||
|
88E197CB29EDCD4900BF1D19 /* NIOCore */,
|
||||||
|
88E197CD29EDCD7500BF1D19 /* CBORCoding */,
|
||||||
|
88E197CF29EDCD7D00BF1D19 /* SFSafeSymbols */,
|
||||||
|
);
|
||||||
|
productName = "Sesame-Watch Watch App";
|
||||||
|
productReference = 88E197AC29EDC9BC00BF1D19 /* Sesame-Watch Watch App.app */;
|
||||||
|
productType = "com.apple.product-type.application";
|
||||||
|
};
|
||||||
|
E268E0802A85302000185913 /* Sesame-WidgetExtension */ = {
|
||||||
|
isa = PBXNativeTarget;
|
||||||
|
buildConfigurationList = E268E08D2A85302000185913 /* Build configuration list for PBXNativeTarget "Sesame-WidgetExtension" */;
|
||||||
|
buildPhases = (
|
||||||
|
E268E07D2A85302000185913 /* Sources */,
|
||||||
|
E268E07E2A85302000185913 /* Frameworks */,
|
||||||
|
E268E07F2A85302000185913 /* Resources */,
|
||||||
|
);
|
||||||
|
buildRules = (
|
||||||
|
);
|
||||||
|
dependencies = (
|
||||||
|
);
|
||||||
|
name = "Sesame-WidgetExtension";
|
||||||
|
packageProductDependencies = (
|
||||||
|
E268E0902A85318500185913 /* SFSafeSymbols */,
|
||||||
|
);
|
||||||
|
productName = "Sesame-WidgetExtension";
|
||||||
|
productReference = E268E0812A85302000185913 /* Sesame-WidgetExtension.appex */;
|
||||||
|
productType = "com.apple.product-type.app-extension";
|
||||||
|
};
|
||||||
/* End PBXNativeTarget section */
|
/* End PBXNativeTarget section */
|
||||||
|
|
||||||
/* Begin PBXProject section */
|
/* Begin PBXProject section */
|
||||||
@ -160,11 +479,18 @@
|
|||||||
isa = PBXProject;
|
isa = PBXProject;
|
||||||
attributes = {
|
attributes = {
|
||||||
BuildIndependentTargetsInParallel = 1;
|
BuildIndependentTargetsInParallel = 1;
|
||||||
LastSwiftUpdateCheck = 1320;
|
LastSwiftUpdateCheck = 1430;
|
||||||
LastUpgradeCheck = 1320;
|
LastUpgradeCheck = 1500;
|
||||||
TargetAttributes = {
|
TargetAttributes = {
|
||||||
884A45B2279F48C100D6E650 = {
|
884A45B2279F48C100D6E650 = {
|
||||||
CreatedOnToolsVersion = 13.2.1;
|
CreatedOnToolsVersion = 13.2.1;
|
||||||
|
LastSwiftMigration = 1430;
|
||||||
|
};
|
||||||
|
88E197AB29EDC9BC00BF1D19 = {
|
||||||
|
CreatedOnToolsVersion = 14.3;
|
||||||
|
};
|
||||||
|
E268E0802A85302000185913 = {
|
||||||
|
CreatedOnToolsVersion = 14.3.1;
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
@ -179,12 +505,16 @@
|
|||||||
mainGroup = 884A45AA279F48C100D6E650;
|
mainGroup = 884A45AA279F48C100D6E650;
|
||||||
packageReferences = (
|
packageReferences = (
|
||||||
E24EE77527FF95C00011CFD2 /* XCRemoteSwiftPackageReference "swift-nio" */,
|
E24EE77527FF95C00011CFD2 /* XCRemoteSwiftPackageReference "swift-nio" */,
|
||||||
|
8864664D29E5684C004FE2BE /* XCRemoteSwiftPackageReference "CBORCoding" */,
|
||||||
|
8864665029E5939C004FE2BE /* XCRemoteSwiftPackageReference "SFSafeSymbols" */,
|
||||||
);
|
);
|
||||||
productRefGroup = 884A45B4279F48C100D6E650 /* Products */;
|
productRefGroup = 884A45B4279F48C100D6E650 /* Products */;
|
||||||
projectDirPath = "";
|
projectDirPath = "";
|
||||||
projectRoot = "";
|
projectRoot = "";
|
||||||
targets = (
|
targets = (
|
||||||
884A45B2279F48C100D6E650 /* Sesame */,
|
884A45B2279F48C100D6E650 /* Sesame */,
|
||||||
|
88E197AB29EDC9BC00BF1D19 /* Sesame-Watch Watch App */,
|
||||||
|
E268E0802A85302000185913 /* Sesame-WidgetExtension */,
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
/* End PBXProject section */
|
/* End PBXProject section */
|
||||||
@ -194,11 +524,26 @@
|
|||||||
isa = PBXResourcesBuildPhase;
|
isa = PBXResourcesBuildPhase;
|
||||||
buildActionMask = 2147483647;
|
buildActionMask = 2147483647;
|
||||||
files = (
|
files = (
|
||||||
884A45BE279F48C300D6E650 /* Preview Assets.xcassets in Resources */,
|
|
||||||
884A45BB279F48C300D6E650 /* Assets.xcassets in Resources */,
|
884A45BB279F48C300D6E650 /* Assets.xcassets in Resources */,
|
||||||
);
|
);
|
||||||
runOnlyForDeploymentPostprocessing = 0;
|
runOnlyForDeploymentPostprocessing = 0;
|
||||||
};
|
};
|
||||||
|
88E197AA29EDC9BC00BF1D19 /* Resources */ = {
|
||||||
|
isa = PBXResourcesBuildPhase;
|
||||||
|
buildActionMask = 2147483647;
|
||||||
|
files = (
|
||||||
|
88E197B629EDC9BD00BF1D19 /* Assets.xcassets in Resources */,
|
||||||
|
);
|
||||||
|
runOnlyForDeploymentPostprocessing = 0;
|
||||||
|
};
|
||||||
|
E268E07F2A85302000185913 /* Resources */ = {
|
||||||
|
isa = PBXResourcesBuildPhase;
|
||||||
|
buildActionMask = 2147483647;
|
||||||
|
files = (
|
||||||
|
E268E0882A85302000185913 /* Assets.xcassets in Resources */,
|
||||||
|
);
|
||||||
|
runOnlyForDeploymentPostprocessing = 0;
|
||||||
|
};
|
||||||
/* End PBXResourcesBuildPhase section */
|
/* End PBXResourcesBuildPhase section */
|
||||||
|
|
||||||
/* Begin PBXSourcesBuildPhase section */
|
/* Begin PBXSourcesBuildPhase section */
|
||||||
@ -207,34 +552,122 @@
|
|||||||
buildActionMask = 2147483647;
|
buildActionMask = 2147483647;
|
||||||
files = (
|
files = (
|
||||||
884A45CF27A5402D00D6E650 /* MessageResult.swift in Sources */,
|
884A45CF27A5402D00D6E650 /* MessageResult.swift in Sources */,
|
||||||
884A45B9279F48C100D6E650 /* ContentView.swift in Sources */,
|
884A45B9279F48C100D6E650 /* MainView.swift in Sources */,
|
||||||
|
88AEE3882B226FED0034EDA9 /* MessageResult+UI.swift in Sources */,
|
||||||
E28DED2F281E8A0500259690 /* SingleKeyView.swift in Sources */,
|
E28DED2F281E8A0500259690 /* SingleKeyView.swift in Sources */,
|
||||||
|
888A11332B32DBBB0099A12B /* UInt8+Extensions.swift in Sources */,
|
||||||
E28DED37281EC7FB00259690 /* HistoryManager.swift in Sources */,
|
E28DED37281EC7FB00259690 /* HistoryManager.swift in Sources */,
|
||||||
E2C5C1DB2806FE8900769EF6 /* RouteAPI.swift in Sources */,
|
E2C5C1DB2806FE8900769EF6 /* SesameRoute.swift in Sources */,
|
||||||
E2C5C1DD281B3AC400769EF6 /* UInt32+Extensions.swift in Sources */,
|
E2C5C1DD281B3AC400769EF6 /* UInt32+Coding.swift in Sources */,
|
||||||
884A45CD27A465F500D6E650 /* Client.swift in Sources */,
|
E24EE77227FDCCC00011CFD2 /* Data+Hex.swift in Sources */,
|
||||||
E24EE77227FDCCC00011CFD2 /* Data+Extensions.swift in Sources */,
|
8860D7482B23294600849FAC /* SignedMessage+Crypto.swift in Sources */,
|
||||||
E24EE77427FF95920011CFD2 /* DeviceResponse.swift in Sources */,
|
8860D74C2B232A7700849FAC /* SesameHeader.swift in Sources */,
|
||||||
|
88E35EF52B3B0A9800485A66 /* App+Extensions.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 */,
|
884A45CB27A464C000D6E650 /* SymmetricKey+Extensions.swift in Sources */,
|
||||||
E28DED31281EAE9100259690 /* HistoryView.swift in Sources */,
|
E28DED31281EAE9100259690 /* HistoryView.swift in Sources */,
|
||||||
E24EE77927FF95E00011CFD2 /* Message.swift in Sources */,
|
E24EE77927FF95E00011CFD2 /* Message.swift in Sources */,
|
||||||
|
E2F5DCCA2A88E913002858B9 /* Array+Extensions.swift in Sources */,
|
||||||
E28DED35281EB17600259690 /* HistoryItem.swift in Sources */,
|
E28DED35281EB17600259690 /* HistoryItem.swift in Sources */,
|
||||||
884A45C927A43D7900D6E650 /* ClientState.swift in Sources */,
|
|
||||||
E28DED33281EB15B00259690 /* HistoryListItem.swift in Sources */,
|
E28DED33281EB15B00259690 /* HistoryListItem.swift in Sources */,
|
||||||
E28DED2D281E840B00259690 /* KeyView.swift in Sources */,
|
E28DED2D281E840B00259690 /* SettingsView.swift in Sources */,
|
||||||
884A45B7279F48C100D6E650 /* SesameApp.swift in Sources */,
|
884A45B7279F48C100D6E650 /* SesameApp.swift in Sources */,
|
||||||
|
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 */,
|
884A45C5279F4BBE00D6E650 /* KeyManagement.swift in Sources */,
|
||||||
E2C5C1F8281E769F00769EF6 /* ServerMessage.swift in Sources */,
|
88BA7DD32BD41B8A008F2A3C /* UDPClient.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;
|
||||||
|
};
|
||||||
|
88E197A829EDC9BC00BF1D19 /* Sources */ = {
|
||||||
|
isa = PBXSourcesBuildPhase;
|
||||||
|
buildActionMask = 2147483647;
|
||||||
|
files = (
|
||||||
|
8860D7662B23B5B200849FAC /* RequestCoordinator.swift in Sources */,
|
||||||
|
8860D75D2B237FC000849FAC /* Data+Coding.swift in Sources */,
|
||||||
|
888362342A80F3F90032BBB2 /* SettingsView.swift in Sources */,
|
||||||
|
88E197B429EDC9BC00BF1D19 /* UnlockView.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 */,
|
||||||
|
88E35EF62B3B0A9800485A66 /* App+Extensions.swift in Sources */,
|
||||||
|
88E197D329EDCE6E00BF1D19 /* MessageResult.swift in Sources */,
|
||||||
|
88E197D129EDCE5F00BF1D19 /* Data+Hex.swift in Sources */,
|
||||||
|
8860D76C2B246F5E00849FAC /* UInt32+Random.swift in Sources */,
|
||||||
|
E240655A2A82218D009C1AD8 /* SettingsKeyInputView.swift in Sources */,
|
||||||
|
8860D76F2B246FC400849FAC /* Text+Extensions.swift in Sources */,
|
||||||
|
E25231782C0227B500FFE373 /* UDPClient.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 */,
|
||||||
|
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 */,
|
||||||
|
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+Coding.swift in Sources */,
|
||||||
|
E240655B2A822397009C1AD8 /* KeyManagement.swift in Sources */,
|
||||||
|
888A11342B32DC3C0099A12B /* UInt8+Extensions.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 */,
|
||||||
|
);
|
||||||
|
runOnlyForDeploymentPostprocessing = 0;
|
||||||
|
};
|
||||||
|
E268E07D2A85302000185913 /* Sources */ = {
|
||||||
|
isa = PBXSourcesBuildPhase;
|
||||||
|
buildActionMask = 2147483647;
|
||||||
|
files = (
|
||||||
|
E268E0862A85302000185913 /* Sesame_Widget.swift in Sources */,
|
||||||
);
|
);
|
||||||
runOnlyForDeploymentPostprocessing = 0;
|
runOnlyForDeploymentPostprocessing = 0;
|
||||||
};
|
};
|
||||||
/* End PBXSourcesBuildPhase section */
|
/* End PBXSourcesBuildPhase section */
|
||||||
|
|
||||||
|
/* Begin PBXTargetDependency section */
|
||||||
|
E268E08B2A85302000185913 /* PBXTargetDependency */ = {
|
||||||
|
isa = PBXTargetDependency;
|
||||||
|
target = E268E0802A85302000185913 /* Sesame-WidgetExtension */;
|
||||||
|
targetProxy = E268E08A2A85302000185913 /* PBXContainerItemProxy */;
|
||||||
|
};
|
||||||
|
/* End PBXTargetDependency section */
|
||||||
|
|
||||||
/* Begin XCBuildConfiguration section */
|
/* Begin XCBuildConfiguration section */
|
||||||
884A45BF279F48C300D6E650 /* Debug */ = {
|
884A45BF279F48C300D6E650 /* Debug */ = {
|
||||||
isa = XCBuildConfiguration;
|
isa = XCBuildConfiguration;
|
||||||
buildSettings = {
|
buildSettings = {
|
||||||
ALWAYS_SEARCH_USER_PATHS = NO;
|
ALWAYS_SEARCH_USER_PATHS = NO;
|
||||||
|
ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES;
|
||||||
CLANG_ANALYZER_NONNULL = YES;
|
CLANG_ANALYZER_NONNULL = YES;
|
||||||
CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE;
|
CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE;
|
||||||
CLANG_CXX_LANGUAGE_STANDARD = "gnu++17";
|
CLANG_CXX_LANGUAGE_STANDARD = "gnu++17";
|
||||||
@ -268,6 +701,7 @@
|
|||||||
DEBUG_INFORMATION_FORMAT = dwarf;
|
DEBUG_INFORMATION_FORMAT = dwarf;
|
||||||
ENABLE_STRICT_OBJC_MSGSEND = YES;
|
ENABLE_STRICT_OBJC_MSGSEND = YES;
|
||||||
ENABLE_TESTABILITY = YES;
|
ENABLE_TESTABILITY = YES;
|
||||||
|
ENABLE_USER_SCRIPT_SANDBOXING = YES;
|
||||||
GCC_C_LANGUAGE_STANDARD = gnu11;
|
GCC_C_LANGUAGE_STANDARD = gnu11;
|
||||||
GCC_DYNAMIC_NO_PIC = NO;
|
GCC_DYNAMIC_NO_PIC = NO;
|
||||||
GCC_NO_COMMON_BLOCKS = YES;
|
GCC_NO_COMMON_BLOCKS = YES;
|
||||||
@ -282,7 +716,7 @@
|
|||||||
GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
|
GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
|
||||||
GCC_WARN_UNUSED_FUNCTION = YES;
|
GCC_WARN_UNUSED_FUNCTION = YES;
|
||||||
GCC_WARN_UNUSED_VARIABLE = YES;
|
GCC_WARN_UNUSED_VARIABLE = YES;
|
||||||
IPHONEOS_DEPLOYMENT_TARGET = 15.2;
|
IPHONEOS_DEPLOYMENT_TARGET = 17.0;
|
||||||
MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE;
|
MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE;
|
||||||
MTL_FAST_MATH = YES;
|
MTL_FAST_MATH = YES;
|
||||||
ONLY_ACTIVE_ARCH = YES;
|
ONLY_ACTIVE_ARCH = YES;
|
||||||
@ -296,6 +730,7 @@
|
|||||||
isa = XCBuildConfiguration;
|
isa = XCBuildConfiguration;
|
||||||
buildSettings = {
|
buildSettings = {
|
||||||
ALWAYS_SEARCH_USER_PATHS = NO;
|
ALWAYS_SEARCH_USER_PATHS = NO;
|
||||||
|
ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES;
|
||||||
CLANG_ANALYZER_NONNULL = YES;
|
CLANG_ANALYZER_NONNULL = YES;
|
||||||
CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE;
|
CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE;
|
||||||
CLANG_CXX_LANGUAGE_STANDARD = "gnu++17";
|
CLANG_CXX_LANGUAGE_STANDARD = "gnu++17";
|
||||||
@ -329,6 +764,7 @@
|
|||||||
DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym";
|
DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym";
|
||||||
ENABLE_NS_ASSERTIONS = NO;
|
ENABLE_NS_ASSERTIONS = NO;
|
||||||
ENABLE_STRICT_OBJC_MSGSEND = YES;
|
ENABLE_STRICT_OBJC_MSGSEND = YES;
|
||||||
|
ENABLE_USER_SCRIPT_SANDBOXING = YES;
|
||||||
GCC_C_LANGUAGE_STANDARD = gnu11;
|
GCC_C_LANGUAGE_STANDARD = gnu11;
|
||||||
GCC_NO_COMMON_BLOCKS = YES;
|
GCC_NO_COMMON_BLOCKS = YES;
|
||||||
GCC_WARN_64_TO_32_BIT_CONVERSION = YES;
|
GCC_WARN_64_TO_32_BIT_CONVERSION = YES;
|
||||||
@ -337,7 +773,7 @@
|
|||||||
GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
|
GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
|
||||||
GCC_WARN_UNUSED_FUNCTION = YES;
|
GCC_WARN_UNUSED_FUNCTION = YES;
|
||||||
GCC_WARN_UNUSED_VARIABLE = YES;
|
GCC_WARN_UNUSED_VARIABLE = YES;
|
||||||
IPHONEOS_DEPLOYMENT_TARGET = 15.2;
|
IPHONEOS_DEPLOYMENT_TARGET = 17.0;
|
||||||
MTL_ENABLE_DEBUG_INFO = NO;
|
MTL_ENABLE_DEBUG_INFO = NO;
|
||||||
MTL_FAST_MATH = YES;
|
MTL_FAST_MATH = YES;
|
||||||
SDKROOT = iphoneos;
|
SDKROOT = iphoneos;
|
||||||
@ -352,9 +788,10 @@
|
|||||||
buildSettings = {
|
buildSettings = {
|
||||||
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
|
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
|
||||||
ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor;
|
ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor;
|
||||||
|
CLANG_ENABLE_MODULES = YES;
|
||||||
CODE_SIGN_STYLE = Automatic;
|
CODE_SIGN_STYLE = Automatic;
|
||||||
CURRENT_PROJECT_VERSION = 1;
|
CURRENT_PROJECT_VERSION = 1;
|
||||||
DEVELOPMENT_ASSET_PATHS = "\"Sesame/Preview Content\"";
|
DEVELOPMENT_ASSET_PATHS = "";
|
||||||
DEVELOPMENT_TEAM = H8WR4M6QQ4;
|
DEVELOPMENT_TEAM = H8WR4M6QQ4;
|
||||||
ENABLE_PREVIEWS = YES;
|
ENABLE_PREVIEWS = YES;
|
||||||
GENERATE_INFOPLIST_FILE = YES;
|
GENERATE_INFOPLIST_FILE = YES;
|
||||||
@ -365,6 +802,7 @@
|
|||||||
INFOPLIST_KEY_UISupportedInterfaceOrientations = UIInterfaceOrientationPortrait;
|
INFOPLIST_KEY_UISupportedInterfaceOrientations = UIInterfaceOrientationPortrait;
|
||||||
INFOPLIST_KEY_UISupportedInterfaceOrientations_iPad = "UIInterfaceOrientationPortrait UIInterfaceOrientationPortraitUpsideDown UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight";
|
INFOPLIST_KEY_UISupportedInterfaceOrientations_iPad = "UIInterfaceOrientationPortrait UIInterfaceOrientationPortraitUpsideDown UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight";
|
||||||
INFOPLIST_KEY_UISupportedInterfaceOrientations_iPhone = UIInterfaceOrientationPortrait;
|
INFOPLIST_KEY_UISupportedInterfaceOrientations_iPhone = UIInterfaceOrientationPortrait;
|
||||||
|
IPHONEOS_DEPLOYMENT_TARGET = 17.0;
|
||||||
LD_RUNPATH_SEARCH_PATHS = (
|
LD_RUNPATH_SEARCH_PATHS = (
|
||||||
"$(inherited)",
|
"$(inherited)",
|
||||||
"@executable_path/Frameworks",
|
"@executable_path/Frameworks",
|
||||||
@ -373,6 +811,8 @@
|
|||||||
PRODUCT_BUNDLE_IDENTIFIER = de.christophhagen.Sesame;
|
PRODUCT_BUNDLE_IDENTIFIER = de.christophhagen.Sesame;
|
||||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||||
SWIFT_EMIT_LOC_STRINGS = YES;
|
SWIFT_EMIT_LOC_STRINGS = YES;
|
||||||
|
SWIFT_OBJC_BRIDGING_HEADER = "";
|
||||||
|
SWIFT_OPTIMIZATION_LEVEL = "-Onone";
|
||||||
SWIFT_VERSION = 5.0;
|
SWIFT_VERSION = 5.0;
|
||||||
TARGETED_DEVICE_FAMILY = 1;
|
TARGETED_DEVICE_FAMILY = 1;
|
||||||
};
|
};
|
||||||
@ -383,9 +823,10 @@
|
|||||||
buildSettings = {
|
buildSettings = {
|
||||||
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
|
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
|
||||||
ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor;
|
ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor;
|
||||||
|
CLANG_ENABLE_MODULES = YES;
|
||||||
CODE_SIGN_STYLE = Automatic;
|
CODE_SIGN_STYLE = Automatic;
|
||||||
CURRENT_PROJECT_VERSION = 1;
|
CURRENT_PROJECT_VERSION = 1;
|
||||||
DEVELOPMENT_ASSET_PATHS = "\"Sesame/Preview Content\"";
|
DEVELOPMENT_ASSET_PATHS = "";
|
||||||
DEVELOPMENT_TEAM = H8WR4M6QQ4;
|
DEVELOPMENT_TEAM = H8WR4M6QQ4;
|
||||||
ENABLE_PREVIEWS = YES;
|
ENABLE_PREVIEWS = YES;
|
||||||
GENERATE_INFOPLIST_FILE = YES;
|
GENERATE_INFOPLIST_FILE = YES;
|
||||||
@ -396,6 +837,7 @@
|
|||||||
INFOPLIST_KEY_UISupportedInterfaceOrientations = UIInterfaceOrientationPortrait;
|
INFOPLIST_KEY_UISupportedInterfaceOrientations = UIInterfaceOrientationPortrait;
|
||||||
INFOPLIST_KEY_UISupportedInterfaceOrientations_iPad = "UIInterfaceOrientationPortrait UIInterfaceOrientationPortraitUpsideDown UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight";
|
INFOPLIST_KEY_UISupportedInterfaceOrientations_iPad = "UIInterfaceOrientationPortrait UIInterfaceOrientationPortraitUpsideDown UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight";
|
||||||
INFOPLIST_KEY_UISupportedInterfaceOrientations_iPhone = UIInterfaceOrientationPortrait;
|
INFOPLIST_KEY_UISupportedInterfaceOrientations_iPhone = UIInterfaceOrientationPortrait;
|
||||||
|
IPHONEOS_DEPLOYMENT_TARGET = 17.0;
|
||||||
LD_RUNPATH_SEARCH_PATHS = (
|
LD_RUNPATH_SEARCH_PATHS = (
|
||||||
"$(inherited)",
|
"$(inherited)",
|
||||||
"@executable_path/Frameworks",
|
"@executable_path/Frameworks",
|
||||||
@ -404,11 +846,138 @@
|
|||||||
PRODUCT_BUNDLE_IDENTIFIER = de.christophhagen.Sesame;
|
PRODUCT_BUNDLE_IDENTIFIER = de.christophhagen.Sesame;
|
||||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||||
SWIFT_EMIT_LOC_STRINGS = YES;
|
SWIFT_EMIT_LOC_STRINGS = YES;
|
||||||
|
SWIFT_OBJC_BRIDGING_HEADER = "";
|
||||||
SWIFT_VERSION = 5.0;
|
SWIFT_VERSION = 5.0;
|
||||||
TARGETED_DEVICE_FAMILY = 1;
|
TARGETED_DEVICE_FAMILY = 1;
|
||||||
};
|
};
|
||||||
name = Release;
|
name = Release;
|
||||||
};
|
};
|
||||||
|
88E197BD29EDC9BD00BF1D19 /* Debug */ = {
|
||||||
|
isa = XCBuildConfiguration;
|
||||||
|
buildSettings = {
|
||||||
|
ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES;
|
||||||
|
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
|
||||||
|
ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor;
|
||||||
|
CLANG_CXX_LANGUAGE_STANDARD = "gnu++20";
|
||||||
|
CODE_SIGN_STYLE = Automatic;
|
||||||
|
CURRENT_PROJECT_VERSION = 1;
|
||||||
|
DEVELOPMENT_ASSET_PATHS = "";
|
||||||
|
DEVELOPMENT_TEAM = H8WR4M6QQ4;
|
||||||
|
ENABLE_PREVIEWS = YES;
|
||||||
|
GENERATE_INFOPLIST_FILE = YES;
|
||||||
|
INFOPLIST_KEY_CFBundleDisplayName = "Sesame-Watch";
|
||||||
|
INFOPLIST_KEY_UISupportedInterfaceOrientations = "UIInterfaceOrientationPortrait UIInterfaceOrientationPortraitUpsideDown";
|
||||||
|
INFOPLIST_KEY_WKWatchOnly = YES;
|
||||||
|
LD_RUNPATH_SEARCH_PATHS = (
|
||||||
|
"$(inherited)",
|
||||||
|
"@executable_path/Frameworks",
|
||||||
|
);
|
||||||
|
MARKETING_VERSION = 1.0;
|
||||||
|
PRODUCT_BUNDLE_IDENTIFIER = "de.christophhagen.Sesame-Watch.watchkitapp";
|
||||||
|
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||||
|
SDKROOT = watchos;
|
||||||
|
SKIP_INSTALL = YES;
|
||||||
|
SWIFT_EMIT_LOC_STRINGS = YES;
|
||||||
|
SWIFT_VERSION = 5.0;
|
||||||
|
TARGETED_DEVICE_FAMILY = 4;
|
||||||
|
WATCHOS_DEPLOYMENT_TARGET = 10.0;
|
||||||
|
};
|
||||||
|
name = Debug;
|
||||||
|
};
|
||||||
|
88E197BE29EDC9BD00BF1D19 /* Release */ = {
|
||||||
|
isa = XCBuildConfiguration;
|
||||||
|
buildSettings = {
|
||||||
|
ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES;
|
||||||
|
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
|
||||||
|
ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor;
|
||||||
|
CLANG_CXX_LANGUAGE_STANDARD = "gnu++20";
|
||||||
|
CODE_SIGN_STYLE = Automatic;
|
||||||
|
CURRENT_PROJECT_VERSION = 1;
|
||||||
|
DEVELOPMENT_ASSET_PATHS = "";
|
||||||
|
DEVELOPMENT_TEAM = H8WR4M6QQ4;
|
||||||
|
ENABLE_PREVIEWS = YES;
|
||||||
|
GENERATE_INFOPLIST_FILE = YES;
|
||||||
|
INFOPLIST_KEY_CFBundleDisplayName = "Sesame-Watch";
|
||||||
|
INFOPLIST_KEY_UISupportedInterfaceOrientations = "UIInterfaceOrientationPortrait UIInterfaceOrientationPortraitUpsideDown";
|
||||||
|
INFOPLIST_KEY_WKWatchOnly = YES;
|
||||||
|
LD_RUNPATH_SEARCH_PATHS = (
|
||||||
|
"$(inherited)",
|
||||||
|
"@executable_path/Frameworks",
|
||||||
|
);
|
||||||
|
MARKETING_VERSION = 1.0;
|
||||||
|
PRODUCT_BUNDLE_IDENTIFIER = "de.christophhagen.Sesame-Watch.watchkitapp";
|
||||||
|
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||||
|
SDKROOT = watchos;
|
||||||
|
SKIP_INSTALL = YES;
|
||||||
|
SWIFT_EMIT_LOC_STRINGS = YES;
|
||||||
|
SWIFT_VERSION = 5.0;
|
||||||
|
TARGETED_DEVICE_FAMILY = 4;
|
||||||
|
WATCHOS_DEPLOYMENT_TARGET = 10.0;
|
||||||
|
};
|
||||||
|
name = Release;
|
||||||
|
};
|
||||||
|
E268E08E2A85302000185913 /* Debug */ = {
|
||||||
|
isa = XCBuildConfiguration;
|
||||||
|
buildSettings = {
|
||||||
|
ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor;
|
||||||
|
ASSETCATALOG_COMPILER_WIDGET_BACKGROUND_COLOR_NAME = WidgetBackground;
|
||||||
|
CLANG_CXX_LANGUAGE_STANDARD = "gnu++20";
|
||||||
|
CODE_SIGN_STYLE = Automatic;
|
||||||
|
CURRENT_PROJECT_VERSION = 1;
|
||||||
|
DEVELOPMENT_TEAM = H8WR4M6QQ4;
|
||||||
|
GENERATE_INFOPLIST_FILE = YES;
|
||||||
|
INFOPLIST_FILE = "Sesame-Widget/Info.plist";
|
||||||
|
INFOPLIST_KEY_CFBundleDisplayName = "Sesame-Widget";
|
||||||
|
INFOPLIST_KEY_NSHumanReadableCopyright = "";
|
||||||
|
LD_RUNPATH_SEARCH_PATHS = (
|
||||||
|
"$(inherited)",
|
||||||
|
"@executable_path/Frameworks",
|
||||||
|
"@executable_path/../../Frameworks",
|
||||||
|
"@executable_path/../../../../Frameworks",
|
||||||
|
);
|
||||||
|
MARKETING_VERSION = 1.0;
|
||||||
|
PRODUCT_BUNDLE_IDENTIFIER = "de.christophhagen.Sesame-Watch.watchkitapp.Sesame-Widget";
|
||||||
|
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||||
|
SDKROOT = watchos;
|
||||||
|
SKIP_INSTALL = YES;
|
||||||
|
SWIFT_EMIT_LOC_STRINGS = YES;
|
||||||
|
SWIFT_VERSION = 5.0;
|
||||||
|
TARGETED_DEVICE_FAMILY = 4;
|
||||||
|
WATCHOS_DEPLOYMENT_TARGET = 10.0;
|
||||||
|
};
|
||||||
|
name = Debug;
|
||||||
|
};
|
||||||
|
E268E08F2A85302000185913 /* Release */ = {
|
||||||
|
isa = XCBuildConfiguration;
|
||||||
|
buildSettings = {
|
||||||
|
ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor;
|
||||||
|
ASSETCATALOG_COMPILER_WIDGET_BACKGROUND_COLOR_NAME = WidgetBackground;
|
||||||
|
CLANG_CXX_LANGUAGE_STANDARD = "gnu++20";
|
||||||
|
CODE_SIGN_STYLE = Automatic;
|
||||||
|
CURRENT_PROJECT_VERSION = 1;
|
||||||
|
DEVELOPMENT_TEAM = H8WR4M6QQ4;
|
||||||
|
GENERATE_INFOPLIST_FILE = YES;
|
||||||
|
INFOPLIST_FILE = "Sesame-Widget/Info.plist";
|
||||||
|
INFOPLIST_KEY_CFBundleDisplayName = "Sesame-Widget";
|
||||||
|
INFOPLIST_KEY_NSHumanReadableCopyright = "";
|
||||||
|
LD_RUNPATH_SEARCH_PATHS = (
|
||||||
|
"$(inherited)",
|
||||||
|
"@executable_path/Frameworks",
|
||||||
|
"@executable_path/../../Frameworks",
|
||||||
|
"@executable_path/../../../../Frameworks",
|
||||||
|
);
|
||||||
|
MARKETING_VERSION = 1.0;
|
||||||
|
PRODUCT_BUNDLE_IDENTIFIER = "de.christophhagen.Sesame-Watch.watchkitapp.Sesame-Widget";
|
||||||
|
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||||
|
SDKROOT = watchos;
|
||||||
|
SKIP_INSTALL = YES;
|
||||||
|
SWIFT_EMIT_LOC_STRINGS = YES;
|
||||||
|
SWIFT_VERSION = 5.0;
|
||||||
|
TARGETED_DEVICE_FAMILY = 4;
|
||||||
|
WATCHOS_DEPLOYMENT_TARGET = 10.0;
|
||||||
|
};
|
||||||
|
name = Release;
|
||||||
|
};
|
||||||
/* End XCBuildConfiguration section */
|
/* End XCBuildConfiguration section */
|
||||||
|
|
||||||
/* Begin XCConfigurationList section */
|
/* Begin XCConfigurationList section */
|
||||||
@ -430,9 +999,43 @@
|
|||||||
defaultConfigurationIsVisible = 0;
|
defaultConfigurationIsVisible = 0;
|
||||||
defaultConfigurationName = Release;
|
defaultConfigurationName = Release;
|
||||||
};
|
};
|
||||||
|
88E197BF29EDC9BD00BF1D19 /* Build configuration list for PBXNativeTarget "Sesame-Watch Watch App" */ = {
|
||||||
|
isa = XCConfigurationList;
|
||||||
|
buildConfigurations = (
|
||||||
|
88E197BD29EDC9BD00BF1D19 /* Debug */,
|
||||||
|
88E197BE29EDC9BD00BF1D19 /* Release */,
|
||||||
|
);
|
||||||
|
defaultConfigurationIsVisible = 0;
|
||||||
|
defaultConfigurationName = Release;
|
||||||
|
};
|
||||||
|
E268E08D2A85302000185913 /* Build configuration list for PBXNativeTarget "Sesame-WidgetExtension" */ = {
|
||||||
|
isa = XCConfigurationList;
|
||||||
|
buildConfigurations = (
|
||||||
|
E268E08E2A85302000185913 /* Debug */,
|
||||||
|
E268E08F2A85302000185913 /* Release */,
|
||||||
|
);
|
||||||
|
defaultConfigurationIsVisible = 0;
|
||||||
|
defaultConfigurationName = Release;
|
||||||
|
};
|
||||||
/* End XCConfigurationList section */
|
/* End XCConfigurationList section */
|
||||||
|
|
||||||
/* Begin XCRemoteSwiftPackageReference section */
|
/* Begin XCRemoteSwiftPackageReference section */
|
||||||
|
8864664D29E5684C004FE2BE /* XCRemoteSwiftPackageReference "CBORCoding" */ = {
|
||||||
|
isa = XCRemoteSwiftPackageReference;
|
||||||
|
repositoryURL = "https://github.com/christophhagen/CBORCoding";
|
||||||
|
requirement = {
|
||||||
|
kind = upToNextMajorVersion;
|
||||||
|
minimumVersion = 1.0.0;
|
||||||
|
};
|
||||||
|
};
|
||||||
|
8864665029E5939C004FE2BE /* XCRemoteSwiftPackageReference "SFSafeSymbols" */ = {
|
||||||
|
isa = XCRemoteSwiftPackageReference;
|
||||||
|
repositoryURL = "https://github.com/SFSafeSymbols/SFSafeSymbols";
|
||||||
|
requirement = {
|
||||||
|
kind = upToNextMajorVersion;
|
||||||
|
minimumVersion = 4.0.0;
|
||||||
|
};
|
||||||
|
};
|
||||||
E24EE77527FF95C00011CFD2 /* XCRemoteSwiftPackageReference "swift-nio" */ = {
|
E24EE77527FF95C00011CFD2 /* XCRemoteSwiftPackageReference "swift-nio" */ = {
|
||||||
isa = XCRemoteSwiftPackageReference;
|
isa = XCRemoteSwiftPackageReference;
|
||||||
repositoryURL = "https://github.com/apple/swift-nio.git";
|
repositoryURL = "https://github.com/apple/swift-nio.git";
|
||||||
@ -444,11 +1047,41 @@
|
|||||||
/* End XCRemoteSwiftPackageReference section */
|
/* End XCRemoteSwiftPackageReference section */
|
||||||
|
|
||||||
/* Begin XCSwiftPackageProductDependency section */
|
/* Begin XCSwiftPackageProductDependency section */
|
||||||
|
8864664E29E5684C004FE2BE /* CBORCoding */ = {
|
||||||
|
isa = XCSwiftPackageProductDependency;
|
||||||
|
package = 8864664D29E5684C004FE2BE /* XCRemoteSwiftPackageReference "CBORCoding" */;
|
||||||
|
productName = CBORCoding;
|
||||||
|
};
|
||||||
|
8864665129E5939C004FE2BE /* SFSafeSymbols */ = {
|
||||||
|
isa = XCSwiftPackageProductDependency;
|
||||||
|
package = 8864665029E5939C004FE2BE /* XCRemoteSwiftPackageReference "SFSafeSymbols" */;
|
||||||
|
productName = SFSafeSymbols;
|
||||||
|
};
|
||||||
|
88E197CB29EDCD4900BF1D19 /* NIOCore */ = {
|
||||||
|
isa = XCSwiftPackageProductDependency;
|
||||||
|
package = E24EE77527FF95C00011CFD2 /* XCRemoteSwiftPackageReference "swift-nio" */;
|
||||||
|
productName = NIOCore;
|
||||||
|
};
|
||||||
|
88E197CD29EDCD7500BF1D19 /* CBORCoding */ = {
|
||||||
|
isa = XCSwiftPackageProductDependency;
|
||||||
|
package = 8864664D29E5684C004FE2BE /* XCRemoteSwiftPackageReference "CBORCoding" */;
|
||||||
|
productName = CBORCoding;
|
||||||
|
};
|
||||||
|
88E197CF29EDCD7D00BF1D19 /* SFSafeSymbols */ = {
|
||||||
|
isa = XCSwiftPackageProductDependency;
|
||||||
|
package = 8864665029E5939C004FE2BE /* XCRemoteSwiftPackageReference "SFSafeSymbols" */;
|
||||||
|
productName = SFSafeSymbols;
|
||||||
|
};
|
||||||
E24EE77627FF95C00011CFD2 /* NIOCore */ = {
|
E24EE77627FF95C00011CFD2 /* NIOCore */ = {
|
||||||
isa = XCSwiftPackageProductDependency;
|
isa = XCSwiftPackageProductDependency;
|
||||||
package = E24EE77527FF95C00011CFD2 /* XCRemoteSwiftPackageReference "swift-nio" */;
|
package = E24EE77527FF95C00011CFD2 /* XCRemoteSwiftPackageReference "swift-nio" */;
|
||||||
productName = NIOCore;
|
productName = NIOCore;
|
||||||
};
|
};
|
||||||
|
E268E0902A85318500185913 /* SFSafeSymbols */ = {
|
||||||
|
isa = XCSwiftPackageProductDependency;
|
||||||
|
package = 8864665029E5939C004FE2BE /* XCRemoteSwiftPackageReference "SFSafeSymbols" */;
|
||||||
|
productName = SFSafeSymbols;
|
||||||
|
};
|
||||||
/* End XCSwiftPackageProductDependency section */
|
/* End XCSwiftPackageProductDependency section */
|
||||||
};
|
};
|
||||||
rootObject = 884A45AB279F48C100D6E650 /* Project object */;
|
rootObject = 884A45AB279F48C100D6E650 /* Project object */;
|
||||||
|
@ -1,5 +1,23 @@
|
|||||||
{
|
{
|
||||||
"pins" : [
|
"pins" : [
|
||||||
|
{
|
||||||
|
"identity" : "cborcoding",
|
||||||
|
"kind" : "remoteSourceControl",
|
||||||
|
"location" : "https://github.com/christophhagen/CBORCoding",
|
||||||
|
"state" : {
|
||||||
|
"revision" : "1e52c77523fca12cc290b17eed12fadb50ad72af",
|
||||||
|
"version" : "1.0.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"identity" : "sfsafesymbols",
|
||||||
|
"kind" : "remoteSourceControl",
|
||||||
|
"location" : "https://github.com/SFSafeSymbols/SFSafeSymbols",
|
||||||
|
"state" : {
|
||||||
|
"revision" : "7cca2d60925876b5953a2cf7341cd80fbeac983c",
|
||||||
|
"version" : "4.1.1"
|
||||||
|
}
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"identity" : "swift-nio",
|
"identity" : "swift-nio",
|
||||||
"kind" : "remoteSourceControl",
|
"kind" : "remoteSourceControl",
|
||||||
|
Binary file not shown.
Binary file not shown.
@ -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,120 @@
|
|||||||
|
<?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 = "E268E0802A85302000185913"
|
||||||
|
BuildableName = "Sesame-WidgetExtension.appex"
|
||||||
|
BlueprintName = "Sesame-WidgetExtension"
|
||||||
|
ReferencedContainer = "container:Sesame.xcodeproj">
|
||||||
|
</BuildableReference>
|
||||||
|
</BuildableProductRunnable>
|
||||||
|
<MacroExpansion>
|
||||||
|
<BuildableReference
|
||||||
|
BuildableIdentifier = "primary"
|
||||||
|
BlueprintIdentifier = "88E197AB29EDC9BC00BF1D19"
|
||||||
|
BuildableName = "Sesame-Watch Watch App.app"
|
||||||
|
BlueprintName = "Sesame-Watch Watch App"
|
||||||
|
ReferencedContainer = "container:Sesame.xcodeproj">
|
||||||
|
</BuildableReference>
|
||||||
|
</MacroExpansion>
|
||||||
|
<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>
|
@ -67,11 +67,26 @@
|
|||||||
<key>orderHint</key>
|
<key>orderHint</key>
|
||||||
<integer>0</integer>
|
<integer>0</integer>
|
||||||
</dict>
|
</dict>
|
||||||
<key>Sesame.xcscheme_^#shared#^_</key>
|
<key>Sesame Watch App.xcscheme_^#shared#^_</key>
|
||||||
|
<dict>
|
||||||
|
<key>orderHint</key>
|
||||||
|
<integer>1</integer>
|
||||||
|
</dict>
|
||||||
|
<key>Sesame-Watch Watch App.xcscheme_^#shared#^_</key>
|
||||||
|
<dict>
|
||||||
|
<key>orderHint</key>
|
||||||
|
<integer>1</integer>
|
||||||
|
</dict>
|
||||||
|
<key>Sesame-WidgetExtension.xcscheme_^#shared#^_</key>
|
||||||
<dict>
|
<dict>
|
||||||
<key>orderHint</key>
|
<key>orderHint</key>
|
||||||
<integer>0</integer>
|
<integer>0</integer>
|
||||||
</dict>
|
</dict>
|
||||||
|
<key>Sesame.xcscheme_^#shared#^_</key>
|
||||||
|
<dict>
|
||||||
|
<key>orderHint</key>
|
||||||
|
<integer>2</integer>
|
||||||
|
</dict>
|
||||||
</dict>
|
</dict>
|
||||||
</dict>
|
</dict>
|
||||||
</plist>
|
</plist>
|
||||||
|
@ -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,11 +4,39 @@
|
|||||||
<dict>
|
<dict>
|
||||||
<key>SchemeUserState</key>
|
<key>SchemeUserState</key>
|
||||||
<dict>
|
<dict>
|
||||||
<key>Sesame.xcscheme_^#shared#^_</key>
|
<key>Sesame Watch App.xcscheme_^#shared#^_</key>
|
||||||
|
<dict>
|
||||||
|
<key>orderHint</key>
|
||||||
|
<integer>1</integer>
|
||||||
|
</dict>
|
||||||
|
<key>Sesame-Watch Watch App.xcscheme_^#shared#^_</key>
|
||||||
|
<dict>
|
||||||
|
<key>orderHint</key>
|
||||||
|
<integer>2</integer>
|
||||||
|
</dict>
|
||||||
|
<key>Sesame-WidgetExtension.xcscheme_^#shared#^_</key>
|
||||||
<dict>
|
<dict>
|
||||||
<key>orderHint</key>
|
<key>orderHint</key>
|
||||||
<integer>0</integer>
|
<integer>0</integer>
|
||||||
</dict>
|
</dict>
|
||||||
|
<key>Sesame.xcscheme_^#shared#^_</key>
|
||||||
|
<dict>
|
||||||
|
<key>orderHint</key>
|
||||||
|
<integer>2</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>
|
||||||
</dict>
|
</dict>
|
||||||
</plist>
|
</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
|
||||||
|
}
|
||||||
|
}
|
128
Sesame/API Extensions/Message.swift
Normal file
128
Sesame/API Extensions/Message.swift
Normal file
@ -0,0 +1,128 @@
|
|||||||
|
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
|
||||||
|
let messageTypeEndIndex = data.startIndex+1
|
||||||
|
let clientChallengeEndIndex = messageTypeEndIndex + UInt32.byteSize
|
||||||
|
let clientChallengeData = Array(data[messageTypeEndIndex..<clientChallengeEndIndex])
|
||||||
|
self.clientChallenge = UInt32(bytes: clientChallengeData)
|
||||||
|
let serverChallengeEndIndex = clientChallengeEndIndex + UInt32.byteSize
|
||||||
|
let serverChallengeData = Array(data[clientChallengeEndIndex..<serverChallengeEndIndex])
|
||||||
|
self.serverChallenge = UInt32(bytes: serverChallengeData)
|
||||||
|
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 invalidMessageData: DeviceResponse {
|
|
||||||
.init(event: .invalidMessageData)
|
|
||||||
}
|
|
||||||
|
|
||||||
/// 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 Data([event.rawValue])
|
|
||||||
}
|
|
||||||
return Data([event.rawValue]) + message.encoded
|
|
||||||
}
|
|
||||||
}
|
|
11
Sesame/API/Extensions/Data+Coding.swift
Normal file
11
Sesame/API/Extensions/Data+Coding.swift
Normal file
@ -0,0 +1,11 @@
|
|||||||
|
import Foundation
|
||||||
|
|
||||||
|
extension Data {
|
||||||
|
|
||||||
|
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)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
@ -6,8 +6,8 @@ extension UInt32 {
|
|||||||
Create a value from a little-endian data representation (MSB first)
|
Create a value from a little-endian data representation (MSB first)
|
||||||
- Note: The data must contain exactly four bytes.
|
- Note: The data must contain exactly four bytes.
|
||||||
*/
|
*/
|
||||||
init(data: Data) {
|
init(bytes: [UInt8]) {
|
||||||
let value = data.convert(into: UInt32.zero)
|
let value = bytes.convert(to: UInt32.self)
|
||||||
self = CFSwapInt32LittleToHost(value)
|
self = CFSwapInt32LittleToHost(value)
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -15,4 +15,7 @@ extension UInt32 {
|
|||||||
var encoded: Data {
|
var encoded: Data {
|
||||||
Data(from: CFSwapInt32HostToLittle(self))
|
Data(from: CFSwapInt32HostToLittle(self))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// The size of a `UInt32` when converted to data
|
||||||
|
static let byteSize = MemoryLayout<UInt32>.size
|
||||||
}
|
}
|
12
Sesame/API/Extensions/UInt8+Extensions.swift
Normal file
12
Sesame/API/Extensions/UInt8+Extensions.swift
Normal file
@ -0,0 +1,12 @@
|
|||||||
|
import Foundation
|
||||||
|
|
||||||
|
extension Array where Element == UInt8 {
|
||||||
|
|
||||||
|
func convert<T>(to _: T.Type) -> T {
|
||||||
|
withUnsafeBufferPointer {
|
||||||
|
$0.baseAddress!.withMemoryRebound(to: T.self, capacity: 1) {
|
||||||
|
$0.pointee
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
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,159 +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 {
|
|
||||||
|
|
||||||
/**
|
|
||||||
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
|
|
||||||
|
|
||||||
/**
|
|
||||||
Create new message content.
|
|
||||||
- Parameter time: The time of message creation,
|
|
||||||
- Parameter id: The counter of the message
|
|
||||||
*/
|
|
||||||
init(time: UInt32, id: UInt32) {
|
|
||||||
self.time = time
|
|
||||||
self.id = id
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
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.dropFirst(MemoryLayout<UInt32>.size)))
|
|
||||||
}
|
|
||||||
|
|
||||||
/// The byte length of an encoded message content
|
|
||||||
static var length: Int {
|
|
||||||
MemoryLayout<UInt32>.size * 2
|
|
||||||
}
|
|
||||||
|
|
||||||
/// The message content encoded to data
|
|
||||||
var encoded: Data {
|
|
||||||
time.encoded + id.encoded
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
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,72 +5,231 @@ import Foundation
|
|||||||
*/
|
*/
|
||||||
enum MessageResult: UInt8 {
|
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
|
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
|
case unexpectedSocketEvent = 2
|
||||||
|
|
||||||
/// The size of the payload (i.e. message) was invalid, or the data could not be read
|
/// The received message size is invalid.
|
||||||
case invalidMessageData = 3
|
case invalidMessageSizeFromRemote = 3
|
||||||
|
|
||||||
/// The transmitted message could not be authenticated using the key
|
/// The message signature was incorrect.
|
||||||
case messageAuthenticationFailed = 4
|
case invalidSignatureFromRemote = 4
|
||||||
|
|
||||||
/// The message time was not within the acceptable bounds
|
/// The server challenge of the message did not match previous messages
|
||||||
case messageTimeMismatch = 5
|
case invalidServerChallengeFromRemote = 5
|
||||||
|
|
||||||
/// A later key has been used, invalidating this key (to prevent replay attacks after blocked communication)
|
/// The client challenge of the message did not match previous messages
|
||||||
case messageCounterInvalid = 6
|
case invalidClientChallengeFromRemote = 6
|
||||||
|
|
||||||
/// The key was accepted by the device, and the door will be opened
|
/// An unexpected or unsupported message type was received
|
||||||
case messageAccepted = 7
|
case invalidMessageTypeFromRemote = 7
|
||||||
|
|
||||||
|
/// A message is already being processed
|
||||||
|
case tooManyRequests = 8
|
||||||
|
|
||||||
/// The request did not contain body data with the key
|
/// The received message result was not ``messageAccepted``
|
||||||
case noBodyData = 10
|
case invalidMessageResultFromRemote = 9
|
||||||
|
|
||||||
/// The device is not connected
|
/// An invalid Url parameter was set sending a message to the device over a local connection
|
||||||
case deviceNotConnected = 12
|
case invalidUrlParameter = 10
|
||||||
|
|
||||||
/// The device did not respond within the timeout
|
// MARK: Server status
|
||||||
case deviceTimedOut = 13
|
|
||||||
|
|
||||||
/// Another message is being processed by the device
|
/// The body data posting a message was missing or of wrong length
|
||||||
case operationInProgress = 14
|
case noOrInvalidBodyDataFromRemote = 21
|
||||||
|
|
||||||
|
/// The authentication token for the server was invalid
|
||||||
|
case invalidServerAuthenticationFromRemote = 22
|
||||||
|
|
||||||
|
/// The request took too long to complete
|
||||||
|
case deviceTimedOut = 23
|
||||||
|
|
||||||
|
/// 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 {
|
||||||
|
|
||||||
/// The device is connected
|
|
||||||
case deviceConnected = 15
|
|
||||||
}
|
}
|
||||||
|
|
||||||
extension MessageResult: CustomStringConvertible {
|
extension MessageResult: CustomStringConvertible {
|
||||||
|
|
||||||
var description: String {
|
var description: String {
|
||||||
switch self {
|
switch self {
|
||||||
|
case .messageAccepted:
|
||||||
|
return "Message accepted"
|
||||||
case .textReceived:
|
case .textReceived:
|
||||||
return "The device received unexpected text"
|
return "The device received unexpected text"
|
||||||
case .unexpectedSocketEvent:
|
case .unexpectedSocketEvent:
|
||||||
return "Unexpected socket event for the device"
|
return "Unexpected socket event for the device"
|
||||||
case .invalidMessageData:
|
case .invalidMessageSizeFromRemote:
|
||||||
return "Invalid message data"
|
return "Invalid message data from remote"
|
||||||
case .messageAuthenticationFailed:
|
case .invalidSignatureFromRemote:
|
||||||
return "Message authentication failed"
|
return "Message authentication failed"
|
||||||
case .messageTimeMismatch:
|
case .invalidServerChallengeFromRemote:
|
||||||
return "Message time invalid"
|
return "Remote used wrong server challenge"
|
||||||
case .messageCounterInvalid:
|
case .invalidClientChallengeFromRemote:
|
||||||
return "Message counter invalid"
|
return "Wrong client challenge sent"
|
||||||
case .messageAccepted:
|
case .invalidMessageTypeFromRemote:
|
||||||
return "Message accepted"
|
return "Message type from remote invalid"
|
||||||
case .noBodyData:
|
case .tooManyRequests:
|
||||||
return "No body data included in the request"
|
return "Device busy"
|
||||||
case .deviceNotConnected:
|
case .invalidMessageResultFromRemote:
|
||||||
return "Device not connected"
|
return "Invalid message result"
|
||||||
|
case .invalidUrlParameter:
|
||||||
|
return "The url parameter could not be found"
|
||||||
|
|
||||||
|
case .noOrInvalidBodyDataFromRemote:
|
||||||
|
return "Invalid body data in server request"
|
||||||
|
case .invalidServerAuthenticationFromRemote:
|
||||||
|
return "Invalid server token"
|
||||||
case .deviceTimedOut:
|
case .deviceTimedOut:
|
||||||
return "The device did not respond"
|
return "The device did not respond"
|
||||||
case .operationInProgress:
|
case .deviceNotConnected:
|
||||||
return "Another operation is in progress"
|
return "Device not connected to server"
|
||||||
case .deviceConnected:
|
case .invalidMessageSizeFromDevice:
|
||||||
return "The device is connected"
|
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,51 +0,0 @@
|
|||||||
import Foundation
|
|
||||||
import NIOCore
|
|
||||||
|
|
||||||
#if canImport(CryptoKit)
|
|
||||||
import CryptoKit
|
|
||||||
#else
|
|
||||||
import Crypto
|
|
||||||
#endif
|
|
||||||
|
|
||||||
struct ServerMessage {
|
|
||||||
|
|
||||||
static let authTokenSize = SHA256.byteCount
|
|
||||||
|
|
||||||
static let length = authTokenSize + Message.length
|
|
||||||
|
|
||||||
let authToken: Data
|
|
||||||
|
|
||||||
let message: Message
|
|
||||||
|
|
||||||
init(authToken: Data, message: Message) {
|
|
||||||
self.authToken = authToken
|
|
||||||
self.message = message
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
Decode a message from a byte buffer.
|
|
||||||
The buffer must contain at least `ServerMessage.length` bytes, or it will return `nil`.
|
|
||||||
- Parameter buffer: The buffer containing the bytes.
|
|
||||||
*/
|
|
||||||
init?(decodeFrom buffer: ByteBuffer) {
|
|
||||||
guard let data = buffer.getBytes(at: 0, length: ServerMessage.length) else {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
self.authToken = Data(data.prefix(ServerMessage.authTokenSize))
|
|
||||||
self.message = Message(decodeFrom: Data(data.dropFirst(ServerMessage.authTokenSize)))
|
|
||||||
}
|
|
||||||
|
|
||||||
var encoded: Data {
|
|
||||||
authToken + message.encoded
|
|
||||||
}
|
|
||||||
|
|
||||||
static func token(from buffer: ByteBuffer) -> Data? {
|
|
||||||
guard buffer.readableBytes == authTokenSize else {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
guard let bytes = buffer.getBytes(at: 0, length: authTokenSize) else {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
return Data(bytes)
|
|
||||||
}
|
|
||||||
}
|
|
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
|
The active urls on the server, for the device and the remote to connect
|
||||||
*/
|
*/
|
||||||
enum RouteAPI: String {
|
enum SesameRoute: String {
|
||||||
|
|
||||||
/// Check the device status
|
|
||||||
case getDeviceStatus = "status"
|
|
||||||
|
|
||||||
/// Send a message to the server, to relay to the device
|
/// Send a message to the server, to relay to the device
|
||||||
case postMessage = "message"
|
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
|
||||||
|
}
|
||||||
|
}
|
@ -1,70 +0,0 @@
|
|||||||
import Foundation
|
|
||||||
import CryptoKit
|
|
||||||
|
|
||||||
struct Client {
|
|
||||||
|
|
||||||
let server: URL
|
|
||||||
|
|
||||||
private let delegate = NeverCacheDelegate()
|
|
||||||
|
|
||||||
init(server: URL) {
|
|
||||||
self.server = server
|
|
||||||
}
|
|
||||||
|
|
||||||
func deviceStatus(authToken: Data) async -> ClientState {
|
|
||||||
await send(path: .getDeviceStatus, data: authToken).state
|
|
||||||
}
|
|
||||||
|
|
||||||
func send(_ message: Message, authToken: Data) async -> (state: ClientState, response: Message?) {
|
|
||||||
let serverMessage = ServerMessage(authToken: authToken, message: message)
|
|
||||||
return await send(path: .postMessage, data: serverMessage.encoded)
|
|
||||||
}
|
|
||||||
|
|
||||||
private func send(path: RouteAPI, data: Data) async -> (state: ClientState, response: Message?) {
|
|
||||||
let url = server.appendingPathComponent(path.rawValue)
|
|
||||||
var request = URLRequest(url: url)
|
|
||||||
request.httpBody = data
|
|
||||||
request.httpMethod = "POST"
|
|
||||||
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 {
|
|
||||||
return (result, nil)
|
|
||||||
}
|
|
||||||
let messageData = Array(data.advanced(by: 1))
|
|
||||||
let message = Message(decodeFrom: messageData)
|
|
||||||
return (result, message)
|
|
||||||
}
|
|
||||||
|
|
||||||
private func fulfill(_ request: URLRequest) async -> 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
|
|
||||||
}
|
|
||||||
guard code == 200 else {
|
|
||||||
print("Invalid server response \(code)")
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
return data
|
|
||||||
} catch {
|
|
||||||
print("Request failed: \(error)")
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
class NeverCacheDelegate: NSObject, NSURLConnectionDataDelegate {
|
|
||||||
|
|
||||||
func connection(_ connection: NSURLConnection, willCacheResponse cachedResponse: CachedURLResponse) -> CachedURLResponse? {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,283 +0,0 @@
|
|||||||
import Foundation
|
|
||||||
import SwiftUI
|
|
||||||
|
|
||||||
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 invalidCounter
|
|
||||||
case invalidTime
|
|
||||||
case invalidAuthentication
|
|
||||||
case timeout
|
|
||||||
case missingKey
|
|
||||||
}
|
|
||||||
|
|
||||||
extension RejectionCause: CustomStringConvertible {
|
|
||||||
|
|
||||||
var description: String {
|
|
||||||
switch self {
|
|
||||||
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 .noBodyData, .invalidMessageData, .textReceived, .unexpectedSocketEvent:
|
|
||||||
self = .internalError(keyResult.description)
|
|
||||||
case .deviceNotConnected:
|
|
||||||
self = .deviceNotAvailable(.deviceDisconnected)
|
|
||||||
case .operationInProgress:
|
|
||||||
self = .waitingForResponse
|
|
||||||
case .deviceConnected:
|
|
||||||
self = .ready
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
var actionText: String {
|
|
||||||
"Unlock"
|
|
||||||
}
|
|
||||||
|
|
||||||
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 .requestingStatus, .deviceNotAvailable, .waitingForResponse, .noKeyAvailable:
|
|
||||||
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])
|
|
||||||
}
|
|
||||||
|
|
||||||
private 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 .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 .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("")
|
|
||||||
default:
|
|
||||||
self = .internalError("Unknown code \(code)")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
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"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
103
Sesame/Common/Client.swift
Normal file
103
Sesame/Common/Client.swift
Normal file
@ -0,0 +1,103 @@
|
|||||||
|
import Foundation
|
||||||
|
import CryptoKit
|
||||||
|
|
||||||
|
final class Client {
|
||||||
|
|
||||||
|
init() {}
|
||||||
|
|
||||||
|
func send(_ message: Message, to url: String, port: UInt16, 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, toLocalDevice: url, port: port, 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)
|
||||||
|
}
|
||||||
|
|
||||||
|
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, toLocalDevice host: String, port: UInt16, verifyUsing deviceKey: SymmetricKey) async -> Message {
|
||||||
|
let client = UDPClient(host: host, port: port)
|
||||||
|
let response: Data? = await withCheckedContinuation { continuation in
|
||||||
|
client.begin()
|
||||||
|
client.send(message: message.encoded) { res in
|
||||||
|
continuation.resume(returning: res)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
guard let data = response else {
|
||||||
|
return message.message.with(result: .deviceNotConnected)
|
||||||
|
}
|
||||||
|
return decode(data, inResponseTo: message.message, verifyUsing: deviceKey)
|
||||||
|
}
|
||||||
|
|
||||||
|
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)
|
||||||
|
}
|
||||||
|
|
||||||
|
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)
|
||||||
|
}
|
||||||
|
return decode(data, inResponseTo: message, verifyUsing: deviceKey)
|
||||||
|
}
|
||||||
|
|
||||||
|
private func decode(_ data: Data, inResponseTo message: Message, verifyUsing deviceKey: SymmetricKey) -> Message {
|
||||||
|
guard data.count == SignedMessage.size else {
|
||||||
|
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 {
|
||||||
|
return (.unexpectedUrlResponseType, nil)
|
||||||
|
}
|
||||||
|
return (.init(httpCode: code), data)
|
||||||
|
} catch {
|
||||||
|
print("Request failed: \(error)")
|
||||||
|
return (.deviceTimedOut, nil)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
27
Sesame/Common/ConnectionStrategy.swift
Normal file
27
Sesame/Common/ConnectionStrategy.swift
Normal file
@ -0,0 +1,27 @@
|
|||||||
|
import Foundation
|
||||||
|
import CryptoKit
|
||||||
|
|
||||||
|
enum ConnectionStrategy: Int, CaseIterable, Identifiable {
|
||||||
|
case local = 0
|
||||||
|
case remote = 1
|
||||||
|
case localFirst = 2
|
||||||
|
case remoteFirst = 3
|
||||||
|
|
||||||
|
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: " + ")
|
||||||
|
}
|
||||||
|
}
|
37
Sesame/Common/Extensions/App+Extensions.swift
Normal file
37
Sesame/Common/Extensions/App+Extensions.swift
Normal file
@ -0,0 +1,37 @@
|
|||||||
|
import SwiftUI
|
||||||
|
import SwiftData
|
||||||
|
|
||||||
|
extension App {
|
||||||
|
|
||||||
|
static func loadModelContainer() -> ModelContainer {
|
||||||
|
do {
|
||||||
|
return try ModelContainer(for: HistoryItem.self)
|
||||||
|
} catch {
|
||||||
|
print("[WARNING] Removing default SwiftData storage")
|
||||||
|
removeDefaultModelContainer()
|
||||||
|
}
|
||||||
|
// Try again to load an empty container
|
||||||
|
do {
|
||||||
|
return try ModelContainer(for: HistoryItem.self)
|
||||||
|
} catch {
|
||||||
|
fatalError("Failed to create empty model container: \(error)")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static func removeDefaultModelContainer() {
|
||||||
|
guard let appSupportDir = FileManager.default.urls(for: .applicationSupportDirectory, in: .userDomainMask).last else {
|
||||||
|
fatalError("Failed to get application support directory")
|
||||||
|
}
|
||||||
|
do {
|
||||||
|
try FileManager.default.contentsOfDirectory(at: appSupportDir, includingPropertiesForKeys: nil)
|
||||||
|
.filter { $0.lastPathComponent.hasPrefix("default") }
|
||||||
|
.forEach {
|
||||||
|
try FileManager.default.removeItem(at: $0)
|
||||||
|
}
|
||||||
|
} catch {
|
||||||
|
print(error)
|
||||||
|
fatalError("Failed to remove default SwiftData database files")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
14
Sesame/Common/Extensions/Array+Extensions.swift
Normal file
14
Sesame/Common/Extensions/Array+Extensions.swift
Normal file
@ -0,0 +1,14 @@
|
|||||||
|
import Foundation
|
||||||
|
|
||||||
|
extension Array {
|
||||||
|
|
||||||
|
func count(where closure: (Element) -> Bool) -> Int {
|
||||||
|
var result = 0
|
||||||
|
forEach { element in
|
||||||
|
if closure(element) {
|
||||||
|
result += 1
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
}
|
71
Sesame/Common/Extensions/SymmetricKey+Extensions.swift
Normal file
71
Sesame/Common/Extensions/SymmetricKey+Extensions.swift
Normal file
@ -0,0 +1,71 @@
|
|||||||
|
import Foundation
|
||||||
|
import CryptoKit
|
||||||
|
|
||||||
|
extension SymmetricKey {
|
||||||
|
|
||||||
|
var data: Data {
|
||||||
|
withUnsafeBytes { Data(Array($0)) }
|
||||||
|
}
|
||||||
|
|
||||||
|
var base64: String {
|
||||||
|
data.base64EncodedString()
|
||||||
|
}
|
||||||
|
|
||||||
|
var displayString: String {
|
||||||
|
data.hexEncoded.uppercased().split(by: 4).joined(separator: " ")
|
||||||
|
}
|
||||||
|
|
||||||
|
var codeString: String {
|
||||||
|
" {" +
|
||||||
|
withUnsafeBytes {
|
||||||
|
return Data(Array($0))
|
||||||
|
}.map(String.init).joined(separator: ", ") +
|
||||||
|
"},"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
extension SHA256.Digest {
|
||||||
|
|
||||||
|
var hexEncoded: String {
|
||||||
|
Data(map { $0 }).hexEncoded
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
extension String {
|
||||||
|
|
||||||
|
func split(by length: Int) -> [String] {
|
||||||
|
var startIndex = self.startIndex
|
||||||
|
var results = [Substring]()
|
||||||
|
|
||||||
|
while startIndex < self.endIndex {
|
||||||
|
let endIndex = self.index(startIndex, offsetBy: length, limitedBy: self.endIndex) ?? self.endIndex
|
||||||
|
results.append(self[startIndex..<endIndex])
|
||||||
|
startIndex = endIndex
|
||||||
|
}
|
||||||
|
|
||||||
|
return results.map { String($0) }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let protocolSalt = "CryptoKit Playgrounds Putting It Together".data(using: .utf8)!
|
||||||
|
|
||||||
|
/// Generates an ephemeral key agreement key and performs key agreement to get the shared secret and derive the symmetric encryption key.
|
||||||
|
func encrypt(_ data: Data, to theirEncryptionKey: Curve25519.KeyAgreement.PublicKey, signedBy ourSigningKey: Curve25519.Signing.PrivateKey) throws ->
|
||||||
|
(ephemeralPublicKeyData: Data, ciphertext: Data, signature: Data) {
|
||||||
|
let ephemeralKey = Curve25519.KeyAgreement.PrivateKey()
|
||||||
|
let ephemeralPublicKey = ephemeralKey.publicKey.rawRepresentation
|
||||||
|
|
||||||
|
let sharedSecret = try ephemeralKey.sharedSecretFromKeyAgreement(with: theirEncryptionKey)
|
||||||
|
|
||||||
|
let symmetricKey = sharedSecret.hkdfDerivedSymmetricKey(using: SHA256.self,
|
||||||
|
salt: protocolSalt,
|
||||||
|
sharedInfo: ephemeralPublicKey +
|
||||||
|
theirEncryptionKey.rawRepresentation +
|
||||||
|
ourSigningKey.publicKey.rawRepresentation,
|
||||||
|
outputByteCount: 32)
|
||||||
|
|
||||||
|
let ciphertext = try ChaChaPoly.seal(data, using: symmetricKey).combined
|
||||||
|
let signature = try ourSigningKey.signature(for: ciphertext + ephemeralPublicKey + theirEncryptionKey.rawRepresentation)
|
||||||
|
|
||||||
|
return (ephemeralPublicKey, ciphertext, signature)
|
||||||
|
}
|
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)
|
||||||
|
}
|
||||||
|
}
|
145
Sesame/Common/HistoryManager.swift
Normal file
145
Sesame/Common/HistoryManager.swift
Normal file
@ -0,0 +1,145 @@
|
|||||||
|
import Foundation
|
||||||
|
import CBORCoding
|
||||||
|
|
||||||
|
/*
|
||||||
|
class HistoryManagerBase: ObservableObject {
|
||||||
|
|
||||||
|
@Published
|
||||||
|
var entries: [HistoryItem] = []
|
||||||
|
}
|
||||||
|
|
||||||
|
protocol HistoryManagerProtocol: HistoryManagerBase {
|
||||||
|
|
||||||
|
var entries: [HistoryItem] { get }
|
||||||
|
|
||||||
|
func save(item: HistoryItem) throws
|
||||||
|
|
||||||
|
func delete(item: HistoryItem) -> Bool
|
||||||
|
}
|
||||||
|
|
||||||
|
final class HistoryManager: HistoryManagerBase, HistoryManagerProtocol {
|
||||||
|
|
||||||
|
private let encoder = CBOREncoder(dateEncodingStrategy: .secondsSince1970)
|
||||||
|
|
||||||
|
private var fm: FileManager {
|
||||||
|
.default
|
||||||
|
}
|
||||||
|
|
||||||
|
static var documentDirectory: URL {
|
||||||
|
try! FileManager.default.url(
|
||||||
|
for: .documentDirectory,
|
||||||
|
in: .userDomainMask,
|
||||||
|
appropriateFor: nil, create: true)
|
||||||
|
}
|
||||||
|
|
||||||
|
private let fileUrl: URL
|
||||||
|
|
||||||
|
override init() {
|
||||||
|
self.fileUrl = HistoryManager.documentDirectory.appendingPathComponent("history2.bin")
|
||||||
|
super.init()
|
||||||
|
Task {
|
||||||
|
print("Loading history...")
|
||||||
|
let all = loadEntries()
|
||||||
|
DispatchQueue.main.async {
|
||||||
|
self.entries = all
|
||||||
|
print("History loaded (\(self.entries.count) entries)")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private func loadEntries() -> [HistoryItem] {
|
||||||
|
guard fm.fileExists(atPath: fileUrl.path) else {
|
||||||
|
print("No history data found")
|
||||||
|
return []
|
||||||
|
}
|
||||||
|
let content: Data
|
||||||
|
do {
|
||||||
|
content = try Data(contentsOf: fileUrl)
|
||||||
|
} catch {
|
||||||
|
print("Failed to read history data: \(error)")
|
||||||
|
return []
|
||||||
|
}
|
||||||
|
let decoder = CBORDecoder()
|
||||||
|
var index = 0
|
||||||
|
var entries = [HistoryItem]()
|
||||||
|
while index < content.count {
|
||||||
|
let length = Int(content[index])
|
||||||
|
index += 1
|
||||||
|
if index + length > content.count {
|
||||||
|
print("Missing bytes in history file: needed \(length), has only \(content.count - index)")
|
||||||
|
return entries
|
||||||
|
}
|
||||||
|
let entryData = content[index..<index+length]
|
||||||
|
index += length
|
||||||
|
do {
|
||||||
|
let entry: HistoryItem = try decoder.decode(from: entryData)
|
||||||
|
entries.append(entry)
|
||||||
|
} catch {
|
||||||
|
print("Failed to decode history (index: \(index), length \(length)): \(error)")
|
||||||
|
return entries
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return entries.sorted().reversed()
|
||||||
|
}
|
||||||
|
|
||||||
|
func save(item: HistoryItem) throws {
|
||||||
|
let data = try convertForStorage(item)
|
||||||
|
guard fm.fileExists(atPath: fileUrl.path) else {
|
||||||
|
try data.write(to: fileUrl)
|
||||||
|
print("First history item written (\(data[0]))")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
let handle = try FileHandle(forWritingTo: fileUrl)
|
||||||
|
try handle.seekToEnd()
|
||||||
|
try handle.write(contentsOf: data)
|
||||||
|
try handle.close()
|
||||||
|
print("History item written (\(data[0]))")
|
||||||
|
}
|
||||||
|
|
||||||
|
@discardableResult
|
||||||
|
func delete(item: HistoryItem) -> Bool {
|
||||||
|
let newItems = entries
|
||||||
|
.filter { $0 != item }
|
||||||
|
|
||||||
|
let data: FlattenSequence<[Data]>
|
||||||
|
do {
|
||||||
|
data = try newItems
|
||||||
|
.map(convertForStorage)
|
||||||
|
.joined()
|
||||||
|
} catch {
|
||||||
|
print("Failed to encode items: \(error)")
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
do {
|
||||||
|
try Data(data).write(to: fileUrl)
|
||||||
|
} catch {
|
||||||
|
print("Failed to save items: \(error)")
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
entries = newItems
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
private func convertForStorage(_ item: HistoryItem) throws -> Data {
|
||||||
|
let entryData = try encoder.encode(item)
|
||||||
|
return Data([UInt8(entryData.count)]) + entryData
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
final class HistoryManagerMock: HistoryManagerBase, HistoryManagerProtocol {
|
||||||
|
|
||||||
|
override init() {
|
||||||
|
super.init()
|
||||||
|
self.entries = [.mock]
|
||||||
|
}
|
||||||
|
|
||||||
|
func save(item: HistoryItem) throws {
|
||||||
|
entries.append(item)
|
||||||
|
}
|
||||||
|
|
||||||
|
func delete(item: HistoryItem) -> Bool {
|
||||||
|
entries = entries.filter { $0 != item }
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
*/
|
@ -2,6 +2,15 @@ import Foundation
|
|||||||
import CryptoKit
|
import CryptoKit
|
||||||
import SwiftUI
|
import SwiftUI
|
||||||
|
|
||||||
|
struct KeySet {
|
||||||
|
|
||||||
|
let remote: SymmetricKey
|
||||||
|
|
||||||
|
let device: SymmetricKey
|
||||||
|
|
||||||
|
let server: Data
|
||||||
|
}
|
||||||
|
|
||||||
extension KeyManagement {
|
extension KeyManagement {
|
||||||
|
|
||||||
enum KeyType: String, Identifiable, CaseIterable {
|
enum KeyType: String, Identifiable, CaseIterable {
|
||||||
@ -17,11 +26,11 @@ extension KeyManagement {
|
|||||||
var displayName: String {
|
var displayName: String {
|
||||||
switch self {
|
switch self {
|
||||||
case .deviceKey:
|
case .deviceKey:
|
||||||
return "Device Key"
|
return "Unlock Key"
|
||||||
case .remoteKey:
|
case .remoteKey:
|
||||||
return "Remote Key"
|
return "Response Key"
|
||||||
case .authToken:
|
case .authToken:
|
||||||
return "Authentication Token"
|
return "Server Token"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -82,6 +91,9 @@ private struct KeyChain {
|
|||||||
|
|
||||||
var item: CFTypeRef?
|
var item: CFTypeRef?
|
||||||
let status = SecItemCopyMatching(query as CFDictionary, &item)
|
let status = SecItemCopyMatching(query as CFDictionary, &item)
|
||||||
|
guard status != -25300 else {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
guard status == errSecSuccess else {
|
guard status == errSecSuccess else {
|
||||||
print("Failed to get \(type): \(status)")
|
print("Failed to get \(type): \(status)")
|
||||||
return nil
|
return nil
|
||||||
@ -117,9 +129,8 @@ final class KeyManagement: ObservableObject {
|
|||||||
@Published
|
@Published
|
||||||
private(set) var hasAuthToken = false
|
private(set) var hasAuthToken = false
|
||||||
|
|
||||||
var hasAllKeys: Bool {
|
@Published
|
||||||
hasRemoteKey && hasDeviceKey && hasAuthToken
|
private(set) var hasAllKeys = false
|
||||||
}
|
|
||||||
|
|
||||||
init() {
|
init() {
|
||||||
self.keyChain = KeyChain(domain: "christophhagen.de")
|
self.keyChain = KeyChain(domain: "christophhagen.de")
|
||||||
@ -137,6 +148,15 @@ final class KeyManagement: ObservableObject {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func getAllKeys() -> KeySet? {
|
||||||
|
guard let remoteKey = get(.remoteKey),
|
||||||
|
let token = get(.authToken)?.data,
|
||||||
|
let deviceKey = get(.deviceKey) else {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
return .init(remote: remoteKey, device: deviceKey, server: token)
|
||||||
|
}
|
||||||
|
|
||||||
func get(_ type: KeyType) -> SymmetricKey? {
|
func get(_ type: KeyType) -> SymmetricKey? {
|
||||||
keyChain.load(type)
|
keyChain.load(type)
|
||||||
}
|
}
|
||||||
@ -148,6 +168,15 @@ final class KeyManagement: ObservableObject {
|
|||||||
|
|
||||||
func generate(_ type: KeyType) {
|
func generate(_ type: KeyType) {
|
||||||
let key = SymmetricKey(size: type.keyLength)
|
let key = SymmetricKey(size: type.keyLength)
|
||||||
|
save(type, key: key)
|
||||||
|
}
|
||||||
|
|
||||||
|
func save(_ type: KeyType, data: Data) {
|
||||||
|
let key = SymmetricKey(data: data)
|
||||||
|
save(type, key: key)
|
||||||
|
}
|
||||||
|
|
||||||
|
private func save(_ type: KeyType, key: SymmetricKey) {
|
||||||
if keyChain.has(type) {
|
if keyChain.has(type) {
|
||||||
keyChain.delete(type)
|
keyChain.delete(type)
|
||||||
}
|
}
|
||||||
@ -159,5 +188,6 @@ final class KeyManagement: ObservableObject {
|
|||||||
self.hasRemoteKey = keyChain.has(.remoteKey)
|
self.hasRemoteKey = keyChain.has(.remoteKey)
|
||||||
self.hasDeviceKey = keyChain.has(.deviceKey)
|
self.hasDeviceKey = keyChain.has(.deviceKey)
|
||||||
self.hasAuthToken = keyChain.has(.authToken)
|
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 {
|
||||||
|
|
||||||
|
}
|
215
Sesame/Common/RequestCoordinator.swift
Normal file
215
Sesame/Common/RequestCoordinator.swift
Normal file
@ -0,0 +1,215 @@
|
|||||||
|
import Foundation
|
||||||
|
import SwiftUI
|
||||||
|
import SwiftData
|
||||||
|
|
||||||
|
final class RequestCoordinator: ObservableObject {
|
||||||
|
|
||||||
|
@Published
|
||||||
|
var state: MessageResult = .noKeyAvailable
|
||||||
|
|
||||||
|
@Published
|
||||||
|
var pendingRequests: [PendingOperation] = []
|
||||||
|
|
||||||
|
@Published
|
||||||
|
var activeRequest: PendingOperation?
|
||||||
|
|
||||||
|
@Published
|
||||||
|
var keyManager = KeyManagement()
|
||||||
|
|
||||||
|
@Published
|
||||||
|
var isPerformingRequest: Bool = false
|
||||||
|
|
||||||
|
@AppStorage("server")
|
||||||
|
var serverPath: String = "https://christophhagen.de/sesame/"
|
||||||
|
|
||||||
|
@AppStorage("localIP")
|
||||||
|
var localAddress: String = "192.168.178.104/"
|
||||||
|
|
||||||
|
@AppStorage("localPort")
|
||||||
|
var localPort: UInt16 = 8888
|
||||||
|
|
||||||
|
@AppStorage("connectionType")
|
||||||
|
var connectionType: ConnectionStrategy = .remoteFirst
|
||||||
|
|
||||||
|
private let modelContext: ModelContext
|
||||||
|
|
||||||
|
private let client = Client()
|
||||||
|
|
||||||
|
private var timer: Timer?
|
||||||
|
|
||||||
|
init(modelContext: ModelContext) {
|
||||||
|
self.modelContext = modelContext
|
||||||
|
if keyManager.hasAllKeys {
|
||||||
|
self.state = .notChecked
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func checkConnection(using route: TransmissionType? = nil) {
|
||||||
|
guard !isPerformingRequest else {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
isPerformingRequest = true
|
||||||
|
Task {
|
||||||
|
let route = route ?? connectionType.transmissionTypes.first!
|
||||||
|
let (finalResult, _) = await performChallenge(route: route)
|
||||||
|
DispatchQueue.main.async {
|
||||||
|
self.state = finalResult.result
|
||||||
|
self.isPerformingRequest = false
|
||||||
|
}
|
||||||
|
print("Finished connection test: \(finalResult)")
|
||||||
|
scheduleReturnToReadyState()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func startUnlock(quitAfterSuccess: Bool = false) {
|
||||||
|
guard !isPerformingRequest else {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
isPerformingRequest = true
|
||||||
|
Task {
|
||||||
|
let finalResult = await performFullChallengeResponse()
|
||||||
|
|
||||||
|
DispatchQueue.main.async {
|
||||||
|
self.state = finalResult
|
||||||
|
self.isPerformingRequest = false
|
||||||
|
}
|
||||||
|
if finalResult == .unlocked, quitAfterSuccess {
|
||||||
|
DispatchQueue.main.asyncAfter(deadline: .now() + .seconds(3)) {
|
||||||
|
exit(EXIT_SUCCESS)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
scheduleReturnToReadyState()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private func performFullChallengeResponse() async -> MessageResult {
|
||||||
|
let transmissionTypes = connectionType.transmissionTypes
|
||||||
|
for route in transmissionTypes.dropLast() {
|
||||||
|
if await performUnlockAndSaveItem(route: route) == .unlocked {
|
||||||
|
return .unlocked
|
||||||
|
}
|
||||||
|
}
|
||||||
|
guard let route = transmissionTypes.last else {
|
||||||
|
// No transmission types at all
|
||||||
|
return keyManager.hasAllKeys ? .notChecked : .noKeyAvailable
|
||||||
|
}
|
||||||
|
return await performUnlockAndSaveItem(route: route)
|
||||||
|
}
|
||||||
|
|
||||||
|
private func performUnlockAndSaveItem(route: TransmissionType) async -> MessageResult {
|
||||||
|
let startTime = Date.now
|
||||||
|
let result = await performFullChallengeResponse(route: route)
|
||||||
|
let endTime = Date.now
|
||||||
|
let roundTripTime = endTime.timeIntervalSince(startTime)
|
||||||
|
print("Unlock took \(Int(roundTripTime * 1000)) ms (\(result.result))")
|
||||||
|
let item = HistoryItem(message: result, startDate: startTime, route: route, finishDate: endTime)
|
||||||
|
modelContext.insert(item)
|
||||||
|
return result.result
|
||||||
|
}
|
||||||
|
|
||||||
|
private func performFullChallengeResponse(route: TransmissionType) async -> Message {
|
||||||
|
let (challengeResponse, challenge) = await performChallenge(route: route)
|
||||||
|
guard let challenge else {
|
||||||
|
return challengeResponse
|
||||||
|
}
|
||||||
|
|
||||||
|
let (unlockResponse, secondaryChallenge) = await performUnlock(with: challenge.message, route: route)
|
||||||
|
guard let secondaryChallenge else {
|
||||||
|
return unlockResponse
|
||||||
|
}
|
||||||
|
let (secondUnlockResponse, _) = await performUnlock(with: secondaryChallenge.message, route: route)
|
||||||
|
return secondUnlockResponse
|
||||||
|
}
|
||||||
|
|
||||||
|
private func performChallenge(route: TransmissionType) async -> ServerResponse {
|
||||||
|
let initialMessage = Message.initial()
|
||||||
|
let (result, challenge) = await send(initialMessage, route: route)
|
||||||
|
guard let message = challenge?.message else {
|
||||||
|
return (result, nil)
|
||||||
|
}
|
||||||
|
// Can't get here without the message being accepted
|
||||||
|
guard message.messageType == .challenge else {
|
||||||
|
print("Invalid message type for challenge: \(message)")
|
||||||
|
return (result.with(result: .invalidMessageTypeFromDevice), nil)
|
||||||
|
}
|
||||||
|
return (result.with(result: .deviceAvailable), challenge)
|
||||||
|
}
|
||||||
|
|
||||||
|
private func performUnlock(with challenge: Message, route: TransmissionType) async -> ServerResponse {
|
||||||
|
let request = challenge.requestMessage()
|
||||||
|
let (unlockState, responseData) = await send(request, route: route)
|
||||||
|
|
||||||
|
guard let response = responseData?.message else {
|
||||||
|
return (unlockState, nil)
|
||||||
|
}
|
||||||
|
switch response.messageType {
|
||||||
|
case .initial, .request:
|
||||||
|
print("Invalid message type for response: \(response)")
|
||||||
|
return (response.with(result: .invalidMessageTypeFromDevice), nil)
|
||||||
|
case .challenge:
|
||||||
|
// New challenge received, challenge was expired
|
||||||
|
return (unlockState, responseData)
|
||||||
|
case .response:
|
||||||
|
break
|
||||||
|
}
|
||||||
|
|
||||||
|
guard response.serverChallenge == request.serverChallenge else {
|
||||||
|
print("Invalid server challenge for unlock: \(response)")
|
||||||
|
return (response.with(result: .invalidServerChallengeFromDevice), nil)
|
||||||
|
}
|
||||||
|
return (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, port: localPort, through: route, using: keys)
|
||||||
|
}
|
||||||
|
|
||||||
|
func resetState() {
|
||||||
|
let hasKeys = keyManager.hasAllKeys
|
||||||
|
DispatchQueue.main.async {
|
||||||
|
self.state = hasKeys ? .notChecked : .noKeyAvailable
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func scheduleReturnToReadyState() {
|
||||||
|
timer?.invalidate()
|
||||||
|
|
||||||
|
DispatchQueue.main.async {
|
||||||
|
self.timer = Timer.scheduledTimer(withTimeInterval: 3, repeats: false) { [weak self] timer in
|
||||||
|
defer { timer.invalidate() }
|
||||||
|
guard let self else {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
self.resetState()
|
||||||
|
self.timer = nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
extension UInt16: RawRepresentable {
|
||||||
|
|
||||||
|
public var rawValue: String {
|
||||||
|
"\(self)"
|
||||||
|
}
|
||||||
|
|
||||||
|
public init?(rawValue: String) {
|
||||||
|
guard let value = UInt16(rawValue) else {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
self = value
|
||||||
|
}
|
||||||
|
}
|
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"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
170
Sesame/Common/UDPClient.swift
Normal file
170
Sesame/Common/UDPClient.swift
Normal file
@ -0,0 +1,170 @@
|
|||||||
|
import Foundation
|
||||||
|
import Network
|
||||||
|
|
||||||
|
enum UDPState: String {
|
||||||
|
case initial
|
||||||
|
case connectionCreated
|
||||||
|
case preparingConnection
|
||||||
|
case sending
|
||||||
|
case waitingForResponse
|
||||||
|
}
|
||||||
|
|
||||||
|
final class UDPClient {
|
||||||
|
|
||||||
|
let host: NWEndpoint.Host
|
||||||
|
|
||||||
|
let port: NWEndpoint.Port
|
||||||
|
|
||||||
|
private var connection: NWConnection?
|
||||||
|
|
||||||
|
private var completion: ((Data?) -> Void)?
|
||||||
|
|
||||||
|
private var state: UDPState = .initial
|
||||||
|
|
||||||
|
init(host: String, port: UInt16) {
|
||||||
|
self.host = .init("192.168.188.118")
|
||||||
|
self.port = .init(rawValue: port)!
|
||||||
|
}
|
||||||
|
|
||||||
|
deinit {
|
||||||
|
print("Destroying UDP Client")
|
||||||
|
finish()
|
||||||
|
}
|
||||||
|
|
||||||
|
func begin() {
|
||||||
|
guard state == .initial else {
|
||||||
|
print("Invalid state for begin(): \(state)")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
connection = NWConnection(host: host, port: port, using: .udp)
|
||||||
|
state = .connectionCreated
|
||||||
|
print("Created connection: \(connection != nil)")
|
||||||
|
}
|
||||||
|
|
||||||
|
func send(message: Data, completion: @escaping (Data?) -> Void) {
|
||||||
|
print("Sending message to \(host) at port \(port)")
|
||||||
|
guard state == .connectionCreated else {
|
||||||
|
print("Invalid state preparing for send: \(state)")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
guard let connection else {
|
||||||
|
print("Failed to send, no connection")
|
||||||
|
completion(nil)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
self.completion = completion
|
||||||
|
connection.stateUpdateHandler = { [weak self] (newState) in
|
||||||
|
switch (newState) {
|
||||||
|
case .ready:
|
||||||
|
print("State: Ready\n")
|
||||||
|
self?.send(message, over: connection)
|
||||||
|
case .setup:
|
||||||
|
print("State: Setup\n")
|
||||||
|
case .cancelled:
|
||||||
|
print("Cancelled UDP connection")
|
||||||
|
self?.finish()
|
||||||
|
case .preparing:
|
||||||
|
print("Preparing UDP connection")
|
||||||
|
case .failed(let error):
|
||||||
|
print("Failed to start UDP connection: \(error)")
|
||||||
|
self?.finish()
|
||||||
|
// default:
|
||||||
|
// print("ERROR! State not defined!\n")
|
||||||
|
// self?.finish()
|
||||||
|
case .waiting(_):
|
||||||
|
print("Waiting for UDP connection path change")
|
||||||
|
@unknown default:
|
||||||
|
print("Unknown UDP connection state: \(newState)")
|
||||||
|
self?.finish()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
print("Preparing connection")
|
||||||
|
state = .preparingConnection
|
||||||
|
connection.start(queue: .global())
|
||||||
|
|
||||||
|
DispatchQueue.main.asyncAfter(deadline: .now() + .seconds(5)) {
|
||||||
|
guard self.state == .preparingConnection else {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
print("Timed out preparing connection")
|
||||||
|
self.finish()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private func finish(_ data: Data? = nil) {
|
||||||
|
completion?(data)
|
||||||
|
completion = nil
|
||||||
|
connection?.stateUpdateHandler = nil
|
||||||
|
connection?.cancel()
|
||||||
|
connection = nil
|
||||||
|
state = .initial
|
||||||
|
}
|
||||||
|
|
||||||
|
private func send(_ data: Data, over connection: NWConnection) {
|
||||||
|
guard state == .preparingConnection else {
|
||||||
|
print("Invalid state for send: \(state)")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
connection.stateUpdateHandler = nil
|
||||||
|
|
||||||
|
let completion = NWConnection.SendCompletion.contentProcessed { [weak self] error in
|
||||||
|
if let error {
|
||||||
|
print("Failed to send UDP packet: \(error)")
|
||||||
|
self?.finish()
|
||||||
|
} else {
|
||||||
|
print("Finished sending message")
|
||||||
|
self?.waitForResponse(over: connection)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
state = .sending
|
||||||
|
connection.send(content: data, completion: completion)
|
||||||
|
print("Started to send message")
|
||||||
|
|
||||||
|
DispatchQueue.main.asyncAfter(deadline: .now() + .seconds(5)) {
|
||||||
|
guard self.state == .sending else {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
print("Timed out waiting for for send to complete")
|
||||||
|
self.finish()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private func waitForResponse(over connection: NWConnection) {
|
||||||
|
guard state == .sending else {
|
||||||
|
print("Invalid state for send: \(state)")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
state = .waitingForResponse
|
||||||
|
connection.receiveMessage { [weak self] (data, context, isComplete, error) in
|
||||||
|
guard self?.state == .waitingForResponse else {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
guard isComplete else {
|
||||||
|
if let error {
|
||||||
|
print("Failed to receive UDP message: \(error)")
|
||||||
|
} else {
|
||||||
|
print("Failed to receive complete UDP message without error")
|
||||||
|
}
|
||||||
|
self?.finish()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
guard let data else {
|
||||||
|
print("Received UDP message without data")
|
||||||
|
self?.finish()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
print("Received \(data.count) bytes")
|
||||||
|
self?.finish(data)
|
||||||
|
}
|
||||||
|
|
||||||
|
DispatchQueue.main.asyncAfter(deadline: .now() + .seconds(5)) {
|
||||||
|
guard self.state == .waitingForResponse else {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
print("Timed out waiting for response")
|
||||||
|
self.finish()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -1,252 +0,0 @@
|
|||||||
import SwiftUI
|
|
||||||
import CryptoKit
|
|
||||||
|
|
||||||
let server = Client(server: URL(string: "https://christophhagen.de/sesame/")!)
|
|
||||||
|
|
||||||
struct ContentView: View {
|
|
||||||
|
|
||||||
@AppStorage("counter")
|
|
||||||
var nextMessageCounter: Int = 0
|
|
||||||
|
|
||||||
@AppStorage("compensate")
|
|
||||||
var isCompensatingDaylightTime: Bool = false
|
|
||||||
|
|
||||||
@State
|
|
||||||
var keyManager = KeyManagement()
|
|
||||||
|
|
||||||
let history = HistoryManager()
|
|
||||||
|
|
||||||
@State
|
|
||||||
var state: ClientState = .noKeyAvailable
|
|
||||||
|
|
||||||
@State
|
|
||||||
private var timer: Timer?
|
|
||||||
|
|
||||||
@State
|
|
||||||
private var hasActiveRequest = false
|
|
||||||
|
|
||||||
@State
|
|
||||||
private var responseTime: Date? = nil
|
|
||||||
|
|
||||||
@State
|
|
||||||
private var showKeySheet = false
|
|
||||||
|
|
||||||
@State
|
|
||||||
private var showHistorySheet = false
|
|
||||||
|
|
||||||
var compensationTime: UInt32 {
|
|
||||||
isCompensatingDaylightTime ? 3600 : 0
|
|
||||||
}
|
|
||||||
|
|
||||||
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("Keys", action: { showKeySheet = 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()
|
|
||||||
if state.requiresDescription {
|
|
||||||
Text(state.description)
|
|
||||||
.padding()
|
|
||||||
}
|
|
||||||
Button(state.actionText, action: mainButtonPressed)
|
|
||||||
.frame(width: buttonWidth,
|
|
||||||
height: buttonWidth)
|
|
||||||
.background(buttonBackground)
|
|
||||||
.cornerRadius(buttonWidth / 2)
|
|
||||||
.overlay(RoundedRectangle(cornerRadius: buttonWidth / 2).stroke(lineWidth: buttonBorderWidth).foregroundColor(buttonColor))
|
|
||||||
.foregroundColor(buttonColor)
|
|
||||||
.font(.title)
|
|
||||||
.disabled(!state.allowsAction)
|
|
||||||
.padding(.bottom, (geo.size.width-buttonWidth) / 2)
|
|
||||||
}
|
|
||||||
.background(state.color)
|
|
||||||
.onAppear {
|
|
||||||
if keyManager.hasAllKeys {
|
|
||||||
state = .requestingStatus
|
|
||||||
}
|
|
||||||
startRegularStatusUpdates()
|
|
||||||
}
|
|
||||||
.onDisappear {
|
|
||||||
endRegularStatusUpdates()
|
|
||||||
}
|
|
||||||
.frame(width: geo.size.width, height: geo.size.height)
|
|
||||||
.animation(.easeInOut, value: state.color)
|
|
||||||
.sheet(isPresented: $showKeySheet) {
|
|
||||||
KeyView(keyManager: $keyManager, isCompensatingDaylightTime: $isCompensatingDaylightTime)
|
|
||||||
}
|
|
||||||
.sheet(isPresented: $showHistorySheet) {
|
|
||||||
HistoryView(manager: history)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
.preferredColorScheme(.dark)
|
|
||||||
}
|
|
||||||
|
|
||||||
func mainButtonPressed() {
|
|
||||||
guard let key = keyManager.get(.remoteKey),
|
|
||||||
let token = keyManager.get(.authToken)?.data 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)
|
|
||||||
let message = content.authenticate(using: key)
|
|
||||||
let historyItem = HistoryItem(sent: message, date: sentTime)
|
|
||||||
state = .waitingForResponse
|
|
||||||
print("Sending message \(count)")
|
|
||||||
Task {
|
|
||||||
let (newState, message) = await server.send(message, authToken: token)
|
|
||||||
let receivedTime = Date.now
|
|
||||||
responseTime = receivedTime
|
|
||||||
state = newState
|
|
||||||
let finishedItem = historyItem.didReceive(response: newState, date: receivedTime, message: message)
|
|
||||||
process(item: finishedItem)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private func process(item: HistoryItem) {
|
|
||||||
guard let message = item.incomingMessage else {
|
|
||||||
save(historyItem: item)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
guard let key = keyManager.get(.deviceKey) else {
|
|
||||||
save(historyItem: item.notAuthenticated())
|
|
||||||
return
|
|
||||||
}
|
|
||||||
guard message.isValid(using: key) else {
|
|
||||||
save(historyItem: item.invalidated())
|
|
||||||
return
|
|
||||||
}
|
|
||||||
nextMessageCounter = Int(message.content.id)
|
|
||||||
save(historyItem: item)
|
|
||||||
}
|
|
||||||
|
|
||||||
private func save(historyItem: HistoryItem) {
|
|
||||||
do {
|
|
||||||
try history.save(item: historyItem)
|
|
||||||
} 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 let authToken = keyManager.get(.authToken) else {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
guard !hasActiveRequest else {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
hasActiveRequest = true
|
|
||||||
Task {
|
|
||||||
let newState = await server.deviceStatus(authToken: authToken.data)
|
|
||||||
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))
|
|
||||||
}
|
|
||||||
}
|
|
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))
|
||||||
|
}
|
||||||
|
}
|
59
Sesame/History/HistoryItem.swift
Normal file
59
Sesame/History/HistoryItem.swift
Normal file
@ -0,0 +1,59 @@
|
|||||||
|
import Foundation
|
||||||
|
import SwiftData
|
||||||
|
|
||||||
|
@Model
|
||||||
|
final class HistoryItem {
|
||||||
|
|
||||||
|
let startDate: Date
|
||||||
|
|
||||||
|
let messageData: Data
|
||||||
|
|
||||||
|
var message: Message {
|
||||||
|
try! .init(decodeFrom: messageData)
|
||||||
|
}
|
||||||
|
|
||||||
|
let route: TransmissionType
|
||||||
|
|
||||||
|
let finishDate: Date
|
||||||
|
|
||||||
|
init(message: Message, startDate: Date, route: TransmissionType, finishDate: Date) {
|
||||||
|
self.startDate = startDate
|
||||||
|
self.messageData = message.encoded
|
||||||
|
self.finishDate = finishDate
|
||||||
|
self.route = route
|
||||||
|
}
|
||||||
|
|
||||||
|
var roundTripTime: TimeInterval {
|
||||||
|
finishDate.timeIntervalSince(startDate)
|
||||||
|
}
|
||||||
|
|
||||||
|
var response: MessageResult {
|
||||||
|
message.result
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
extension HistoryItem: Identifiable {
|
||||||
|
|
||||||
|
var id: Double {
|
||||||
|
startDate.timeIntervalSince1970
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
extension HistoryItem: Comparable {
|
||||||
|
|
||||||
|
static func < (lhs: HistoryItem, rhs: HistoryItem) -> Bool {
|
||||||
|
lhs.startDate < rhs.startDate
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
extension HistoryItem {
|
||||||
|
|
||||||
|
static var mock: HistoryItem {
|
||||||
|
let message = Message(messageType: .request, clientChallenge: 123, serverChallenge: 234, result: .unlocked)
|
||||||
|
return .init(
|
||||||
|
message: message,
|
||||||
|
startDate: Date.now.addingTimeInterval(-5),
|
||||||
|
route: .throughServer,
|
||||||
|
finishDate: Date.now)
|
||||||
|
}
|
||||||
|
}
|
63
Sesame/History/HistoryListItem.swift
Normal file
63
Sesame/History/HistoryListItem.swift
Normal file
@ -0,0 +1,63 @@
|
|||||||
|
import SwiftUI
|
||||||
|
import SwiftData
|
||||||
|
import SFSafeSymbols
|
||||||
|
|
||||||
|
|
||||||
|
private let df: DateFormatter = {
|
||||||
|
let df = DateFormatter()
|
||||||
|
df.dateStyle = .short
|
||||||
|
df.timeStyle = .short
|
||||||
|
return df
|
||||||
|
}()
|
||||||
|
|
||||||
|
struct HistoryListItem: View {
|
||||||
|
|
||||||
|
let entry: HistoryItem
|
||||||
|
|
||||||
|
var entryTime: String {
|
||||||
|
df.string(from: entry.startDate)
|
||||||
|
}
|
||||||
|
|
||||||
|
var roundTripText: String {
|
||||||
|
"\(Int(entry.roundTripTime * 1000)) ms"
|
||||||
|
}
|
||||||
|
|
||||||
|
var noncesText: String {
|
||||||
|
"\(entry.message.clientChallenge) → \(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)
|
||||||
|
}
|
||||||
|
HStack {
|
||||||
|
Image(systemSymbol: .arrowUpArrowDownCircle)
|
||||||
|
Text(roundTripText).padding(.trailing)
|
||||||
|
Image(systemSymbol: .keyHorizontal)
|
||||||
|
Text(noncesText)
|
||||||
|
}
|
||||||
|
.foregroundColor(.secondary)
|
||||||
|
.font(.footnote)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#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.")
|
||||||
|
}
|
||||||
|
}
|
74
Sesame/History/HistoryView.swift
Normal file
74
Sesame/History/HistoryView.swift
Normal file
@ -0,0 +1,74 @@
|
|||||||
|
import SwiftUI
|
||||||
|
import SwiftData
|
||||||
|
|
||||||
|
struct HistoryView: View {
|
||||||
|
|
||||||
|
@Environment(\.modelContext)
|
||||||
|
private var context
|
||||||
|
|
||||||
|
@Query(sort: \HistoryItem.startDate, order: .reverse)
|
||||||
|
private var items: [HistoryItem] = []
|
||||||
|
|
||||||
|
private var unlockCount: Int {
|
||||||
|
items.count { $0.response == .unlocked }
|
||||||
|
}
|
||||||
|
|
||||||
|
private var percentage: Double {
|
||||||
|
guard items.count > 0 else {
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
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(requestNumberText)
|
||||||
|
.foregroundColor(.primary)
|
||||||
|
.font(.body)
|
||||||
|
Spacer()
|
||||||
|
Text(String(format: "%d successful (%.1f %%)", unlockCount, percentage))
|
||||||
|
.foregroundColor(.secondary)
|
||||||
|
.font(.footnote)
|
||||||
|
}
|
||||||
|
ForEach(items) {entry in
|
||||||
|
HistoryListItem(entry: entry)
|
||||||
|
}.onDelete(perform: { indexSet in
|
||||||
|
let objects = indexSet.map { items[$0] }
|
||||||
|
for object in objects {
|
||||||
|
context.delete(object)
|
||||||
|
}
|
||||||
|
do {
|
||||||
|
try context.save()
|
||||||
|
} catch {
|
||||||
|
print("Failed to save after deleting \(objects.count) object(s): \(error)")
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
.navigationTitle("History")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#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,168 +0,0 @@
|
|||||||
import Foundation
|
|
||||||
|
|
||||||
struct HistoryItem {
|
|
||||||
|
|
||||||
let outgoingDate: Date
|
|
||||||
|
|
||||||
let outgoingMessage: Message
|
|
||||||
|
|
||||||
let incomingDate: Date?
|
|
||||||
|
|
||||||
let incomingMessage: Message?
|
|
||||||
|
|
||||||
let response: ClientState?
|
|
||||||
|
|
||||||
init(sent message: Message, date: Date) {
|
|
||||||
self.outgoingDate = date
|
|
||||||
self.outgoingMessage = message
|
|
||||||
self.incomingDate = nil
|
|
||||||
self.incomingMessage = nil
|
|
||||||
self.response = nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func didReceive(response: ClientState, date: Date?, message: Message?) -> HistoryItem {
|
|
||||||
.init(sent: self, response: response, date: date, message: message)
|
|
||||||
}
|
|
||||||
|
|
||||||
func invalidated() -> HistoryItem {
|
|
||||||
didReceive(response: .responseRejected(.invalidAuthentication), date: incomingDate, message: incomingMessage)
|
|
||||||
}
|
|
||||||
|
|
||||||
func notAuthenticated() -> HistoryItem {
|
|
||||||
didReceive(response: .responseRejected(.missingKey), date: incomingDate, message: incomingMessage)
|
|
||||||
}
|
|
||||||
|
|
||||||
private init(sent: HistoryItem, response: ClientState, date: Date?, message: Message?) {
|
|
||||||
self.outgoingDate = sent.outgoingDate
|
|
||||||
self.outgoingMessage = sent.outgoingMessage
|
|
||||||
self.incomingDate = date
|
|
||||||
self.incomingMessage = message
|
|
||||||
self.response = response
|
|
||||||
}
|
|
||||||
|
|
||||||
// MARK: Statistics
|
|
||||||
|
|
||||||
var roundTripTime: TimeInterval? {
|
|
||||||
incomingDate?.timeIntervalSince(outgoingDate)
|
|
||||||
}
|
|
||||||
|
|
||||||
var deviceTime: Date? {
|
|
||||||
guard let timestamp = incomingMessage?.content.time else {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
return Date(timestamp: timestamp)
|
|
||||||
}
|
|
||||||
|
|
||||||
var requestLatency: TimeInterval? {
|
|
||||||
deviceTime?.timeIntervalSince(outgoingDate)
|
|
||||||
}
|
|
||||||
|
|
||||||
var responseLatency: TimeInterval? {
|
|
||||||
guard let deviceTime = deviceTime else {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
return incomingDate?.timeIntervalSince(deviceTime)
|
|
||||||
}
|
|
||||||
|
|
||||||
var clockOffset: Int? {
|
|
||||||
guard let interval = roundTripTime, let deviceTime = deviceTime else {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
let estimatedArrival = outgoingDate.advanced(by: interval / 2)
|
|
||||||
return Int(deviceTime.timeIntervalSince(estimatedArrival))
|
|
||||||
}
|
|
||||||
|
|
||||||
// MARK: Coding
|
|
||||||
|
|
||||||
static func testEncoding() {
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
var encoded: Data {
|
|
||||||
var result = outgoingDate.encoded + outgoingMessage.encoded
|
|
||||||
if let date = incomingDate {
|
|
||||||
result += Data([1]) + date.encoded
|
|
||||||
} else {
|
|
||||||
result += Data([0])
|
|
||||||
}
|
|
||||||
if let message = incomingMessage {
|
|
||||||
result += Data([1]) + message.encoded
|
|
||||||
} else {
|
|
||||||
result += Data([0])
|
|
||||||
}
|
|
||||||
result += response?.encoded ?? Data([0])
|
|
||||||
return result
|
|
||||||
}
|
|
||||||
|
|
||||||
init?(decodeFrom data: Data, index: inout Int) {
|
|
||||||
guard let outgoingDate = Date(decodeFrom: data, index: &index) else {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
self.outgoingDate = outgoingDate
|
|
||||||
|
|
||||||
guard let outgoingMessage = Message(decodeFrom: data, index: &index) else {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
self.outgoingMessage = outgoingMessage
|
|
||||||
|
|
||||||
if data[index] > 0 {
|
|
||||||
index += 1
|
|
||||||
guard let incomingDate = Date(decodeFrom: data, index: &index) else {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
self.incomingDate = incomingDate
|
|
||||||
} else {
|
|
||||||
self.incomingDate = nil
|
|
||||||
index += 1
|
|
||||||
}
|
|
||||||
|
|
||||||
if data[index] > 0 {
|
|
||||||
index += 1
|
|
||||||
guard let incomingMessage = Message(decodeFrom: data, index: &index) else {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
self.incomingMessage = incomingMessage
|
|
||||||
} else {
|
|
||||||
self.incomingMessage = nil
|
|
||||||
index += 1
|
|
||||||
}
|
|
||||||
guard index < data.count else {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
self.response = ClientState(code: data[index])
|
|
||||||
index += 1
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private extension Date {
|
|
||||||
|
|
||||||
static var encodedSize: Int {
|
|
||||||
MemoryLayout<Double>.size
|
|
||||||
}
|
|
||||||
|
|
||||||
var encoded: Data {
|
|
||||||
.init(from: timeIntervalSince1970)
|
|
||||||
}
|
|
||||||
|
|
||||||
init?(decodeFrom data: Data, index: inout Int) {
|
|
||||||
guard index + Date.encodedSize <= data.count else {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
self.init(timeIntervalSince1970: data.advanced(by: index).convert(into: .zero))
|
|
||||||
index += Date.encodedSize
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
extension HistoryItem: Identifiable {
|
|
||||||
|
|
||||||
var id: UInt32 {
|
|
||||||
outgoingDate.timestamp
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
extension HistoryItem: Comparable {
|
|
||||||
|
|
||||||
static func < (lhs: HistoryItem, rhs: HistoryItem) -> Bool {
|
|
||||||
lhs.outgoingDate < rhs.outgoingDate
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,89 +0,0 @@
|
|||||||
import SwiftUI
|
|
||||||
|
|
||||||
private let df: DateFormatter = {
|
|
||||||
let df = DateFormatter()
|
|
||||||
df.dateStyle = .short
|
|
||||||
df.timeStyle = .short
|
|
||||||
return df
|
|
||||||
}()
|
|
||||||
|
|
||||||
struct HistoryListItem: View {
|
|
||||||
|
|
||||||
let entry: HistoryItem
|
|
||||||
|
|
||||||
var entryTime: String {
|
|
||||||
df.string(from: entry.outgoingDate)
|
|
||||||
}
|
|
||||||
|
|
||||||
var roundTripText: String {
|
|
||||||
guard let time = entry.roundTripTime else {
|
|
||||||
return ""
|
|
||||||
}
|
|
||||||
return "⇆ \(Int(time * 1000)) ms"
|
|
||||||
}
|
|
||||||
|
|
||||||
var counterText: String {
|
|
||||||
let sentCounter = entry.outgoingMessage.content.id
|
|
||||||
let startText = "🔗 \(sentCounter)"
|
|
||||||
guard let rCounter = entry.incomingMessage?.content.id else {
|
|
||||||
return startText
|
|
||||||
}
|
|
||||||
let diff = Int(rCounter) - Int(sentCounter)
|
|
||||||
guard diff != 1 && diff != 0 else {
|
|
||||||
return startText
|
|
||||||
}
|
|
||||||
return startText + " (\(diff))"
|
|
||||||
}
|
|
||||||
|
|
||||||
var timeOffsetText: String {
|
|
||||||
guard let offset = entry.clockOffset, offset != 0 else {
|
|
||||||
return ""
|
|
||||||
}
|
|
||||||
return "🕓 \(offset) s"
|
|
||||||
}
|
|
||||||
|
|
||||||
var body: some View {
|
|
||||||
VStack {
|
|
||||||
HStack {
|
|
||||||
Text(entry.response?.description ?? "")
|
|
||||||
.font(.headline)
|
|
||||||
Spacer()
|
|
||||||
Text(entryTime)
|
|
||||||
}.padding(.bottom, 1)
|
|
||||||
HStack {
|
|
||||||
Text(roundTripText)
|
|
||||||
.font(.subheadline)
|
|
||||||
.foregroundColor(.secondary)
|
|
||||||
Text(counterText)
|
|
||||||
.font(.subheadline)
|
|
||||||
.foregroundColor(.secondary)
|
|
||||||
Text(timeOffsetText)
|
|
||||||
.font(.subheadline)
|
|
||||||
.foregroundColor(.secondary)
|
|
||||||
Spacer()
|
|
||||||
}
|
|
||||||
}.padding()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
struct HistoryListItem_Previews: PreviewProvider {
|
|
||||||
static var previews: some View {
|
|
||||||
HistoryListItem(entry: .mock)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private extension HistoryItem {
|
|
||||||
|
|
||||||
static var mock: HistoryItem {
|
|
||||||
let mac = Data(repeating: 42, count: 32)
|
|
||||||
let content = Message.Content(time: Date.now.timestamp, id: 123)
|
|
||||||
let content2 = Message.Content(time: (Date.now + 1).timestamp, id: 124)
|
|
||||||
return .init(
|
|
||||||
sent: Message(mac: mac, content: content),
|
|
||||||
date: .now)
|
|
||||||
.didReceive(
|
|
||||||
response: .openSesame,
|
|
||||||
date: .now + 2,
|
|
||||||
message: Message(mac: mac, content: content2))
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,59 +0,0 @@
|
|||||||
import Foundation
|
|
||||||
|
|
||||||
final class HistoryManager {
|
|
||||||
|
|
||||||
private var fm: FileManager {
|
|
||||||
.default
|
|
||||||
}
|
|
||||||
|
|
||||||
var documentDirectory: URL {
|
|
||||||
try! fm.url(
|
|
||||||
for: .documentDirectory,
|
|
||||||
in: .userDomainMask,
|
|
||||||
appropriateFor: nil, create: true)
|
|
||||||
}
|
|
||||||
|
|
||||||
private var fileUrl: URL {
|
|
||||||
documentDirectory.appendingPathComponent("history.bin")
|
|
||||||
}
|
|
||||||
|
|
||||||
func loadEntries() -> [HistoryItem] {
|
|
||||||
let url = fileUrl
|
|
||||||
guard fm.fileExists(atPath: url.path) else {
|
|
||||||
print("No history data found")
|
|
||||||
return []
|
|
||||||
}
|
|
||||||
let content: Data
|
|
||||||
do {
|
|
||||||
content = try Data(contentsOf: url)
|
|
||||||
} catch {
|
|
||||||
print("Failed to read history data: \(error)")
|
|
||||||
return []
|
|
||||||
}
|
|
||||||
var index = 0
|
|
||||||
var entries = [HistoryItem]()
|
|
||||||
while index < content.count {
|
|
||||||
guard let entry = HistoryItem(decodeFrom: content, index: &index) else {
|
|
||||||
print("Failed to read entry at index \(index)")
|
|
||||||
return entries
|
|
||||||
}
|
|
||||||
entries.append(entry)
|
|
||||||
}
|
|
||||||
return entries.sorted().reversed()
|
|
||||||
}
|
|
||||||
|
|
||||||
func save(item: HistoryItem) throws {
|
|
||||||
let url = fileUrl
|
|
||||||
let data = item.encoded
|
|
||||||
guard fm.fileExists(atPath: url.path) else {
|
|
||||||
try data.write(to: url)
|
|
||||||
print("First history item written")
|
|
||||||
return
|
|
||||||
}
|
|
||||||
let handle = try FileHandle(forWritingTo: url)
|
|
||||||
try handle.seekToEnd()
|
|
||||||
try handle.write(contentsOf: data)
|
|
||||||
try handle.close()
|
|
||||||
print("History item written")
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,18 +0,0 @@
|
|||||||
import SwiftUI
|
|
||||||
|
|
||||||
struct HistoryView: View {
|
|
||||||
|
|
||||||
let manager: HistoryManager
|
|
||||||
|
|
||||||
var body: some View {
|
|
||||||
List(manager.loadEntries()) { entry in
|
|
||||||
HistoryListItem(entry: entry)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
struct HistoryView_Previews: PreviewProvider {
|
|
||||||
static var previews: some View {
|
|
||||||
HistoryView(manager: .init())
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,36 +0,0 @@
|
|||||||
import SwiftUI
|
|
||||||
|
|
||||||
struct KeyView: View {
|
|
||||||
|
|
||||||
@Binding
|
|
||||||
var keyManager: KeyManagement
|
|
||||||
|
|
||||||
@Binding
|
|
||||||
var isCompensatingDaylightTime: Bool
|
|
||||||
|
|
||||||
var body: some View {
|
|
||||||
ScrollView {
|
|
||||||
VStack(alignment: .leading, spacing: 8) {
|
|
||||||
ForEach(KeyManagement.KeyType.allCases) { keyType in
|
|
||||||
SingleKeyView(
|
|
||||||
keyManager: $keyManager,
|
|
||||||
type: keyType)
|
|
||||||
}
|
|
||||||
Toggle(isOn: $isCompensatingDaylightTime) {
|
|
||||||
Text("Compensate daylight savings time")
|
|
||||||
}
|
|
||||||
Text("If the remote has daylight savings time wrongly set, then the time validation will fail. Use this option to send messages with adjusted timestamps. Warning: Incorrect use of this option will allow replay attacks.")
|
|
||||||
.font(.caption)
|
|
||||||
.foregroundColor(.secondary)
|
|
||||||
}.padding()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
struct KeyView_Previews: PreviewProvider {
|
|
||||||
static var previews: some View {
|
|
||||||
KeyView(
|
|
||||||
keyManager: .constant(KeyManagement()),
|
|
||||||
isCompensatingDaylightTime: .constant(true))
|
|
||||||
}
|
|
||||||
}
|
|
104
Sesame/MainView.swift
Normal file
104
Sesame/MainView.swift
Normal file
@ -0,0 +1,104 @@
|
|||||||
|
import SwiftUI
|
||||||
|
import SwiftData
|
||||||
|
import CryptoKit
|
||||||
|
|
||||||
|
struct ContentView: View {
|
||||||
|
|
||||||
|
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 coordinator: RequestCoordinator
|
||||||
|
|
||||||
|
@State private var showSettingsSheet = false
|
||||||
|
@State private var showHistorySheet = false
|
||||||
|
@State private var didShowKeySheetOnce = false
|
||||||
|
|
||||||
|
init(modelContext: ModelContext) {
|
||||||
|
self.coordinator = .init(modelContext: modelContext)
|
||||||
|
}
|
||||||
|
|
||||||
|
var body: some View {
|
||||||
|
VStack(spacing: 20) {
|
||||||
|
HStack {
|
||||||
|
Spacer()
|
||||||
|
Text("Sesame")
|
||||||
|
.font(.title)
|
||||||
|
Spacer()
|
||||||
|
}
|
||||||
|
if coordinator.state != .notChecked {
|
||||||
|
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: unlock)
|
||||||
|
.frame(width: unlockButtonSize, height: unlockButtonSize)
|
||||||
|
.background(buttonBackground)
|
||||||
|
.cornerRadius(unlockButtonSize / 2)
|
||||||
|
.foregroundColor(buttonColor)
|
||||||
|
.font(.title)
|
||||||
|
Button(action: { showSettingsSheet = true }) {
|
||||||
|
Image(systemSymbol: .gearshape)
|
||||||
|
.foregroundColor(.white)
|
||||||
|
.frame(width: smallButtonSize, height: smallButtonSize)
|
||||||
|
.background(.white.opacity(0.2))
|
||||||
|
.cornerRadius(smallButtonSize / 2)
|
||||||
|
.font(.title2)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Picker("Connection type", selection: $coordinator.connectionType) {
|
||||||
|
Text(ConnectionStrategy.local.description).tag(ConnectionStrategy.local)
|
||||||
|
Text(ConnectionStrategy.remote.description).tag(ConnectionStrategy.remote)
|
||||||
|
Text(ConnectionStrategy.remoteFirst.description).tag(ConnectionStrategy.remoteFirst)
|
||||||
|
// ForEach(ConnectionStrategy.allCases, id: \.rawValue) { connection in
|
||||||
|
// Text(connection.description).tag(connection)
|
||||||
|
// }
|
||||||
|
}
|
||||||
|
.pickerStyle(.segmented)
|
||||||
|
.padding(.horizontal, 30)
|
||||||
|
}
|
||||||
|
.background(coordinator.state.color)
|
||||||
|
.animation(.easeInOut, value: coordinator.state.color)
|
||||||
|
.sheet(isPresented: $showSettingsSheet) {
|
||||||
|
SettingsView(
|
||||||
|
keyManager: coordinator.keyManager,
|
||||||
|
coordinator: coordinator,
|
||||||
|
serverAddress: $coordinator.serverPath,
|
||||||
|
localAddress: $coordinator.localAddress,
|
||||||
|
localPort: $coordinator.localPort)
|
||||||
|
}
|
||||||
|
.sheet(isPresented: $showHistorySheet) { HistoryView() }
|
||||||
|
.preferredColorScheme(.dark)
|
||||||
|
}
|
||||||
|
|
||||||
|
private func unlock() {
|
||||||
|
coordinator.startUnlock()
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
#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 ContentView(modelContext: container.mainContext)
|
||||||
|
.modelContainer(container)
|
||||||
|
} catch {
|
||||||
|
fatalError("Failed to create model container.")
|
||||||
|
}
|
||||||
|
}
|
@ -1,10 +1,20 @@
|
|||||||
import SwiftUI
|
import SwiftUI
|
||||||
|
import SwiftData
|
||||||
|
|
||||||
@main
|
@main
|
||||||
struct SesameApp: App {
|
struct SesameApp: App {
|
||||||
|
|
||||||
|
@State
|
||||||
|
var modelContainer: ModelContainer
|
||||||
|
|
||||||
|
init() {
|
||||||
|
self.modelContainer = SesameApp.loadModelContainer()
|
||||||
|
}
|
||||||
|
|
||||||
var body: some Scene {
|
var body: some Scene {
|
||||||
WindowGroup {
|
WindowGroup {
|
||||||
ContentView()
|
ContentView(modelContext: modelContainer.mainContext)
|
||||||
}
|
}
|
||||||
|
.modelContainer(modelContainer)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
100
Sesame/SettingsView.swift
Normal file
100
Sesame/SettingsView.swift
Normal file
@ -0,0 +1,100 @@
|
|||||||
|
import SwiftUI
|
||||||
|
import SwiftData
|
||||||
|
import SFSafeSymbols
|
||||||
|
import Combine
|
||||||
|
|
||||||
|
struct SettingsView: View {
|
||||||
|
|
||||||
|
let keyManager: KeyManagement
|
||||||
|
|
||||||
|
@ObservedObject
|
||||||
|
var coordinator: RequestCoordinator
|
||||||
|
|
||||||
|
@Binding
|
||||||
|
var serverAddress: String
|
||||||
|
|
||||||
|
@Binding
|
||||||
|
var localAddress: String
|
||||||
|
|
||||||
|
@Binding
|
||||||
|
var localPort: UInt16
|
||||||
|
|
||||||
|
@State
|
||||||
|
private var localPortString = ""
|
||||||
|
|
||||||
|
var body: some View {
|
||||||
|
NavigationView {
|
||||||
|
ScrollView {
|
||||||
|
VStack(alignment: .leading, spacing: 8) {
|
||||||
|
VStack(alignment: .leading) {
|
||||||
|
Text("Server address")
|
||||||
|
.bold()
|
||||||
|
TextField("Server address", text: $serverAddress)
|
||||||
|
.foregroundColor(.secondary)
|
||||||
|
.padding(.leading, 8)
|
||||||
|
HStack {
|
||||||
|
Button("Test") {
|
||||||
|
coordinator.checkConnection(using: .throughServer)
|
||||||
|
}.padding(8)
|
||||||
|
if coordinator.state == .deviceAvailable {
|
||||||
|
Image(systemSymbol: .checkmarkCircle)
|
||||||
|
.foregroundColor(.green)
|
||||||
|
} else if coordinator.state != .notChecked {
|
||||||
|
Text(coordinator.state.description)
|
||||||
|
.font(.caption)
|
||||||
|
.foregroundStyle(.secondary)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}.padding(.vertical, 8)
|
||||||
|
VStack(alignment: .leading) {
|
||||||
|
Text("Local address")
|
||||||
|
.bold()
|
||||||
|
TextField("Local address", text: $localAddress)
|
||||||
|
.foregroundColor(.secondary)
|
||||||
|
.padding(.leading, 8)
|
||||||
|
TextField("UDP Port", text: $localPortString)
|
||||||
|
.keyboardType(.numberPad)
|
||||||
|
.onReceive(Just(localPortString)) { newValue in
|
||||||
|
let filtered = newValue.filter { "0123456789".contains($0) }
|
||||||
|
if filtered != newValue {
|
||||||
|
self.localPortString = filtered
|
||||||
|
if let value = UInt16(filtered) {
|
||||||
|
self.localPort = value
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.foregroundColor(.secondary)
|
||||||
|
.padding(.leading, 8)
|
||||||
|
}.padding(.vertical, 8)
|
||||||
|
ForEach(KeyManagement.KeyType.allCases) { keyType in
|
||||||
|
SingleKeyView(
|
||||||
|
keyManager: keyManager,
|
||||||
|
type: keyType)
|
||||||
|
}
|
||||||
|
}.padding()
|
||||||
|
}.onAppear {
|
||||||
|
self.localPortString = "\(localPort)"
|
||||||
|
}
|
||||||
|
.navigationTitle("Settings")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#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 SettingsView(
|
||||||
|
keyManager: KeyManagement(),
|
||||||
|
coordinator: .init(modelContext: container.mainContext),
|
||||||
|
serverAddress: .constant("https://example.com"),
|
||||||
|
localAddress: .constant("192.168.178.42"),
|
||||||
|
localPort: .constant(1234))
|
||||||
|
} catch {
|
||||||
|
fatalError("Failed to create model container.")
|
||||||
|
}
|
||||||
|
}
|
@ -6,8 +6,13 @@ struct SingleKeyView: View {
|
|||||||
@State
|
@State
|
||||||
private var needRefresh = false
|
private var needRefresh = false
|
||||||
|
|
||||||
@Binding
|
let keyManager: KeyManagement
|
||||||
var keyManager: KeyManagement
|
|
||||||
|
@State
|
||||||
|
private var showEditWindow = false
|
||||||
|
|
||||||
|
@State
|
||||||
|
private var keyText = ""
|
||||||
|
|
||||||
let type: KeyManagement.KeyType
|
let type: KeyManagement.KeyType
|
||||||
|
|
||||||
@ -54,16 +59,49 @@ struct SingleKeyView: View {
|
|||||||
.disabled(!hasKey)
|
.disabled(!hasKey)
|
||||||
.padding([.horizontal, .bottom])
|
.padding([.horizontal, .bottom])
|
||||||
.padding(.top, 4)
|
.padding(.top, 4)
|
||||||
|
Button("Edit") {
|
||||||
|
keyText = keyManager.get(type)?.displayString ?? ""
|
||||||
|
print("Set key text to '\(keyText)'")
|
||||||
|
showEditWindow = true
|
||||||
|
}
|
||||||
|
.padding([.horizontal, .bottom])
|
||||||
|
.padding(.top, 4)
|
||||||
Spacer()
|
Spacer()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
.alert("Update key", isPresented: $showEditWindow, actions: {
|
||||||
|
TextField("Key data", text: $keyText)
|
||||||
|
.lineLimit(4)
|
||||||
|
.font(.system(.body, design: .monospaced))
|
||||||
|
.foregroundColor(.primary)
|
||||||
|
Button("Save", action: saveKey)
|
||||||
|
Button("Cancel", role: .cancel, action: {})
|
||||||
|
}, message: {
|
||||||
|
Text("Enter the hex encoded key")
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
private func saveKey() {
|
||||||
|
let cleanText = keyText.replacingOccurrences(of: " ", with: "")
|
||||||
|
guard let keyData = Data(fromHexEncodedString: cleanText) else {
|
||||||
|
print("Invalid key string")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
let keyLength = type.keyLength.bitCount
|
||||||
|
guard keyData.count * 8 == keyLength else {
|
||||||
|
print("Invalid key length \(keyData.count * 8) bits, expected \(keyLength)")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
keyManager.save(type, data: keyData)
|
||||||
|
print("Key \(type) saved")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
struct SingleKeyView_Previews: PreviewProvider {
|
struct SingleKeyView_Previews: PreviewProvider {
|
||||||
static var previews: some View {
|
static var previews: some View {
|
||||||
SingleKeyView(
|
SingleKeyView(
|
||||||
keyManager: .constant(KeyManagement()),
|
keyManager: KeyManagement(),
|
||||||
type: .deviceKey)
|
type: .deviceKey)
|
||||||
|
.previewLayout(.fixed(width: 350, height: 100))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,48 +0,0 @@
|
|||||||
import Foundation
|
|
||||||
import CryptoKit
|
|
||||||
|
|
||||||
extension SymmetricKey {
|
|
||||||
|
|
||||||
var data: Data {
|
|
||||||
withUnsafeBytes { Data(Array($0)) }
|
|
||||||
}
|
|
||||||
|
|
||||||
var base64: String {
|
|
||||||
data.base64EncodedString()
|
|
||||||
}
|
|
||||||
|
|
||||||
var displayString: String {
|
|
||||||
data.hexEncoded.uppercased().split(by: 4).joined(separator: " ")
|
|
||||||
}
|
|
||||||
|
|
||||||
var codeString: String {
|
|
||||||
" {" +
|
|
||||||
withUnsafeBytes {
|
|
||||||
return Data(Array($0))
|
|
||||||
}.map(String.init).joined(separator: ", ") +
|
|
||||||
"},"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
extension SHA256.Digest {
|
|
||||||
|
|
||||||
var hexEncoded: String {
|
|
||||||
Data(map { $0 }).hexEncoded
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
extension String {
|
|
||||||
|
|
||||||
func split(by length: Int) -> [String] {
|
|
||||||
var startIndex = self.startIndex
|
|
||||||
var results = [Substring]()
|
|
||||||
|
|
||||||
while startIndex < self.endIndex {
|
|
||||||
let endIndex = self.index(startIndex, offsetBy: length, limitedBy: self.endIndex) ?? self.endIndex
|
|
||||||
results.append(self[startIndex..<endIndex])
|
|
||||||
startIndex = endIndex
|
|
||||||
}
|
|
||||||
|
|
||||||
return results.map { String($0) }
|
|
||||||
}
|
|
||||||
}
|
|
Loading…
Reference in New Issue
Block a user