Add swift-format as an Xcode build phase

This commit is contained in:
Milan Stute
2021-03-08 14:42:59 +01:00
parent 036b99c2bd
commit 8127628ea5
23 changed files with 397 additions and 309 deletions

View File

@@ -0,0 +1,7 @@
{
"version": 1,
"lineLength": 180,
"indentation": {
"spaces": 4
}
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -7,8 +7,8 @@
//
import Foundation
import SwiftUI
import MapKit
import SwiftUI
struct AccessoryMapView: NSViewControllerRepresentable {
@ObservedObject var accessoryController: AccessoryController

View File

@@ -5,9 +5,9 @@
//
// SPDX-License-Identifier: AGPL-3.0-only
import AppKit
import Foundation
import SwiftUI
import AppKit
final class ActivityIndicator: NSViewRepresentable {

View File

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

View File

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

View File

@@ -25,8 +25,9 @@ struct PopUpAlertView: View {
}
}
.background(RoundedRectangle(cornerRadius: 7.5)
.fill(Color.gray))
.background(
RoundedRectangle(cornerRadius: 7.5)
.fill(Color.gray))
}
}

View File

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

View File

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

View File

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