// OpenHaystack – Tracking personal Bluetooth devices via Apple's Find My network // // Copyright © 2021 Secure Mobile Networking Lab (SEEMOO) // Copyright © 2021 The Open Wireless Link Project // // SPDX-License-Identifier: AGPL-3.0-only import SwiftUI struct OFFetchReportsMainView: View { @Environment(\.findMyController) var findMyController @State var targetedDrop: Bool = false @State var error: Error? @State var showMap = false @State var loading = false @State var searchPartyToken: Data? @State var searchPartyTokenString: String = "" @State var keyPlistFile: Data? @State var showTokenPrompt = false var dropView: some View { ZStack(alignment: .center) { HStack { Spacer() Spacer() } VStack { Spacer() Text("Drop exported keys here") .font(Font.system(size: 44, weight: .bold, design: .default)) .padding() Text("The keys can be exported into the right format using the Read FindMy Keys App.") .font(.body) .multilineTextAlignment(.center) .padding() Spacer() } } .background( RoundedRectangle(cornerRadius: 20.0) .stroke(Color.gray, style: StrokeStyle(lineWidth: 5.0, lineCap: .round, lineJoin: .round, miterLimit: 10, dash: [15])) ) .padding() .onDrop(of: ["public.file-url"], isTargeted: self.$targetedDrop) { (droppedData) -> Bool in return self.droppedData(data: droppedData) } } var loadingView: some View { VStack { Text("Downloading locations and decrypting...") .font(Font.system(size: 44, weight: .bold, design: .default)) .padding() } } /// This view is shown if the search party token cannot be accessed from keychain var missingSearchPartyTokenView: some View { VStack { Text("Search Party token could not be fetched") Text("Please paste the search party token below after copying it from the macOS Keychain.") Text("The item that contains the key can be found by searching for: ") Text("com.apple.account.DeviceLocator.search-party-token") .font(.system(Font.TextStyle.body, design: Font.Design.monospaced)) TextField("Search Party Token", text: self.$searchPartyTokenString) Button(action: { if !self.searchPartyTokenString.isEmpty, let file = self.keyPlistFile, let searchPartyToken = self.searchPartyTokenString.data(using: .utf8) { self.searchPartyToken = searchPartyToken self.downloadAndDecryptLocations(with: file, searchPartyToken: searchPartyToken) } }, label: { Text("Download reports") }) } } var mapView: some View { ZStack { MapView() VStack { HStack { Spacer() Button(action: { self.showMap = false self.showTokenPrompt = false }, label: { Text("Import other tokens") }) Button(action: { self.exportDecryptedLocations() }, label: { Text("Export") }) } .padding() Spacer() } } } var body: some View { GeometryReader { geo in if self.loading { self.loadingView } else if self.showMap { self.mapView } else if self.showTokenPrompt { self.missingSearchPartyTokenView } else { self.dropView .frame(width: geo.size.width, height: geo.size.height) } } } // swiftlint:disable identifier_name func droppedData(data: [NSItemProvider]) -> Bool { guard let itemProvider = data.first else {return false} itemProvider.loadItem(forTypeIdentifier: "public.file-url", options: nil) { (u, _) in guard let urlData = u as? Data, let fileURL = URL(dataRepresentation: urlData, relativeTo: nil), // Only plist supported fileURL.pathExtension == "plist", // Load the file let file = try? Data(contentsOf: fileURL) else {return} print("Received data \(fileURL)") self.keyPlistFile = file let reportsFetcher = ReportsFetcher() self.searchPartyToken = reportsFetcher.fetchSearchpartyToken() if let searchPartyToken = self.searchPartyToken { self.downloadAndDecryptLocations(with: file, searchPartyToken: searchPartyToken) } else { self.showTokenPrompt = true } } return true } func downloadAndDecryptLocations(with keyFile: Data, searchPartyToken: Data) { self.loading = true self.findMyController.loadPrivateKeys(from: keyFile, with: searchPartyToken, completion: { error in // Check if an error occurred guard error == nil else { self.error = error return } // Show map view self.loading = false self.showMap = true }) } func exportDecryptedLocations() { do { let devices = self.findMyController.devices let deviceData = try PropertyListEncoder().encode(devices) SavePanel().saveFile(file: deviceData, fileExtension: "plist") } catch { print("Error: \(error)") } } } struct ContentView_Previews: PreviewProvider { static var previews: some View { OFFetchReportsMainView() } }