Export the created firmware file (instead of flashing directly)

Running swift-format
This commit is contained in:
Alexander Heinrich
2021-04-06 11:05:24 +02:00
parent cf5103f62f
commit edf2b59754
23 changed files with 1354 additions and 1102 deletions

View File

@@ -7,8 +7,8 @@
// SPDX-License-Identifier: AGPL-3.0-only // SPDX-License-Identifier: AGPL-3.0-only
// //
import Foundation
import CryptoKit import CryptoKit
import Foundation
struct DecryptReports { struct DecryptReports {
@@ -25,9 +25,11 @@ struct DecryptReports {
let privateKey = keyData let privateKey = keyData
let ephemeralKey = payloadData.subdata(in: 5..<62) let ephemeralKey = payloadData.subdata(in: 5..<62)
guard let sharedKey = BoringSSL.deriveSharedKey( guard
let sharedKey = BoringSSL.deriveSharedKey(
fromPrivateKey: privateKey, fromPrivateKey: privateKey,
andEphemeralKey: ephemeralKey) else { andEphemeralKey: ephemeralKey)
else {
throw FindMyError.decryptionError(description: "Failed generating shared key") throw FindMyError.decryptionError(description: "Failed generating shared key")
} }
@@ -38,7 +40,8 @@ struct DecryptReports {
let encData = payloadData.subdata(in: 62..<72) let encData = payloadData.subdata(in: 62..<72)
let tag = payloadData.subdata(in: 72..<payloadData.endIndex) 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) let locationReport = self.decode(content: decryptedContent, report: report)
print(locationReport) print(locationReport)
return locationReport return locationReport
@@ -58,7 +61,8 @@ struct DecryptReports {
print("Decryption Key \(decryptionKey.base64EncodedString())") print("Decryption Key \(decryptionKey.base64EncodedString())")
print("IV \(iv.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 symKey = SymmetricKey(data: decryptionKey)
let decrypted = try AES.GCM.open(sealedBox, using: symKey) let decrypted = try AES.GCM.open(sealedBox, using: symKey)
@@ -80,7 +84,9 @@ struct DecryptReports {
let latitudeDec = Double(latitude) / 10000000.0 let latitudeDec = Double(latitude) / 10000000.0
let longitudeDec = Double(longitude) / 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 { static func kdf(fromSharedSecret secret: Data, andEphemeralKey ephKey: Data) -> Data {
@@ -88,7 +94,8 @@ struct DecryptReports {
var shaDigest = SHA256() var shaDigest = SHA256()
shaDigest.update(data: secret) shaDigest.update(data: secret)
var counter: Int32 = 1 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: counterData)
shaDigest.update(data: ephKey) shaDigest.update(data: ephKey)

View File

@@ -7,9 +7,9 @@
// SPDX-License-Identifier: AGPL-3.0-only // SPDX-License-Identifier: AGPL-3.0-only
// //
import Combine
import Foundation import Foundation
import SwiftUI import SwiftUI
import Combine
class FindMyController: ObservableObject { class FindMyController: ObservableObject {
static let shared = FindMyController() static let shared = FindMyController()
@@ -17,7 +17,9 @@ class FindMyController: ObservableObject {
@Published var error: Error? @Published var error: Error?
@Published var devices = [FindMyDevice]() @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 { do {
let devices = try PropertyListDecoder().decode([FindMyDevice].self, from: data) 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) var devices = try PropertyListDecoder().decode([FindMyDevice].self, from: keys)
// Decrypt the reports with the imported 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 // Add the reports to the according device by finding the right key for the report
for report in reports { 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 device.keys.contains { (key) -> Bool in
key.hashedKey.base64EncodedString() == report.id key.hashedKey.base64EncodedString() == report.id
} }
}) else { })
else {
print("No device found for id") print("No device found for id")
continue continue
} }
@@ -101,10 +107,12 @@ class FindMyController: ObservableObject {
let duration: Double = (24 * 60 * 60) * 21 let duration: Double = (24 * 60 * 60) * 21
let startDate = Date() - duration let startDate = Date() - duration
fetcher.query(forHashes: keyHashes, fetcher.query(
forHashes: keyHashes,
start: startDate, start: startDate,
duration: duration, duration: duration,
searchPartyToken: searchPartyToken) { jd in searchPartyToken: searchPartyToken
) { jd in
guard let jsonData = jd else { guard let jsonData = jd else {
fetchReportGroup.leave() fetchReportGroup.leave()
return return
@@ -138,7 +146,9 @@ class FindMyController: ObservableObject {
#if EXPORT #if EXPORT
if let encoded = try? JSONEncoder().encode(reports) { 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")) try? encoded.write(to: outputDirectory.appendingPathComponent("reports.json"))
} }
#endif #endif
@@ -166,13 +176,16 @@ class FindMyController: ObservableObject {
// Map the keys in a dictionary for faster access // Map the keys in a dictionary for faster access
guard let reports = device.reports else { continue } 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, qos: .userInitiated,
attributes: .concurrent, attributes: .concurrent,
autoreleaseFrequency: .workItem, target: nil) 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), FindMyLocationReport(lat: 0, lng: 0, acc: 0, dP: Date(), t: Date(), c: 0),
count: reports.count) count: reports.count)
DispatchQueue.concurrentPerform(iterations: reports.count) { (reportIdx) in DispatchQueue.concurrentPerform(iterations: reports.count) { (reportIdx) in
@@ -201,7 +214,8 @@ class FindMyController: ObservableObject {
func exportDevices() { func exportDevices() {
if let encoded = try? PropertyListEncoder().encode(self.devices) { 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")) try? encoded.write(to: outputDirectory.appendingPathComponent("devices-\(Date()).plist"))
} }
} }

View File

@@ -7,8 +7,8 @@
// SPDX-License-Identifier: AGPL-3.0-only // SPDX-License-Identifier: AGPL-3.0-only
// //
import Foundation
import CryptoKit import CryptoKit
import Foundation
/// Decode key files found in newer macOS versions. /// Decode key files found in newer macOS versions.
class FindMyKeyDecoder { class FindMyKeyDecoder {
@@ -92,7 +92,8 @@ class FindMyKeyDecoder {
shaDigest.update(data: advertisedKey) shaDigest.update(data: advertisedKey)
let hashedKey = Data(shaDigest.finalize()) let hashedKey = Data(shaDigest.finalize())
let fmKey = FindMyKey(advertisedKey: advertisedKey, let fmKey = FindMyKey(
advertisedKey: advertisedKey,
hashedKey: hashedKey, hashedKey: hashedKey,
privateKey: fullKey, privateKey: fullKey,
startTime: nil, startTime: nil,

View File

@@ -9,8 +9,8 @@
// swiftlint:disable identifier_name // swiftlint:disable identifier_name
import Foundation
import CoreLocation import CoreLocation
import Foundation
struct FindMyDevice: Codable, Hashable { struct FindMyDevice: Codable, Hashable {
@@ -34,7 +34,10 @@ struct FindMyDevice: Codable, Hashable {
} }
struct FindMyKey: Codable { 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.advertisedKey = advertisedKey
self.hashedKey = hashedKey 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 // 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) let payloadBase64 = try values.decode(String.self, forKey: .payload)
guard let payload = Data(base64Encoded: payloadBase64) else { 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 self.payload = payload

View File

@@ -7,9 +7,9 @@
// SPDX-License-Identifier: AGPL-3.0-only // SPDX-License-Identifier: AGPL-3.0-only
// //
import SwiftUI
import Cocoa import Cocoa
import MapKit import MapKit
import SwiftUI
struct MapView: NSViewControllerRepresentable { struct MapView: NSViewControllerRepresentable {
@Environment(\.findMyController) var findMyController @Environment(\.findMyController) var findMyController

View File

@@ -26,7 +26,8 @@ final class MapViewController: NSViewController, MKMapViewDelegate {
// Zoom to first location // Zoom to first location
if let location = devices.first?.decryptedReports?.first { 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 span = MKCoordinateSpan(latitudeDelta: 5.0, longitudeDelta: 5.0)
let region = MKCoordinateRegion(center: coordinate, span: span) let region = MKCoordinateRegion(center: coordinate, span: span)
@@ -40,7 +41,8 @@ final class MapViewController: NSViewController, MKMapViewDelegate {
for report in reports { for report in reports {
let pin = MKPointAnnotation() let pin = MKPointAnnotation()
pin.title = device.deviceId 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) self.mapView.addAnnotation(pin)
} }
} }

View File

@@ -47,7 +47,10 @@ struct OFFetchReportsMainView: View {
} }
.background( .background(
RoundedRectangle(cornerRadius: 20.0) 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() .padding()
.onDrop(of: ["public.file-url"], isTargeted: self.$targetedDrop) { (droppedData) -> Bool in .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) TextField("Search Party Token", text: self.$searchPartyTokenString)
Button(action: { Button(
action: {
if !self.searchPartyTokenString.isEmpty, if !self.searchPartyTokenString.isEmpty,
let file = self.keyPlistFile, let file = self.keyPlistFile,
let searchPartyToken = self.searchPartyTokenString.data(using: .utf8) { let searchPartyToken = self.searchPartyTokenString.data(using: .utf8)
{
self.searchPartyToken = searchPartyToken self.searchPartyToken = searchPartyToken
self.downloadAndDecryptLocations(with: file, searchPartyToken: searchPartyToken) self.downloadAndDecryptLocations(with: file, searchPartyToken: searchPartyToken)
} }
}, label: { },
label: {
Text("Download reports") Text("Download reports")
}) })
} }
@@ -94,17 +100,21 @@ struct OFFetchReportsMainView: View {
VStack { VStack {
HStack { HStack {
Spacer() Spacer()
Button(action: { Button(
action: {
self.showMap = false self.showMap = false
self.showTokenPrompt = false self.showTokenPrompt = false
}, label: { },
label: {
Text("Import other tokens") Text("Import other tokens")
}) })
Button(action: { Button(
action: {
self.exportDecryptedLocations() self.exportDecryptedLocations()
}, label: { },
label: {
Text("Export") Text("Export")
}) })
@@ -164,7 +174,9 @@ struct OFFetchReportsMainView: View {
func downloadAndDecryptLocations(with keyFile: Data, searchPartyToken: Data) { func downloadAndDecryptLocations(with keyFile: Data, searchPartyToken: Data) {
self.loading = true 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 // Check if an error occurred
guard error == nil else { guard error == nil else {
self.error = error self.error = error

View File

@@ -7,8 +7,8 @@
// SPDX-License-Identifier: AGPL-3.0-only // SPDX-License-Identifier: AGPL-3.0-only
// //
import Foundation
import AppKit import AppKit
import Foundation
class SavePanel: NSObject, NSOpenSavePanelDelegate { 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 } guard okFlag else { return nil }
return filename return filename

View File

@@ -8,8 +8,8 @@
// //
import Cocoa import Cocoa
import SwiftUI
import CoreLocation import CoreLocation
import SwiftUI
@NSApplicationMain @NSApplicationMain
class AppDelegate: NSObject, NSApplicationDelegate { class AppDelegate: NSObject, NSApplicationDelegate {

View File

@@ -7,8 +7,8 @@
// SPDX-License-Identifier: AGPL-3.0-only // SPDX-License-Identifier: AGPL-3.0-only
// //
import SwiftUI
import OSLog import OSLog
import SwiftUI
struct ContentView: View { struct ContentView: View {
@@ -22,9 +22,11 @@ struct ContentView: View {
self.infoText self.infoText
.padding() .padding()
Button(action: { Button(
action: {
self.readPrivateKeys() self.readPrivateKeys()
}, label: { },
label: {
Text("Read private offline finding keys") Text("Read private offline finding keys")
.font(.headline) .font(.headline)
.foregroundColor(Color.black) .foregroundColor(Color.black)
@@ -35,7 +37,8 @@ struct ContentView: View {
.shadow(color: Color.black, radius: 10.0, x: 0, y: 0) .shadow(color: Color.black, radius: 10.0, x: 0, y: 0)
) )
}) }
)
.buttonStyle(PlainButtonStyle()) .buttonStyle(PlainButtonStyle())
self.keysInfo.map { (keysInfo) in self.keysInfo.map { (keysInfo) in
@@ -54,10 +57,12 @@ struct ContentView: View {
var infoText: some View { var infoText: some View {
// swiftlint:disable line_length // 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(
Text(" all ").bold() + "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("Apple devices of the current user.\n\n") + ) + 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(
"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 // swiftlint:enable line_length
} }

View File

@@ -7,8 +7,8 @@
// SPDX-License-Identifier: AGPL-3.0-only // SPDX-License-Identifier: AGPL-3.0-only
// //
import Foundation
import CryptoKit import CryptoKit
import Foundation
import OSLog import OSLog
struct FindMyKeyExtractor { struct FindMyKeyExtractor {
@@ -53,7 +53,8 @@ struct FindMyKeyExtractor {
let fm = FileManager.default let fm = FileManager.default
let privateKeysPath = fm.urls(for: .libraryDirectory, in: .userDomainMask) let privateKeysPath = fm.urls(for: .libraryDirectory, in: .userDomainMask)
.first?.appendingPathComponent(directoryPath) .first?.appendingPathComponent(directoryPath)
let folders = try fm.contentsOfDirectory(at: privateKeysPath!, let folders = try fm.contentsOfDirectory(
at: privateKeysPath!,
includingPropertiesForKeys: nil, options: .skipsHiddenFiles) includingPropertiesForKeys: nil, options: .skipsHiddenFiles)
guard folders.isEmpty == false else { throw FindMyError.noFoldersFound } guard folders.isEmpty == false else { throw FindMyError.noFoldersFound }
@@ -61,7 +62,8 @@ struct FindMyKeyExtractor {
var devices = [FindMyDevice]() var devices = [FindMyDevice]()
for folderURL in folders { for folderURL in folders {
let keyFiles = try fm.contentsOfDirectory(at: folderURL, let keyFiles = try fm.contentsOfDirectory(
at: folderURL,
includingPropertiesForKeys: nil, options: .skipsHiddenFiles) includingPropertiesForKeys: nil, options: .skipsHiddenFiles)
// Check if keys are available // Check if keys are available
print("Found \(keyFiles.count) in folder \(folderURL.lastPathComponent)") print("Found \(keyFiles.count) in folder \(folderURL.lastPathComponent)")
@@ -91,7 +93,9 @@ struct FindMyKeyExtractor {
/// - Parameter keyFile: Propery list data /// - Parameter keyFile: Propery list data
/// - Returns: Find My private Key /// - Returns: Find My private Key
static func parseKeyFile(keyFile: Data) throws -> FindMyKey { 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], options: .init(), format: nil) as? [String: Any],
let advertisedKey = keyDict["A"] as? Data, let advertisedKey = keyDict["A"] as? Data,
let privateKey = keyDict["PR"] as? Data, let privateKey = keyDict["PR"] as? Data,
@@ -106,7 +110,8 @@ struct FindMyKeyExtractor {
let time = Date(timeIntervalSinceReferenceDate: timeValues[0]) let time = Date(timeIntervalSinceReferenceDate: timeValues[0])
let duration = timeValues[1] let duration = timeValues[1]
return FindMyKey(advertisedKey: advertisedKey, return FindMyKey(
advertisedKey: advertisedKey,
hashedKey: hashedKey, hashedKey: hashedKey,
privateKey: privateKey, privateKey: privateKey,
startTime: time, startTime: time,
@@ -128,7 +133,8 @@ struct FindMyKeyExtractor {
func recursiveSearch(from url: URL, urlArray: inout [URL]) { func recursiveSearch(from url: URL, urlArray: inout [URL]) {
do { do {
let randomSubfolders = try fm.contentsOfDirectory(at: url, let randomSubfolders = try fm.contentsOfDirectory(
at: url,
includingPropertiesForKeys: nil, includingPropertiesForKeys: nil,
options: .includesDirectoriesPostOrder) options: .includesDirectoriesPostOrder)
@@ -180,7 +186,8 @@ struct FindMyKeyExtractor {
static func loadNewKeyFilesIn(directory: URL) throws -> [FindMyDevice] { static func loadNewKeyFilesIn(directory: URL) throws -> [FindMyDevice] {
os_log(.debug, "Loading key files from %@", directory.path) os_log(.debug, "Loading key files from %@", directory.path)
let fm = FileManager.default let fm = FileManager.default
let subDirectories = try fm.contentsOfDirectory(at: directory, let subDirectories = try fm.contentsOfDirectory(
at: directory,
includingPropertiesForKeys: nil, options: .skipsHiddenFiles) includingPropertiesForKeys: nil, options: .skipsHiddenFiles)
var devices = [FindMyDevice]() var devices = [FindMyDevice]()
@@ -189,7 +196,8 @@ struct FindMyKeyExtractor {
do { do {
var keyFiles = [Data]() var keyFiles = [Data]()
let keyDirectory = deviceDirectory.appendingPathComponent("Primary") let keyDirectory = deviceDirectory.appendingPathComponent("Primary")
let keyFileURLs = try fm.contentsOfDirectory(at: keyDirectory, let keyFileURLs = try fm.contentsOfDirectory(
at: keyDirectory,
includingPropertiesForKeys: nil, includingPropertiesForKeys: nil,
options: .skipsHiddenFiles) options: .skipsHiddenFiles)
for keyfileURL in keyFileURLs { for keyfileURL in keyFileURLs {

View File

@@ -7,9 +7,9 @@
// SPDX-License-Identifier: AGPL-3.0-only // SPDX-License-Identifier: AGPL-3.0-only
// //
import Foundation
import Combine import Combine
import CryptoKit import CryptoKit
import Foundation
struct FindMyDevice: Codable { struct FindMyDevice: Codable {
let deviceId: String let deviceId: String

View File

@@ -7,8 +7,8 @@
// SPDX-License-Identifier: AGPL-3.0-only // SPDX-License-Identifier: AGPL-3.0-only
// //
import Foundation
import AppKit import AppKit
import Foundation
class SavePanel: NSObject, NSOpenSavePanelDelegate { 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 return filename
} }
} }

View File

@@ -51,6 +51,7 @@
78EC226C25DBC2E40042B775 /* OpenHaystackMainView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 78EC226B25DBC2E40042B775 /* OpenHaystackMainView.swift */; }; 78EC226C25DBC2E40042B775 /* OpenHaystackMainView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 78EC226B25DBC2E40042B775 /* OpenHaystackMainView.swift */; };
78EC227225DBC8CE0042B775 /* Accessory.swift in Sources */ = {isa = PBXBuildFile; fileRef = 78EC227125DBC8CE0042B775 /* Accessory.swift */; }; 78EC227225DBC8CE0042B775 /* Accessory.swift in Sources */ = {isa = PBXBuildFile; fileRef = 78EC227125DBC8CE0042B775 /* Accessory.swift */; };
78EC227725DBDB7E0042B775 /* KeychainController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 78EC227625DBDB7E0042B775 /* KeychainController.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 */; }; F126102F2600D1D80066A859 /* Slider+LogScale.swift in Sources */ = {isa = PBXBuildFile; fileRef = F126102E2600D1D80066A859 /* Slider+LogScale.swift */; };
F12D5A5A25FA4F3500CBBA09 /* BluetoothAccessoryScanner.swift in Sources */ = {isa = PBXBuildFile; fileRef = F12D5A5925FA4F3500CBBA09 /* BluetoothAccessoryScanner.swift */; }; F12D5A5A25FA4F3500CBBA09 /* BluetoothAccessoryScanner.swift in Sources */ = {isa = PBXBuildFile; fileRef = F12D5A5925FA4F3500CBBA09 /* BluetoothAccessoryScanner.swift */; };
F12D5A6025FA79FA00CBBA09 /* Advertisement.swift in Sources */ = {isa = PBXBuildFile; fileRef = F12D5A5F25FA79FA00CBBA09 /* Advertisement.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>"; }; 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>"; }; 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>"; }; 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>"; }; 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>"; }; 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>"; }; F12D5A5F25FA79FA00CBBA09 /* Advertisement.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Advertisement.swift; sourceTree = "<group>"; };
@@ -360,6 +362,7 @@
78EC227025DBC8BB0042B775 /* Views */ = { 78EC227025DBC8BB0042B775 /* Views */ = {
isa = PBXGroup; isa = PBXGroup;
children = ( children = (
78F8BB4A261C50D500D9F37F /* Styles */,
78286D7625E5114600F65511 /* ActivityIndicator.swift */, 78286D7625E5114600F65511 /* ActivityIndicator.swift */,
78EC226B25DBC2E40042B775 /* OpenHaystackMainView.swift */, 78EC226B25DBC2E40042B775 /* OpenHaystackMainView.swift */,
78486BEE25DD711E0007ED87 /* PopUpAlertView.swift */, 78486BEE25DD711E0007ED87 /* PopUpAlertView.swift */,
@@ -374,6 +377,14 @@
path = Views; path = Views;
sourceTree = "<group>"; sourceTree = "<group>";
}; };
78F8BB4A261C50D500D9F37F /* Styles */ = {
isa = PBXGroup;
children = (
78F8BB4B261C50EB00D9F37F /* LargeButtonStyle.swift */,
);
path = Styles;
sourceTree = "<group>";
};
F12D5A5E25FA79D600CBBA09 /* Bluetooth */ = { F12D5A5E25FA79D600CBBA09 /* Bluetooth */ = {
isa = PBXGroup; isa = PBXGroup;
children = ( children = (
@@ -541,7 +552,7 @@
); );
runOnlyForDeploymentPostprocessing = 0; runOnlyForDeploymentPostprocessing = 0;
shellPath = /bin/sh; 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 */ = { F14B2C7E25EFBB11002DC056 /* Set Version Number from Git */ = {
isa = PBXShellScriptBuildPhase; isa = PBXShellScriptBuildPhase;
@@ -615,6 +626,7 @@
F12D5A6025FA79FA00CBBA09 /* Advertisement.swift in Sources */, F12D5A6025FA79FA00CBBA09 /* Advertisement.swift in Sources */,
781EB3F225DAD7EA00FEAA19 /* OpenHaystackApp.swift in Sources */, 781EB3F225DAD7EA00FEAA19 /* OpenHaystackApp.swift in Sources */,
781EB3F325DAD7EA00FEAA19 /* Models.swift in Sources */, 781EB3F325DAD7EA00FEAA19 /* Models.swift in Sources */,
78F8BB4C261C50EB00D9F37F /* LargeButtonStyle.swift in Sources */,
781EB3F425DAD7EA00FEAA19 /* FindMyController.swift in Sources */, 781EB3F425DAD7EA00FEAA19 /* FindMyController.swift in Sources */,
781EB3F525DAD7EA00FEAA19 /* BoringSSL.m in Sources */, 781EB3F525DAD7EA00FEAA19 /* BoringSSL.m in Sources */,
F12D5A5A25FA4F3500CBBA09 /* BluetoothAccessoryScanner.swift in Sources */, F12D5A5A25FA4F3500CBBA09 /* BluetoothAccessoryScanner.swift in Sources */,

View File

@@ -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
}
}

View File

@@ -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
}
}

View File

@@ -157,6 +157,7 @@ class AccessoryController: ObservableObject {
//MARK: Location reports //MARK: Location reports
/// Download the location reports from. /// Download the location reports from.
///
/// - Parameter completion: called when the reports have been succesfully downloaded or the request has failed /// - Parameter completion: called when the reports have been succesfully downloaded or the request has failed
func downloadLocationReports(completion: @escaping (Result<Void, OpenHaystackMainView.AlertType>) -> Void) { func downloadLocationReports(completion: @escaping (Result<Void, OpenHaystackMainView.AlertType>) -> Void) {
AnisetteDataManager.shared.requestAnisetteData { result in AnisetteDataManager.shared.requestAnisetteData { result in

View File

@@ -74,10 +74,8 @@ struct MicrobitController {
return patchedFirmware return patchedFirmware
} }
static func deploy(accessory: Accessory) throws { static func patchFirmware(for accessory: Accessory) throws -> Data {
let microbits = try MicrobitController.findMicrobits() guard let firmwareURL = Bundle.main.url(forResource: "firmware", withExtension: "bin")
guard let microBitURL = microbits.first,
let firmwareURL = Bundle.main.url(forResource: "firmware", withExtension: "bin")
else { else {
throw FirmwareFlashError.notFound throw FirmwareFlashError.notFound
} }
@@ -87,6 +85,18 @@ struct MicrobitController {
let publicKey = try accessory.getAdvertisementKey() let publicKey = try accessory.getAdvertisementKey()
let patchedFirmware = try MicrobitController.patchFirmware(firmware, pattern: pattern, with: publicKey) 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) try MicrobitController.deployToMicrobit(microBitURL, firmwareFile: patchedFirmware)
} }

View File

@@ -8,6 +8,7 @@
// //
import SwiftUI import SwiftUI
import os
struct ManageAccessoriesView: View { struct ManageAccessoriesView: View {
@@ -21,6 +22,7 @@ struct ManageAccessoriesView: View {
@Binding var focusedAccessory: Accessory? @Binding var focusedAccessory: Accessory?
@Binding var accessoryToDeploy: Accessory? @Binding var accessoryToDeploy: Accessory?
@Binding var showESP32DeploySheet: Bool @Binding var showESP32DeploySheet: Bool
@State var sheetShown: SheetType?
@State var showMailPopup = false @State var showMailPopup = false
@@ -42,11 +44,14 @@ struct ManageAccessoriesView: View {
.toolbar(content: { .toolbar(content: {
self.toolbarView self.toolbarView
}) })
.sheet( .sheet(item: self.$sheetShown) { sheetType in
isPresented: self.$showESP32DeploySheet, switch sheetType {
content: { case .esp32Install:
ESP32InstallSheet(accessory: self.$accessoryToDeploy, alertType: self.$alertType) ESP32InstallSheet(accessory: self.$accessoryToDeploy, alertType: self.$alertType)
}) case .deployFirmware:
self.selectTargetView
}
}
} }
/// Accessory List view. /// 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. /// Delete an accessory from the list of accessories.
func delete(accessory: Accessory) { func delete(accessory: Accessory) {
do { do {
@@ -114,7 +170,7 @@ struct ManageAccessoriesView: View {
func deploy(accessory: Accessory) { func deploy(accessory: Accessory) {
self.accessoryToDeploy = accessory self.accessoryToDeploy = accessory
self.alertType = .selectDepoyTarget self.sheetShown = .deployFirmware
} }
/// Add an accessory with the provided details. /// 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 { struct ManageAccessoriesView_Previews: PreviewProvider {

View File

@@ -221,26 +221,6 @@ struct OpenHaystackMainView: View {
.frame(width: 250, height: 120) .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. /// Ask to install and activate the mail plugin.
func installMailPlugin() { func installMailPlugin() {
let pluginManager = MailPluginManager() let pluginManager = MailPluginManager()
@@ -373,20 +353,6 @@ struct OpenHaystackMainView: View {
action: { action: {
self.downloadPlugin() self.downloadPlugin()
}), secondaryButton: .cancel()) }), 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: case .downloadingReportsFailed:
return Alert( return Alert(
title: Text("Downloading locations failed"), title: Text("Downloading locations failed"),
@@ -419,7 +385,6 @@ struct OpenHaystackMainView: View {
case downloadingReportsFailed case downloadingReportsFailed
case activatePlugin case activatePlugin
case pluginInstallFailed case pluginInstallFailed
case selectDepoyTarget
case exportFailed case exportFailed
case importFailed case importFailed
} }

View File

@@ -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)
}
}
}