mirror of
https://github.com/seemoo-lab/openhaystack.git
synced 2026-02-15 18:19:52 +00:00
Compare commits
10 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
8d214aa5eb | ||
|
|
27d975b1f0 | ||
|
|
173eb741fa | ||
|
|
17410e2c00 | ||
|
|
3ef4280df1 | ||
|
|
1b66d15cad | ||
|
|
e8dcf61daa | ||
|
|
7d72fa1ac1 | ||
|
|
6eb2822632 | ||
|
|
fe1e286182 |
13
.github/workflows/build-app.yml
vendored
13
.github/workflows/build-app.yml
vendored
@@ -9,6 +9,7 @@ on:
|
||||
branches: [ main ]
|
||||
paths:
|
||||
- OpenHaystack/**
|
||||
workflow_dispatch:
|
||||
|
||||
env:
|
||||
APP: OpenHaystack
|
||||
@@ -18,7 +19,7 @@ defaults:
|
||||
|
||||
jobs:
|
||||
format-swift:
|
||||
runs-on: macos-12
|
||||
runs-on: macos-14
|
||||
steps:
|
||||
- name: "Checkout code"
|
||||
uses: actions/checkout@v2
|
||||
@@ -28,7 +29,7 @@ jobs:
|
||||
run: swift-format lint --recursive .
|
||||
|
||||
format-objc:
|
||||
runs-on: macos-12
|
||||
runs-on: macos-14
|
||||
steps:
|
||||
- name: "Checkout code"
|
||||
uses: actions/checkout@v2
|
||||
@@ -38,16 +39,16 @@ jobs:
|
||||
run: clang-format -n **/*.{h,m}
|
||||
|
||||
build-app:
|
||||
runs-on: macos-12
|
||||
runs-on: macos-14
|
||||
needs:
|
||||
- format-swift
|
||||
- format-objc
|
||||
steps:
|
||||
- name: "Checkout code"
|
||||
uses: actions/checkout@v2
|
||||
- name: "Select Xcode 13"
|
||||
uses: devbotsxyz/xcode-select@v1
|
||||
- name: "Select Xcode 15.3"
|
||||
uses: keehun/xcode-select@v1
|
||||
with:
|
||||
version: "13"
|
||||
version: "15.3"
|
||||
- name: "Archive project"
|
||||
run: xcodebuild archive -scheme ${APP} -configuration release -archivePath ${APP}.xcarchive
|
||||
|
||||
4
.github/workflows/build-cve-2020-9986.yaml
vendored
4
.github/workflows/build-cve-2020-9986.yaml
vendored
@@ -32,7 +32,7 @@ jobs:
|
||||
- name: "Checkout code"
|
||||
uses: actions/checkout@v2
|
||||
- name: "Select Xcode 12"
|
||||
uses: devbotsxyz/xcode-select@v1
|
||||
uses: keehun/xcode-select@v1
|
||||
with:
|
||||
version: "12"
|
||||
- name: "Archive project"
|
||||
@@ -47,7 +47,7 @@ jobs:
|
||||
- name: "Checkout code"
|
||||
uses: actions/checkout@v2
|
||||
- name: "Select Xcode 12"
|
||||
uses: devbotsxyz/xcode-select@v1
|
||||
uses: keehun/xcode-select@v1
|
||||
with:
|
||||
version: "12"
|
||||
- name: "Archive project"
|
||||
|
||||
1
.github/workflows/build-firmware-esp32.yaml
vendored
1
.github/workflows/build-firmware-esp32.yaml
vendored
@@ -9,6 +9,7 @@ on:
|
||||
branches: [ main ]
|
||||
paths:
|
||||
- Firmware/ESP32/**
|
||||
workflow_dispatch:
|
||||
|
||||
jobs:
|
||||
build-firmware-esp32:
|
||||
|
||||
3
.github/workflows/build-firmware.yaml
vendored
3
.github/workflows/build-firmware.yaml
vendored
@@ -9,6 +9,7 @@ on:
|
||||
branches: [ main ]
|
||||
paths:
|
||||
- Firmware/Microbit_v1/**
|
||||
workflow_dispatch:
|
||||
|
||||
defaults:
|
||||
run:
|
||||
@@ -16,7 +17,7 @@ defaults:
|
||||
|
||||
jobs:
|
||||
build-firmware:
|
||||
runs-on: macos-11
|
||||
runs-on: macos-14
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
|
||||
|
||||
9
.github/workflows/release.yml
vendored
9
.github/workflows/release.yml
vendored
@@ -4,6 +4,7 @@ on:
|
||||
push:
|
||||
tags:
|
||||
- 'v*' # Push events to matching v*, i.e. v1.0, v20.15.10
|
||||
workflow_dispatch:
|
||||
|
||||
jobs:
|
||||
build-firmware-esp32:
|
||||
@@ -30,7 +31,7 @@ jobs:
|
||||
|
||||
build-and-release:
|
||||
name: "Create release on GitHub"
|
||||
runs-on: macos-11
|
||||
runs-on: macos-14
|
||||
env:
|
||||
APP: OpenHaystack
|
||||
PROJECT_DIR: OpenHaystack
|
||||
@@ -42,10 +43,10 @@ jobs:
|
||||
steps:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@v2
|
||||
- name: "Select Xcode 12"
|
||||
uses: devbotsxyz/xcode-select@v1
|
||||
- name: "Select Xcode 15.3"
|
||||
uses: keehun/xcode-select@v1
|
||||
with:
|
||||
version: "12"
|
||||
version: "15.3"
|
||||
- name: "Add ESP32 firmware"
|
||||
uses: actions/download-artifact@v2
|
||||
with:
|
||||
|
||||
31
CITATION.cff
Normal file
31
CITATION.cff
Normal file
@@ -0,0 +1,31 @@
|
||||
# This CITATION.cff file was generated with cffinit.
|
||||
# Visit https://bit.ly/cffinit to generate yours today!
|
||||
|
||||
cff-version: 1.2.0
|
||||
title: OpenHaystack
|
||||
message: 'If you use this software, please cite it as below.'
|
||||
type: software
|
||||
authors:
|
||||
- given-names: Alexander
|
||||
family-names: Heinrich
|
||||
affiliation: 'SEEMOO, TU Darmstadt'
|
||||
orcid: 'https://orcid.org/0000-0002-1150-1922'
|
||||
- given-names: Milan
|
||||
family-names: Stute
|
||||
affiliation: 'SEEMOO, TU Darmstadt'
|
||||
orcid: 'https://orcid.org/0000-0003-4921-8476'
|
||||
- given-names: Matthias
|
||||
family-names: Hollick
|
||||
affiliation: 'SEEMOO, TU Darmstadt'
|
||||
orcid: 'https://orcid.org/0000-0002-9163-5989'
|
||||
repository-code: 'https://github.com/seemoo-lab/openhaystack'
|
||||
abstract: >-
|
||||
OpenHaystack is a framework for tracking personal
|
||||
Bluetooth devices via Apple's massive Find My network. Use
|
||||
it to create your own tracking tags that you can append to
|
||||
physical objects (keyrings, backpacks, ...) or integrate
|
||||
it into other Bluetooth-capable devices such as notebooks.
|
||||
license: AGPL-3.0
|
||||
commit: 7d72fa1ac19d2a9f6dec43011be07df8976a8b02
|
||||
version: 0.5.3
|
||||
date-released: '2023-10-09'
|
||||
@@ -3,7 +3,7 @@
|
||||
archiveVersion = 1;
|
||||
classes = {
|
||||
};
|
||||
objectVersion = 52;
|
||||
objectVersion = 54;
|
||||
objects = {
|
||||
|
||||
/* Begin PBXBuildFile section */
|
||||
@@ -58,6 +58,7 @@
|
||||
78EC227225DBC8CE0042B775 /* Accessory.swift in Sources */ = {isa = PBXBuildFile; fileRef = 78EC227125DBC8CE0042B775 /* Accessory.swift */; };
|
||||
78EC227725DBDB7E0042B775 /* KeychainController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 78EC227625DBDB7E0042B775 /* KeychainController.swift */; };
|
||||
78F8BB4C261C50EB00D9F37F /* LargeButtonStyle.swift in Sources */ = {isa = PBXBuildFile; fileRef = 78F8BB4B261C50EB00D9F37F /* LargeButtonStyle.swift */; };
|
||||
9ED440A02C1605EF002574D1 /* OpenHaystackSettingsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9ED4409F2C1605EF002574D1 /* OpenHaystackSettingsView.swift */; };
|
||||
F126102F2600D1D80066A859 /* Slider+LogScale.swift in Sources */ = {isa = PBXBuildFile; fileRef = F126102E2600D1D80066A859 /* Slider+LogScale.swift */; };
|
||||
F12D5A5A25FA4F3500CBBA09 /* BluetoothAccessoryScanner.swift in Sources */ = {isa = PBXBuildFile; fileRef = F12D5A5925FA4F3500CBBA09 /* BluetoothAccessoryScanner.swift */; };
|
||||
F12D5A6025FA79FA00CBBA09 /* Advertisement.swift in Sources */ = {isa = PBXBuildFile; fileRef = F12D5A5F25FA79FA00CBBA09 /* Advertisement.swift */; };
|
||||
@@ -169,6 +170,7 @@
|
||||
78EC227125DBC8CE0042B775 /* Accessory.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Accessory.swift; sourceTree = "<group>"; };
|
||||
78EC227625DBDB7E0042B775 /* KeychainController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = KeychainController.swift; sourceTree = "<group>"; };
|
||||
78F8BB4B261C50EB00D9F37F /* LargeButtonStyle.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LargeButtonStyle.swift; sourceTree = "<group>"; };
|
||||
9ED4409F2C1605EF002574D1 /* OpenHaystackSettingsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OpenHaystackSettingsView.swift; sourceTree = "<group>"; };
|
||||
F126102E2600D1D80066A859 /* Slider+LogScale.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Slider+LogScale.swift"; sourceTree = "<group>"; };
|
||||
F12D5A5925FA4F3500CBBA09 /* BluetoothAccessoryScanner.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BluetoothAccessoryScanner.swift; sourceTree = "<group>"; };
|
||||
F12D5A5F25FA79FA00CBBA09 /* Advertisement.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Advertisement.swift; sourceTree = "<group>"; };
|
||||
@@ -380,6 +382,7 @@
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
78F8BB4A261C50D500D9F37F /* Styles */,
|
||||
9ED4409F2C1605EF002574D1 /* OpenHaystackSettingsView.swift */,
|
||||
78286D7625E5114600F65511 /* ActivityIndicator.swift */,
|
||||
78EC226B25DBC2E40042B775 /* OpenHaystackMainView.swift */,
|
||||
78486BEE25DD711E0007ED87 /* PopUpAlertView.swift */,
|
||||
@@ -644,6 +647,7 @@
|
||||
7821DAD325F7C39A0054DC33 /* ESP32InstallSheet.swift in Sources */,
|
||||
781EB3F125DAD7EA00FEAA19 /* FindMyKeyDecoder.swift in Sources */,
|
||||
787D8AC125DECD3C00148766 /* AccessoryController.swift in Sources */,
|
||||
9ED440A02C1605EF002574D1 /* OpenHaystackSettingsView.swift in Sources */,
|
||||
78023CAB25F7767000B083EF /* ESP32Controller.swift in Sources */,
|
||||
F12D5A6025FA79FA00CBBA09 /* Advertisement.swift in Sources */,
|
||||
781EB3F225DAD7EA00FEAA19 /* OpenHaystackApp.swift in Sources */,
|
||||
|
||||
@@ -1,34 +1,60 @@
|
||||
{
|
||||
"object": {
|
||||
"pins": [
|
||||
{
|
||||
"package": "swift-crypto",
|
||||
"repositoryURL": "https://github.com/apple/swift-crypto.git",
|
||||
"state": {
|
||||
"branch": null,
|
||||
"revision": "3bea268b223651c4ab7b7b9ad62ef9b2d4143eb6",
|
||||
"version": "1.1.6"
|
||||
}
|
||||
},
|
||||
{
|
||||
"package": "swift-nio",
|
||||
"repositoryURL": "https://github.com/apple/swift-nio.git",
|
||||
"state": {
|
||||
"branch": null,
|
||||
"revision": "6aa9347d9bc5bbfe6a84983aec955c17ffea96ef",
|
||||
"version": "2.33.0"
|
||||
}
|
||||
},
|
||||
{
|
||||
"package": "swift-nio-ssl",
|
||||
"repositoryURL": "https://github.com/apple/swift-nio-ssl",
|
||||
"state": {
|
||||
"branch": null,
|
||||
"revision": "5e68c1ded15619bb281b273fa8c2d8fd7f7b2b7d",
|
||||
"version": "2.16.1"
|
||||
}
|
||||
"originHash" : "bfeb00ee66eb6db71ff8535b5ea7585725e9fe73d97f066170be55b745d346e9",
|
||||
"pins" : [
|
||||
{
|
||||
"identity" : "swift-atomics",
|
||||
"kind" : "remoteSourceControl",
|
||||
"location" : "https://github.com/apple/swift-atomics.git",
|
||||
"state" : {
|
||||
"revision" : "cd142fd2f64be2100422d658e7411e39489da985",
|
||||
"version" : "1.2.0"
|
||||
}
|
||||
]
|
||||
},
|
||||
"version": 1
|
||||
},
|
||||
{
|
||||
"identity" : "swift-collections",
|
||||
"kind" : "remoteSourceControl",
|
||||
"location" : "https://github.com/apple/swift-collections.git",
|
||||
"state" : {
|
||||
"revision" : "ee97538f5b81ae89698fd95938896dec5217b148",
|
||||
"version" : "1.1.1"
|
||||
}
|
||||
},
|
||||
{
|
||||
"identity" : "swift-crypto",
|
||||
"kind" : "remoteSourceControl",
|
||||
"location" : "https://github.com/apple/swift-crypto.git",
|
||||
"state" : {
|
||||
"revision" : "ddb07e896a2a8af79512543b1c7eb9797f8898a5",
|
||||
"version" : "1.1.7"
|
||||
}
|
||||
},
|
||||
{
|
||||
"identity" : "swift-nio",
|
||||
"kind" : "remoteSourceControl",
|
||||
"location" : "https://github.com/apple/swift-nio.git",
|
||||
"state" : {
|
||||
"revision" : "9428f62793696d9a0cc1f26a63f63bb31da0516d",
|
||||
"version" : "2.66.0"
|
||||
}
|
||||
},
|
||||
{
|
||||
"identity" : "swift-nio-ssl",
|
||||
"kind" : "remoteSourceControl",
|
||||
"location" : "https://github.com/apple/swift-nio-ssl",
|
||||
"state" : {
|
||||
"revision" : "2b09805797f21c380f7dc9bedaab3157c5508efb",
|
||||
"version" : "2.27.0"
|
||||
}
|
||||
},
|
||||
{
|
||||
"identity" : "swift-system",
|
||||
"kind" : "remoteSourceControl",
|
||||
"location" : "https://github.com/apple/swift-system.git",
|
||||
"state" : {
|
||||
"revision" : "f9266c85189c2751589a50ea5aec72799797e471",
|
||||
"version" : "1.3.0"
|
||||
}
|
||||
}
|
||||
],
|
||||
"version" : 3
|
||||
}
|
||||
|
||||
@@ -10,6 +10,20 @@
|
||||
import Foundation
|
||||
import OSLog
|
||||
|
||||
/// Uses AOSKit to get anisette headers
|
||||
@objc private protocol AOSUtilitiesProtocol
|
||||
{
|
||||
static var machineSerialNumber: String? { get }
|
||||
static var machineUDID: String? { get }
|
||||
|
||||
static func retrieveOTPHeadersForDSID(_ dsid: String) -> [String: Any]?
|
||||
|
||||
// Non-static versions used for respondsToSelector:
|
||||
var machineSerialNumber: String? { get }
|
||||
var machineUDID: String? { get }
|
||||
func retrieveOTPHeadersForDSID(_ dsid: String) -> [String: Any]?
|
||||
}
|
||||
|
||||
/// Uses the AltStore Mail plugin to access recent anisette data.
|
||||
public class AnisetteDataManager: NSObject {
|
||||
@objc static let shared = AnisetteDataManager()
|
||||
@@ -28,7 +42,7 @@ public class AnisetteDataManager: NSObject {
|
||||
}
|
||||
|
||||
func requestAnisetteData(_ completion: @escaping (Result<AppleAccountData, Error>) -> Void) {
|
||||
if let accountData = self.requestAnisetteDataAuthKit() {
|
||||
if let accountData = self.requestAnisetteDataAOSKit() {
|
||||
os_log(.debug, "Anisette Data loaded %@", accountData.debugDescription)
|
||||
completion(.success(accountData))
|
||||
return
|
||||
@@ -86,6 +100,61 @@ public class AnisetteDataManager: NSObject {
|
||||
return accountData
|
||||
}
|
||||
|
||||
/// Adapted from: https://github.com/altstoreio/AltStore/blob/main/AltServer/Anisette%20Data/AnisetteDataManager.swift
|
||||
func requestAnisetteDataAOSKit() -> AppleAccountData? {
|
||||
do
|
||||
{
|
||||
let aosKitURL = URL(fileURLWithPath: "/System/Library/PrivateFrameworks/AOSKit.framework")
|
||||
guard let aosKit = Bundle(url: aosKitURL) else { throw AnisetteDataError.aosKitFailure }
|
||||
try aosKit.loadAndReturnError()
|
||||
|
||||
guard let AOSUtilitiesClass = NSClassFromString("AOSUtilities"),
|
||||
AOSUtilitiesClass.responds(to: #selector(AOSUtilitiesProtocol.retrieveOTPHeadersForDSID(_:))),
|
||||
AOSUtilitiesClass.responds(to: #selector(getter: AOSUtilitiesProtocol.machineSerialNumber)),
|
||||
AOSUtilitiesClass.responds(to: #selector(getter: AOSUtilitiesProtocol.machineUDID))
|
||||
else { throw AnisetteDataError.aosKitFailure }
|
||||
|
||||
let AOSUtilities = unsafeBitCast(AOSUtilitiesClass, to: AOSUtilitiesProtocol.Type.self)
|
||||
|
||||
guard let anisetteData = AOSUtilities.retrieveOTPHeadersForDSID("-2") else { throw AnisetteDataError.aosKitFailure }
|
||||
|
||||
guard let machineID = anisetteData["X-Apple-MD-M"] as? String,
|
||||
let otp = anisetteData["X-Apple-MD"] as? String,
|
||||
let deviceId = AOSUtilities.machineUDID,
|
||||
let localUserId = deviceId.data(using: .utf8)?.base64EncodedString(),
|
||||
let deviceClass = NSClassFromString("AKDevice")
|
||||
else {
|
||||
print("Failure retrieving anisette headers from AOSKit")
|
||||
throw AnisetteDataError.aosKitFailure
|
||||
}
|
||||
let device: AKDevice = deviceClass.current()
|
||||
|
||||
let routingInfo: UInt64 = 84215040
|
||||
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)
|
||||
|
||||
/// This only works with SIP disabled
|
||||
if let spToken = ReportsFetcher().fetchSearchpartyToken() {
|
||||
accountData.searchPartyToken = spToken
|
||||
}
|
||||
return accountData
|
||||
}
|
||||
catch
|
||||
{
|
||||
return nil
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@objc func requestAnisetteDataObjc(_ completion: @escaping ([AnyHashable: Any]?) -> Void) {
|
||||
self.requestAnisetteData { result in
|
||||
switch result {
|
||||
@@ -98,7 +167,8 @@ public class AnisetteDataManager: NSObject {
|
||||
"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-Client-Time": ISO8601DateFormatter().string(from: data.date),
|
||||
"X-Apple-I-Client-Time": ISO8601DateFormatter().string(from: Date()),
|
||||
"X-Apple-I-MD-RINFO": String(data.routingInfo),
|
||||
] as [AnyHashable: Any])
|
||||
}
|
||||
@@ -162,4 +232,5 @@ extension AnisetteDataManager {
|
||||
enum AnisetteDataError: Error {
|
||||
case pluginNotFound
|
||||
case invalidAnisetteData
|
||||
case aosKitFailure
|
||||
}
|
||||
|
||||
@@ -20,7 +20,12 @@ struct DecryptReports {
|
||||
/// - Throws: Errors if the decryption fails
|
||||
/// - Returns: An decrypted location report
|
||||
static func decrypt(report: FindMyReport, with key: FindMyKey) throws -> FindMyLocationReport {
|
||||
let payloadData = report.payload
|
||||
var payloadData = report.payload
|
||||
/// Fix decryption for new report format
|
||||
/// See: https://github.com/biemster/FindMy/issues/52
|
||||
if payloadData.count > 88 {
|
||||
payloadData.remove(at: 5)
|
||||
}
|
||||
let keyData = key.privateKey
|
||||
|
||||
let privateKey = keyData
|
||||
|
||||
@@ -148,6 +148,10 @@ class FindMyController: ObservableObject {
|
||||
|
||||
} catch {
|
||||
print("Failed with error \(error)")
|
||||
if jsonData.isEmpty {
|
||||
print("Empty response, consider updating your Search Party Token")
|
||||
completion(FindMyErrors.invalidSearchPartyToken)
|
||||
}
|
||||
devices[deviceIndex].reports = []
|
||||
}
|
||||
fetchReportGroup.leave()
|
||||
@@ -241,4 +245,5 @@ class FindMyController: ObservableObject {
|
||||
enum FindMyErrors: Error {
|
||||
case decodingPlistFailed(message: String)
|
||||
case objectReleased
|
||||
case invalidSearchPartyToken
|
||||
}
|
||||
|
||||
@@ -13,11 +13,12 @@ import OSLog
|
||||
import SwiftUI
|
||||
|
||||
class AccessoryController: ObservableObject {
|
||||
@AppStorage("searchPartyToken") private var searchPartyToken: String = ""
|
||||
@Published var accessories: [Accessory]
|
||||
var selfObserver: AnyCancellable?
|
||||
var listElementsObserver = [AnyCancellable]()
|
||||
let findMyController: FindMyController
|
||||
|
||||
|
||||
weak var savePanel: NSSavePanel?
|
||||
|
||||
init(accessories: [Accessory], findMyController: FindMyController) {
|
||||
@@ -99,13 +100,13 @@ class AccessoryController: ObservableObject {
|
||||
func export(accessories: [Accessory]) throws -> URL {
|
||||
|
||||
let savePanel = NSSavePanel()
|
||||
// savePanel.allowedFileTypes = ["plist", "json"]
|
||||
// savePanel.allowedFileTypes = ["plist", "json"]
|
||||
if #available(macOS 12.0, *) {
|
||||
savePanel.allowedContentTypes = [.propertyList]
|
||||
}else {
|
||||
} else {
|
||||
savePanel.allowedFileTypes = ["plist"]
|
||||
}
|
||||
|
||||
|
||||
savePanel.canCreateDirectories = true
|
||||
savePanel.directoryURL = try FileManager.default.url(for: .documentDirectory, in: .userDomainMask, appropriateFor: nil, create: false)
|
||||
savePanel.message = "This export contains all private keys! Keep the file save to protect your location data"
|
||||
@@ -114,7 +115,7 @@ class AccessoryController: ObservableObject {
|
||||
savePanel.prompt = "Export"
|
||||
savePanel.title = "Export accessories & keys"
|
||||
savePanel.isExtensionHidden = false
|
||||
|
||||
|
||||
let accessoryView = NSView()
|
||||
let popUpButton = NSPopUpButton(title: "File type", target: self, action: #selector(exportFileTypeChanged(button:)))
|
||||
popUpButton.addItems(withTitles: ["Property List", "JSON"])
|
||||
@@ -122,23 +123,23 @@ class AccessoryController: ObservableObject {
|
||||
popUpButton.stringValue = "File type"
|
||||
popUpButton.translatesAutoresizingMaskIntoConstraints = false
|
||||
accessoryView.addSubview(popUpButton)
|
||||
|
||||
|
||||
let popUpButtonLabel = NSTextField(labelWithString: "File type")
|
||||
popUpButtonLabel.translatesAutoresizingMaskIntoConstraints = false
|
||||
accessoryView.addSubview(popUpButtonLabel)
|
||||
accessoryView.translatesAutoresizingMaskIntoConstraints = false
|
||||
|
||||
// popUpButtonLabel.leadingAnchor.constraint(greaterThanOrEqualTo: accessoryView.leadingAnchor, constant: 20.0).isActive = true
|
||||
|
||||
// popUpButtonLabel.leadingAnchor.constraint(greaterThanOrEqualTo: accessoryView.leadingAnchor, constant: 20.0).isActive = true
|
||||
popUpButtonLabel.trailingAnchor.constraint(equalTo: popUpButton.leadingAnchor, constant: -8.0).isActive = true
|
||||
popUpButtonLabel.trailingAnchor.constraint(lessThanOrEqualTo: accessoryView.centerXAnchor, constant: 0).isActive = true
|
||||
popUpButtonLabel.centerYAnchor.constraint(equalTo: popUpButton.centerYAnchor, constant: 0).isActive = true
|
||||
// popUpButton.trailingAnchor.constraint(lessThanOrEqualTo: accessoryView.trailingAnchor, constant: -20.0).isActive = true
|
||||
// popUpButton.trailingAnchor.constraint(lessThanOrEqualTo: accessoryView.trailingAnchor, constant: -20.0).isActive = true
|
||||
popUpButton.leadingAnchor.constraint(lessThanOrEqualTo: accessoryView.centerXAnchor, constant: 0).isActive = true
|
||||
popUpButton.topAnchor.constraint(equalTo: accessoryView.topAnchor, constant: 8.0).isActive = true
|
||||
popUpButton.bottomAnchor.constraint(equalTo: accessoryView.bottomAnchor, constant: -8.0).isActive = true
|
||||
popUpButton.heightAnchor.constraint(greaterThanOrEqualToConstant: 20.0).isActive = true
|
||||
popUpButton.widthAnchor.constraint(lessThanOrEqualToConstant: 200.0).isActive = true
|
||||
|
||||
|
||||
savePanel.accessoryView = accessoryView
|
||||
self.savePanel = savePanel
|
||||
|
||||
@@ -148,7 +149,7 @@ class AccessoryController: ObservableObject {
|
||||
var url = savePanel.url
|
||||
{
|
||||
let selectedItemIndex = popUpButton.indexOfSelectedItem
|
||||
|
||||
|
||||
// Store the accessory file
|
||||
if selectedItemIndex == 0 {
|
||||
if url.pathExtension != "plist" {
|
||||
@@ -156,7 +157,7 @@ class AccessoryController: ObservableObject {
|
||||
}
|
||||
let propertyList = try PropertyListEncoder().encode(accessories)
|
||||
try propertyList.write(to: url)
|
||||
}else if selectedItemIndex == 1 {
|
||||
} else if selectedItemIndex == 1 {
|
||||
if url.pathExtension != "json" {
|
||||
url = url.appendingPathExtension("json")
|
||||
}
|
||||
@@ -168,18 +169,18 @@ class AccessoryController: ObservableObject {
|
||||
}
|
||||
throw ImportError.cancelled
|
||||
}
|
||||
|
||||
|
||||
@objc func exportFileTypeChanged(button: NSPopUpButton) {
|
||||
if button.indexOfSelectedItem == 0 {
|
||||
if #available(macOS 12.0, *) {
|
||||
self.savePanel?.allowedContentTypes = [.propertyList]
|
||||
}else {
|
||||
} else {
|
||||
self.savePanel?.allowedFileTypes = ["plist"]
|
||||
}
|
||||
}else {
|
||||
} else {
|
||||
if #available(macOS 12.0, *) {
|
||||
self.savePanel?.allowedContentTypes = [.json]
|
||||
}else {
|
||||
} else {
|
||||
self.savePanel?.allowedFileTypes = ["json"]
|
||||
}
|
||||
}
|
||||
@@ -190,10 +191,10 @@ class AccessoryController: ObservableObject {
|
||||
let openPanel = NSOpenPanel()
|
||||
if #available(macOS 12.0, *) {
|
||||
openPanel.allowedContentTypes = [.json, .propertyList]
|
||||
}else {
|
||||
} else {
|
||||
openPanel.allowedFileTypes = ["json", "plist"]
|
||||
}
|
||||
|
||||
|
||||
openPanel.canCreateDirectories = true
|
||||
openPanel.directoryURL = try FileManager.default.url(for: .documentDirectory, in: .userDomainMask, appropriateFor: nil, create: false)
|
||||
openPanel.message = "Import an accessories file that includes the private keys"
|
||||
@@ -208,10 +209,10 @@ class AccessoryController: ObservableObject {
|
||||
var importedAccessories: [Accessory]
|
||||
if url.pathExtension == "plist" {
|
||||
importedAccessories = try PropertyListDecoder().decode([Accessory].self, from: accessoryData)
|
||||
}else {
|
||||
} else {
|
||||
importedAccessories = try JSONDecoder().decode([Accessory].self, from: accessoryData)
|
||||
}
|
||||
|
||||
|
||||
var updatedAccessories = self.accessories
|
||||
// Filter out accessories with the same id (no duplicates)
|
||||
importedAccessories = importedAccessories.filter({ acc in !self.accessories.contains(where: { acc.id == $0.id }) })
|
||||
@@ -244,10 +245,8 @@ class AccessoryController: ObservableObject {
|
||||
case .failure(_):
|
||||
completion(.failure(.activatePlugin))
|
||||
case .success(let accountData):
|
||||
|
||||
guard let token = accountData.searchPartyToken,
|
||||
token.isEmpty == false
|
||||
else {
|
||||
let token = accountData.searchPartyToken ?? self.searchPartyToken.data(using: .utf8) ?? Data()
|
||||
if token.isEmpty {
|
||||
completion(.failure(.searchPartyToken))
|
||||
return
|
||||
}
|
||||
@@ -256,7 +255,12 @@ class AccessoryController: ObservableObject {
|
||||
switch result {
|
||||
case .failure(let error):
|
||||
os_log(.error, "Downloading reports failed %@", error.localizedDescription)
|
||||
completion(.failure(.downloadingReportsFailed))
|
||||
switch error {
|
||||
case FindMyErrors.invalidSearchPartyToken:
|
||||
completion(.failure(.invalidSearchPartyToken))
|
||||
default:
|
||||
completion(.failure(.downloadingReportsFailed))
|
||||
}
|
||||
case .success(let devices):
|
||||
let reports = devices.compactMap({ $0.reports }).flatMap({ $0 })
|
||||
if reports.isEmpty {
|
||||
|
||||
@@ -214,7 +214,7 @@ class Accessory: ObservableObject, Codable, Identifiable, Equatable, Hashable {
|
||||
/// Derive FindMyKeys until we have symmetric key from one week before now
|
||||
while self.lastDerivationTimestamp < Date() - TimeInterval(7 * 24 * 60 * 60) {
|
||||
self.lastDerivationTimestamp.addTimeInterval(self.updateInterval)
|
||||
self.oldestRelevantSymmetricKey = Accessory.kdf(inputData: self.symmetricKey, sharedInfo: "update".data(using: .ascii)!, bytesToReturn: 32)
|
||||
self.oldestRelevantSymmetricKey = Accessory.kdf(inputData: self.oldestRelevantSymmetricKey, sharedInfo: "update".data(using: .ascii)!, bytesToReturn: 32)
|
||||
}
|
||||
|
||||
/// we need to generate Keys from seven days in the past until now and 10 extra keys in case of desynchronization
|
||||
|
||||
@@ -10,7 +10,6 @@
|
||||
import CoreLocation
|
||||
import Foundation
|
||||
import SwiftUI
|
||||
import CoreLocation
|
||||
|
||||
// swiftlint:disable force_try
|
||||
struct PreviewData {
|
||||
|
||||
@@ -7,83 +7,83 @@
|
||||
// SPDX-License-Identifier: AGPL-3.0-only
|
||||
//
|
||||
|
||||
import Foundation
|
||||
import AppKit
|
||||
|
||||
import Foundation
|
||||
|
||||
/// Can check if a new OpenHaystack version is needed and download it.
|
||||
public struct UpdateCheckController {
|
||||
|
||||
|
||||
public static func checkForNewVersion() {
|
||||
// Load the GitHub Releases page
|
||||
let releasesURL = URL(string: "https://github.com/seemoo-lab/openhaystack/releases")!
|
||||
URLSession.shared.dataTask(with: releasesURL) { optionalData, response, error in
|
||||
guard let data = optionalData,
|
||||
(response as? HTTPURLResponse)?.statusCode == 200,
|
||||
let htmlString = String(data:data, encoding: .utf8)
|
||||
(response as? HTTPURLResponse)?.statusCode == 200,
|
||||
let htmlString = String(data: data, encoding: .utf8)
|
||||
else {
|
||||
return
|
||||
}
|
||||
|
||||
|
||||
|
||||
guard let availableVersion = getVersion(from: htmlString) else {
|
||||
return
|
||||
}
|
||||
|
||||
|
||||
//Get installed version
|
||||
let version = Bundle.main.infoDictionary?["CFBundleVersionShortString"] as? String ?? "0"
|
||||
|
||||
|
||||
let comparisonResult = compareVersions(availableVersion: availableVersion, installedVersion: version)
|
||||
|
||||
|
||||
DispatchQueue.main.async {
|
||||
if comparisonResult == .older, askToDownloadUpdate() == .alertSecondButtonReturn {
|
||||
//The currently installed version is older. Install an update
|
||||
self.downloadUpdate(version: availableVersion, finished: { success in
|
||||
if success {
|
||||
let result = successDownloadAlert()
|
||||
if result == .alertSecondButtonReturn {
|
||||
//Open the download folder
|
||||
let downloadURL = FileManager.default.urls(for: .downloadsDirectory, in: .userDomainMask)[0]
|
||||
NSWorkspace.shared.open(downloadURL)
|
||||
self.downloadUpdate(
|
||||
version: availableVersion,
|
||||
finished: { success in
|
||||
if success {
|
||||
let result = successDownloadAlert()
|
||||
if result == .alertSecondButtonReturn {
|
||||
//Open the download folder
|
||||
let downloadURL = FileManager.default.urls(for: .downloadsDirectory, in: .userDomainMask)[0]
|
||||
NSWorkspace.shared.open(downloadURL)
|
||||
}
|
||||
} else {
|
||||
if downloadFailedAlert() == .alertSecondButtonReturn {
|
||||
NSWorkspace.shared.open(URL(string: "https://github.com/seemoo-lab/openhaystack/releases")!)
|
||||
}
|
||||
}
|
||||
}else {
|
||||
if downloadFailedAlert() == .alertSecondButtonReturn {
|
||||
NSWorkspace.shared.open(URL(string: "https://github.com/seemoo-lab/openhaystack/releases")!)
|
||||
}
|
||||
}
|
||||
})
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
}.resume()
|
||||
}
|
||||
|
||||
|
||||
internal static func getVersion(from htmlString: String) -> String? {
|
||||
guard let regex = try? NSRegularExpression(pattern: "Release (v[0-9]+(.[0-9]+)?(.[0-9]+)?)") else {
|
||||
return nil
|
||||
}
|
||||
|
||||
|
||||
let htmlNSString = htmlString as NSString
|
||||
|
||||
|
||||
let htmlRange = NSRange(location: 0, length: htmlNSString.length)
|
||||
|
||||
|
||||
if let checkResult = regex.firstMatch(in: htmlNSString as String, options: [], range: htmlRange),
|
||||
checkResult.numberOfRanges >= 2 {
|
||||
|
||||
checkResult.numberOfRanges >= 2
|
||||
{
|
||||
|
||||
//Get the latest release version range
|
||||
// A result should have multiple ranges for each capture group. 1 is the capture group for the version number
|
||||
let releaseVersionRange = checkResult.range(at: 1)
|
||||
let releaseVersion = htmlNSString.substring(with: releaseVersionRange)
|
||||
|
||||
|
||||
let releaseVersionNumber = releaseVersion.replacingOccurrences(of: "v", with: "")
|
||||
|
||||
|
||||
return releaseVersionNumber
|
||||
}
|
||||
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
|
||||
|
||||
/// Compares two version strings and returns if the installed version is older, newer or the same
|
||||
/// - Parameters:
|
||||
/// - availableVersion: The latest available version
|
||||
@@ -92,110 +92,110 @@ public struct UpdateCheckController {
|
||||
internal static func compareVersions(availableVersion: String, installedVersion: String) -> VersionCompare {
|
||||
let availableVersionSplit = availableVersion.split(separator: ".")
|
||||
let installedVersionSplit = installedVersion.split(separator: ".")
|
||||
|
||||
|
||||
for (idx, availableVersionPart) in availableVersionSplit.enumerated() {
|
||||
|
||||
|
||||
if idx < installedVersionSplit.count {
|
||||
guard let avpi = Int(availableVersionPart),
|
||||
let ivpi = Int(installedVersionSplit[idx]) else {return .older}
|
||||
|
||||
let ivpi = Int(installedVersionSplit[idx])
|
||||
else { return .older }
|
||||
|
||||
if avpi > ivpi {
|
||||
return .older
|
||||
}else if ivpi > avpi {
|
||||
} else if ivpi > avpi {
|
||||
return .newer
|
||||
}
|
||||
|
||||
}else {
|
||||
|
||||
} else {
|
||||
//The installed version is x.x
|
||||
// The new version is x.x.y so it must be older
|
||||
return .older
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
if installedVersionSplit.count > availableVersionSplit.count {
|
||||
//The installed version has a higher sub-version. So it must be newer
|
||||
return .newer
|
||||
}
|
||||
|
||||
|
||||
// All numbers were equal
|
||||
return .same
|
||||
}
|
||||
|
||||
|
||||
enum VersionCompare {
|
||||
case same, newer, older
|
||||
}
|
||||
|
||||
|
||||
static func downloadUpdate(version: String, finished: @escaping (Bool)->()) {
|
||||
|
||||
|
||||
static func downloadUpdate(version: String, finished: @escaping (Bool) -> Void) {
|
||||
|
||||
//Download the current version into a file in Downloads
|
||||
let downloadURL = URL(string: "https://github.com/seemoo-lab/openhaystack/releases/download/v\(version)/OpenHaystack.zip")!
|
||||
|
||||
|
||||
let task = URLSession.shared.downloadTask(with: downloadURL) { optionalFileURL, response, error in
|
||||
|
||||
guard let downloadLocation = optionalFileURL else {
|
||||
finished(false)
|
||||
return
|
||||
}
|
||||
|
||||
//Move the file to the downloads folder
|
||||
let downloadURL = FileManager.default.urls(for: .downloadsDirectory, in: .userDomainMask)[0]
|
||||
let openHaystackURL = downloadURL.appendingPathComponent("OpenHaystack.zip")
|
||||
do {
|
||||
let fm = FileManager.default
|
||||
if fm.fileExists(atPath: openHaystackURL.path) {
|
||||
_ = try fm.replaceItemAt(openHaystackURL, withItemAt: downloadLocation)
|
||||
}else {
|
||||
try fm.moveItem(at: downloadLocation, to: openHaystackURL)
|
||||
}
|
||||
|
||||
DispatchQueue.main.async {finished(true)}
|
||||
}catch let error {
|
||||
print(error.localizedDescription)
|
||||
DispatchQueue.main.async {finished(false)}
|
||||
|
||||
|
||||
guard let downloadLocation = optionalFileURL else {
|
||||
finished(false)
|
||||
return
|
||||
}
|
||||
|
||||
//Move the file to the downloads folder
|
||||
let downloadURL = FileManager.default.urls(for: .downloadsDirectory, in: .userDomainMask)[0]
|
||||
let openHaystackURL = downloadURL.appendingPathComponent("OpenHaystack.zip")
|
||||
do {
|
||||
let fm = FileManager.default
|
||||
if fm.fileExists(atPath: openHaystackURL.path) {
|
||||
_ = try fm.replaceItemAt(openHaystackURL, withItemAt: downloadLocation)
|
||||
} else {
|
||||
try fm.moveItem(at: downloadLocation, to: openHaystackURL)
|
||||
}
|
||||
|
||||
DispatchQueue.main.async { finished(true) }
|
||||
} catch let error {
|
||||
print(error.localizedDescription)
|
||||
DispatchQueue.main.async { finished(false) }
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
task.resume()
|
||||
}
|
||||
|
||||
|
||||
private static func askToDownloadUpdate() -> NSApplication.ModalResponse {
|
||||
let alert = NSAlert()
|
||||
alert.messageText = NSLocalizedString("New version available", comment: "Alert title")
|
||||
alert.informativeText = NSLocalizedString("A new version of OpenHaystack is available. Do you want to download it now?", comment: "Alert text")
|
||||
alert.addButton(withTitle: "Cancel")
|
||||
alert.addButton(withTitle: "Download")
|
||||
|
||||
|
||||
return alert.runModal()
|
||||
}
|
||||
|
||||
|
||||
private static func successDownloadAlert() -> NSApplication.ModalResponse {
|
||||
let alert = NSAlert()
|
||||
alert.messageText = NSLocalizedString("Successfully downloaded update", comment: "Alert title")
|
||||
alert.informativeText = NSLocalizedString("The new version has been downloaded successfully and it was placed in your Downloads folder.", comment: "Alert text")
|
||||
alert.addButton(withTitle: "Okay")
|
||||
alert.addButton(withTitle: "Open folder")
|
||||
|
||||
|
||||
return alert.runModal()
|
||||
}
|
||||
|
||||
|
||||
private static func downloadFailedAlert() -> NSApplication.ModalResponse {
|
||||
let alert = NSAlert()
|
||||
alert.messageText = NSLocalizedString("Download failed", comment: "Alert title")
|
||||
alert.informativeText = NSLocalizedString("To update to the newest version, please open the releases page on GitHub", comment: "Alert text")
|
||||
alert.addButton(withTitle: "Cancel")
|
||||
alert.addButton(withTitle: "Open")
|
||||
|
||||
|
||||
return alert.runModal()
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
||||
extension String {
|
||||
func substring(from range: NSRange) -> String {
|
||||
let substring = self[self.index(startIndex, offsetBy: range.lowerBound)..<self.index(startIndex, offsetBy: range.upperBound)]
|
||||
|
||||
|
||||
return String(substring)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -98,8 +98,13 @@ struct AccessoryListEntry: View {
|
||||
}
|
||||
Divider()
|
||||
Button("Mark as \(accessory.isDeployed ? "deployable" : "deployed")", action: { accessory.isDeployed.toggle() })
|
||||
|
||||
Button("Copy private Key B64", action: { copyPrivateKey(accessory: accessory) })
|
||||
|
||||
Group {
|
||||
Button("Copy private Key B64", action: { copyPrivateKey(accessory: accessory) })
|
||||
|
||||
Button("Export Locations", action: { exportLocations(accessory: accessory) })
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
@@ -190,16 +195,27 @@ struct AccessoryListEntry: View {
|
||||
assert(false)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
func copyPrivateKey(accessory: Accessory) {
|
||||
let privateKey = accessory.privateKey
|
||||
let keyB64 = privateKey.base64EncodedString()
|
||||
|
||||
|
||||
let pasteboard = NSPasteboard.general
|
||||
pasteboard.prepareForNewContents(with: .currentHostOnly)
|
||||
pasteboard.setString(keyB64, forType: .string)
|
||||
}
|
||||
|
||||
func exportLocations(accessory: Accessory) {
|
||||
guard let locations = accessory.locations,
|
||||
let locationData = try? JSONEncoder().encode(locations)
|
||||
else {
|
||||
return
|
||||
}
|
||||
|
||||
let savePanel = SavePanel.shared
|
||||
savePanel.saveFile(file: locationData, fileExtension: "json")
|
||||
}
|
||||
|
||||
struct AccessoryListEntry_Previews: PreviewProvider {
|
||||
@StateObject static var accessory = PreviewData.accessories.first!
|
||||
@State static var alertType: OpenHaystackMainView.AlertType?
|
||||
|
||||
@@ -11,7 +11,7 @@ import AppKit
|
||||
import Foundation
|
||||
import SwiftUI
|
||||
|
||||
final class ActivityIndicator: NSViewRepresentable {
|
||||
struct ActivityIndicator: NSViewRepresentable {
|
||||
|
||||
init(size: NSControl.ControlSize) {
|
||||
self.size = size
|
||||
|
||||
@@ -43,9 +43,11 @@ struct NRFInstallSheet: View {
|
||||
|
||||
Divider()
|
||||
|
||||
Text("The new NRF firmware uses rotating keys. This means that the device changes its public key after a specific number of days. This disallows ad networks to track your device over several days when you are moving around the city. Shorter update cycles then days are not supported")
|
||||
Text(
|
||||
"The new NRF firmware uses rotating keys. This means that the device changes its public key after a specific number of days. This disallows ad networks to track your device over several days when you are moving around the city. Shorter update cycles then days are not supported"
|
||||
)
|
||||
self.timePicker
|
||||
|
||||
|
||||
Text("One day is a reasonable amount of time")
|
||||
.font(.footnote)
|
||||
.foregroundColor(.secondary)
|
||||
@@ -82,7 +84,7 @@ struct NRFInstallSheet: View {
|
||||
self.presentationMode.wrappedValue.dismiss()
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
HStack {
|
||||
Spacer()
|
||||
Text("Flashing from M1 Macs might fail due to missing ARM support by NRF")
|
||||
|
||||
@@ -38,6 +38,9 @@ struct OpenHaystackMainView: View {
|
||||
|
||||
@State var showESP32DeploySheet = false
|
||||
|
||||
@AppStorage("searchPartyToken") private var settingsSPToken: String?
|
||||
@AppStorage("useMailPlugin") private var settingsUseMailPlugin: Bool = false
|
||||
|
||||
var body: some View {
|
||||
|
||||
NavigationView {
|
||||
@@ -135,7 +138,7 @@ struct OpenHaystackMainView: View {
|
||||
|
||||
Button(
|
||||
action: {
|
||||
if !self.mailPluginIsActive {
|
||||
if self.settingsUseMailPlugin && !self.mailPluginIsActive {
|
||||
self.showMailPlugInPopover.toggle()
|
||||
self.checkPluginIsRunning(silent: true, nil)
|
||||
} else {
|
||||
@@ -174,17 +177,26 @@ struct OpenHaystackMainView: View {
|
||||
return
|
||||
}
|
||||
|
||||
let pluginManager = MailPluginManager()
|
||||
|
||||
// Check if the plugin is installed
|
||||
if pluginManager.isMailPluginInstalled == false {
|
||||
// Install the mail plugin
|
||||
self.alertType = .activatePlugin
|
||||
self.checkPluginIsRunning(silent: true, nil)
|
||||
} else {
|
||||
self.checkPluginIsRunning(nil)
|
||||
/// Checks if the search party token was set in the settings. If true the plugin is also not needed
|
||||
if let tokenString = self.settingsSPToken {
|
||||
self.searchPartyToken = tokenString
|
||||
return
|
||||
}
|
||||
|
||||
/// Uses mail plugin if enabled in settings
|
||||
if self.settingsUseMailPlugin {
|
||||
let pluginManager = MailPluginManager()
|
||||
// Check if the plugin is installed
|
||||
if pluginManager.isMailPluginInstalled == false {
|
||||
// Install the mail plugin
|
||||
self.alertType = .activatePlugin
|
||||
self.checkPluginIsRunning(silent: true, nil)
|
||||
} else {
|
||||
self.checkPluginIsRunning(nil)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
||||
/// Download the location reports for all current accessories. Shows an error if something fails, like plug-in is missing
|
||||
@@ -308,7 +320,19 @@ struct OpenHaystackMainView: View {
|
||||
title: Text("Add the search party token"),
|
||||
message: Text(
|
||||
"""
|
||||
Please paste the search party token below after copying itfrom the macOS Keychain.
|
||||
Please paste the search party token in the settings after copying it from 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 .invalidSearchPartyToken:
|
||||
return Alert(
|
||||
title: Text("Invalid search party token"),
|
||||
message: Text(
|
||||
"""
|
||||
The request returned an empty result, this is probably due to an invalid search party token.
|
||||
Please consider updating your search party token in the settings after copying it from the macOS Keychain.
|
||||
The item that contains the key can be found by searching for:
|
||||
com.apple.account.DeviceLocator.search-party-token
|
||||
"""
|
||||
@@ -388,6 +412,7 @@ struct OpenHaystackMainView: View {
|
||||
|
||||
case keyError
|
||||
case searchPartyToken
|
||||
case invalidSearchPartyToken
|
||||
case deployFailed
|
||||
case nrfDeployFailed
|
||||
case deployedSuccessfully
|
||||
|
||||
@@ -0,0 +1,36 @@
|
||||
//
|
||||
// OpenHaystack – Tracking personal Bluetooth devices via Apple's Find My network
|
||||
//
|
||||
// Copyright © 2024 Secure Mobile Networking Lab (SEEMOO)
|
||||
// Copyright © 2024 The Open Wireless Link Project
|
||||
//
|
||||
// SPDX-License-Identifier: AGPL-3.0-only
|
||||
//
|
||||
|
||||
import Foundation
|
||||
import SwiftUI
|
||||
|
||||
struct OpenHaystackSettingsView: View {
|
||||
var body: some View {
|
||||
TabView {
|
||||
GeneralSettingsView()
|
||||
.tabItem {
|
||||
Label("General", systemImage: "gear")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
struct GeneralSettingsView: View {
|
||||
@AppStorage("useMailPlugin") private var useMailPlugin = false
|
||||
@AppStorage("searchPartyToken") private var searchPartyToken = ""
|
||||
|
||||
var body: some View {
|
||||
Form {
|
||||
Toggle("Use Apple Mail Plugin (only works on macOS 13 and lower)", isOn: $useMailPlugin)
|
||||
TextField("Search Party Token", text: $searchPartyToken)
|
||||
}
|
||||
.padding(20)
|
||||
.frame(width: 600, height: 200)
|
||||
}
|
||||
}
|
||||
@@ -15,7 +15,7 @@ struct OpenHaystackApp: App {
|
||||
var accessoryNearbyMonitor: AccessoryNearbyMonitor?
|
||||
var frameWidth: CGFloat? = nil
|
||||
var frameHeight: CGFloat? = nil
|
||||
|
||||
|
||||
@State var checkedForUpdates = false
|
||||
|
||||
init() {
|
||||
@@ -44,10 +44,15 @@ struct OpenHaystackApp: App {
|
||||
.commands {
|
||||
SidebarCommands()
|
||||
}
|
||||
#if os(macOS)
|
||||
Settings {
|
||||
OpenHaystackSettingsView()
|
||||
}
|
||||
#endif
|
||||
}
|
||||
|
||||
|
||||
func checkForUpdates() {
|
||||
guard checkedForUpdates == false, ProcessInfo().arguments.contains("-stopUpdateCheck") == false else {return}
|
||||
guard checkedForUpdates == false, ProcessInfo().arguments.contains("-stopUpdateCheck") == false else { return }
|
||||
UpdateCheckController.checkForNewVersion()
|
||||
checkedForUpdates = true
|
||||
}
|
||||
|
||||
@@ -178,5 +178,93 @@
|
||||
<string>224E7F96-2099-499C-A501-63FB68C79CD2</string>
|
||||
<string>A4B49485-0377-4FAB-8D8E-E3B8018CFC21</string>
|
||||
</array>
|
||||
<key>Supported13.0PluginCompatibilityUUIDs</key>
|
||||
<array>
|
||||
<string>25288CEF-7D9B-49A8-BE6B-E41DA6277CF3</string>
|
||||
<string>6FF8B077-81FA-45A4-BD57-17CDE79F13A5</string>
|
||||
<string>224E7F96-2099-499C-A501-63FB68C79CD2</string>
|
||||
<string>A4B49485-0377-4FAB-8D8E-E3B8018CFC21</string>
|
||||
</array>
|
||||
<key>Supported13.1PluginCompatibilityUUIDs</key>
|
||||
<array>
|
||||
<string>25288CEF-7D9B-49A8-BE6B-E41DA6277CF3</string>
|
||||
<string>6FF8B077-81FA-45A4-BD57-17CDE79F13A5</string>
|
||||
<string>224E7F96-2099-499C-A501-63FB68C79CD2</string>
|
||||
<string>890E3F5B-9490-4828-8F3F-B6561E513FCC</string>
|
||||
<string>A4B49485-0377-4FAB-8D8E-E3B8018CFC21</string>
|
||||
<string>281F8A5C-0AF9-4BE6-8B8A-C0CB9C2068BE</string>
|
||||
</array>
|
||||
<key>Supported13.2PluginCompatibilityUUIDs</key>
|
||||
<array>
|
||||
<string>25288CEF-7D9B-49A8-BE6B-E41DA6277CF3</string>
|
||||
<string>6FF8B077-81FA-45A4-BD57-17CDE79F13A5</string>
|
||||
<string>890E3F5B-9490-4828-8F3F-B6561E513FCC</string>
|
||||
<string>224E7F96-2099-499C-A501-63FB68C79CD2</string>
|
||||
<string>281F8A5C-0AF9-4BE6-8B8A-C0CB9C2068BE</string>
|
||||
<string>A4B49485-0377-4FAB-8D8E-E3B8018CFC21</string>
|
||||
</array>
|
||||
<key>Supported13.3PluginCompatibilityUUIDs</key>
|
||||
<array>
|
||||
<string>25288CEF-7D9B-49A8-BE6B-E41DA6277CF3</string>
|
||||
<string>6FF8B077-81FA-45A4-BD57-17CDE79F13A5</string>
|
||||
<string>890E3F5B-9490-4828-8F3F-B6561E513FCC</string>
|
||||
<string>224E7F96-2099-499C-A501-63FB68C79CD2</string>
|
||||
<string>A4B49485-0377-4FAB-8D8E-E3B8018CFC21</string>
|
||||
<string>281F8A5C-0AF9-4BE6-8B8A-C0CB9C2068BE</string>
|
||||
</array>
|
||||
<key>Supported13.4PluginCompatibilityUUIDs</key>
|
||||
<array>
|
||||
<string>25288CEF-7D9B-49A8-BE6B-E41DA6277CF3</string>
|
||||
<string>6FF8B077-81FA-45A4-BD57-17CDE79F13A5</string>
|
||||
<string>224E7F96-2099-499C-A501-63FB68C79CD2</string>
|
||||
<string>890E3F5B-9490-4828-8F3F-B6561E513FCC</string>
|
||||
<string>A4B49485-0377-4FAB-8D8E-E3B8018CFC21</string>
|
||||
<string>281F8A5C-0AF9-4BE6-8B8A-C0CB9C2068BE</string>
|
||||
</array>
|
||||
<key>Supported13.5PluginCompatibilityUUIDs</key>
|
||||
<array>
|
||||
<string>25288CEF-7D9B-49A8-BE6B-E41DA6277CF3</string>
|
||||
<string>6FF8B077-81FA-45A4-BD57-17CDE79F13A5</string>
|
||||
<string>224E7F96-2099-499C-A501-63FB68C79CD2</string>
|
||||
<string>890E3F5B-9490-4828-8F3F-B6561E513FCC</string>
|
||||
<string>A4B49485-0377-4FAB-8D8E-E3B8018CFC21</string>
|
||||
<string>281F8A5C-0AF9-4BE6-8B8A-C0CB9C2068BE</string>
|
||||
</array>
|
||||
<key>Supported13.6PluginCompatibilityUUIDs</key>
|
||||
<array>
|
||||
<string>25288CEF-7D9B-49A8-BE6B-E41DA6277CF3</string>
|
||||
<string>6FF8B077-81FA-45A4-BD57-17CDE79F13A5</string>
|
||||
<string>224E7F96-2099-499C-A501-63FB68C79CD2</string>
|
||||
<string>890E3F5B-9490-4828-8F3F-B6561E513FCC</string>
|
||||
<string>A4B49485-0377-4FAB-8D8E-E3B8018CFC21</string>
|
||||
<string>281F8A5C-0AF9-4BE6-8B8A-C0CB9C2068BE</string>
|
||||
</array>
|
||||
<key>Supported13.7PluginCompatibilityUUIDs</key>
|
||||
<array>
|
||||
<string>25288CEF-7D9B-49A8-BE6B-E41DA6277CF3</string>
|
||||
<string>6FF8B077-81FA-45A4-BD57-17CDE79F13A5</string>
|
||||
<string>224E7F96-2099-499C-A501-63FB68C79CD2</string>
|
||||
<string>890E3F5B-9490-4828-8F3F-B6561E513FCC</string>
|
||||
<string>A4B49485-0377-4FAB-8D8E-E3B8018CFC21</string>
|
||||
<string>281F8A5C-0AF9-4BE6-8B8A-C0CB9C2068BE</string>
|
||||
</array>
|
||||
<key>Supported14.0PluginCompatibilityUUIDs</key>
|
||||
<array>
|
||||
<string>25288CEF-7D9B-49A8-BE6B-E41DA6277CF3</string>
|
||||
<string>6FF8B077-81FA-45A4-BD57-17CDE79F13A5</string>
|
||||
<string>224E7F96-2099-499C-A501-63FB68C79CD2</string>
|
||||
<string>890E3F5B-9490-4828-8F3F-B6561E513FCC</string>
|
||||
<string>A4B49485-0377-4FAB-8D8E-E3B8018CFC21</string>
|
||||
<string>281F8A5C-0AF9-4BE6-8B8A-C0CB9C2068BE</string>
|
||||
</array>
|
||||
<key>Supported14.1PluginCompatibilityUUIDs</key>
|
||||
<array>
|
||||
<string>25288CEF-7D9B-49A8-BE6B-E41DA6277CF3</string>
|
||||
<string>6FF8B077-81FA-45A4-BD57-17CDE79F13A5</string>
|
||||
<string>224E7F96-2099-499C-A501-63FB68C79CD2</string>
|
||||
<string>890E3F5B-9490-4828-8F3F-B6561E513FCC</string>
|
||||
<string>A4B49485-0377-4FAB-8D8E-E3B8018CFC21</string>
|
||||
<string>281F8A5C-0AF9-4BE6-8B8A-C0CB9C2068BE</string>
|
||||
</array>
|
||||
</dict>
|
||||
</plist>
|
||||
|
||||
@@ -9,46 +9,47 @@
|
||||
|
||||
import Foundation
|
||||
import XCTest
|
||||
|
||||
@testable import OpenHaystack
|
||||
|
||||
class UpdateCheckTests: XCTestCase {
|
||||
|
||||
|
||||
func testCompareVersions() {
|
||||
let i1 = "1.0.3"
|
||||
let a1 = "1.0.4"
|
||||
XCTAssertEqual(UpdateCheckController.compareVersions(availableVersion: a1, installedVersion: i1), .older)
|
||||
let a11 = "1.1"
|
||||
XCTAssertEqual(UpdateCheckController.compareVersions(availableVersion: a11, installedVersion: i1), .older)
|
||||
let a12 = "2"
|
||||
let a12 = "2"
|
||||
XCTAssertEqual(UpdateCheckController.compareVersions(availableVersion: a12, installedVersion: i1), .older)
|
||||
|
||||
|
||||
let a2 = "1.0.3"
|
||||
XCTAssertEqual(UpdateCheckController.compareVersions(availableVersion: a2, installedVersion: i1), .same)
|
||||
|
||||
|
||||
let a3 = "1.0.2"
|
||||
XCTAssertEqual(UpdateCheckController.compareVersions(availableVersion: a3, installedVersion: i1), .newer)
|
||||
let a31 = "1.0"
|
||||
XCTAssertEqual(UpdateCheckController.compareVersions(availableVersion: a31, installedVersion: i1), .newer)
|
||||
let a32 = "0.10.1"
|
||||
XCTAssertEqual(UpdateCheckController.compareVersions(availableVersion: a32, installedVersion: i1), .newer)
|
||||
|
||||
|
||||
let a4 = "1.1.1"
|
||||
let i4 = "1.1.2"
|
||||
XCTAssertEqual(UpdateCheckController.compareVersions(availableVersion: a4, installedVersion: i4), .newer)
|
||||
let a41 = "1.0.2"
|
||||
XCTAssertEqual(UpdateCheckController.compareVersions(availableVersion: a41, installedVersion: i1), .newer)
|
||||
}
|
||||
|
||||
|
||||
func testHTMLVersionCompare() {
|
||||
let github =
|
||||
"""
|
||||
<h1 data-view-component="true" class="d-inline mr-3"><a href="/seemoo-lab/openhaystack/releases/tag/v0.4.1" data-view-component="true" class="Link--primary">Release v0.4.1</a></h1>
|
||||
<h1 data-view-component="true" class="d-inline mr-3"><a href="/seemoo-lab/openhaystack/releases/tag/v0.4.1" data-view-component="true" class="Link--primary">Release v0.4.1</a></h1>
|
||||
<a href="/seemoo-lab/openhaystack/releases/tag/v0.4.1" data-view-component="true" class="Link--primary">Release v0.4.1</a>
|
||||
"""
|
||||
|
||||
"""
|
||||
<h1 data-view-component="true" class="d-inline mr-3"><a href="/seemoo-lab/openhaystack/releases/tag/v0.4.1" data-view-component="true" class="Link--primary">Release v0.4.1</a></h1>
|
||||
<h1 data-view-component="true" class="d-inline mr-3"><a href="/seemoo-lab/openhaystack/releases/tag/v0.4.1" data-view-component="true" class="Link--primary">Release v0.4.1</a></h1>
|
||||
<a href="/seemoo-lab/openhaystack/releases/tag/v0.4.1" data-view-component="true" class="Link--primary">Release v0.4.1</a>
|
||||
"""
|
||||
|
||||
XCTAssertEqual(UpdateCheckController.getVersion(from: github), "0.4.1")
|
||||
|
||||
|
||||
let h1 = "<h1>Release v0.4.1</h1> <h1>Release v0.3.1</h1>"
|
||||
XCTAssertEqual(UpdateCheckController.getVersion(from: h1), "0.4.1")
|
||||
let h2 = "<h1>Release v0.5</h1>"
|
||||
@@ -58,16 +59,16 @@ class UpdateCheckTests: XCTestCase {
|
||||
let h4 = "<h1>Release v1</h1>"
|
||||
XCTAssertEqual(UpdateCheckController.getVersion(from: h4), "1")
|
||||
}
|
||||
|
||||
|
||||
func testDownload() {
|
||||
let expect = expectation(description: "Update download")
|
||||
UpdateCheckController.downloadUpdate(version: "0.4.1", finished: { success in
|
||||
XCTAssertTrue(success)
|
||||
expect.fulfill()
|
||||
})
|
||||
UpdateCheckController.downloadUpdate(
|
||||
version: "0.4.1",
|
||||
finished: { success in
|
||||
XCTAssertTrue(success)
|
||||
expect.fulfill()
|
||||
})
|
||||
wait(for: [expect], timeout: 20.0)
|
||||
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
Reference in New Issue
Block a user