mirror of
https://github.com/seemoo-lab/openhaystack.git
synced 2026-08-23 21:46:15 +00:00
Added import and export options
Added the AccessoryController and the FindMyController to the SwiftUI Environment
This commit is contained in:
@@ -15,11 +15,7 @@ import SwiftUI
|
||||
class FindMyController: ObservableObject {
|
||||
@Published var error: Error?
|
||||
@Published var devices = [FindMyDevice]()
|
||||
var accessories: AccessoryController
|
||||
|
||||
init(accessories: AccessoryController) {
|
||||
self.accessories = accessories
|
||||
}
|
||||
@Environment(\.accessoryController) var accessories: AccessoryController
|
||||
|
||||
func loadPrivateKeys(from data: Data, with searchPartyToken: Data, completion: @escaping (Error?) -> Void) {
|
||||
do {
|
||||
|
||||
@@ -10,11 +10,13 @@
|
||||
import Combine
|
||||
import Foundation
|
||||
import SwiftUI
|
||||
import OSLog
|
||||
|
||||
class AccessoryController: ObservableObject {
|
||||
@Published var accessories: [Accessory]
|
||||
var selfObserver: AnyCancellable?
|
||||
var listElementsObserver = [AnyCancellable]()
|
||||
@Environment(\.findMyController) var findMyController: FindMyController
|
||||
|
||||
init(accessories: [Accessory]) {
|
||||
self.accessories = accessories
|
||||
@@ -88,6 +90,104 @@ class AccessoryController: ObservableObject {
|
||||
}
|
||||
return accessory
|
||||
}
|
||||
|
||||
/// Export the accessories property list so it can be imported at another location
|
||||
func export(accessories: [Accessory]) throws -> URL {
|
||||
let propertyList = try PropertyListEncoder().encode(accessories)
|
||||
|
||||
let savePanel = NSSavePanel()
|
||||
savePanel.allowedFileTypes = ["plist"]
|
||||
savePanel.canCreateDirectories = true
|
||||
savePanel.directoryURL = try FileManager.default.url(for: .documentDirectory, in: .userDomainMask, appropriateFor: nil, create: false)
|
||||
savePanel.message = "This export contains all private keys! Keep the file save to protect your location data"
|
||||
savePanel.nameFieldLabel = "Filename"
|
||||
savePanel.nameFieldStringValue = "openhaystack_accessories.plist"
|
||||
savePanel.prompt = "Export"
|
||||
savePanel.title = "Export accessories & keys"
|
||||
|
||||
let result = savePanel.runModal()
|
||||
|
||||
if result == .OK,
|
||||
let url = savePanel.url {
|
||||
// Store the accessory file
|
||||
try propertyList.write(to: url)
|
||||
|
||||
return url
|
||||
}
|
||||
throw ImportError.cancelled
|
||||
}
|
||||
|
||||
/// Let the user select a file to import the accessories exported by another OpenHaystack instance
|
||||
func importAccessories() throws {
|
||||
let openPanel = NSOpenPanel()
|
||||
openPanel.allowedFileTypes = ["plist"]
|
||||
openPanel.canCreateDirectories = true
|
||||
openPanel.directoryURL = try FileManager.default.url(for: .documentDirectory, in: .userDomainMask, appropriateFor: nil, create: false)
|
||||
openPanel.message = "Import an accessories file that includes the private keys"
|
||||
openPanel.prompt = "Import"
|
||||
openPanel.title = "Import accessories & keys"
|
||||
|
||||
let result = openPanel.runModal()
|
||||
if result == .OK,
|
||||
let url = openPanel.url {
|
||||
let propertyList = try Data(contentsOf: url)
|
||||
var importedAccessories = try PropertyListDecoder().decode([Accessory].self, from: propertyList)
|
||||
|
||||
var updatedAccessories = self.accessories
|
||||
// Filter out accessories with the same id (no duplicates)
|
||||
importedAccessories = importedAccessories.filter({acc in !self.accessories.contains(where: {acc.id == $0.id})})
|
||||
updatedAccessories.append(contentsOf: importedAccessories)
|
||||
updatedAccessories.sort(by: {$0.name < $1.name})
|
||||
|
||||
|
||||
self.accessories = updatedAccessories
|
||||
|
||||
//Update reports automatically. Do not report errors from here
|
||||
self.downloadLocationReports { result in}
|
||||
}
|
||||
}
|
||||
|
||||
enum ImportError: Error {
|
||||
case cancelled
|
||||
}
|
||||
|
||||
|
||||
//MARK: Location reports
|
||||
|
||||
|
||||
/// Download the location reports from
|
||||
/// - Parameter completion: called when the reports have been succesfully downloaded or the request has failed
|
||||
func downloadLocationReports(completion: @escaping (Result<Void,OpenHaystackMainView.AlertType>) -> Void) {
|
||||
AnisetteDataManager.shared.requestAnisetteData { result in
|
||||
switch result {
|
||||
case .failure(_):
|
||||
completion(.failure(.activatePlugin))
|
||||
case .success(let accountData):
|
||||
let token = accountData.searchPartyToken
|
||||
guard token.isEmpty == false else {
|
||||
completion(.failure(.searchPartyToken))
|
||||
return
|
||||
}
|
||||
|
||||
self.findMyController.fetchReports(for: self.accessories, with: token) { result in
|
||||
switch result {
|
||||
case .failure(let error):
|
||||
os_log(.error, "Downloading reports failed %@", error.localizedDescription)
|
||||
completion(.failure(.downloadingReportsFailed))
|
||||
case .success(let devices):
|
||||
let reports = devices.compactMap({ $0.reports }).flatMap({ $0 })
|
||||
if reports.isEmpty {
|
||||
completion(.failure(.noReportsFound))
|
||||
}else {
|
||||
completion(.success(()))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
class AccessoryControllerPreview: AccessoryController {
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
//
|
||||
//
|
||||
// OpenHaystack – Tracking personal Bluetooth devices via Apple's Find My network
|
||||
//
|
||||
// Copyright © 2021 Secure Mobile Networking Lab (SEEMOO)
|
||||
@@ -11,7 +11,7 @@ import SwiftUI
|
||||
|
||||
struct ManageAccessoriesView: View {
|
||||
|
||||
@EnvironmentObject var accessoryController: AccessoryController
|
||||
@Environment(\.accessoryController) var accessoryController: AccessoryController
|
||||
var accessories: [Accessory] {
|
||||
return self.accessoryController.accessories
|
||||
}
|
||||
@@ -39,6 +39,15 @@ struct ManageAccessoriesView: View {
|
||||
}
|
||||
.toolbar(content: {
|
||||
Spacer()
|
||||
|
||||
Button(action: self.importAccessories, label: {
|
||||
Label("Export accessories", systemImage: "square.and.arrow.down")
|
||||
})
|
||||
|
||||
Button(action: self.exportAccessories, label: {
|
||||
Label("Export accessories", systemImage: "square.and.arrow.up")
|
||||
})
|
||||
|
||||
Button(action: self.addAccessory) {
|
||||
Label("Add accessory", systemImage: "plus")
|
||||
}
|
||||
@@ -98,6 +107,22 @@ struct ManageAccessoriesView: View {
|
||||
self.alertType = .keyError
|
||||
}
|
||||
}
|
||||
|
||||
func exportAccessories() {
|
||||
do {
|
||||
_ = try self.accessoryController.export(accessories: self.accessories)
|
||||
}catch {
|
||||
//TODO: Show alert
|
||||
}
|
||||
}
|
||||
|
||||
func importAccessories() {
|
||||
do {
|
||||
try self.accessoryController.importAccessories()
|
||||
}catch {
|
||||
//TODO: Show alert
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
||||
@@ -14,8 +14,8 @@ import SwiftUI
|
||||
struct OpenHaystackMainView: View {
|
||||
|
||||
@State var loading = false
|
||||
@EnvironmentObject var accessoryController: AccessoryController
|
||||
@EnvironmentObject var findMyController: FindMyController
|
||||
@Environment(\.accessoryController) var accessoryController: AccessoryController
|
||||
@Environment(\.findMyController) var findMyController: FindMyController
|
||||
var accessories: [Accessory] {
|
||||
return self.accessoryController.accessories
|
||||
}
|
||||
@@ -43,7 +43,7 @@ struct OpenHaystackMainView: View {
|
||||
accessoryToDeploy: self.$accessoryToDeploy,
|
||||
showESP32DeploySheet: self.$showESP32DeploySheet
|
||||
)
|
||||
.frame(minWidth: 200, idealWidth: 200, maxWidth: .infinity, minHeight: 300, idealHeight: 400, maxHeight: .infinity, alignment: .center)
|
||||
.frame(minWidth: 240, idealWidth: 250, maxWidth: .infinity, minHeight: 300, idealHeight: 400, maxHeight: .infinity, alignment: .center)
|
||||
|
||||
ZStack {
|
||||
AccessoryMapView(accessoryController: self.accessoryController, mapType: self.$mapType, focusedAccessory: self.focusedAccessory)
|
||||
@@ -135,42 +135,18 @@ struct OpenHaystackMainView: View {
|
||||
|
||||
/// Download the location reports for all current accessories. Shows an error if something fails, like plug-in is missing
|
||||
func downloadLocationReports() {
|
||||
|
||||
self.checkPluginIsRunning { (running) in
|
||||
guard running else {
|
||||
self.alertType = .activatePlugin
|
||||
return
|
||||
}
|
||||
|
||||
guard !self.searchPartyToken.isEmpty,
|
||||
let tokenData = self.searchPartyToken.data(using: .utf8)
|
||||
else {
|
||||
self.alertType = .searchPartyToken
|
||||
return
|
||||
}
|
||||
|
||||
withAnimation {
|
||||
self.isLoading = true
|
||||
}
|
||||
|
||||
findMyController.fetchReports(for: accessories, with: tokenData) { result in
|
||||
switch result {
|
||||
case .failure(let error):
|
||||
os_log(.error, "Downloading reports failed %@", error.localizedDescription)
|
||||
case .success(let devices):
|
||||
let reports = devices.compactMap({ $0.reports }).flatMap({ $0 })
|
||||
if reports.isEmpty {
|
||||
withAnimation {
|
||||
self.popUpAlertType = .noReportsFound
|
||||
}
|
||||
}
|
||||
}
|
||||
withAnimation {
|
||||
self.isLoading = false
|
||||
self.accessoryController.downloadLocationReports { result in
|
||||
switch result {
|
||||
case .failure(let alert):
|
||||
if alert == .noReportsFound {
|
||||
self.popUpAlertType = .noReportsFound
|
||||
}else {
|
||||
self.alertType = alert
|
||||
}
|
||||
case .success(_):
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
func deploy(accessory: Accessory) {
|
||||
@@ -327,10 +303,14 @@ struct OpenHaystackMainView: View {
|
||||
message: Text("Please select to which device you want to deploy"),
|
||||
primaryButton: microbitButton,
|
||||
secondaryButton: esp32Button)
|
||||
case .downloadingReportsFailed:
|
||||
return Alert(title: Text("Downloading locations failed"),
|
||||
message: Text("We could not download any locations from Apple. Please try again later"),
|
||||
dismissButton: Alert.Button.okay())
|
||||
}
|
||||
}
|
||||
|
||||
enum AlertType: Int, Identifiable {
|
||||
enum AlertType: Int, Identifiable, Error {
|
||||
var id: Int {
|
||||
return self.rawValue
|
||||
}
|
||||
@@ -341,6 +321,7 @@ struct OpenHaystackMainView: View {
|
||||
case deployedSuccessfully
|
||||
case deletionFailed
|
||||
case noReportsFound
|
||||
case downloadingReportsFailed
|
||||
case activatePlugin
|
||||
case pluginInstallFailed
|
||||
case selectDepoyTarget
|
||||
@@ -353,7 +334,7 @@ struct OpenHaystackMainView_Previews: PreviewProvider {
|
||||
|
||||
static var previews: some View {
|
||||
OpenHaystackMainView()
|
||||
.environmentObject(accessoryController)
|
||||
.environment(\.accessoryController, accessoryController)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -11,29 +11,45 @@ import SwiftUI
|
||||
|
||||
@main
|
||||
struct OpenHaystackApp: App {
|
||||
@StateObject var accessoryController: AccessoryController
|
||||
@StateObject var findMyController: FindMyController
|
||||
@Environment(\.accessoryController) var accessoryController: AccessoryController
|
||||
@Environment(\.findMyController) var findMyController: FindMyController
|
||||
|
||||
init() {
|
||||
var accessoryController: AccessoryController
|
||||
if ProcessInfo().arguments.contains("-preview") {
|
||||
accessoryController = AccessoryControllerPreview(accessories: PreviewData.accessories)
|
||||
} else {
|
||||
accessoryController = AccessoryController()
|
||||
}
|
||||
self._accessoryController = StateObject(wrappedValue: accessoryController)
|
||||
self._findMyController = StateObject(wrappedValue: FindMyController(accessories: accessoryController))
|
||||
}
|
||||
init() {}
|
||||
|
||||
var body: some Scene {
|
||||
WindowGroup {
|
||||
OpenHaystackMainView()
|
||||
.environmentObject(accessoryController)
|
||||
.environmentObject(findMyController)
|
||||
}
|
||||
.commands {
|
||||
SidebarCommands()
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
//MARK: Environment objects
|
||||
private struct FindMyControllerEnvironmentKey: EnvironmentKey {
|
||||
static let defaultValue: FindMyController = FindMyController()
|
||||
}
|
||||
|
||||
private struct AccessoryControllerEnvironmentKey: EnvironmentKey {
|
||||
static let defaultValue: AccessoryController = {
|
||||
if ProcessInfo().arguments.contains("-preview") {
|
||||
return AccessoryControllerPreview(accessories: PreviewData.accessories)
|
||||
} else {
|
||||
return AccessoryController()
|
||||
}
|
||||
}()
|
||||
}
|
||||
|
||||
extension EnvironmentValues {
|
||||
var findMyController: FindMyController {
|
||||
get {self[FindMyControllerEnvironmentKey]}
|
||||
}
|
||||
|
||||
var accessoryController: AccessoryController {
|
||||
get{self[AccessoryControllerEnvironmentKey]}
|
||||
set{self[AccessoryControllerEnvironmentKey] = newValue}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user