mirror of
https://github.com/seemoo-lab/openhaystack.git
synced 2026-05-19 06:56:51 +00:00
199 lines
6.1 KiB
Swift
Executable File
199 lines
6.1 KiB
Swift
Executable File
//
|
||
// 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()
|
||
}
|
||
}
|