mirror of
https://github.com/seemoo-lab/openhaystack.git
synced 2026-02-14 09:39:52 +00:00
Export the created firmware file (instead of flashing directly)
Running swift-format
This commit is contained in:
@@ -7,8 +7,8 @@
|
||||
// SPDX-License-Identifier: AGPL-3.0-only
|
||||
//
|
||||
|
||||
import Foundation
|
||||
import CryptoKit
|
||||
import Foundation
|
||||
|
||||
struct DecryptReports {
|
||||
|
||||
@@ -25,9 +25,11 @@ struct DecryptReports {
|
||||
let privateKey = keyData
|
||||
let ephemeralKey = payloadData.subdata(in: 5..<62)
|
||||
|
||||
guard let sharedKey = BoringSSL.deriveSharedKey(
|
||||
guard
|
||||
let sharedKey = BoringSSL.deriveSharedKey(
|
||||
fromPrivateKey: privateKey,
|
||||
andEphemeralKey: ephemeralKey) else {
|
||||
andEphemeralKey: ephemeralKey)
|
||||
else {
|
||||
throw FindMyError.decryptionError(description: "Failed generating shared key")
|
||||
}
|
||||
|
||||
@@ -38,7 +40,8 @@ struct DecryptReports {
|
||||
let encData = payloadData.subdata(in: 62..<72)
|
||||
let tag = payloadData.subdata(in: 72..<payloadData.endIndex)
|
||||
|
||||
let decryptedContent = try self.decryptPayload(payload: encData, symmetricKey: derivedKey, tag: tag)
|
||||
let decryptedContent = try self.decryptPayload(
|
||||
payload: encData, symmetricKey: derivedKey, tag: tag)
|
||||
let locationReport = self.decode(content: decryptedContent, report: report)
|
||||
print(locationReport)
|
||||
return locationReport
|
||||
@@ -58,7 +61,8 @@ struct DecryptReports {
|
||||
print("Decryption Key \(decryptionKey.base64EncodedString())")
|
||||
print("IV \(iv.base64EncodedString())")
|
||||
|
||||
let sealedBox = try AES.GCM.SealedBox(nonce: AES.GCM.Nonce(data: iv), ciphertext: payload, tag: tag)
|
||||
let sealedBox = try AES.GCM.SealedBox(
|
||||
nonce: AES.GCM.Nonce(data: iv), ciphertext: payload, tag: tag)
|
||||
let symKey = SymmetricKey(data: decryptionKey)
|
||||
let decrypted = try AES.GCM.open(sealedBox, using: symKey)
|
||||
|
||||
@@ -80,7 +84,9 @@ struct DecryptReports {
|
||||
let latitudeDec = Double(latitude) / 10000000.0
|
||||
let longitudeDec = Double(longitude) / 10000000.0
|
||||
|
||||
return FindMyLocationReport(lat: latitudeDec, lng: longitudeDec, acc: accuracy, dP: report.datePublished, t: report.timestamp, c: report.confidence)
|
||||
return FindMyLocationReport(
|
||||
lat: latitudeDec, lng: longitudeDec, acc: accuracy, dP: report.datePublished,
|
||||
t: report.timestamp, c: report.confidence)
|
||||
}
|
||||
|
||||
static func kdf(fromSharedSecret secret: Data, andEphemeralKey ephKey: Data) -> Data {
|
||||
@@ -88,7 +94,8 @@ struct DecryptReports {
|
||||
var shaDigest = SHA256()
|
||||
shaDigest.update(data: secret)
|
||||
var counter: Int32 = 1
|
||||
let counterData = Data(Data(bytes: &counter, count: MemoryLayout.size(ofValue: counter)).reversed())
|
||||
let counterData = Data(
|
||||
Data(bytes: &counter, count: MemoryLayout.size(ofValue: counter)).reversed())
|
||||
shaDigest.update(data: counterData)
|
||||
shaDigest.update(data: ephKey)
|
||||
|
||||
|
||||
@@ -7,9 +7,9 @@
|
||||
// SPDX-License-Identifier: AGPL-3.0-only
|
||||
//
|
||||
|
||||
import Combine
|
||||
import Foundation
|
||||
import SwiftUI
|
||||
import Combine
|
||||
|
||||
class FindMyController: ObservableObject {
|
||||
static let shared = FindMyController()
|
||||
@@ -17,7 +17,9 @@ class FindMyController: ObservableObject {
|
||||
@Published var error: Error?
|
||||
@Published var devices = [FindMyDevice]()
|
||||
|
||||
func loadPrivateKeys(from data: Data, with searchPartyToken: Data, completion: @escaping (Error?) -> Void) {
|
||||
func loadPrivateKeys(
|
||||
from data: Data, with searchPartyToken: Data, completion: @escaping (Error?) -> Void
|
||||
) {
|
||||
do {
|
||||
let devices = try PropertyListDecoder().decode([FindMyDevice].self, from: data)
|
||||
|
||||
@@ -28,7 +30,9 @@ class FindMyController: ObservableObject {
|
||||
}
|
||||
}
|
||||
|
||||
func importReports(reports: [FindMyReport], and keys: Data, completion:@escaping () -> Void) throws {
|
||||
func importReports(reports: [FindMyReport], and keys: Data, completion: @escaping () -> Void)
|
||||
throws
|
||||
{
|
||||
var devices = try PropertyListDecoder().decode([FindMyDevice].self, from: keys)
|
||||
|
||||
// Decrypt the reports with the imported keys
|
||||
@@ -36,11 +40,13 @@ class FindMyController: ObservableObject {
|
||||
// Add the reports to the according device by finding the right key for the report
|
||||
for report in reports {
|
||||
|
||||
guard let deviceIndex = devices.firstIndex(where: { (device) -> Bool in
|
||||
guard
|
||||
let deviceIndex = devices.firstIndex(where: { (device) -> Bool in
|
||||
device.keys.contains { (key) -> Bool in
|
||||
key.hashedKey.base64EncodedString() == report.id
|
||||
}
|
||||
}) else {
|
||||
})
|
||||
else {
|
||||
print("No device found for id")
|
||||
continue
|
||||
}
|
||||
@@ -101,10 +107,12 @@ class FindMyController: ObservableObject {
|
||||
let duration: Double = (24 * 60 * 60) * 21
|
||||
let startDate = Date() - duration
|
||||
|
||||
fetcher.query(forHashes: keyHashes,
|
||||
fetcher.query(
|
||||
forHashes: keyHashes,
|
||||
start: startDate,
|
||||
duration: duration,
|
||||
searchPartyToken: searchPartyToken) { jd in
|
||||
searchPartyToken: searchPartyToken
|
||||
) { jd in
|
||||
guard let jsonData = jd else {
|
||||
fetchReportGroup.leave()
|
||||
return
|
||||
@@ -138,7 +146,9 @@ class FindMyController: ObservableObject {
|
||||
|
||||
#if EXPORT
|
||||
if let encoded = try? JSONEncoder().encode(reports) {
|
||||
let outputDirectory = FileManager.default.urls(for: .desktopDirectory, in: .userDomainMask).first!
|
||||
let outputDirectory = FileManager.default.urls(
|
||||
for: .desktopDirectory, in: .userDomainMask
|
||||
).first!
|
||||
try? encoded.write(to: outputDirectory.appendingPathComponent("reports.json"))
|
||||
}
|
||||
#endif
|
||||
@@ -166,13 +176,16 @@ class FindMyController: ObservableObject {
|
||||
|
||||
// Map the keys in a dictionary for faster access
|
||||
guard let reports = device.reports else { continue }
|
||||
let keyMap = device.keys.reduce(into: [String: FindMyKey](), {$0[$1.hashedKey.base64EncodedString()] = $1})
|
||||
let keyMap = device.keys.reduce(
|
||||
into: [String: FindMyKey](), { $0[$1.hashedKey.base64EncodedString()] = $1 })
|
||||
|
||||
let accessQueue = DispatchQueue(label: "threadSafeAccess",
|
||||
let accessQueue = DispatchQueue(
|
||||
label: "threadSafeAccess",
|
||||
qos: .userInitiated,
|
||||
attributes: .concurrent,
|
||||
autoreleaseFrequency: .workItem, target: nil)
|
||||
var decryptedReports = [FindMyLocationReport](repeating:
|
||||
var decryptedReports = [FindMyLocationReport](
|
||||
repeating:
|
||||
FindMyLocationReport(lat: 0, lng: 0, acc: 0, dP: Date(), t: Date(), c: 0),
|
||||
count: reports.count)
|
||||
DispatchQueue.concurrentPerform(iterations: reports.count) { (reportIdx) in
|
||||
@@ -201,7 +214,8 @@ class FindMyController: ObservableObject {
|
||||
func exportDevices() {
|
||||
|
||||
if let encoded = try? PropertyListEncoder().encode(self.devices) {
|
||||
let outputDirectory = FileManager.default.urls(for: .desktopDirectory, in: .userDomainMask).first!
|
||||
let outputDirectory = FileManager.default.urls(for: .desktopDirectory, in: .userDomainMask)
|
||||
.first!
|
||||
try? encoded.write(to: outputDirectory.appendingPathComponent("devices-\(Date()).plist"))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -7,8 +7,8 @@
|
||||
// SPDX-License-Identifier: AGPL-3.0-only
|
||||
//
|
||||
|
||||
import Foundation
|
||||
import CryptoKit
|
||||
import Foundation
|
||||
|
||||
/// Decode key files found in newer macOS versions.
|
||||
class FindMyKeyDecoder {
|
||||
@@ -92,7 +92,8 @@ class FindMyKeyDecoder {
|
||||
shaDigest.update(data: advertisedKey)
|
||||
let hashedKey = Data(shaDigest.finalize())
|
||||
|
||||
let fmKey = FindMyKey(advertisedKey: advertisedKey,
|
||||
let fmKey = FindMyKey(
|
||||
advertisedKey: advertisedKey,
|
||||
hashedKey: hashedKey,
|
||||
privateKey: fullKey,
|
||||
startTime: nil,
|
||||
|
||||
@@ -9,8 +9,8 @@
|
||||
|
||||
// swiftlint:disable identifier_name
|
||||
|
||||
import Foundation
|
||||
import CoreLocation
|
||||
import Foundation
|
||||
|
||||
struct FindMyDevice: Codable, Hashable {
|
||||
|
||||
@@ -34,7 +34,10 @@ struct FindMyDevice: Codable, Hashable {
|
||||
}
|
||||
|
||||
struct FindMyKey: Codable {
|
||||
internal init(advertisedKey: Data, hashedKey: Data, privateKey: Data, startTime: Date?, duration: Double?, pu: Data?, yCoordinate: Data?, fullKey: Data?) {
|
||||
internal init(
|
||||
advertisedKey: Data, hashedKey: Data, privateKey: Data, startTime: Date?, duration: Double?,
|
||||
pu: Data?, yCoordinate: Data?, fullKey: Data?
|
||||
) {
|
||||
self.advertisedKey = advertisedKey
|
||||
self.hashedKey = hashedKey
|
||||
// The private key should only be 28 bytes long. If a 85 bytes full private public key is entered we truncate it here
|
||||
@@ -126,7 +129,8 @@ struct FindMyReport: Codable {
|
||||
let payloadBase64 = try values.decode(String.self, forKey: .payload)
|
||||
|
||||
guard let payload = Data(base64Encoded: payloadBase64) else {
|
||||
throw DecodingError.dataCorruptedError(forKey: CodingKeys.payload, in: values, debugDescription: "")
|
||||
throw DecodingError.dataCorruptedError(
|
||||
forKey: CodingKeys.payload, in: values, debugDescription: "")
|
||||
}
|
||||
self.payload = payload
|
||||
|
||||
|
||||
@@ -7,9 +7,9 @@
|
||||
// SPDX-License-Identifier: AGPL-3.0-only
|
||||
//
|
||||
|
||||
import SwiftUI
|
||||
import Cocoa
|
||||
import MapKit
|
||||
import SwiftUI
|
||||
|
||||
struct MapView: NSViewControllerRepresentable {
|
||||
@Environment(\.findMyController) var findMyController
|
||||
|
||||
@@ -26,7 +26,8 @@ final class MapViewController: NSViewController, MKMapViewDelegate {
|
||||
|
||||
// Zoom to first location
|
||||
if let location = devices.first?.decryptedReports?.first {
|
||||
let coordinate = CLLocationCoordinate2D(latitude: location.latitude, longitude: location.longitude)
|
||||
let coordinate = CLLocationCoordinate2D(
|
||||
latitude: location.latitude, longitude: location.longitude)
|
||||
let span = MKCoordinateSpan(latitudeDelta: 5.0, longitudeDelta: 5.0)
|
||||
let region = MKCoordinateRegion(center: coordinate, span: span)
|
||||
|
||||
@@ -40,7 +41,8 @@ final class MapViewController: NSViewController, MKMapViewDelegate {
|
||||
for report in reports {
|
||||
let pin = MKPointAnnotation()
|
||||
pin.title = device.deviceId
|
||||
pin.coordinate = CLLocationCoordinate2D(latitude: report.latitude, longitude: report.longitude)
|
||||
pin.coordinate = CLLocationCoordinate2D(
|
||||
latitude: report.latitude, longitude: report.longitude)
|
||||
self.mapView.addAnnotation(pin)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -47,7 +47,10 @@ struct OFFetchReportsMainView: View {
|
||||
}
|
||||
.background(
|
||||
RoundedRectangle(cornerRadius: 20.0)
|
||||
.stroke(Color.gray, style: StrokeStyle(lineWidth: 5.0, lineCap: .round, lineJoin: .round, miterLimit: 10, dash: [15]))
|
||||
.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
|
||||
@@ -75,14 +78,17 @@ struct OFFetchReportsMainView: View {
|
||||
|
||||
TextField("Search Party Token", text: self.$searchPartyTokenString)
|
||||
|
||||
Button(action: {
|
||||
Button(
|
||||
action: {
|
||||
if !self.searchPartyTokenString.isEmpty,
|
||||
let file = self.keyPlistFile,
|
||||
let searchPartyToken = self.searchPartyTokenString.data(using: .utf8) {
|
||||
let searchPartyToken = self.searchPartyTokenString.data(using: .utf8)
|
||||
{
|
||||
self.searchPartyToken = searchPartyToken
|
||||
self.downloadAndDecryptLocations(with: file, searchPartyToken: searchPartyToken)
|
||||
}
|
||||
}, label: {
|
||||
},
|
||||
label: {
|
||||
Text("Download reports")
|
||||
})
|
||||
}
|
||||
@@ -94,17 +100,21 @@ struct OFFetchReportsMainView: View {
|
||||
VStack {
|
||||
HStack {
|
||||
Spacer()
|
||||
Button(action: {
|
||||
Button(
|
||||
action: {
|
||||
self.showMap = false
|
||||
self.showTokenPrompt = false
|
||||
}, label: {
|
||||
},
|
||||
label: {
|
||||
Text("Import other tokens")
|
||||
})
|
||||
|
||||
Button(action: {
|
||||
Button(
|
||||
action: {
|
||||
self.exportDecryptedLocations()
|
||||
|
||||
}, label: {
|
||||
},
|
||||
label: {
|
||||
Text("Export")
|
||||
})
|
||||
|
||||
@@ -164,7 +174,9 @@ struct OFFetchReportsMainView: View {
|
||||
func downloadAndDecryptLocations(with keyFile: Data, searchPartyToken: Data) {
|
||||
self.loading = true
|
||||
|
||||
self.findMyController.loadPrivateKeys(from: keyFile, with: searchPartyToken, completion: { error in
|
||||
self.findMyController.loadPrivateKeys(
|
||||
from: keyFile, with: searchPartyToken,
|
||||
completion: { error in
|
||||
// Check if an error occurred
|
||||
guard error == nil else {
|
||||
self.error = error
|
||||
|
||||
@@ -7,8 +7,8 @@
|
||||
// SPDX-License-Identifier: AGPL-3.0-only
|
||||
//
|
||||
|
||||
import Foundation
|
||||
import AppKit
|
||||
import Foundation
|
||||
|
||||
class SavePanel: NSObject, NSOpenSavePanelDelegate {
|
||||
|
||||
@@ -40,7 +40,8 @@ class SavePanel: NSObject, NSOpenSavePanelDelegate {
|
||||
|
||||
}
|
||||
|
||||
func panel(_ sender: Any, userEnteredFilename filename: String, confirmed okFlag: Bool) -> String? {
|
||||
func panel(_ sender: Any, userEnteredFilename filename: String, confirmed okFlag: Bool) -> String?
|
||||
{
|
||||
guard okFlag else { return nil }
|
||||
|
||||
return filename
|
||||
|
||||
@@ -8,8 +8,8 @@
|
||||
//
|
||||
|
||||
import Cocoa
|
||||
import SwiftUI
|
||||
import CoreLocation
|
||||
import SwiftUI
|
||||
|
||||
@NSApplicationMain
|
||||
class AppDelegate: NSObject, NSApplicationDelegate {
|
||||
|
||||
@@ -7,8 +7,8 @@
|
||||
// SPDX-License-Identifier: AGPL-3.0-only
|
||||
//
|
||||
|
||||
import SwiftUI
|
||||
import OSLog
|
||||
import SwiftUI
|
||||
|
||||
struct ContentView: View {
|
||||
|
||||
@@ -22,9 +22,11 @@ struct ContentView: View {
|
||||
self.infoText
|
||||
.padding()
|
||||
|
||||
Button(action: {
|
||||
Button(
|
||||
action: {
|
||||
self.readPrivateKeys()
|
||||
}, label: {
|
||||
},
|
||||
label: {
|
||||
Text("Read private offline finding keys")
|
||||
.font(.headline)
|
||||
.foregroundColor(Color.black)
|
||||
@@ -35,7 +37,8 @@ struct ContentView: View {
|
||||
.shadow(color: Color.black, radius: 10.0, x: 0, y: 0)
|
||||
)
|
||||
|
||||
})
|
||||
}
|
||||
)
|
||||
.buttonStyle(PlainButtonStyle())
|
||||
|
||||
self.keysInfo.map { (keysInfo) in
|
||||
@@ -54,10 +57,12 @@ struct ContentView: View {
|
||||
|
||||
var infoText: some View {
|
||||
// swiftlint:disable line_length
|
||||
Text("This application demonstrates an exploit in macOS 10.15.0 - 10.15.6. It reads unprotected private key files that are used to locate lost devices using Apple's Offline Finding (Find My network). The application exports these key files for a demonstrative purpose. Used in the wild, an adversary would be able to download accurate location data of") +
|
||||
Text(" all ").bold() +
|
||||
Text("Apple devices of the current user.\n\n") +
|
||||
Text("To download the location reports for the exported key files, please use the OFFetchReports app. In our adversary model this app would be placed on an adversary owned Mac while the OFReadKeys might be a benign looking app installed by any user.")
|
||||
Text(
|
||||
"This application demonstrates an exploit in macOS 10.15.0 - 10.15.6. It reads unprotected private key files that are used to locate lost devices using Apple's Offline Finding (Find My network). The application exports these key files for a demonstrative purpose. Used in the wild, an adversary would be able to download accurate location data of"
|
||||
) + Text(" all ").bold() + Text("Apple devices of the current user.\n\n")
|
||||
+ Text(
|
||||
"To download the location reports for the exported key files, please use the OFFetchReports app. In our adversary model this app would be placed on an adversary owned Mac while the OFReadKeys might be a benign looking app installed by any user."
|
||||
)
|
||||
// swiftlint:enable line_length
|
||||
}
|
||||
|
||||
|
||||
@@ -7,8 +7,8 @@
|
||||
// SPDX-License-Identifier: AGPL-3.0-only
|
||||
//
|
||||
|
||||
import Foundation
|
||||
import CryptoKit
|
||||
import Foundation
|
||||
import OSLog
|
||||
|
||||
struct FindMyKeyExtractor {
|
||||
@@ -53,7 +53,8 @@ struct FindMyKeyExtractor {
|
||||
let fm = FileManager.default
|
||||
let privateKeysPath = fm.urls(for: .libraryDirectory, in: .userDomainMask)
|
||||
.first?.appendingPathComponent(directoryPath)
|
||||
let folders = try fm.contentsOfDirectory(at: privateKeysPath!,
|
||||
let folders = try fm.contentsOfDirectory(
|
||||
at: privateKeysPath!,
|
||||
includingPropertiesForKeys: nil, options: .skipsHiddenFiles)
|
||||
guard folders.isEmpty == false else { throw FindMyError.noFoldersFound }
|
||||
|
||||
@@ -61,7 +62,8 @@ struct FindMyKeyExtractor {
|
||||
var devices = [FindMyDevice]()
|
||||
|
||||
for folderURL in folders {
|
||||
let keyFiles = try fm.contentsOfDirectory(at: folderURL,
|
||||
let keyFiles = try fm.contentsOfDirectory(
|
||||
at: folderURL,
|
||||
includingPropertiesForKeys: nil, options: .skipsHiddenFiles)
|
||||
// Check if keys are available
|
||||
print("Found \(keyFiles.count) in folder \(folderURL.lastPathComponent)")
|
||||
@@ -91,7 +93,9 @@ struct FindMyKeyExtractor {
|
||||
/// - Parameter keyFile: Propery list data
|
||||
/// - Returns: Find My private Key
|
||||
static func parseKeyFile(keyFile: Data) throws -> FindMyKey {
|
||||
guard let keyDict = try PropertyListSerialization.propertyList(from: keyFile,
|
||||
guard
|
||||
let keyDict = try PropertyListSerialization.propertyList(
|
||||
from: keyFile,
|
||||
options: .init(), format: nil) as? [String: Any],
|
||||
let advertisedKey = keyDict["A"] as? Data,
|
||||
let privateKey = keyDict["PR"] as? Data,
|
||||
@@ -106,7 +110,8 @@ struct FindMyKeyExtractor {
|
||||
let time = Date(timeIntervalSinceReferenceDate: timeValues[0])
|
||||
let duration = timeValues[1]
|
||||
|
||||
return FindMyKey(advertisedKey: advertisedKey,
|
||||
return FindMyKey(
|
||||
advertisedKey: advertisedKey,
|
||||
hashedKey: hashedKey,
|
||||
privateKey: privateKey,
|
||||
startTime: time,
|
||||
@@ -128,7 +133,8 @@ struct FindMyKeyExtractor {
|
||||
|
||||
func recursiveSearch(from url: URL, urlArray: inout [URL]) {
|
||||
do {
|
||||
let randomSubfolders = try fm.contentsOfDirectory(at: url,
|
||||
let randomSubfolders = try fm.contentsOfDirectory(
|
||||
at: url,
|
||||
includingPropertiesForKeys: nil,
|
||||
options: .includesDirectoriesPostOrder)
|
||||
|
||||
@@ -180,7 +186,8 @@ struct FindMyKeyExtractor {
|
||||
static func loadNewKeyFilesIn(directory: URL) throws -> [FindMyDevice] {
|
||||
os_log(.debug, "Loading key files from %@", directory.path)
|
||||
let fm = FileManager.default
|
||||
let subDirectories = try fm.contentsOfDirectory(at: directory,
|
||||
let subDirectories = try fm.contentsOfDirectory(
|
||||
at: directory,
|
||||
includingPropertiesForKeys: nil, options: .skipsHiddenFiles)
|
||||
|
||||
var devices = [FindMyDevice]()
|
||||
@@ -189,7 +196,8 @@ struct FindMyKeyExtractor {
|
||||
do {
|
||||
var keyFiles = [Data]()
|
||||
let keyDirectory = deviceDirectory.appendingPathComponent("Primary")
|
||||
let keyFileURLs = try fm.contentsOfDirectory(at: keyDirectory,
|
||||
let keyFileURLs = try fm.contentsOfDirectory(
|
||||
at: keyDirectory,
|
||||
includingPropertiesForKeys: nil,
|
||||
options: .skipsHiddenFiles)
|
||||
for keyfileURL in keyFileURLs {
|
||||
|
||||
@@ -7,9 +7,9 @@
|
||||
// SPDX-License-Identifier: AGPL-3.0-only
|
||||
//
|
||||
|
||||
import Foundation
|
||||
import Combine
|
||||
import CryptoKit
|
||||
import Foundation
|
||||
|
||||
struct FindMyDevice: Codable {
|
||||
let deviceId: String
|
||||
|
||||
@@ -7,8 +7,8 @@
|
||||
// SPDX-License-Identifier: AGPL-3.0-only
|
||||
//
|
||||
|
||||
import Foundation
|
||||
import AppKit
|
||||
import Foundation
|
||||
|
||||
class SavePanel: NSObject, NSOpenSavePanelDelegate {
|
||||
|
||||
@@ -40,7 +40,8 @@ class SavePanel: NSObject, NSOpenSavePanelDelegate {
|
||||
|
||||
}
|
||||
|
||||
func panel(_ sender: Any, userEnteredFilename filename: String, confirmed okFlag: Bool) -> String? {
|
||||
func panel(_ sender: Any, userEnteredFilename filename: String, confirmed okFlag: Bool) -> String?
|
||||
{
|
||||
return filename
|
||||
}
|
||||
}
|
||||
|
||||
@@ -51,6 +51,7 @@
|
||||
78EC226C25DBC2E40042B775 /* OpenHaystackMainView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 78EC226B25DBC2E40042B775 /* OpenHaystackMainView.swift */; };
|
||||
78EC227225DBC8CE0042B775 /* Accessory.swift in Sources */ = {isa = PBXBuildFile; fileRef = 78EC227125DBC8CE0042B775 /* Accessory.swift */; };
|
||||
78EC227725DBDB7E0042B775 /* KeychainController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 78EC227625DBDB7E0042B775 /* KeychainController.swift */; };
|
||||
78F8BB4C261C50EB00D9F37F /* LargeButtonStyle.swift in Sources */ = {isa = PBXBuildFile; fileRef = 78F8BB4B261C50EB00D9F37F /* LargeButtonStyle.swift */; };
|
||||
F126102F2600D1D80066A859 /* Slider+LogScale.swift in Sources */ = {isa = PBXBuildFile; fileRef = F126102E2600D1D80066A859 /* Slider+LogScale.swift */; };
|
||||
F12D5A5A25FA4F3500CBBA09 /* BluetoothAccessoryScanner.swift in Sources */ = {isa = PBXBuildFile; fileRef = F12D5A5925FA4F3500CBBA09 /* BluetoothAccessoryScanner.swift */; };
|
||||
F12D5A6025FA79FA00CBBA09 /* Advertisement.swift in Sources */ = {isa = PBXBuildFile; fileRef = F12D5A5F25FA79FA00CBBA09 /* Advertisement.swift */; };
|
||||
@@ -155,6 +156,7 @@
|
||||
78EC226B25DBC2E40042B775 /* OpenHaystackMainView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OpenHaystackMainView.swift; sourceTree = "<group>"; };
|
||||
78EC227125DBC8CE0042B775 /* Accessory.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Accessory.swift; sourceTree = "<group>"; };
|
||||
78EC227625DBDB7E0042B775 /* KeychainController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = KeychainController.swift; sourceTree = "<group>"; };
|
||||
78F8BB4B261C50EB00D9F37F /* LargeButtonStyle.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LargeButtonStyle.swift; sourceTree = "<group>"; };
|
||||
F126102E2600D1D80066A859 /* Slider+LogScale.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Slider+LogScale.swift"; sourceTree = "<group>"; };
|
||||
F12D5A5925FA4F3500CBBA09 /* BluetoothAccessoryScanner.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BluetoothAccessoryScanner.swift; sourceTree = "<group>"; };
|
||||
F12D5A5F25FA79FA00CBBA09 /* Advertisement.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Advertisement.swift; sourceTree = "<group>"; };
|
||||
@@ -360,6 +362,7 @@
|
||||
78EC227025DBC8BB0042B775 /* Views */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
78F8BB4A261C50D500D9F37F /* Styles */,
|
||||
78286D7625E5114600F65511 /* ActivityIndicator.swift */,
|
||||
78EC226B25DBC2E40042B775 /* OpenHaystackMainView.swift */,
|
||||
78486BEE25DD711E0007ED87 /* PopUpAlertView.swift */,
|
||||
@@ -374,6 +377,14 @@
|
||||
path = Views;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
78F8BB4A261C50D500D9F37F /* Styles */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
78F8BB4B261C50EB00D9F37F /* LargeButtonStyle.swift */,
|
||||
);
|
||||
path = Styles;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
F12D5A5E25FA79D600CBBA09 /* Bluetooth */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
@@ -541,7 +552,7 @@
|
||||
);
|
||||
runOnlyForDeploymentPostprocessing = 0;
|
||||
shellPath = /bin/sh;
|
||||
shellScript = "if command -v swift-format >/dev/null; then\n swift-format lint -r \"$SRCROOT\"\nelse\n echo \"warning: swift-format not installed, download from https://github.com/apple/swift-format\"\nfi\n";
|
||||
shellScript = "if command -v swift-format >/dev/null; then\n swift-format format -i -r \"$SRCROOT\"; swift-format lint -r \"$SRCROOT\"\nelse\n echo \"warning: swift-format not installed, download from https://github.com/apple/swift-format\"\nfi\n";
|
||||
};
|
||||
F14B2C7E25EFBB11002DC056 /* Set Version Number from Git */ = {
|
||||
isa = PBXShellScriptBuildPhase;
|
||||
@@ -615,6 +626,7 @@
|
||||
F12D5A6025FA79FA00CBBA09 /* Advertisement.swift in Sources */,
|
||||
781EB3F225DAD7EA00FEAA19 /* OpenHaystackApp.swift in Sources */,
|
||||
781EB3F325DAD7EA00FEAA19 /* Models.swift in Sources */,
|
||||
78F8BB4C261C50EB00D9F37F /* LargeButtonStyle.swift in Sources */,
|
||||
781EB3F425DAD7EA00FEAA19 /* FindMyController.swift in Sources */,
|
||||
781EB3F525DAD7EA00FEAA19 /* BoringSSL.m in Sources */,
|
||||
F12D5A5A25FA4F3500CBBA09 /* BluetoothAccessoryScanner.swift in Sources */,
|
||||
|
||||
@@ -0,0 +1,34 @@
|
||||
{
|
||||
"colors" : [
|
||||
{
|
||||
"color" : {
|
||||
"color-space" : "gray-gamma-22",
|
||||
"components" : {
|
||||
"alpha" : "1.000",
|
||||
"white" : "0.866"
|
||||
}
|
||||
},
|
||||
"idiom" : "universal"
|
||||
},
|
||||
{
|
||||
"appearances" : [
|
||||
{
|
||||
"appearance" : "luminosity",
|
||||
"value" : "dark"
|
||||
}
|
||||
],
|
||||
"color" : {
|
||||
"color-space" : "gray-gamma-22",
|
||||
"components" : {
|
||||
"alpha" : "0.758",
|
||||
"white" : "0.310"
|
||||
}
|
||||
},
|
||||
"idiom" : "universal"
|
||||
}
|
||||
],
|
||||
"info" : {
|
||||
"author" : "xcode",
|
||||
"version" : 1
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,34 @@
|
||||
{
|
||||
"colors" : [
|
||||
{
|
||||
"color" : {
|
||||
"color-space" : "gray-gamma-22",
|
||||
"components" : {
|
||||
"alpha" : "1.000",
|
||||
"white" : "0.657"
|
||||
}
|
||||
},
|
||||
"idiom" : "universal"
|
||||
},
|
||||
{
|
||||
"appearances" : [
|
||||
{
|
||||
"appearance" : "luminosity",
|
||||
"value" : "dark"
|
||||
}
|
||||
],
|
||||
"color" : {
|
||||
"color-space" : "gray-gamma-22",
|
||||
"components" : {
|
||||
"alpha" : "0.758",
|
||||
"white" : "0.237"
|
||||
}
|
||||
},
|
||||
"idiom" : "universal"
|
||||
}
|
||||
],
|
||||
"info" : {
|
||||
"author" : "xcode",
|
||||
"version" : 1
|
||||
}
|
||||
}
|
||||
@@ -157,6 +157,7 @@ class AccessoryController: ObservableObject {
|
||||
//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
|
||||
|
||||
@@ -74,10 +74,8 @@ struct MicrobitController {
|
||||
return patchedFirmware
|
||||
}
|
||||
|
||||
static func deploy(accessory: Accessory) throws {
|
||||
let microbits = try MicrobitController.findMicrobits()
|
||||
guard let microBitURL = microbits.first,
|
||||
let firmwareURL = Bundle.main.url(forResource: "firmware", withExtension: "bin")
|
||||
static func patchFirmware(for accessory: Accessory) throws -> Data {
|
||||
guard let firmwareURL = Bundle.main.url(forResource: "firmware", withExtension: "bin")
|
||||
else {
|
||||
throw FirmwareFlashError.notFound
|
||||
}
|
||||
@@ -87,6 +85,18 @@ struct MicrobitController {
|
||||
let publicKey = try accessory.getAdvertisementKey()
|
||||
let patchedFirmware = try MicrobitController.patchFirmware(firmware, pattern: pattern, with: publicKey)
|
||||
|
||||
return patchedFirmware
|
||||
}
|
||||
|
||||
static func deploy(accessory: Accessory) throws {
|
||||
let microbits = try MicrobitController.findMicrobits()
|
||||
guard let microBitURL = microbits.first
|
||||
else {
|
||||
throw FirmwareFlashError.notFound
|
||||
}
|
||||
|
||||
let patchedFirmware = try self.patchFirmware(for: accessory)
|
||||
|
||||
try MicrobitController.deployToMicrobit(microBitURL, firmwareFile: patchedFirmware)
|
||||
}
|
||||
|
||||
|
||||
@@ -8,6 +8,7 @@
|
||||
//
|
||||
|
||||
import SwiftUI
|
||||
import os
|
||||
|
||||
struct ManageAccessoriesView: View {
|
||||
|
||||
@@ -21,6 +22,7 @@ struct ManageAccessoriesView: View {
|
||||
@Binding var focusedAccessory: Accessory?
|
||||
@Binding var accessoryToDeploy: Accessory?
|
||||
@Binding var showESP32DeploySheet: Bool
|
||||
@State var sheetShown: SheetType?
|
||||
|
||||
@State var showMailPopup = false
|
||||
|
||||
@@ -42,11 +44,14 @@ struct ManageAccessoriesView: View {
|
||||
.toolbar(content: {
|
||||
self.toolbarView
|
||||
})
|
||||
.sheet(
|
||||
isPresented: self.$showESP32DeploySheet,
|
||||
content: {
|
||||
.sheet(item: self.$sheetShown) { sheetType in
|
||||
switch sheetType {
|
||||
case .esp32Install:
|
||||
ESP32InstallSheet(accessory: self.$accessoryToDeploy, alertType: self.$alertType)
|
||||
})
|
||||
case .deployFirmware:
|
||||
self.selectTargetView
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Accessory List view.
|
||||
@@ -103,6 +108,57 @@ struct ManageAccessoriesView: View {
|
||||
}
|
||||
}
|
||||
|
||||
var selectTargetView: some View {
|
||||
VStack {
|
||||
Text("Select target")
|
||||
.font(.title)
|
||||
Text("Please select to which device you want to deply")
|
||||
.padding(.bottom, 4)
|
||||
|
||||
VStack {
|
||||
Button(
|
||||
"Micro:bit",
|
||||
action: {
|
||||
self.sheetShown = nil
|
||||
if let accessory = self.accessoryToDeploy {
|
||||
self.deployAccessoryToMicrobit(accessory: accessory)
|
||||
}
|
||||
}
|
||||
)
|
||||
.buttonStyle(LargeButtonStyle())
|
||||
|
||||
Button(
|
||||
"Export Microbit firmware",
|
||||
action: {
|
||||
self.sheetShown = nil
|
||||
if let accessory = self.accessoryToDeploy {
|
||||
self.exportMicrobitFirmware(for: accessory)
|
||||
}
|
||||
}
|
||||
)
|
||||
.buttonStyle(LargeButtonStyle())
|
||||
|
||||
Button(
|
||||
"ESP32",
|
||||
action: {
|
||||
self.sheetShown = .esp32Install
|
||||
}
|
||||
)
|
||||
.buttonStyle(LargeButtonStyle())
|
||||
|
||||
Button(
|
||||
"Cancel",
|
||||
action: {
|
||||
self.sheetShown = nil
|
||||
}
|
||||
)
|
||||
.buttonStyle(LargeButtonStyle(destructive: true))
|
||||
}
|
||||
|
||||
}
|
||||
.padding()
|
||||
}
|
||||
|
||||
/// Delete an accessory from the list of accessories.
|
||||
func delete(accessory: Accessory) {
|
||||
do {
|
||||
@@ -114,7 +170,7 @@ struct ManageAccessoriesView: View {
|
||||
|
||||
func deploy(accessory: Accessory) {
|
||||
self.accessoryToDeploy = accessory
|
||||
self.alertType = .selectDepoyTarget
|
||||
self.sheetShown = .deployFirmware
|
||||
}
|
||||
|
||||
/// Add an accessory with the provided details.
|
||||
@@ -149,6 +205,58 @@ struct ManageAccessoriesView: View {
|
||||
}
|
||||
}
|
||||
|
||||
/// Deploy the public key of the accessory to a BBC microbit.
|
||||
func deployAccessoryToMicrobit(accessory: Accessory) {
|
||||
do {
|
||||
try MicrobitController.deploy(accessory: accessory)
|
||||
} catch {
|
||||
os_log("Error occurred %@", String(describing: error))
|
||||
self.alertType = .deployFailed
|
||||
return
|
||||
}
|
||||
|
||||
self.alertType = .deployedSuccessfully
|
||||
accessory.isDeployed = true
|
||||
self.accessoryToDeploy = nil
|
||||
}
|
||||
|
||||
func exportMicrobitFirmware(for accessory: Accessory) {
|
||||
do {
|
||||
let firmware = try MicrobitController.patchFirmware(for: accessory)
|
||||
|
||||
let savePanel = NSSavePanel()
|
||||
savePanel.allowedFileTypes = ["bin"]
|
||||
savePanel.canCreateDirectories = true
|
||||
savePanel.directoryURL = try FileManager.default.url(for: .documentDirectory, in: .userDomainMask, appropriateFor: nil, create: false)
|
||||
savePanel.message = "Export the micro:bit firmware"
|
||||
savePanel.nameFieldLabel = "Firmware name"
|
||||
savePanel.nameFieldStringValue = "openhaystack_firmware.bin"
|
||||
savePanel.prompt = "Export"
|
||||
savePanel.title = "Export firmware"
|
||||
|
||||
let result = savePanel.runModal()
|
||||
|
||||
if result == .OK,
|
||||
let url = savePanel.url
|
||||
{
|
||||
// Store the accessory file
|
||||
try firmware.write(to: url)
|
||||
}
|
||||
|
||||
} catch {
|
||||
os_log("Error occurred %@", String(describing: error))
|
||||
self.alertType = .exportFailed
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
enum SheetType: Int, Identifiable {
|
||||
var id: Int {
|
||||
return self.rawValue
|
||||
}
|
||||
case esp32Install
|
||||
case deployFirmware
|
||||
}
|
||||
}
|
||||
|
||||
struct ManageAccessoriesView_Previews: PreviewProvider {
|
||||
|
||||
@@ -221,26 +221,6 @@ struct OpenHaystackMainView: View {
|
||||
.frame(width: 250, height: 120)
|
||||
}
|
||||
|
||||
func deploy(accessory: Accessory) {
|
||||
self.accessoryToDeploy = accessory
|
||||
self.alertType = .selectDepoyTarget
|
||||
}
|
||||
|
||||
/// Deploy the public key of the accessory to a BBC microbit.
|
||||
func deployAccessoryToMicrobit(accessory: Accessory) {
|
||||
do {
|
||||
try MicrobitController.deploy(accessory: accessory)
|
||||
} catch {
|
||||
os_log("Error occurred %@", String(describing: error))
|
||||
self.alertType = .deployFailed
|
||||
return
|
||||
}
|
||||
|
||||
self.alertType = .deployedSuccessfully
|
||||
accessory.isDeployed = true
|
||||
self.accessoryToDeploy = nil
|
||||
}
|
||||
|
||||
/// Ask to install and activate the mail plugin.
|
||||
func installMailPlugin() {
|
||||
let pluginManager = MailPluginManager()
|
||||
@@ -373,20 +353,6 @@ struct OpenHaystackMainView: View {
|
||||
action: {
|
||||
self.downloadPlugin()
|
||||
}), secondaryButton: .cancel())
|
||||
case .selectDepoyTarget:
|
||||
let microbitButton = Alert.Button.default(Text("Microbit"), action: { self.deployAccessoryToMicrobit(accessory: self.accessoryToDeploy!) })
|
||||
|
||||
let esp32Button = Alert.Button.default(
|
||||
Text("ESP32"),
|
||||
action: {
|
||||
self.showESP32DeploySheet = true
|
||||
})
|
||||
|
||||
return Alert(
|
||||
title: Text("Select target"),
|
||||
message: Text("Please select to which device you want to deploy"),
|
||||
primaryButton: microbitButton,
|
||||
secondaryButton: esp32Button)
|
||||
case .downloadingReportsFailed:
|
||||
return Alert(
|
||||
title: Text("Downloading locations failed"),
|
||||
@@ -419,7 +385,6 @@ struct OpenHaystackMainView: View {
|
||||
case downloadingReportsFailed
|
||||
case activatePlugin
|
||||
case pluginInstallFailed
|
||||
case selectDepoyTarget
|
||||
case exportFailed
|
||||
case importFailed
|
||||
}
|
||||
|
||||
@@ -0,0 +1,33 @@
|
||||
//
|
||||
// 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 Foundation
|
||||
import SwiftUI
|
||||
|
||||
struct LargeButtonStyle: ButtonStyle {
|
||||
|
||||
var active: Bool = false
|
||||
var destructive: Bool = false
|
||||
|
||||
func makeBody(configuration: Configuration) -> some View {
|
||||
ZStack {
|
||||
if configuration.isPressed {
|
||||
RoundedRectangle(cornerRadius: 5.0)
|
||||
.fill(Color.accentColor)
|
||||
} else {
|
||||
RoundedRectangle(cornerRadius: 5.0)
|
||||
.fill(self.active ? Color.accentColor : self.destructive ? Color.red : Color("Button"))
|
||||
}
|
||||
|
||||
configuration.label
|
||||
.font(Font.headline)
|
||||
.padding(6)
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user