mirror of
https://github.com/seemoo-lab/openhaystack.git
synced 2026-02-14 17:49:54 +00:00
Add swift-format as an Xcode build phase
This commit is contained in:
7
OpenHaystack/.swift-format
Normal file
7
OpenHaystack/.swift-format
Normal file
@@ -0,0 +1,7 @@
|
||||
{
|
||||
"version": 1,
|
||||
"lineLength": 180,
|
||||
"indentation": {
|
||||
"spaces": 4
|
||||
}
|
||||
}
|
||||
@@ -343,6 +343,7 @@
|
||||
781EB3F625DAD7EA00FEAA19 /* Frameworks */,
|
||||
781EB3FC25DAD7EA00FEAA19 /* Resources */,
|
||||
78EC227325DBC9240042B775 /* SwiftLint */,
|
||||
F125DE4525F65E0700135D32 /* Run swift-format */,
|
||||
78286DC225E5669100F65511 /* Embed PlugIns */,
|
||||
F14B2C7E25EFBB11002DC056 /* Set Version Number from Git */,
|
||||
);
|
||||
@@ -493,6 +494,24 @@
|
||||
shellPath = /bin/sh;
|
||||
shellScript = "if which swiftlint >/dev/null; then\n swiftlint autocorrect && swiftlint\nelse\n echo \"warning: SwiftLint not installed, download from https://github.com/realm/SwiftLint\"\nfi\n";
|
||||
};
|
||||
F125DE4525F65E0700135D32 /* Run swift-format */ = {
|
||||
isa = PBXShellScriptBuildPhase;
|
||||
buildActionMask = 2147483647;
|
||||
files = (
|
||||
);
|
||||
inputFileListPaths = (
|
||||
);
|
||||
inputPaths = (
|
||||
);
|
||||
name = "Run swift-format";
|
||||
outputFileListPaths = (
|
||||
);
|
||||
outputPaths = (
|
||||
);
|
||||
runOnlyForDeploymentPostprocessing = 0;
|
||||
shellPath = /bin/sh;
|
||||
shellScript = "if which swift-format >/dev/null; then\n swift-format format -r -i \"$SRCROOT\" && swift-format lint -r \"$SRCROOT\"\nelse\n echo \"warning: swift-format not installed, download from https://github.com/apple/swift-format\"\nfi\n";
|
||||
};
|
||||
F14B2C7E25EFBB11002DC056 /* Set Version Number from Git */ = {
|
||||
isa = PBXShellScriptBuildPhase;
|
||||
buildActionMask = 2147483647;
|
||||
|
||||
@@ -8,21 +8,22 @@
|
||||
import Foundation
|
||||
import OSLog
|
||||
|
||||
/// Uses the AltStore Mail plugin to access recent anisette data
|
||||
/// Uses the AltStore Mail plugin to access recent anisette data.
|
||||
public class AnisetteDataManager: NSObject {
|
||||
@objc static let shared = AnisetteDataManager()
|
||||
private var anisetteDataCompletionHandlers: [String: (Result<AppleAccountData, Error>) -> Void] = [:]
|
||||
private var anisetteDataTimers: [String: Timer] = [:]
|
||||
|
||||
private override init() {
|
||||
super.init()
|
||||
super.init()
|
||||
|
||||
dlopen("/System/Library/PrivateFrameworks/AuthKit.framework/AuthKit", RTLD_NOW)
|
||||
dlopen("/System/Library/PrivateFrameworks/AuthKit.framework/AuthKit", RTLD_NOW)
|
||||
|
||||
DistributedNotificationCenter.default()
|
||||
.addObserver(self, selector: #selector(AnisetteDataManager.handleAppleDataResponse(_:)),
|
||||
name: Notification.Name("de.tu-darmstadt.seemoo.OpenHaystack.AnisetteDataResponse"), object: nil)
|
||||
}
|
||||
DistributedNotificationCenter.default()
|
||||
.addObserver(
|
||||
self, selector: #selector(AnisetteDataManager.handleAppleDataResponse(_:)),
|
||||
name: Notification.Name("de.tu-darmstadt.seemoo.OpenHaystack.AnisetteDataResponse"), object: nil)
|
||||
}
|
||||
|
||||
func requestAnisetteData(_ completion: @escaping (Result<AppleAccountData, Error>) -> Void) {
|
||||
if let accountData = self.requestAnisetteDataAuthKit() {
|
||||
@@ -31,19 +32,20 @@ public class AnisetteDataManager: NSObject {
|
||||
return
|
||||
}
|
||||
|
||||
let requestUUID = UUID().uuidString
|
||||
self.anisetteDataCompletionHandlers[requestUUID] = completion
|
||||
let requestUUID = UUID().uuidString
|
||||
self.anisetteDataCompletionHandlers[requestUUID] = completion
|
||||
|
||||
let timer = Timer(timeInterval: 1.0, repeats: false) { (_) in
|
||||
self.finishRequest(forUUID: requestUUID, result: .failure(AnisetteDataError.pluginNotFound))
|
||||
}
|
||||
self.anisetteDataTimers[requestUUID] = timer
|
||||
let timer = Timer(timeInterval: 1.0, repeats: false) { (_) in
|
||||
self.finishRequest(forUUID: requestUUID, result: .failure(AnisetteDataError.pluginNotFound))
|
||||
}
|
||||
self.anisetteDataTimers[requestUUID] = timer
|
||||
|
||||
RunLoop.main.add(timer, forMode: .default)
|
||||
RunLoop.main.add(timer, forMode: .default)
|
||||
|
||||
DistributedNotificationCenter.default()
|
||||
.postNotificationName(Notification.Name("de.tu-darmstadt.seemoo.OpenHaystack.FetchAnisetteData"),
|
||||
object: nil, userInfo: ["requestUUID": requestUUID], options: .deliverImmediately)
|
||||
DistributedNotificationCenter.default()
|
||||
.postNotificationName(
|
||||
Notification.Name("de.tu-darmstadt.seemoo.OpenHaystack.FetchAnisetteData"),
|
||||
object: nil, userInfo: ["requestUUID": requestUUID], options: .deliverImmediately)
|
||||
}
|
||||
|
||||
func requestAnisetteDataAuthKit() -> AppleAccountData? {
|
||||
@@ -52,27 +54,28 @@ public class AnisetteDataManager: NSObject {
|
||||
let dateFormatter = ISO8601DateFormatter()
|
||||
|
||||
guard let machineID = anisetteData["X-Apple-I-MD-M"] as? String,
|
||||
let otp = anisetteData["X-Apple-I-MD"] as? String,
|
||||
let localUserId = anisetteData["X-Apple-I-MD-LU"] as? String,
|
||||
let dateString = anisetteData["X-Apple-I-Client-Time"] as? String,
|
||||
let date = dateFormatter.date(from: dateString),
|
||||
let deviceClass = NSClassFromString("AKDevice")
|
||||
let otp = anisetteData["X-Apple-I-MD"] as? String,
|
||||
let localUserId = anisetteData["X-Apple-I-MD-LU"] as? String,
|
||||
let dateString = anisetteData["X-Apple-I-Client-Time"] as? String,
|
||||
let date = dateFormatter.date(from: dateString),
|
||||
let deviceClass = NSClassFromString("AKDevice")
|
||||
else {
|
||||
return nil
|
||||
}
|
||||
let device: AKDevice = deviceClass.current()
|
||||
|
||||
let routingInfo = (anisetteData["X-Apple-I-MD-RINFO"] as? NSNumber)?.uint64Value ?? 0
|
||||
let accountData = AppleAccountData(machineID: machineID,
|
||||
oneTimePassword: otp,
|
||||
localUserID: localUserId,
|
||||
routingInfo: routingInfo,
|
||||
deviceUniqueIdentifier: device.uniqueDeviceIdentifier(),
|
||||
deviceSerialNumber: device.serialNumber(),
|
||||
deviceDescription: device.serverFriendlyDescription(),
|
||||
date: date,
|
||||
locale: Locale.current,
|
||||
timeZone: TimeZone.current)
|
||||
let accountData = AppleAccountData(
|
||||
machineID: machineID,
|
||||
oneTimePassword: otp,
|
||||
localUserID: localUserId,
|
||||
routingInfo: routingInfo,
|
||||
deviceUniqueIdentifier: device.uniqueDeviceIdentifier(),
|
||||
deviceSerialNumber: device.serialNumber(),
|
||||
deviceDescription: device.serverFriendlyDescription(),
|
||||
date: date,
|
||||
locale: Locale.current,
|
||||
timeZone: TimeZone.current)
|
||||
|
||||
if let spToken = ReportsFetcher().fetchSearchpartyToken() {
|
||||
accountData.searchPartyToken = spToken
|
||||
@@ -88,25 +91,25 @@ public class AnisetteDataManager: NSObject {
|
||||
completion(nil)
|
||||
case .success(let data):
|
||||
// Return only the headers
|
||||
completion([
|
||||
"X-Apple-I-MD-M": data.machineID,
|
||||
"X-Apple-I-MD": data.oneTimePassword,
|
||||
"X-Apple-I-TimeZone": String(data.timeZone.abbreviation() ?? "UTC"),
|
||||
"X-Apple-I-Client-Time": ISO8601DateFormatter().string(from: data.date),
|
||||
"X-Apple-I-MD-RINFO": String(data.routingInfo)
|
||||
completion(
|
||||
[
|
||||
"X-Apple-I-MD-M": data.machineID,
|
||||
"X-Apple-I-MD": data.oneTimePassword,
|
||||
"X-Apple-I-TimeZone": String(data.timeZone.abbreviation() ?? "UTC"),
|
||||
"X-Apple-I-Client-Time": ISO8601DateFormatter().string(from: data.date),
|
||||
"X-Apple-I-MD-RINFO": String(data.routingInfo),
|
||||
] as [AnyHashable: Any])
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private extension AnisetteDataManager {
|
||||
extension AnisetteDataManager {
|
||||
|
||||
@objc func handleAppleDataResponse(_ notification: Notification) {
|
||||
@objc fileprivate func handleAppleDataResponse(_ notification: Notification) {
|
||||
guard let userInfo = notification.userInfo, let requestUUID = userInfo["requestUUID"] as? String else { return }
|
||||
|
||||
if
|
||||
let archivedAnisetteData = userInfo["anisetteData"] as? Data,
|
||||
if let archivedAnisetteData = userInfo["anisetteData"] as? Data,
|
||||
let appleAccountData = try? NSKeyedUnarchiver.unarchivedObject(ofClass: AppleAccountData.self, from: archivedAnisetteData)
|
||||
{
|
||||
if let range = appleAccountData.deviceDescription.lowercased().range(of: "(com.apple.mail") {
|
||||
@@ -122,11 +125,10 @@ private extension AnisetteDataManager {
|
||||
}
|
||||
}
|
||||
|
||||
@objc func handleAnisetteDataResponse(_ notification: Notification) {
|
||||
@objc fileprivate func handleAnisetteDataResponse(_ notification: Notification) {
|
||||
guard let userInfo = notification.userInfo, let requestUUID = userInfo["requestUUID"] as? String else { return }
|
||||
|
||||
if
|
||||
let archivedAnisetteData = userInfo["anisetteData"] as? Data,
|
||||
if let archivedAnisetteData = userInfo["anisetteData"] as? Data,
|
||||
let anisetteData = try? NSKeyedUnarchiver.unarchivedObject(ofClass: ALTAnisetteData.self, from: archivedAnisetteData)
|
||||
{
|
||||
if let range = anisetteData.deviceDescription.lowercased().range(of: "(com.apple.mail") {
|
||||
@@ -143,7 +145,7 @@ private extension AnisetteDataManager {
|
||||
}
|
||||
}
|
||||
|
||||
func finishRequest(forUUID requestUUID: String, result: Result<AppleAccountData, Error>) {
|
||||
fileprivate func finishRequest(forUUID requestUUID: String, result: Result<AppleAccountData, Error>) {
|
||||
let completionHandler = self.anisetteDataCompletionHandlers[requestUUID]
|
||||
self.anisetteDataCompletionHandlers[requestUUID] = nil
|
||||
|
||||
|
||||
@@ -5,12 +5,13 @@
|
||||
//
|
||||
// SPDX-License-Identifier: AGPL-3.0-only
|
||||
|
||||
import Foundation
|
||||
import CryptoKit
|
||||
import Foundation
|
||||
|
||||
struct DecryptReports {
|
||||
|
||||
/// Decrypt a find my report with the according key
|
||||
/// Decrypt a find my report with the according key.
|
||||
///
|
||||
/// - Parameters:
|
||||
/// - report: An encrypted FindMy Report
|
||||
/// - key: A FindMyKey
|
||||
@@ -40,7 +41,8 @@ struct DecryptReports {
|
||||
return locationReport
|
||||
}
|
||||
|
||||
/// Decrypt the payload
|
||||
/// Decrypt the payload.
|
||||
///
|
||||
/// - Parameters:
|
||||
/// - payload: Encrypted payload part
|
||||
/// - symmetricKey: Symmetric key
|
||||
@@ -63,18 +65,18 @@ struct DecryptReports {
|
||||
|
||||
static func decode(content: Data, report: FindMyReport) -> FindMyLocationReport {
|
||||
var longitude: Int32 = 0
|
||||
_ = withUnsafeMutableBytes(of: &longitude, {content.subdata(in: 4..<8).copyBytes(to: $0)})
|
||||
_ = withUnsafeMutableBytes(of: &longitude, { content.subdata(in: 4..<8).copyBytes(to: $0) })
|
||||
longitude = Int32(bigEndian: longitude)
|
||||
|
||||
var latitude: Int32 = 0
|
||||
_ = withUnsafeMutableBytes(of: &latitude, {content.subdata(in: 0..<4).copyBytes(to: $0)})
|
||||
_ = withUnsafeMutableBytes(of: &latitude, { content.subdata(in: 0..<4).copyBytes(to: $0) })
|
||||
latitude = Int32(bigEndian: latitude)
|
||||
|
||||
var accuracy: UInt8 = 0
|
||||
_ = withUnsafeMutableBytes(of: &accuracy, {content.subdata(in: 8..<9).copyBytes(to: $0)})
|
||||
_ = withUnsafeMutableBytes(of: &accuracy, { content.subdata(in: 8..<9).copyBytes(to: $0) })
|
||||
|
||||
let latitudeDec = Double(latitude)/10000000.0
|
||||
let longitudeDec = Double(longitude)/10000000.0
|
||||
let latitudeDec = Double(latitude) / 10000000.0
|
||||
let longitudeDec = Double(longitude) / 10000000.0
|
||||
|
||||
return FindMyLocationReport(lat: latitudeDec, lng: longitudeDec, acc: accuracy, dP: report.datePublished, t: report.timestamp, c: report.confidence)
|
||||
}
|
||||
|
||||
@@ -5,9 +5,9 @@
|
||||
//
|
||||
// SPDX-License-Identifier: AGPL-3.0-only
|
||||
|
||||
import Combine
|
||||
import Foundation
|
||||
import SwiftUI
|
||||
import Combine
|
||||
|
||||
class FindMyController: ObservableObject {
|
||||
static let shared = FindMyController()
|
||||
@@ -26,7 +26,7 @@ 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 {
|
||||
let devices = try PropertyListDecoder().decode([FindMyDevice].self, from: keys)
|
||||
self.devices = devices
|
||||
|
||||
@@ -76,7 +76,7 @@ class FindMyController: ObservableObject {
|
||||
|
||||
self.devices = devices
|
||||
|
||||
// Decrypt reports again with additional information
|
||||
// Decrypt reports again with additional information
|
||||
self.decryptReports {
|
||||
|
||||
}
|
||||
@@ -97,10 +97,10 @@ class FindMyController: ObservableObject {
|
||||
// Only use the newest keys for testing
|
||||
let keys = devices[deviceIndex].keys
|
||||
|
||||
let keyHashes = keys.map({$0.hashedKey.base64EncodedString()})
|
||||
let keyHashes = keys.map({ $0.hashedKey.base64EncodedString() })
|
||||
|
||||
// 21 days
|
||||
let duration: Double = (24 * 60 * 60) * 21
|
||||
let duration: Double = (24 * 60 * 60) * 21
|
||||
let startDate = Date() - duration
|
||||
|
||||
fetcher.query(forHashes: keyHashes, start: startDate, duration: duration, searchPartyToken: searchPartyToken) { jd in
|
||||
@@ -136,10 +136,10 @@ class FindMyController: ObservableObject {
|
||||
}
|
||||
|
||||
#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"))
|
||||
}
|
||||
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 {
|
||||
@@ -164,14 +164,14 @@ class FindMyController: ObservableObject {
|
||||
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})
|
||||
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}
|
||||
guard let key = keyMap[report.id] else { return }
|
||||
do {
|
||||
// Decrypt the report
|
||||
let locationReport = try DecryptReports.decrypt(report: report, with: key)
|
||||
@@ -208,8 +208,8 @@ struct FindMyControllerKey: EnvironmentKey {
|
||||
|
||||
extension EnvironmentValues {
|
||||
var findMyController: FindMyController {
|
||||
get {self[FindMyControllerKey.self]}
|
||||
set {self[FindMyControllerKey.self] = newValue}
|
||||
get { self[FindMyControllerKey.self] }
|
||||
set { self[FindMyControllerKey.self] = newValue }
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -5,16 +5,18 @@
|
||||
//
|
||||
// SPDX-License-Identifier: AGPL-3.0-only
|
||||
|
||||
import Foundation
|
||||
import CryptoKit
|
||||
import Foundation
|
||||
|
||||
/// Decode key files found in newer macOS versions.
|
||||
class FindMyKeyDecoder {
|
||||
/// Key files can be in different format. The old <= 10.15.3 have been using normal plists. Newer once use a binary format which needs different parsing
|
||||
/// Key files can be in different format.
|
||||
///
|
||||
/// The old <= 10.15.3 have been using normal plists. Newer once use a binary format which needs different parsing.
|
||||
enum KeyFileFormat {
|
||||
/// Catalina > 10.15.4 key file format | Big Sur 11.0 Beta 1 uses a similar key file format that can be parsed identically.
|
||||
/// 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
|
||||
}
|
||||
|
||||
@@ -59,7 +61,7 @@ class FindMyKeyDecoder {
|
||||
|
||||
while i + 117 < keyFile.count {
|
||||
// We could not identify what those keys were
|
||||
_ = keyFile.subdata(in: i..<i+32)
|
||||
_ = keyFile.subdata(in: i..<i + 32)
|
||||
i += 32
|
||||
if keyFile[i] == 0x00 {
|
||||
// Public key only.
|
||||
@@ -72,9 +74,9 @@ class FindMyKeyDecoder {
|
||||
throw ParsingError.wrongFormat
|
||||
}
|
||||
// Step over 0x01
|
||||
i+=1
|
||||
i += 1
|
||||
// Read the key (starting with 0x04)
|
||||
let fullKey = keyFile.subdata(in: i..<i+85)
|
||||
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)
|
||||
@@ -84,14 +86,15 @@ class FindMyKeyDecoder {
|
||||
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)
|
||||
let fmKey = FindMyKey(
|
||||
advertisedKey: advertisedKey,
|
||||
hashedKey: hashedKey,
|
||||
privateKey: fullKey,
|
||||
startTime: nil,
|
||||
duration: nil,
|
||||
pu: nil,
|
||||
yCoordinate: yCoordinate,
|
||||
fullKey: fullKey)
|
||||
|
||||
keys.append(fmKey)
|
||||
}
|
||||
|
||||
@@ -5,8 +5,8 @@
|
||||
//
|
||||
// SPDX-License-Identifier: AGPL-3.0-only
|
||||
|
||||
import Foundation
|
||||
import CoreLocation
|
||||
import Foundation
|
||||
|
||||
struct FindMyDevice: Codable, Hashable {
|
||||
|
||||
@@ -15,7 +15,7 @@ struct FindMyDevice: Codable, Hashable {
|
||||
|
||||
var catalinaBigSurKeyFiles: [Data]?
|
||||
|
||||
/// KeyHash: Report results
|
||||
/// KeyHash: Report results.
|
||||
var reports: [FindMyReport]?
|
||||
|
||||
var decryptedReports: [FindMyLocationReport]?
|
||||
@@ -65,22 +65,22 @@ struct FindMyKey: Codable {
|
||||
self.fullKey = try? container.decode(Data.self, forKey: .fullKey)
|
||||
}
|
||||
|
||||
/// The advertising key
|
||||
/// The advertising key.
|
||||
let advertisedKey: Data
|
||||
/// Hashed advertisement key using SHA256
|
||||
/// Hashed advertisement key using SHA256.
|
||||
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
|
||||
/// When this key was used to send out BLE advertisements
|
||||
/// 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
|
||||
/// 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
|
||||
/// As exported from Big Sur.
|
||||
let yCoordinate: Data?
|
||||
/// As exported from BigSur
|
||||
/// As exported from Big Sur.
|
||||
let fullKey: Data?
|
||||
}
|
||||
|
||||
@@ -108,7 +108,7 @@ struct FindMyReport: Codable {
|
||||
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 dP = Date(timeIntervalSince1970: dateTimestamp / 1000)
|
||||
let df = DateFormatter()
|
||||
df.dateFormat = "YYYY-MM-dd"
|
||||
|
||||
|
||||
@@ -31,12 +31,12 @@ class AccessoryController: ObservableObject {
|
||||
func updateWithDecryptedReports(devices: [FindMyDevice]) {
|
||||
// Assign last locations
|
||||
for device in FindMyController.shared.devices {
|
||||
if let idx = self.accessories.firstIndex(where: {$0.id == Int(device.deviceId)}) {
|
||||
if let idx = self.accessories.firstIndex(where: { $0.id == Int(device.deviceId) }) {
|
||||
self.objectWillChange.send()
|
||||
let accessory = self.accessories[idx]
|
||||
|
||||
let report = device.decryptedReports?
|
||||
.sorted(by: {$0.timestamp ?? Date.distantPast > $1.timestamp ?? Date.distantPast })
|
||||
.sorted(by: { $0.timestamp ?? Date.distantPast > $1.timestamp ?? Date.distantPast })
|
||||
.first
|
||||
|
||||
accessory.lastLocation = report?.location
|
||||
|
||||
@@ -6,18 +6,18 @@
|
||||
// SPDX-License-Identifier: AGPL-3.0-only
|
||||
|
||||
import Foundation
|
||||
import Security
|
||||
import OSLog
|
||||
import Security
|
||||
|
||||
struct KeychainController {
|
||||
|
||||
static func loadAccessoriesFromKeychain(test: Bool=false) -> [Accessory] {
|
||||
static func loadAccessoriesFromKeychain(test: Bool = false) -> [Accessory] {
|
||||
var query: [CFString: Any] = [
|
||||
kSecClass: kSecClassGenericPassword,
|
||||
kSecAttrLabel: "FindMyAccessories",
|
||||
kSecAttrService: "SEEMOO-FINDMY",
|
||||
kSecMatchLimit: kSecMatchLimitOne,
|
||||
kSecReturnData: true
|
||||
kSecReturnData: true,
|
||||
]
|
||||
|
||||
if test {
|
||||
@@ -27,7 +27,8 @@ struct KeychainController {
|
||||
var result: CFTypeRef?
|
||||
let status = SecItemCopyMatching(query as CFDictionary, &result)
|
||||
guard status == errSecSuccess,
|
||||
let resultData = result as? Data else {
|
||||
let resultData = result as? Data
|
||||
else {
|
||||
return []
|
||||
}
|
||||
|
||||
@@ -42,13 +43,13 @@ struct KeychainController {
|
||||
return []
|
||||
}
|
||||
|
||||
static func storeInKeychain(accessories: [Accessory], test: Bool=false) throws {
|
||||
static func storeInKeychain(accessories: [Accessory], test: Bool = false) throws {
|
||||
// Store or update
|
||||
var attributes: [CFString: Any] = [
|
||||
kSecClass: kSecClassGenericPassword,
|
||||
kSecAttrLabel: "FindMyAccessories",
|
||||
kSecAttrService: "SEEMOO-FINDMY",
|
||||
kSecValueData: try PropertyListEncoder().encode(accessories)
|
||||
kSecValueData: try PropertyListEncoder().encode(accessories),
|
||||
]
|
||||
|
||||
if test {
|
||||
@@ -62,7 +63,7 @@ struct KeychainController {
|
||||
var query: [CFString: Any] = [
|
||||
kSecClass: kSecClassGenericPassword,
|
||||
kSecAttrLabel: "FindMyAccessories",
|
||||
kSecAttrService: "SEEMOO-FINDMY"
|
||||
kSecAttrService: "SEEMOO-FINDMY",
|
||||
]
|
||||
|
||||
if test {
|
||||
|
||||
@@ -5,13 +5,13 @@
|
||||
//
|
||||
// SPDX-License-Identifier: AGPL-3.0-only
|
||||
|
||||
import AppKit
|
||||
import Foundation
|
||||
import OSLog
|
||||
import AppKit
|
||||
|
||||
let mailBundleName = "OpenHaystackMail"
|
||||
|
||||
/// Manages plugin installation
|
||||
/// Manages plugin installation.
|
||||
struct MailPluginManager {
|
||||
|
||||
let pluginsFolderURL = FileManager.default.homeDirectoryForCurrentUser.appendingPathComponent("Library/Mail/Bundles")
|
||||
@@ -22,7 +22,7 @@ struct MailPluginManager {
|
||||
return FileManager.default.fileExists(atPath: pluginURL.path)
|
||||
}
|
||||
|
||||
/// Shows a NSSavePanel to install the mail plugin at the required place
|
||||
/// Shows a NSSavePanel to install the mail plugin at the required place.
|
||||
func askForPermission() -> Bool {
|
||||
|
||||
let panel = NSSavePanel()
|
||||
@@ -73,11 +73,12 @@ struct MailPluginManager {
|
||||
|
||||
}
|
||||
|
||||
/// Copy a folder recursively
|
||||
/// Copy a folder recursively.
|
||||
///
|
||||
/// - Parameters:
|
||||
/// - from: Folder source
|
||||
/// - to: Folder destination
|
||||
/// - Throws: An error if copying or acessing files fails
|
||||
/// - Throws: An error if copying or acessing files fails
|
||||
func copyFolder(from: URL, to: URL) throws {
|
||||
// Create the folder
|
||||
try? FileManager.default.createDirectory(at: to, withIntermediateDirectories: false, attributes: nil)
|
||||
@@ -102,11 +103,13 @@ struct MailPluginManager {
|
||||
try FileManager.default.removeItem(at: pluginURL)
|
||||
}
|
||||
|
||||
/// Copy plugin to downloads folder
|
||||
/// Copy plugin to downloads folder.
|
||||
///
|
||||
/// - Throws: An error if the copy fails, because of missing permissions
|
||||
func pluginDownload() throws {
|
||||
func pluginDownload() throws {
|
||||
guard let localPluginURL = Bundle.main.url(forResource: mailBundleName, withExtension: "mailbundle"),
|
||||
let downloadsFolder = FileManager.default.urls(for: .downloadsDirectory, in: .userDomainMask).first else {
|
||||
let downloadsFolder = FileManager.default.urls(for: .downloadsDirectory, in: .userDomainMask).first
|
||||
else {
|
||||
throw PluginError.downloadFailed
|
||||
}
|
||||
|
||||
|
||||
@@ -9,19 +9,21 @@ import Foundation
|
||||
|
||||
struct MicrobitController {
|
||||
|
||||
/// Find all microbits connected to this mac
|
||||
/// Find all microbits connected to this Mac.
|
||||
///
|
||||
/// - Throws: If a volume is inaccessible
|
||||
/// - Returns: an array of urls
|
||||
static func findMicrobits() throws -> [URL] {
|
||||
let fm = FileManager.default
|
||||
let volumes = try fm.contentsOfDirectory(atPath: "/Volumes")
|
||||
|
||||
let microbits: [URL] = volumes.filter({$0.lowercased().contains("microbit")}).map({URL(fileURLWithPath: "/Volumes").appendingPathComponent($0)})
|
||||
let microbits: [URL] = volumes.filter({ $0.lowercased().contains("microbit") }).map({ URL(fileURLWithPath: "/Volumes").appendingPathComponent($0) })
|
||||
|
||||
return microbits
|
||||
}
|
||||
|
||||
/// Deploy the firmware to a USB connected microbit at the given URL
|
||||
/// Deploy the firmware to a USB connected microbit at the given URL.
|
||||
///
|
||||
/// - Parameters:
|
||||
/// - microbitURL: URL to the microbit
|
||||
/// - firmwareFile: Firmware file as binary data
|
||||
@@ -32,6 +34,7 @@ struct MicrobitController {
|
||||
}
|
||||
|
||||
/// Patch the given firmware.
|
||||
///
|
||||
/// This will replace the pattern data (the place for the key) with the actual key
|
||||
/// - Parameters:
|
||||
/// - firmware: The firmware data that should be patched
|
||||
|
||||
@@ -5,11 +5,11 @@
|
||||
//
|
||||
// SPDX-License-Identifier: AGPL-3.0-only
|
||||
|
||||
import Foundation
|
||||
import CoreLocation
|
||||
import CryptoKit
|
||||
import Foundation
|
||||
import Security
|
||||
import SwiftUI
|
||||
import CoreLocation
|
||||
|
||||
class Accessory: ObservableObject, Codable, Identifiable, Equatable {
|
||||
let name: String
|
||||
@@ -39,9 +39,10 @@ class Accessory: ObservableObject, Codable, Identifiable, Equatable {
|
||||
self.privateKey = try container.decode(Data.self, forKey: .privateKey)
|
||||
self.icon = (try? container.decode(String.self, forKey: .icon)) ?? "briefcase.fill"
|
||||
|
||||
if var colorComponents = try? container.decode([CGFloat].self, forKey: .colorComponents),
|
||||
if var colorComponents = try? container.decode([CGFloat].self, forKey: .colorComponents),
|
||||
let spaceName = try? container.decode(String.self, forKey: .colorSpaceName),
|
||||
let cgColor = CGColor(colorSpace: CGColorSpace(name: spaceName as CFString)!, components: &colorComponents) {
|
||||
let cgColor = CGColor(colorSpace: CGColorSpace(name: spaceName as CFString)!, components: &colorComponents)
|
||||
{
|
||||
self.color = Color(cgColor)
|
||||
} else {
|
||||
self.color = Color.white
|
||||
@@ -57,7 +58,8 @@ class Accessory: ObservableObject, Codable, Identifiable, Equatable {
|
||||
try container.encode(self.icon, forKey: .icon)
|
||||
|
||||
if let colorComponents = self.color.cgColor?.components,
|
||||
let colorSpace = self.color.cgColor?.colorSpace?.name {
|
||||
let colorSpace = self.color.cgColor?.colorSpace?.name
|
||||
{
|
||||
try container.encode(colorComponents, forKey: .colorComponents)
|
||||
try container.encode(colorSpace as String, forKey: .colorSpaceName)
|
||||
}
|
||||
@@ -79,7 +81,7 @@ class Accessory: ObservableObject, Codable, Identifiable, Equatable {
|
||||
// Drop the first byte to just have the 28 bytes version
|
||||
publicKey = publicKey.dropFirst()
|
||||
assert(publicKey.count == 28)
|
||||
guard publicKey.count == 28 else {throw KeyError.keyDerivationFailed}
|
||||
guard publicKey.count == 28 else { throw KeyError.keyDerivationFailed }
|
||||
|
||||
return publicKey
|
||||
}
|
||||
@@ -103,20 +105,22 @@ class Accessory: ObservableObject, Codable, Identifiable, Equatable {
|
||||
|
||||
func toFindMyDevice() throws -> FindMyDevice {
|
||||
|
||||
let findMyKey = FindMyKey(advertisedKey: try self.getAdvertisementKey(),
|
||||
hashedKey: try self.hashedPublicKey(),
|
||||
privateKey: self.privateKey,
|
||||
startTime: nil,
|
||||
duration: nil,
|
||||
pu: nil,
|
||||
yCoordinate: nil,
|
||||
fullKey: nil)
|
||||
let findMyKey = FindMyKey(
|
||||
advertisedKey: try self.getAdvertisementKey(),
|
||||
hashedKey: try self.hashedPublicKey(),
|
||||
privateKey: self.privateKey,
|
||||
startTime: nil,
|
||||
duration: nil,
|
||||
pu: nil,
|
||||
yCoordinate: nil,
|
||||
fullKey: nil)
|
||||
|
||||
return FindMyDevice(deviceId: String(self.id),
|
||||
keys: [findMyKey],
|
||||
catalinaBigSurKeyFiles: nil,
|
||||
reports: nil,
|
||||
decryptedReports: nil)
|
||||
return FindMyDevice(
|
||||
deviceId: String(self.id),
|
||||
keys: [findMyKey],
|
||||
catalinaBigSurKeyFiles: nil,
|
||||
reports: nil,
|
||||
decryptedReports: nil)
|
||||
}
|
||||
|
||||
enum CodingKeys: String, CodingKey {
|
||||
|
||||
@@ -20,19 +20,19 @@ struct PreviewData {
|
||||
let longitude: Double = 13.413306
|
||||
|
||||
let backpack = try! Accessory(name: "Backpack", color: Color.green, iconName: "briefcase.fill")
|
||||
backpack.lastLocation = CLLocation(latitude: latitude + (Double(arc4random() % 1000))/100000, longitude: longitude + (Double(arc4random() % 1000))/100000)
|
||||
backpack.lastLocation = CLLocation(latitude: latitude + (Double(arc4random() % 1000)) / 100000, longitude: longitude + (Double(arc4random() % 1000)) / 100000)
|
||||
|
||||
let bag = try! Accessory(name: "Bag", color: Color.blue, iconName: "latch.2.case.fill")
|
||||
bag.lastLocation = CLLocation(latitude: latitude + (Double(arc4random() % 1000))/100000, longitude: longitude + (Double(arc4random() % 1000))/100000)
|
||||
bag.lastLocation = CLLocation(latitude: latitude + (Double(arc4random() % 1000)) / 100000, longitude: longitude + (Double(arc4random() % 1000)) / 100000)
|
||||
|
||||
let car = try! Accessory(name: "Car", color: Color.red, iconName: "car.fill")
|
||||
car.lastLocation = CLLocation(latitude: latitude + (Double(arc4random() % 1000))/100000, longitude: longitude + (Double(arc4random() % 1000))/100000)
|
||||
car.lastLocation = CLLocation(latitude: latitude + (Double(arc4random() % 1000)) / 100000, longitude: longitude + (Double(arc4random() % 1000)) / 100000)
|
||||
|
||||
let keys = try! Accessory(name: "Keys", color: Color.orange, iconName: "key.fill")
|
||||
keys.lastLocation = CLLocation(latitude: latitude + (Double(arc4random() % 1000))/100000, longitude: longitude + (Double(arc4random() % 1000))/100000)
|
||||
keys.lastLocation = CLLocation(latitude: latitude + (Double(arc4random() % 1000)) / 100000, longitude: longitude + (Double(arc4random() % 1000)) / 100000)
|
||||
|
||||
let items = try! Accessory(name: "Items", color: Color.gray, iconName: "mappin")
|
||||
items.lastLocation = CLLocation(latitude: latitude + (Double(arc4random() % 1000))/100000, longitude: longitude + (Double(arc4random() % 1000))/100000)
|
||||
items.lastLocation = CLLocation(latitude: latitude + (Double(arc4random() % 1000)) / 100000, longitude: longitude + (Double(arc4random() % 1000)) / 100000)
|
||||
|
||||
return [backpack, bag, car, keys, items]
|
||||
}
|
||||
|
||||
@@ -5,8 +5,8 @@
|
||||
//
|
||||
// SPDX-License-Identifier: AGPL-3.0-only
|
||||
|
||||
import SwiftUI
|
||||
import OSLog
|
||||
import SwiftUI
|
||||
|
||||
struct AccessoryListEntry: View {
|
||||
var accessory: Accessory
|
||||
@@ -18,39 +18,47 @@ struct AccessoryListEntry: View {
|
||||
var body: some View {
|
||||
VStack {
|
||||
HStack {
|
||||
Button(action: {
|
||||
self.zoomOn(self.accessory)
|
||||
}, label: {
|
||||
HStack {
|
||||
Text(accessory.name)
|
||||
Spacer()
|
||||
Button(
|
||||
action: {
|
||||
self.zoomOn(self.accessory)
|
||||
},
|
||||
label: {
|
||||
HStack {
|
||||
Text(accessory.name)
|
||||
Spacer()
|
||||
}
|
||||
.contentShape(Rectangle())
|
||||
}
|
||||
.contentShape(Rectangle())
|
||||
})
|
||||
)
|
||||
.buttonStyle(PlainButtonStyle())
|
||||
|
||||
HStack(alignment: .center) {
|
||||
|
||||
Button(action: {self.zoomOn(self.accessory)}, label: {
|
||||
Circle()
|
||||
.strokeBorder(accessory.color, lineWidth: 2.0)
|
||||
.background(
|
||||
ZStack {
|
||||
Circle().fill(Color("PinColor"))
|
||||
Image(systemName: accessory.icon)
|
||||
.padding(3)
|
||||
}
|
||||
Button(
|
||||
action: { self.zoomOn(self.accessory) },
|
||||
label: {
|
||||
Circle()
|
||||
.strokeBorder(accessory.color, lineWidth: 2.0)
|
||||
.background(
|
||||
ZStack {
|
||||
Circle().fill(Color("PinColor"))
|
||||
Image(systemName: accessory.icon)
|
||||
.padding(3)
|
||||
}
|
||||
)
|
||||
|
||||
.frame(width: 30, height: 30)
|
||||
})
|
||||
.frame(width: 30, height: 30)
|
||||
}
|
||||
)
|
||||
.buttonStyle(PlainButtonStyle())
|
||||
|
||||
Button(action: {
|
||||
self.deployAccessoryToMicrobit(accessory)
|
||||
}, label: {
|
||||
Text("Deploy")
|
||||
})
|
||||
Button(
|
||||
action: {
|
||||
self.deployAccessoryToMicrobit(accessory)
|
||||
},
|
||||
label: {
|
||||
Text("Deploy")
|
||||
})
|
||||
|
||||
}
|
||||
.padding(.trailing)
|
||||
@@ -60,10 +68,10 @@ struct AccessoryListEntry: View {
|
||||
}
|
||||
.contentShape(Rectangle())
|
||||
.contextMenu {
|
||||
Button("Delete", action: {self.delete(accessory)})
|
||||
Button("Delete", action: { self.delete(accessory) })
|
||||
Divider()
|
||||
Button("Copy advertisment key (Base64)", action: {self.copyPublicKey(of: accessory)})
|
||||
Button("Copy key id (Base64)", action: {self.copyPublicKeyHash(of: accessory)})
|
||||
Button("Copy advertisment key (Base64)", action: { self.copyPublicKey(of: accessory) })
|
||||
Button("Copy key id (Base64)", action: { self.copyPublicKeyHash(of: accessory) })
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -41,7 +41,7 @@ class AccessoryAnnotationView: MKAnnotationView {
|
||||
}
|
||||
|
||||
func updateView() {
|
||||
guard let accessory = (self.annotation as? AccessoryAnnotation)?.accessory else {return}
|
||||
guard let accessory = (self.annotation as? AccessoryAnnotation)?.accessory else { return }
|
||||
self.pinView?.removeFromSuperview()
|
||||
self.pinView = NSHostingView(rootView: AccessoryPinView(accessory: accessory))
|
||||
|
||||
@@ -71,39 +71,39 @@ class AccessoryAnnotationView: MKAnnotationView {
|
||||
self.canShowCallout = true
|
||||
}
|
||||
|
||||
// override func draw(_ dirtyRect: NSRect) {
|
||||
// guard let accessoryAnnotation = self.annotation as? AccessoryAnnotation else {
|
||||
// super.draw(dirtyRect)
|
||||
// return
|
||||
// }
|
||||
//
|
||||
// let path = NSBezierPath(ovalIn: dirtyRect)
|
||||
// path.lineWidth = 2.0
|
||||
//
|
||||
// guard let cgColor = accessoryAnnotation.accessory.color.cgColor,
|
||||
// let strokeColor = NSColor(cgColor: cgColor)?.withAlphaComponent(0.8) else {return}
|
||||
//
|
||||
// NSColor(named: NSColor.Name("PinColor"))?.setFill()
|
||||
//
|
||||
// path.fill()
|
||||
//
|
||||
// strokeColor.setStroke()
|
||||
// path.stroke()
|
||||
//
|
||||
// let accessory = accessoryAnnotation.accessory
|
||||
//
|
||||
// guard let image = NSImage(systemSymbolName: accessory.icon, accessibilityDescription: accessory.name) else {return}
|
||||
//
|
||||
// let ratio = image.size.width / image.size.height
|
||||
// let imageWidth: CGFloat = 20
|
||||
// let imageHeight = imageWidth / ratio
|
||||
// let imageRect = NSRect(
|
||||
// x: dirtyRect.width/2 - imageWidth/2,
|
||||
// y: dirtyRect.height/2 - imageHeight/2,
|
||||
// width: imageWidth, height: imageHeight)
|
||||
//
|
||||
// image.draw(in: imageRect)
|
||||
// }
|
||||
// override func draw(_ dirtyRect: NSRect) {
|
||||
// guard let accessoryAnnotation = self.annotation as? AccessoryAnnotation else {
|
||||
// super.draw(dirtyRect)
|
||||
// return
|
||||
// }
|
||||
//
|
||||
// let path = NSBezierPath(ovalIn: dirtyRect)
|
||||
// path.lineWidth = 2.0
|
||||
//
|
||||
// guard let cgColor = accessoryAnnotation.accessory.color.cgColor,
|
||||
// let strokeColor = NSColor(cgColor: cgColor)?.withAlphaComponent(0.8) else {return}
|
||||
//
|
||||
// NSColor(named: NSColor.Name("PinColor"))?.setFill()
|
||||
//
|
||||
// path.fill()
|
||||
//
|
||||
// strokeColor.setStroke()
|
||||
// path.stroke()
|
||||
//
|
||||
// let accessory = accessoryAnnotation.accessory
|
||||
//
|
||||
// guard let image = NSImage(systemSymbolName: accessory.icon, accessibilityDescription: accessory.name) else {return}
|
||||
//
|
||||
// let ratio = image.size.width / image.size.height
|
||||
// let imageWidth: CGFloat = 20
|
||||
// let imageHeight = imageWidth / ratio
|
||||
// let imageRect = NSRect(
|
||||
// x: dirtyRect.width/2 - imageWidth/2,
|
||||
// y: dirtyRect.height/2 - imageHeight/2,
|
||||
// width: imageWidth, height: imageHeight)
|
||||
//
|
||||
// image.draw(in: imageRect)
|
||||
// }
|
||||
|
||||
struct AccessoryPinView: View {
|
||||
var accessory: Accessory
|
||||
|
||||
@@ -7,8 +7,8 @@
|
||||
//
|
||||
|
||||
import Foundation
|
||||
import SwiftUI
|
||||
import MapKit
|
||||
import SwiftUI
|
||||
|
||||
struct AccessoryMapView: NSViewControllerRepresentable {
|
||||
@ObservedObject var accessoryController: AccessoryController
|
||||
|
||||
@@ -5,9 +5,9 @@
|
||||
//
|
||||
// SPDX-License-Identifier: AGPL-3.0-only
|
||||
|
||||
import AppKit
|
||||
import Foundation
|
||||
import SwiftUI
|
||||
import AppKit
|
||||
|
||||
final class ActivityIndicator: NSViewRepresentable {
|
||||
|
||||
|
||||
@@ -16,24 +16,29 @@ struct IconSelectionView: View {
|
||||
var body: some View {
|
||||
|
||||
ZStack {
|
||||
Button(action: {
|
||||
withAnimation {
|
||||
self.showImagePicker.toggle()
|
||||
Button(
|
||||
action: {
|
||||
withAnimation {
|
||||
self.showImagePicker.toggle()
|
||||
}
|
||||
},
|
||||
label: {
|
||||
Circle()
|
||||
.strokeBorder(Color.gray, lineWidth: 0.5)
|
||||
.background(
|
||||
Image(systemName: self.selectedImageName)
|
||||
)
|
||||
.frame(width: 30, height: 30)
|
||||
}
|
||||
}, label: {
|
||||
Circle()
|
||||
.strokeBorder(Color.gray, lineWidth: 0.5)
|
||||
.background(
|
||||
Image(systemName: self.selectedImageName)
|
||||
)
|
||||
.frame(width: 30, height: 30)
|
||||
})
|
||||
)
|
||||
.buttonStyle(PlainButtonStyle())
|
||||
.popover(isPresented: self.$showImagePicker, content: {
|
||||
ImageSelectionList(selectedImageName: self.$selectedImageName) {
|
||||
self.showImagePicker = false
|
||||
}
|
||||
})
|
||||
.popover(
|
||||
isPresented: self.$showImagePicker,
|
||||
content: {
|
||||
ImageSelectionList(selectedImageName: self.$selectedImageName) {
|
||||
self.showImagePicker = false
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -59,16 +64,19 @@ struct ImageSelectionList: View {
|
||||
|
||||
var body: some View {
|
||||
List(self.selectableIcons, id: \.self) { iconName in
|
||||
Button(action: {
|
||||
self.selectedImageName = iconName
|
||||
self.dismiss()
|
||||
}, label: {
|
||||
HStack {
|
||||
Spacer()
|
||||
Image(systemName: iconName)
|
||||
Spacer()
|
||||
Button(
|
||||
action: {
|
||||
self.selectedImageName = iconName
|
||||
self.dismiss()
|
||||
},
|
||||
label: {
|
||||
HStack {
|
||||
Spacer()
|
||||
Image(systemName: iconName)
|
||||
Spacer()
|
||||
}
|
||||
}
|
||||
})
|
||||
)
|
||||
.buttonStyle(PlainButtonStyle())
|
||||
.contentShape(Rectangle())
|
||||
}
|
||||
|
||||
@@ -5,9 +5,9 @@
|
||||
//
|
||||
// SPDX-License-Identifier: AGPL-3.0-only
|
||||
|
||||
import SwiftUI
|
||||
import OSLog
|
||||
import MapKit
|
||||
import OSLog
|
||||
import SwiftUI
|
||||
|
||||
struct OpenHaystackMainView: View {
|
||||
|
||||
@@ -63,19 +63,25 @@ struct OpenHaystackMainView: View {
|
||||
|
||||
}
|
||||
}
|
||||
.alert(item: self.$alertType, content: { alertType in
|
||||
return self.alert(for: alertType)
|
||||
})
|
||||
.alert(
|
||||
item: self.$alertType,
|
||||
content: { alertType in
|
||||
return self.alert(for: alertType)
|
||||
}
|
||||
)
|
||||
.onChange(of: self.searchPartyToken) { (searchPartyToken) in
|
||||
guard !searchPartyToken.isEmpty, self.accessories.isEmpty == false else {return}
|
||||
guard !searchPartyToken.isEmpty, self.accessories.isEmpty == false else { return }
|
||||
self.downloadLocationReports()
|
||||
}
|
||||
.onChange(of: self.popUpAlertType, perform: { popUpAlert in
|
||||
guard popUpAlert != nil else {return}
|
||||
DispatchQueue.main.asyncAfter(deadline: .now() + 2) {
|
||||
self.popUpAlertType = nil
|
||||
.onChange(
|
||||
of: self.popUpAlertType,
|
||||
perform: { popUpAlert in
|
||||
guard popUpAlert != nil else { return }
|
||||
DispatchQueue.main.asyncAfter(deadline: .now() + 2) {
|
||||
self.popUpAlertType = nil
|
||||
}
|
||||
}
|
||||
})
|
||||
)
|
||||
.onAppear {
|
||||
self.onAppear()
|
||||
}
|
||||
@@ -105,9 +111,12 @@ struct OpenHaystackMainView: View {
|
||||
IconSelectionView(selectedImageName: self.$selectedIcon)
|
||||
}
|
||||
|
||||
Button(action: self.addAccessory, label: {
|
||||
Text("Generate key and deploy")
|
||||
})
|
||||
Button(
|
||||
action: self.addAccessory,
|
||||
label: {
|
||||
Text("Generate key and deploy")
|
||||
}
|
||||
)
|
||||
.disabled(self.keyName.isEmpty)
|
||||
.padding(.bottom)
|
||||
|
||||
@@ -130,20 +139,21 @@ struct OpenHaystackMainView: View {
|
||||
}
|
||||
}
|
||||
|
||||
/// Accessory List view
|
||||
/// Accessory List view.
|
||||
var accessoryList: some View {
|
||||
List(self.accessories) { accessory in
|
||||
AccessoryListEntry(accessory: accessory,
|
||||
alertType: self.$alertType,
|
||||
delete: self.delete(accessory:),
|
||||
deployAccessoryToMicrobit: self.deployAccessoryToMicrobit(accessory:),
|
||||
zoomOn: {self.focusedAccessory = $0})
|
||||
AccessoryListEntry(
|
||||
accessory: accessory,
|
||||
alertType: self.$alertType,
|
||||
delete: self.delete(accessory:),
|
||||
deployAccessoryToMicrobit: self.deployAccessoryToMicrobit(accessory:),
|
||||
zoomOn: { self.focusedAccessory = $0 })
|
||||
}
|
||||
.background(Color.clear)
|
||||
.cornerRadius(15.0)
|
||||
}
|
||||
|
||||
/// Overlay for the map that is gray and shows an activity indicator when loading
|
||||
/// Overlay for the map that is gray and shows an activity indicator when loading.
|
||||
var mapOverlay: some View {
|
||||
ZStack {
|
||||
if self.isLoading {
|
||||
@@ -177,10 +187,13 @@ struct OpenHaystackMainView: View {
|
||||
.pickerStyle(SegmentedPickerStyle())
|
||||
.frame(width: 150, alignment: .center)
|
||||
|
||||
Button(action: self.downloadLocationReports, label: {
|
||||
Image(systemName: "arrow.clockwise")
|
||||
Text("Reload")
|
||||
})
|
||||
Button(
|
||||
action: self.downloadLocationReports,
|
||||
label: {
|
||||
Image(systemName: "arrow.clockwise")
|
||||
Text("Reload")
|
||||
}
|
||||
)
|
||||
.opacity(1.0)
|
||||
.disabled(self.accessories.isEmpty)
|
||||
}
|
||||
@@ -189,7 +202,7 @@ struct OpenHaystackMainView: View {
|
||||
}
|
||||
}
|
||||
|
||||
/// Add an accessory with the provided details
|
||||
/// Add an accessory with the provided details.
|
||||
func addAccessory() {
|
||||
let keyName = self.keyName
|
||||
self.keyName = ""
|
||||
@@ -223,7 +236,8 @@ struct OpenHaystackMainView: View {
|
||||
}
|
||||
|
||||
guard !self.searchPartyToken.isEmpty,
|
||||
let tokenData = self.searchPartyToken.data(using: .utf8) else {
|
||||
let tokenData = self.searchPartyToken.data(using: .utf8)
|
||||
else {
|
||||
self.alertType = .searchPartyToken
|
||||
return
|
||||
}
|
||||
@@ -244,7 +258,7 @@ struct OpenHaystackMainView: View {
|
||||
FindMyController.shared.devices = findMyDevices
|
||||
FindMyController.shared.fetchReports(with: tokenData) { error in
|
||||
|
||||
let reports = FindMyController.shared.devices.compactMap({$0.reports}).flatMap({$0})
|
||||
let reports = FindMyController.shared.devices.compactMap({ $0.reports }).flatMap({ $0 })
|
||||
if reports.isEmpty {
|
||||
withAnimation {
|
||||
self.popUpAlertType = .noReportsFound
|
||||
@@ -257,7 +271,7 @@ struct OpenHaystackMainView: View {
|
||||
self.isLoading = false
|
||||
}
|
||||
|
||||
guard error != nil else {return}
|
||||
guard error != nil else { return }
|
||||
os_log("Error: %@", String(describing: error))
|
||||
|
||||
}
|
||||
@@ -265,11 +279,11 @@ struct OpenHaystackMainView: View {
|
||||
|
||||
}
|
||||
|
||||
/// Delete an accessory from the list of accessories
|
||||
/// Delete an accessory from the list of accessories.
|
||||
func delete(accessory: Accessory) {
|
||||
do {
|
||||
var accessories = self.accessories
|
||||
guard let idx = accessories.firstIndex(of: accessory) else {return}
|
||||
guard let idx = accessories.firstIndex(of: accessory) else { return }
|
||||
|
||||
accessories.remove(at: idx)
|
||||
|
||||
@@ -284,12 +298,13 @@ struct OpenHaystackMainView: View {
|
||||
|
||||
}
|
||||
|
||||
/// Deploy the public key of the accessory to a BBC microbit
|
||||
/// Deploy the public key of the accessory to a BBC microbit.
|
||||
func deployAccessoryToMicrobit(accessory: Accessory) {
|
||||
do {
|
||||
let microbits = try MicrobitController.findMicrobits()
|
||||
guard let microBitURL = microbits.first,
|
||||
let firmwareURL = Bundle.main.url(forResource: "firmware", withExtension: "bin") else {
|
||||
let firmwareURL = Bundle.main.url(forResource: "firmware", withExtension: "bin")
|
||||
else {
|
||||
self.alertType = .deployFailed
|
||||
return
|
||||
}
|
||||
@@ -314,7 +329,8 @@ struct OpenHaystackMainView: View {
|
||||
/// Checks if the search party token can be fetched without the Mail Plugin. If true the plugin is not needed for this environment. (e.g. when SIP is disabled)
|
||||
let reportsFetcher = ReportsFetcher()
|
||||
if let token = reportsFetcher.fetchSearchpartyToken(),
|
||||
let tokenString = String(data: token, encoding: .ascii) {
|
||||
let tokenString = String(data: token, encoding: .ascii)
|
||||
{
|
||||
self.searchPartyToken = tokenString
|
||||
return
|
||||
}
|
||||
@@ -330,7 +346,7 @@ struct OpenHaystackMainView: View {
|
||||
}
|
||||
}
|
||||
|
||||
/// Ask to install and activate the mail plugin
|
||||
/// Ask to install and activate the mail plugin.
|
||||
func installMailPlugin() {
|
||||
let pluginManager = MailPluginManager()
|
||||
guard pluginManager.isMailPluginInstalled == false else {
|
||||
@@ -338,7 +354,7 @@ struct OpenHaystackMainView: View {
|
||||
return
|
||||
}
|
||||
do {
|
||||
try pluginManager.installMailPlugin()
|
||||
try pluginManager.installMailPlugin()
|
||||
} catch {
|
||||
DispatchQueue.main.async {
|
||||
self.alertType = .pluginInstallFailed
|
||||
@@ -371,8 +387,8 @@ struct OpenHaystackMainView: View {
|
||||
}
|
||||
}
|
||||
completion?(false)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -386,7 +402,8 @@ struct OpenHaystackMainView: View {
|
||||
|
||||
// MARK: - Alerts
|
||||
|
||||
/// Create an alert for the given alert type
|
||||
/// Create an alert for the given alert type.
|
||||
///
|
||||
/// - Parameter alertType: current alert type
|
||||
/// - Returns: A SwiftUI Alert
|
||||
func alert(for alertType: AlertType) -> Alert {
|
||||
@@ -394,51 +411,60 @@ struct OpenHaystackMainView: View {
|
||||
case .keyError:
|
||||
return Alert(title: Text("Could not create accessory"), message: Text(String(describing: self.errorDescription)), dismissButton: Alert.Button.cancel())
|
||||
case .searchPartyToken:
|
||||
return Alert(title: Text("Add the search party token"),
|
||||
message: Text(
|
||||
"""
|
||||
Please paste the search party token below after copying itfrom the macOS Keychain.
|
||||
The item that contains the key can be found by searching for:
|
||||
com.apple.account.DeviceLocator.search-party-token
|
||||
"""
|
||||
),
|
||||
dismissButton: Alert.Button.okay())
|
||||
return Alert(
|
||||
title: Text("Add the search party token"),
|
||||
message: Text(
|
||||
"""
|
||||
Please paste the search party token below after copying itfrom the macOS Keychain.
|
||||
The item that contains the key can be found by searching for:
|
||||
com.apple.account.DeviceLocator.search-party-token
|
||||
"""
|
||||
),
|
||||
dismissButton: Alert.Button.okay())
|
||||
case .deployFailed:
|
||||
return Alert(title: Text("Could not deploy"),
|
||||
message: Text("Deploying to microbit failed. Please reconnect the device over USB"),
|
||||
dismissButton: Alert.Button.okay())
|
||||
return Alert(
|
||||
title: Text("Could not deploy"),
|
||||
message: Text("Deploying to microbit failed. Please reconnect the device over USB"),
|
||||
dismissButton: Alert.Button.okay())
|
||||
case .deployedSuccessfully:
|
||||
return Alert(title: Text("Deploy successfull"),
|
||||
message: Text("This device will now be tracked by all iPhones and you can use this app to find its last reported location"),
|
||||
dismissButton: Alert.Button.okay())
|
||||
return Alert(
|
||||
title: Text("Deploy successfull"),
|
||||
message: Text("This device will now be tracked by all iPhones and you can use this app to find its last reported location"),
|
||||
dismissButton: Alert.Button.okay())
|
||||
case .deletionFailed:
|
||||
return Alert(title: Text("Could not delete accessory"), dismissButton: Alert.Button.okay())
|
||||
|
||||
case .noReportsFound:
|
||||
return Alert(title: Text("No reports found"),
|
||||
message: Text("Your accessory might have not been found yet or it is not powered. Make sure it has enough power to be found by nearby iPhones"),
|
||||
dismissButton: Alert.Button.okay())
|
||||
return Alert(
|
||||
title: Text("No reports found"),
|
||||
message: Text("Your accessory might have not been found yet or it is not powered. Make sure it has enough power to be found by nearby iPhones"),
|
||||
dismissButton: Alert.Button.okay())
|
||||
case .activatePlugin:
|
||||
let message =
|
||||
"""
|
||||
To access your Apple ID for downloading location reports we need to use a plugin in Apple Mail.
|
||||
Please make sure Apple Mail is running.
|
||||
Open Mail -> Preferences -> General -> Manage Plug-Ins... -> Select Haystack
|
||||
"""
|
||||
To access your Apple ID for downloading location reports we need to use a plugin in Apple Mail.
|
||||
Please make sure Apple Mail is running.
|
||||
Open Mail -> Preferences -> General -> Manage Plug-Ins... -> Select Haystack
|
||||
|
||||
We do not access any of your e-mail data. This is just necessary, because Apple blocks access to certain iCloud tokens otherwise.
|
||||
"""
|
||||
We do not access any of your e-mail data. This is just necessary, because Apple blocks access to certain iCloud tokens otherwise.
|
||||
"""
|
||||
|
||||
return Alert(title: Text("Install & Activate Mail Plugin"), message: Text(message),
|
||||
primaryButton: .default(Text("Okay"), action: {self.installMailPlugin()}),
|
||||
secondaryButton: .cancel())
|
||||
return Alert(
|
||||
title: Text("Install & Activate Mail Plugin"), message: Text(message),
|
||||
primaryButton: .default(Text("Okay"), action: { self.installMailPlugin() }),
|
||||
secondaryButton: .cancel())
|
||||
|
||||
case .pluginInstallFailed:
|
||||
return Alert(title: Text("Mail Plugin installation failed"),
|
||||
message: Text("To access the location reports of your devices an Apple Mail plugin is necessary" +
|
||||
"\nThe installtion of this plugin has failed.\n\n Please download it manually unzip it and move it to /Library/Mail/Bundles"),
|
||||
primaryButton: .default(Text("Download plug-in"), action: {
|
||||
self.downloadPlugin()
|
||||
}), secondaryButton: .cancel())
|
||||
return Alert(
|
||||
title: Text("Mail Plugin installation failed"),
|
||||
message: Text(
|
||||
"To access the location reports of your devices an Apple Mail plugin is necessary"
|
||||
+ "\nThe installtion of this plugin has failed.\n\n Please download it manually unzip it and move it to /Library/Mail/Bundles"),
|
||||
primaryButton: .default(
|
||||
Text("Download plug-in"),
|
||||
action: {
|
||||
self.downloadPlugin()
|
||||
}), secondaryButton: .cancel())
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -25,8 +25,9 @@ struct PopUpAlertView: View {
|
||||
}
|
||||
|
||||
}
|
||||
.background(RoundedRectangle(cornerRadius: 7.5)
|
||||
.fill(Color.gray))
|
||||
.background(
|
||||
RoundedRectangle(cornerRadius: 7.5)
|
||||
.fill(Color.gray))
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -25,7 +25,7 @@ final class MapViewController: NSViewController, MKMapViewDelegate {
|
||||
}
|
||||
|
||||
// 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 span = MKCoordinateSpan(latitudeDelta: 5.0, longitudeDelta: 5.0)
|
||||
let region = MKCoordinateRegion(center: coordinate, span: span)
|
||||
@@ -36,7 +36,7 @@ final class MapViewController: NSViewController, MKMapViewDelegate {
|
||||
// Add pins
|
||||
for device in devices {
|
||||
|
||||
guard let reports = device.decryptedReports else {continue}
|
||||
guard let reports = device.decryptedReports else { continue }
|
||||
for report in reports {
|
||||
let pin = MKPointAnnotation()
|
||||
pin.title = device.deviceId
|
||||
@@ -49,7 +49,7 @@ final class MapViewController: NSViewController, MKMapViewDelegate {
|
||||
|
||||
func zoom(on accessory: Accessory?) {
|
||||
self.focusedAccessory = accessory
|
||||
guard let location = accessory?.lastLocation else {return}
|
||||
guard let location = accessory?.lastLocation else { return }
|
||||
let span = MKCoordinateSpan(latitudeDelta: 0.005, longitudeDelta: 0.005)
|
||||
let region = MKCoordinateRegion(center: location.coordinate, span: span)
|
||||
DispatchQueue.main.async {
|
||||
@@ -63,7 +63,7 @@ final class MapViewController: NSViewController, MKMapViewDelegate {
|
||||
}
|
||||
|
||||
// Zoom to first location
|
||||
if focusedAccessory == nil, let location = accessories.first(where: {$0.lastLocation != nil})?.lastLocation {
|
||||
if focusedAccessory == nil, let location = accessories.first(where: { $0.lastLocation != nil })?.lastLocation {
|
||||
let span = MKCoordinateSpan(latitudeDelta: 0.005, longitudeDelta: 0.005)
|
||||
let region = MKCoordinateRegion(center: location.coordinate, span: span)
|
||||
DispatchQueue.main.async {
|
||||
@@ -73,7 +73,7 @@ final class MapViewController: NSViewController, MKMapViewDelegate {
|
||||
|
||||
// Add pins
|
||||
for accessory in accessories {
|
||||
guard accessory.lastLocation != nil else {continue}
|
||||
guard accessory.lastLocation != nil else { continue }
|
||||
|
||||
let annotation = AccessoryAnnotation(accessory: accessory)
|
||||
self.mapView.addAnnotation(annotation)
|
||||
|
||||
@@ -5,8 +5,8 @@
|
||||
//
|
||||
// SPDX-License-Identifier: AGPL-3.0-only
|
||||
|
||||
import Foundation
|
||||
import AppKit
|
||||
import Foundation
|
||||
|
||||
class SavePanel: NSObject, NSOpenSavePanelDelegate {
|
||||
|
||||
@@ -40,7 +40,7 @@ class SavePanel: NSObject, NSOpenSavePanelDelegate {
|
||||
}
|
||||
|
||||
func panel(_ sender: Any, userEnteredFilename filename: String, confirmed okFlag: Bool) -> String? {
|
||||
guard okFlag else {return nil}
|
||||
guard okFlag else { return nil }
|
||||
|
||||
return filename
|
||||
}
|
||||
|
||||
@@ -5,8 +5,9 @@
|
||||
//
|
||||
// SPDX-License-Identifier: AGPL-3.0-only
|
||||
|
||||
import XCTest
|
||||
import CryptoKit
|
||||
import XCTest
|
||||
|
||||
@testable import OpenHaystack
|
||||
|
||||
class OpenHaystackTests: XCTestCase {
|
||||
@@ -72,7 +73,7 @@ class OpenHaystackTests: XCTestCase {
|
||||
XCTAssertNotEqual(publicKey, accessory.privateKey)
|
||||
}
|
||||
|
||||
func testStoreAccessories() throws {
|
||||
func testStoreAccessories() throws {
|
||||
let accessory = try Accessory(name: "Test accessory")
|
||||
try KeychainController.storeInKeychain(accessories: [accessory], test: true)
|
||||
let fetchedAccessories = KeychainController.loadAccessoriesFromKeychain(test: true)
|
||||
@@ -107,7 +108,7 @@ class OpenHaystackTests: XCTestCase {
|
||||
_ = try MicrobitController.patchFirmware(firmware, pattern: pattern, with: key)
|
||||
XCTFail("Should thrown an erorr before")
|
||||
} catch PatchingError.patternNotFound {
|
||||
// This should be thrown
|
||||
// This should be thrown
|
||||
} catch {
|
||||
XCTFail("Unexpected error")
|
||||
}
|
||||
@@ -183,7 +184,7 @@ class OpenHaystackTests: XCTestCase {
|
||||
XCTAssertNotNil(sharedKey)
|
||||
|
||||
// Now we follow the standard key derivation used in OF
|
||||
let derivedKey = DecryptReports.kdf(fromSharedSecret: sharedKey, andEphemeralKey: ephPublicKey )
|
||||
let derivedKey = DecryptReports.kdf(fromSharedSecret: sharedKey, andEphemeralKey: ephPublicKey)
|
||||
// Let's encrypt some test string
|
||||
let message = "This is a message that should be encrypted"
|
||||
let messageData = message.data(using: .ascii)!
|
||||
|
||||
Reference in New Issue
Block a user