mirror of
https://github.com/seemoo-lab/openhaystack.git
synced 2026-02-14 17:49:54 +00:00
Adding a class that automatically checks for updates of the app
This commit is contained in:
@@ -33,6 +33,8 @@
|
||||
781EB43125DADF2B00FEAA19 /* AnisetteDataManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 781EB40F25DADB0600FEAA19 /* AnisetteDataManager.swift */; };
|
||||
7821DAD125F7B2C10054DC33 /* FileManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7821DAD025F7B2C10054DC33 /* FileManager.swift */; };
|
||||
7821DAD325F7C39A0054DC33 /* ESP32InstallSheet.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7821DAD225F7C39A0054DC33 /* ESP32InstallSheet.swift */; };
|
||||
782853C22755103A00B18EDE /* UpdateCheckController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 782853C12755103A00B18EDE /* UpdateCheckController.swift */; };
|
||||
782853C427551B4400B18EDE /* UpdateCheckTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 782853C327551B4400B18EDE /* UpdateCheckTests.swift */; };
|
||||
78286CB225E3ACE700F65511 /* OpenHaystackPluginService.m in Sources */ = {isa = PBXBuildFile; fileRef = 78286CAF25E3ACE700F65511 /* OpenHaystackPluginService.m */; };
|
||||
78286D1F25E3D8B800F65511 /* ALTAnisetteData.m in Sources */ = {isa = PBXBuildFile; fileRef = 78286CB025E3ACE700F65511 /* ALTAnisetteData.m */; };
|
||||
78286D2A25E3EC3200F65511 /* AppleAccountData.m in Sources */ = {isa = PBXBuildFile; fileRef = 78286D2925E3EC3200F65511 /* AppleAccountData.m */; };
|
||||
@@ -135,6 +137,8 @@
|
||||
781EB40F25DADB0600FEAA19 /* AnisetteDataManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AnisetteDataManager.swift; sourceTree = "<group>"; };
|
||||
7821DAD025F7B2C10054DC33 /* FileManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FileManager.swift; sourceTree = "<group>"; };
|
||||
7821DAD225F7C39A0054DC33 /* ESP32InstallSheet.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ESP32InstallSheet.swift; sourceTree = "<group>"; };
|
||||
782853C12755103A00B18EDE /* UpdateCheckController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UpdateCheckController.swift; sourceTree = "<group>"; };
|
||||
782853C327551B4400B18EDE /* UpdateCheckTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UpdateCheckTests.swift; sourceTree = "<group>"; };
|
||||
78286C8E25E3AC0400F65511 /* OpenHaystackMail.mailbundle */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = OpenHaystackMail.mailbundle; sourceTree = BUILT_PRODUCTS_DIR; };
|
||||
78286C9025E3AC0400F65511 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = "<group>"; };
|
||||
78286CAE25E3ACE700F65511 /* OpenHaystackPluginService.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = OpenHaystackPluginService.h; sourceTree = "<group>"; };
|
||||
@@ -337,6 +341,7 @@
|
||||
78EC226525DAE0BE0042B775 /* Info.plist */,
|
||||
78023CB025F7841F00B083EF /* MicrocontrollerTests.swift */,
|
||||
F1647C1525FF6C61004144D6 /* BluetoothTests.swift */,
|
||||
782853C327551B4400B18EDE /* UpdateCheckTests.swift */,
|
||||
);
|
||||
path = OpenHaystackTests;
|
||||
sourceTree = "<group>";
|
||||
@@ -357,6 +362,7 @@
|
||||
F1647C1A25FF7954004144D6 /* AccessoryNearbyMonitor.swift */,
|
||||
5A2C908A2734266A0044407E /* DataToHexExtension.swift */,
|
||||
5A2C908C273429360044407E /* NRFController.swift */,
|
||||
782853C12755103A00B18EDE /* UpdateCheckController.swift */,
|
||||
);
|
||||
path = HaystackApp;
|
||||
sourceTree = "<group>";
|
||||
@@ -645,6 +651,7 @@
|
||||
78F8BB4C261C50EB00D9F37F /* LargeButtonStyle.swift in Sources */,
|
||||
781EB3F425DAD7EA00FEAA19 /* FindMyController.swift in Sources */,
|
||||
781EB3F525DAD7EA00FEAA19 /* BoringSSL.m in Sources */,
|
||||
782853C22755103A00B18EDE /* UpdateCheckController.swift in Sources */,
|
||||
F12D5A5A25FA4F3500CBBA09 /* BluetoothAccessoryScanner.swift in Sources */,
|
||||
78286D5625E401F000F65511 /* MailPluginManager.swift in Sources */,
|
||||
);
|
||||
@@ -664,6 +671,7 @@
|
||||
buildActionMask = 2147483647;
|
||||
files = (
|
||||
78023CB125F7841F00B083EF /* MicrocontrollerTests.swift in Sources */,
|
||||
782853C427551B4400B18EDE /* UpdateCheckTests.swift in Sources */,
|
||||
F1647C1625FF6C61004144D6 /* BluetoothTests.swift in Sources */,
|
||||
78EC226425DAE0BE0042B775 /* OpenHaystackTests.swift in Sources */,
|
||||
);
|
||||
|
||||
@@ -74,6 +74,12 @@
|
||||
ReferencedContainer = "container:OpenHaystack.xcodeproj">
|
||||
</BuildableReference>
|
||||
</BuildableProductRunnable>
|
||||
<CommandLineArguments>
|
||||
<CommandLineArgument
|
||||
argument = "-stopUpdateCheck"
|
||||
isEnabled = "YES">
|
||||
</CommandLineArgument>
|
||||
</CommandLineArguments>
|
||||
</LaunchAction>
|
||||
<ProfileAction
|
||||
buildConfiguration = "Release"
|
||||
|
||||
@@ -10,18 +10,18 @@
|
||||
import Foundation
|
||||
|
||||
extension Data {
|
||||
/// A hexadecimal string representation of the bytes.
|
||||
func hexEncodedString() -> String {
|
||||
let hexDigits = Array("0123456789abcdef".utf16)
|
||||
var hexChars = [UTF16.CodeUnit]()
|
||||
hexChars.reserveCapacity(count * 2)
|
||||
/// A hexadecimal string representation of the bytes.
|
||||
func hexEncodedString() -> String {
|
||||
let hexDigits = Array("0123456789abcdef".utf16)
|
||||
var hexChars = [UTF16.CodeUnit]()
|
||||
hexChars.reserveCapacity(count * 2)
|
||||
|
||||
for byte in self {
|
||||
let (index1, index2) = Int(byte).quotientAndRemainder(dividingBy: 16)
|
||||
hexChars.append(hexDigits[index1])
|
||||
hexChars.append(hexDigits[index2])
|
||||
for byte in self {
|
||||
let (index1, index2) = Int(byte).quotientAndRemainder(dividingBy: 16)
|
||||
hexChars.append(hexDigits[index1])
|
||||
hexChars.append(hexDigits[index2])
|
||||
}
|
||||
|
||||
return String(utf16CodeUnits: hexChars, count: hexChars.count)
|
||||
}
|
||||
|
||||
return String(utf16CodeUnits: hexChars, count: hexChars.count)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -49,7 +49,7 @@ class Accessory: ObservableObject, Codable, Identifiable, Equatable, Hashable {
|
||||
self.usesDerivation = false
|
||||
} else if wasDeployed && !isDeployed {
|
||||
self.usesDerivation = false
|
||||
self.updateInterval = TimeInterval(60*60*24)
|
||||
self.updateInterval = TimeInterval(60 * 60 * 24)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -79,7 +79,7 @@ class Accessory: ObservableObject, Codable, Identifiable, Equatable, Hashable {
|
||||
self.usesDerivation = false
|
||||
self.oldestRelevantSymmetricKey = self.symmetricKey
|
||||
self.lastDerivationTimestamp = Date()
|
||||
self.updateInterval = TimeInterval(60*60)
|
||||
self.updateInterval = TimeInterval(60 * 60)
|
||||
self.color = color
|
||||
self.icon = iconName
|
||||
self.isDeployed = false
|
||||
@@ -90,11 +90,11 @@ class Accessory: ObservableObject, Codable, Identifiable, Equatable, Hashable {
|
||||
self.name = try container.decode(String.self, forKey: .name)
|
||||
self.id = try container.decode(Int.self, forKey: .id)
|
||||
self.privateKey = try container.decode(Data.self, forKey: .privateKey)
|
||||
self.symmetricKey = (try? container.decode(Data.self, forKey: .symmetricKey)) ?? SymmetricKey(size: .bits256).withUnsafeBytes{ return Data($0) }
|
||||
self.symmetricKey = (try? container.decode(Data.self, forKey: .symmetricKey)) ?? SymmetricKey(size: .bits256).withUnsafeBytes { return Data($0) }
|
||||
self.usesDerivation = (try? container.decode(Bool.self, forKey: .usesDerivation)) ?? false
|
||||
self.oldestRelevantSymmetricKey = (try? container.decode(Data.self, forKey: .oldestRelevantSymmetricKey)) ?? self.symmetricKey
|
||||
self.lastDerivationTimestamp = (try? container.decode(Date.self, forKey: .lastDerivationTimestamp)) ?? Date()
|
||||
self.updateInterval = (try? container.decode(TimeInterval.self, forKey: .updateInterval)) ?? TimeInterval(60*60*24)
|
||||
self.updateInterval = (try? container.decode(TimeInterval.self, forKey: .updateInterval)) ?? TimeInterval(60 * 60 * 24)
|
||||
self.icon = (try? container.decode(String.self, forKey: .icon)) ?? ""
|
||||
self.isDeployed = (try? container.decode(Bool.self, forKey: .isDeployed)) ?? false
|
||||
self.isActive = (try? container.decode(Bool.self, forKey: .isActive)) ?? false
|
||||
@@ -149,7 +149,7 @@ class Accessory: ObservableObject, Codable, Identifiable, Equatable, Hashable {
|
||||
}
|
||||
return publicKey
|
||||
}
|
||||
|
||||
|
||||
func getAdvertisementKey() throws -> Data {
|
||||
guard var publicKey = BoringSSL.derivePublicKey(fromPrivateKey: self.privateKey) else {
|
||||
throw KeyError.keyDerivationFailed
|
||||
@@ -182,7 +182,7 @@ class Accessory: ObservableObject, Codable, Identifiable, Equatable, Hashable {
|
||||
|
||||
return Data(digest)
|
||||
}
|
||||
|
||||
|
||||
func getNewestSymmetricKey() -> Data {
|
||||
var derivationTimestamp = self.lastDerivationTimestamp
|
||||
var symmetricKey = self.oldestRelevantSymmetricKey
|
||||
@@ -193,87 +193,87 @@ class Accessory: ObservableObject, Codable, Identifiable, Equatable, Hashable {
|
||||
return symmetricKey
|
||||
}
|
||||
|
||||
|
||||
func toFindMyDevice() throws -> FindMyDevice {
|
||||
|
||||
|
||||
var findMyKey = [FindMyKey]()
|
||||
|
||||
|
||||
/// Always append first FindMyKey to support devices without derivation
|
||||
findMyKey.append(FindMyKey(
|
||||
advertisedKey: try self.getAdvertisementKey(),
|
||||
hashedKey: try self.hashedPublicKey(),
|
||||
privateKey: self.privateKey,
|
||||
startTime: nil,
|
||||
duration: nil,
|
||||
pu: nil,
|
||||
yCoordinate: nil,
|
||||
fullKey: nil)
|
||||
findMyKey.append(
|
||||
FindMyKey(
|
||||
advertisedKey: try self.getAdvertisementKey(),
|
||||
hashedKey: try self.hashedPublicKey(),
|
||||
privateKey: self.privateKey,
|
||||
startTime: nil,
|
||||
duration: nil,
|
||||
pu: nil,
|
||||
yCoordinate: nil,
|
||||
fullKey: nil)
|
||||
)
|
||||
if self.usesDerivation {
|
||||
/// Derive FindMyKeys until we have symmetric key from one week before now
|
||||
while self.lastDerivationTimestamp < Date() - TimeInterval(7*24*60*60) {
|
||||
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)
|
||||
}
|
||||
|
||||
|
||||
/// we need to generate Keys from seven days in the past until now and 10 extra keys in case of desynchronization
|
||||
let untilDate = Date() + TimeInterval(self.updateInterval * 11)
|
||||
var derivationTimestamp = self.lastDerivationTimestamp
|
||||
var derivedSymmetricKey = self.oldestRelevantSymmetricKey
|
||||
|
||||
|
||||
print("--- Derived keys for \(self.name) ---")
|
||||
print("Masterbacon symmetric key \(self.symmetricKey.hexEncodedString())")
|
||||
do {
|
||||
let uncompressedMasterBeaconKey = try self.getUncompressedPublicKey()
|
||||
let uncompressedMasterBeaconKey = try self.getUncompressedPublicKey()
|
||||
print("Masterbeacon public key (uncompressed) \(uncompressedMasterBeaconKey.hexEncodedString())")
|
||||
} catch {
|
||||
print("Failed to get master beacon public key (only needed for printing)")
|
||||
}
|
||||
|
||||
|
||||
|
||||
while derivationTimestamp < untilDate {
|
||||
/// Step 1: derive SKN_i
|
||||
derivedSymmetricKey = Accessory.kdf(inputData: derivedSymmetricKey, sharedInfo: "update".data(using: .ascii)!, bytesToReturn: 32)
|
||||
/// Step 2: derive u_i and v_i
|
||||
let derivedAntiTrackingKeys = Accessory.kdf(inputData: derivedSymmetricKey, sharedInfo: "diversify".data(using: .ascii)!, bytesToReturn: 72)
|
||||
/// Step 3 & 4: compute private and public key
|
||||
guard let derivedPrivateKey = BoringSSL.calculatePrivateKey(fromSharedData: derivedAntiTrackingKeys, masterBeaconPrivateKey: self.privateKey) else{
|
||||
guard let derivedPrivateKey = BoringSSL.calculatePrivateKey(fromSharedData: derivedAntiTrackingKeys, masterBeaconPrivateKey: self.privateKey) else {
|
||||
throw KeyError.keyDerivationFailed
|
||||
}
|
||||
guard let derivedPublicKey = BoringSSL.derivePublicKey(fromPrivateKey: derivedPrivateKey) else {
|
||||
throw KeyError.keyDerivationFailed
|
||||
}
|
||||
|
||||
|
||||
/// Drop first byte to get advertisment key
|
||||
let derivedAdvertisementKey = derivedPublicKey.dropFirst()
|
||||
guard derivedAdvertisementKey.count == 28 else { throw KeyError.keyDerivationFailed }
|
||||
|
||||
|
||||
/// Get hash of advertisment key
|
||||
var sha = SHA256()
|
||||
sha.update(data: derivedAdvertisementKey)
|
||||
let derivedAdvertisementKeyHash = Data(sha.finalize())
|
||||
|
||||
|
||||
print("-> Derived keys for \(derivationTimestamp):")
|
||||
//print("Dervided anti tracking keys \(derivedAntiTrackingKeys.hexEncodedString())")
|
||||
//print("SymmetricKey \(derivedSymmetricKey.hexEncodedString())")
|
||||
print("Derived public key \(derivedPublicKey.hexEncodedString())")
|
||||
|
||||
findMyKey.append(FindMyKey(
|
||||
advertisedKey: derivedAdvertisementKey,
|
||||
hashedKey: derivedAdvertisementKeyHash,
|
||||
privateKey: derivedPrivateKey,
|
||||
startTime: nil,
|
||||
duration: nil,
|
||||
pu: nil,
|
||||
yCoordinate: nil,
|
||||
fullKey: nil)
|
||||
|
||||
findMyKey.append(
|
||||
FindMyKey(
|
||||
advertisedKey: derivedAdvertisementKey,
|
||||
hashedKey: derivedAdvertisementKeyHash,
|
||||
privateKey: derivedPrivateKey,
|
||||
startTime: nil,
|
||||
duration: nil,
|
||||
pu: nil,
|
||||
yCoordinate: nil,
|
||||
fullKey: nil)
|
||||
)
|
||||
|
||||
|
||||
/// Add time interval to derivation timestamp
|
||||
derivationTimestamp.addTimeInterval(self.updateInterval)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
return FindMyDevice(
|
||||
deviceId: String(self.id),
|
||||
keys: findMyKey,
|
||||
@@ -281,11 +281,11 @@ class Accessory: ObservableObject, Codable, Identifiable, Equatable, Hashable {
|
||||
reports: nil,
|
||||
decryptedReports: nil)
|
||||
}
|
||||
|
||||
static func kdf(inputData: Data, sharedInfo: Data, bytesToReturn: Int) -> Data{
|
||||
|
||||
static func kdf(inputData: Data, sharedInfo: Data, bytesToReturn: Int) -> Data {
|
||||
var derivedKey = Data()
|
||||
var counter: Int32 = 1
|
||||
|
||||
|
||||
/// derive from input and shared info until we have enough data
|
||||
while derivedKey.count < bytesToReturn {
|
||||
var shaDigest = SHA256()
|
||||
@@ -296,18 +296,17 @@ class Accessory: ObservableObject, Codable, Identifiable, Equatable, Hashable {
|
||||
derivedKey.append(Data(shaDigest.finalize()))
|
||||
counter += 1
|
||||
}
|
||||
|
||||
|
||||
/// drop bytes which are not needed and return
|
||||
derivedKey = derivedKey.dropLast(derivedKey.count - bytesToReturn)
|
||||
return derivedKey
|
||||
}
|
||||
|
||||
func resetDerivationState(){
|
||||
|
||||
func resetDerivationState() {
|
||||
/// reset keys and derivation time in case an accessory is reflashed with old keys
|
||||
self.oldestRelevantSymmetricKey = self.symmetricKey
|
||||
self.lastDerivationTimestamp = Date()
|
||||
}
|
||||
|
||||
|
||||
enum CodingKeys: String, CodingKey {
|
||||
case name
|
||||
|
||||
@@ -7,8 +7,8 @@
|
||||
// SPDX-License-Identifier: AGPL-3.0-only
|
||||
//
|
||||
|
||||
import Foundation
|
||||
import CoreLocation
|
||||
import Foundation
|
||||
import SwiftUI
|
||||
import CoreLocation
|
||||
|
||||
|
||||
@@ -10,7 +10,7 @@
|
||||
import Foundation
|
||||
|
||||
struct NRFController {
|
||||
|
||||
|
||||
static var nrfFirmwareDirectory: URL? {
|
||||
Bundle.main.resourceURL?.appendingPathComponent("NRF")
|
||||
}
|
||||
@@ -28,9 +28,9 @@ struct NRFController {
|
||||
|
||||
try FileManager.default.copyFolder(from: nrfDirectory, to: urlTemp)
|
||||
let urlScript = urlTemp.appendingPathComponent("flash_nrf.sh")
|
||||
try FileManager.default.setAttributes([FileAttributeKey.posixPermissions : 0o755], ofItemAtPath: urlScript.path)
|
||||
try FileManager.default.setAttributes([FileAttributeKey.posixPermissions : 0o755], ofItemAtPath: urlTemp.appendingPathComponent("flash_nrf.py").path)
|
||||
|
||||
try FileManager.default.setAttributes([FileAttributeKey.posixPermissions: 0o755], ofItemAtPath: urlScript.path)
|
||||
try FileManager.default.setAttributes([FileAttributeKey.posixPermissions: 0o755], ofItemAtPath: urlTemp.appendingPathComponent("flash_nrf.py").path)
|
||||
|
||||
// Get public key, newest relevant symmetric key and updateInterval for flashing
|
||||
let masterBeaconPublicKey = try accessory.getUncompressedPublicKey()
|
||||
let masterBeaconSymmetricKey = accessory.getNewestSymmetricKey()
|
||||
@@ -40,7 +40,7 @@ struct NRFController {
|
||||
let loggingFileUrl = urlTemp.appendingPathComponent("nrf_installer.log")
|
||||
try "".write(to: loggingFileUrl, atomically: true, encoding: .utf8)
|
||||
let loggingFileHandle = FileHandle.init(forWritingAtPath: loggingFileUrl.path)!
|
||||
|
||||
|
||||
// Run script
|
||||
let task = try NSUserUnixTask(url: urlScript)
|
||||
task.standardOutput = loggingFileHandle
|
||||
@@ -54,17 +54,16 @@ struct NRFController {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
try loggingFileHandle.close()
|
||||
}
|
||||
}
|
||||
|
||||
enum ClosureResult {
|
||||
case success(URL)
|
||||
case failure(URL, Error)
|
||||
case success(URL)
|
||||
case failure(URL, Error)
|
||||
}
|
||||
|
||||
|
||||
enum NRFFirmwareFlashError: Error {
|
||||
/// Missing files for flashing
|
||||
case notFound
|
||||
|
||||
@@ -0,0 +1,201 @@
|
||||
//
|
||||
// OpenHaystack – Tracking personal Bluetooth devices via Apple's Find My network
|
||||
//
|
||||
// Copyright © 2021 Secure Mobile Networking Lab (SEEMOO)
|
||||
// Copyright © 2021 The Open Wireless Link Project
|
||||
//
|
||||
// SPDX-License-Identifier: AGPL-3.0-only
|
||||
//
|
||||
|
||||
import Foundation
|
||||
import AppKit
|
||||
|
||||
|
||||
/// 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)
|
||||
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)
|
||||
}
|
||||
}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 {
|
||||
|
||||
//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
|
||||
/// - installedVersion: The currently installed version
|
||||
/// - Returns: .older when a newer version is available. .newer when the installed version is newer .same, if both versions are equal
|
||||
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}
|
||||
|
||||
if avpi > ivpi {
|
||||
return .older
|
||||
}else if ivpi > avpi {
|
||||
return .newer
|
||||
}
|
||||
|
||||
}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)->()) {
|
||||
|
||||
//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)}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
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)
|
||||
}
|
||||
}
|
||||
@@ -35,11 +35,11 @@ struct AccessoryListEntry: View {
|
||||
}
|
||||
.font(.footnote)
|
||||
}
|
||||
|
||||
|
||||
func updateIntervalView() -> some View {
|
||||
let intervalFormatter = DateComponentsFormatter()
|
||||
intervalFormatter.unitsStyle = .abbreviated
|
||||
|
||||
|
||||
return Group {
|
||||
Text("Key derivation interval: \(intervalFormatter.string(from: accessory.updateInterval)!)")
|
||||
}.font(.footnote)
|
||||
@@ -158,14 +158,14 @@ struct AccessoryListEntry: View {
|
||||
assert(false)
|
||||
}
|
||||
}
|
||||
|
||||
func copySymmetricAndPublicKey(of accessory: Accessory){
|
||||
do{
|
||||
|
||||
func copySymmetricAndPublicKey(of accessory: Accessory) {
|
||||
do {
|
||||
let symmetricKey = accessory.symmetricKey
|
||||
let publicKey = try accessory.getUncompressedPublicKey()
|
||||
let publicKeyString = [UInt8](publicKey).map { "\\x\(String($0, radix: 16))" }.joined()
|
||||
let symmetricKeyString = [UInt8](symmetricKey).map { "\\x\(String($0, radix: 16))" }.joined()
|
||||
|
||||
|
||||
let pasteboard = NSPasteboard.general
|
||||
pasteboard.prepareForNewContents(with: .currentHostOnly)
|
||||
pasteboard.setString("Symmetric key: \(symmetricKeyString)\n Uncompressed public key: \(publicKeyString) ", forType: .string)
|
||||
@@ -175,11 +175,11 @@ struct AccessoryListEntry: View {
|
||||
}
|
||||
}
|
||||
|
||||
func copySymmetricAndPublicKeyBase64(of accessory: Accessory){
|
||||
do{
|
||||
func copySymmetricAndPublicKeyBase64(of accessory: Accessory) {
|
||||
do {
|
||||
let symmetricKey = accessory.symmetricKey
|
||||
let publicKey = try accessory.getUncompressedPublicKey()
|
||||
|
||||
|
||||
let pasteboard = NSPasteboard.general
|
||||
pasteboard.prepareForNewContents(with: .currentHostOnly)
|
||||
pasteboard.setString("Symmetric key: \(symmetricKey.base64EncodedString())\n Uncompressed public key: \(publicKey.base64EncodedString()) ", forType: .string)
|
||||
|
||||
@@ -157,7 +157,7 @@ struct ManageAccessoriesView: View {
|
||||
self.sheetShown = .nrfDeviceInstall
|
||||
}
|
||||
).buttonStyle(LargeButtonStyle())
|
||||
|
||||
|
||||
Button(
|
||||
"Cancel",
|
||||
action: {
|
||||
@@ -282,7 +282,9 @@ struct ManageAccessoriesView_Previews: PreviewProvider {
|
||||
@State static var showESPSheet: Bool = true
|
||||
|
||||
static var previews: some View {
|
||||
ManageAccessoriesView(alertType: self.$alertType, scriptOutput: self.$scriptOutput, focusedAccessory: self.$focussed, accessoryToDeploy: self.$deploy, showESP32DeploySheet: self.$showESPSheet)
|
||||
ManageAccessoriesView(
|
||||
alertType: self.$alertType, scriptOutput: self.$scriptOutput, focusedAccessory: self.$focussed, accessoryToDeploy: self.$deploy,
|
||||
showESP32DeploySheet: self.$showESPSheet)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -7,7 +7,6 @@
|
||||
// SPDX-License-Identifier: AGPL-3.0-only
|
||||
//
|
||||
|
||||
|
||||
import OSLog
|
||||
import SwiftUI
|
||||
|
||||
@@ -16,14 +15,13 @@ struct NRFInstallSheet: View {
|
||||
@Binding var alertType: OpenHaystackMainView.AlertType?
|
||||
@Binding var scriptOutput: String?
|
||||
@State var isFlashing = false
|
||||
|
||||
|
||||
|
||||
@ObservedObject var days = NumbersOnly()
|
||||
@ObservedObject var hours = NumbersOnly()
|
||||
@ObservedObject var minutes = NumbersOnly()
|
||||
|
||||
|
||||
@Environment(\.presentationMode) var presentationMode
|
||||
|
||||
|
||||
var body: some View {
|
||||
VStack {
|
||||
self.flashView
|
||||
@@ -34,25 +32,25 @@ struct NRFInstallSheet: View {
|
||||
.onAppear {
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
var flashView: some View {
|
||||
VStack {
|
||||
Text("Flash your NRF Device")
|
||||
.font(.title2)
|
||||
|
||||
|
||||
Text("Fill out options for flashing firmware")
|
||||
.foregroundColor(.gray)
|
||||
|
||||
|
||||
Divider()
|
||||
|
||||
|
||||
Text("Put key update time:")
|
||||
self.timePicker
|
||||
|
||||
|
||||
Spacer()
|
||||
|
||||
|
||||
HStack {
|
||||
Spacer()
|
||||
|
||||
|
||||
Button(
|
||||
"Deploy",
|
||||
action: {
|
||||
@@ -60,17 +58,17 @@ struct NRFInstallSheet: View {
|
||||
let daysInt = Int(days.value) ?? 0
|
||||
let hoursInt = Int(hours.value) ?? 0
|
||||
let minutesInt = Int(minutes.value) ?? 0
|
||||
|
||||
|
||||
let updateInterval = daysInt * 24 * 60 + hoursInt * 60 + minutesInt
|
||||
//warn user if no update interval was given
|
||||
if updateInterval > 0 {
|
||||
deployAccessoryToNRFDevice(accessory: accessory, updateInterval: updateInterval)
|
||||
deployAccessoryToNRFDevice(accessory: accessory, updateInterval: updateInterval)
|
||||
} else {
|
||||
|
||||
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
|
||||
Button(
|
||||
"Cancel",
|
||||
action: {
|
||||
@@ -81,8 +79,8 @@ struct NRFInstallSheet: View {
|
||||
}
|
||||
|
||||
var timePicker: some View {
|
||||
Group{
|
||||
HStack{
|
||||
Group {
|
||||
HStack {
|
||||
TextField("", text: $days.value).textFieldStyle(RoundedBorderTextFieldStyle())
|
||||
Text("Day(s)")
|
||||
TextField("", text: $hours.value).textFieldStyle(RoundedBorderTextFieldStyle())
|
||||
@@ -92,40 +90,40 @@ struct NRFInstallSheet: View {
|
||||
}
|
||||
}.padding()
|
||||
}
|
||||
|
||||
|
||||
var loadingOverlay: some View {
|
||||
ZStack {
|
||||
if isFlashing {
|
||||
Rectangle()
|
||||
.fill(Color.gray)
|
||||
.opacity(0.5)
|
||||
|
||||
|
||||
VStack {
|
||||
ActivityIndicator(size: .large)
|
||||
Text("This can take up to 3min")
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
func deployAccessoryToNRFDevice(accessory: Accessory, updateInterval: Int) {
|
||||
do {
|
||||
self.isFlashing = true
|
||||
|
||||
|
||||
try NRFController.flashToNRF(
|
||||
accessory: accessory,
|
||||
updateInterval: updateInterval,
|
||||
completion: { result in
|
||||
presentationMode.wrappedValue.dismiss()
|
||||
|
||||
|
||||
self.isFlashing = false
|
||||
switch result {
|
||||
case .success(_):
|
||||
self.alertType = .deployedSuccessfully
|
||||
accessory.isDeployed = true
|
||||
accessory.usesDerivation = true
|
||||
accessory.updateInterval = TimeInterval(updateInterval*60)
|
||||
accessory.updateInterval = TimeInterval(updateInterval * 60)
|
||||
case .failure(let loggingFileUrl, let error):
|
||||
os_log(.error, "Flashing to NRF device failed %@", String(describing: error))
|
||||
self.presentationMode.wrappedValue.dismiss()
|
||||
@@ -143,17 +141,17 @@ struct NRFInstallSheet: View {
|
||||
self.alertType = .deployFailed
|
||||
self.isFlashing = false
|
||||
}
|
||||
|
||||
|
||||
self.accessory = nil
|
||||
}
|
||||
}
|
||||
|
||||
struct NRFInstallSheet_Previews: PreviewProvider {
|
||||
@State static var acc: Accessory? = try! Accessory(name: "Sample")
|
||||
|
||||
|
||||
@State static var alert: OpenHaystackMainView.AlertType?
|
||||
@State static var scriptOutput: String?
|
||||
|
||||
|
||||
static var previews: some View {
|
||||
NRFInstallSheet(accessory: $acc, alertType: $alert, scriptOutput: $scriptOutput)
|
||||
}
|
||||
@@ -163,7 +161,7 @@ class NumbersOnly: ObservableObject {
|
||||
@Published var value = "" {
|
||||
didSet {
|
||||
let filtered = value.filter { $0.isNumber }
|
||||
|
||||
|
||||
if value != filtered {
|
||||
value = filtered
|
||||
}
|
||||
|
||||
@@ -15,6 +15,8 @@ struct OpenHaystackApp: App {
|
||||
var accessoryNearbyMonitor: AccessoryNearbyMonitor?
|
||||
var frameWidth: CGFloat? = nil
|
||||
var frameHeight: CGFloat? = nil
|
||||
|
||||
@State var checkedForUpdates = false
|
||||
|
||||
init() {
|
||||
let accessoryController: AccessoryController
|
||||
@@ -35,9 +37,18 @@ struct OpenHaystackApp: App {
|
||||
OpenHaystackMainView()
|
||||
.environmentObject(self.accessoryController)
|
||||
.frame(width: self.frameWidth, height: self.frameHeight)
|
||||
.onAppear {
|
||||
self.checkForUpdates()
|
||||
}
|
||||
}
|
||||
.commands {
|
||||
SidebarCommands()
|
||||
}
|
||||
}
|
||||
|
||||
func checkForUpdates() {
|
||||
guard checkedForUpdates == false, ProcessInfo().arguments.contains("-stopUpdateCheck") == false else {return}
|
||||
UpdateCheckController.checkForNewVersion()
|
||||
checkedForUpdates = true
|
||||
}
|
||||
}
|
||||
|
||||
73
OpenHaystack/OpenHaystackTests/UpdateCheckTests.swift
Normal file
73
OpenHaystack/OpenHaystackTests/UpdateCheckTests.swift
Normal file
@@ -0,0 +1,73 @@
|
||||
//
|
||||
// OpenHaystack – Tracking personal Bluetooth devices via Apple's Find My network
|
||||
//
|
||||
// Copyright © 2021 Secure Mobile Networking Lab (SEEMOO)
|
||||
// Copyright © 2021 The Open Wireless Link Project
|
||||
//
|
||||
// SPDX-License-Identifier: AGPL-3.0-only
|
||||
//
|
||||
|
||||
import Foundation
|
||||
import 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"
|
||||
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>
|
||||
"""
|
||||
|
||||
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>"
|
||||
XCTAssertEqual(UpdateCheckController.getVersion(from: h2), "0.5")
|
||||
let h3 = "<h1>Release v1.5</h1>"
|
||||
XCTAssertEqual(UpdateCheckController.getVersion(from: h3), "1.5")
|
||||
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()
|
||||
})
|
||||
wait(for: [expect], timeout: 20.0)
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user