Mark accessories as online when receiving Bluetooth advertisements

This commit is contained in:
Milan Stute
2021-03-15 12:54:51 +01:00
parent d5546e1fa8
commit 5117674ac9
9 changed files with 201 additions and 8 deletions

View File

@@ -51,6 +51,10 @@
78EC226C25DBC2E40042B775 /* OpenHaystackMainView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 78EC226B25DBC2E40042B775 /* OpenHaystackMainView.swift */; };
78EC227225DBC8CE0042B775 /* Accessory.swift in Sources */ = {isa = PBXBuildFile; fileRef = 78EC227125DBC8CE0042B775 /* Accessory.swift */; };
78EC227725DBDB7E0042B775 /* KeychainController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 78EC227625DBDB7E0042B775 /* KeychainController.swift */; };
F12D5A5A25FA4F3500CBBA09 /* BluetoothAccessoryScanner.swift in Sources */ = {isa = PBXBuildFile; fileRef = F12D5A5925FA4F3500CBBA09 /* BluetoothAccessoryScanner.swift */; };
F12D5A6025FA79FA00CBBA09 /* Advertisement.swift in Sources */ = {isa = PBXBuildFile; fileRef = F12D5A5F25FA79FA00CBBA09 /* Advertisement.swift */; };
F1647C1625FF6C61004144D6 /* BluetoothTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = F1647C1525FF6C61004144D6 /* BluetoothTests.swift */; };
F1647C1B25FF7954004144D6 /* AccessoryNearbyMonitor.swift in Sources */ = {isa = PBXBuildFile; fileRef = F1647C1A25FF7954004144D6 /* AccessoryNearbyMonitor.swift */; };
F16BA9E925E7DB2D00238183 /* NIOSSL in Frameworks */ = {isa = PBXBuildFile; productRef = F16BA9E825E7DB2D00238183 /* NIOSSL */; };
/* End PBXBuildFile section */
@@ -150,6 +154,10 @@
78EC226B25DBC2E40042B775 /* OpenHaystackMainView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OpenHaystackMainView.swift; sourceTree = "<group>"; };
78EC227125DBC8CE0042B775 /* Accessory.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Accessory.swift; sourceTree = "<group>"; };
78EC227625DBDB7E0042B775 /* KeychainController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = KeychainController.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>"; };
F1647C1525FF6C61004144D6 /* BluetoothTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BluetoothTests.swift; sourceTree = "<group>"; };
F1647C1A25FF7954004144D6 /* AccessoryNearbyMonitor.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AccessoryNearbyMonitor.swift; sourceTree = "<group>"; };
/* End PBXFileReference section */
/* Begin PBXFrameworksBuildPhase section */
@@ -315,6 +323,7 @@
78EC226325DAE0BE0042B775 /* OpenHaystackTests.swift */,
78EC226525DAE0BE0042B775 /* Info.plist */,
78023CB025F7841F00B083EF /* MicrocontrollerTests.swift */,
F1647C1525FF6C61004144D6 /* BluetoothTests.swift */,
);
path = OpenHaystackTests;
sourceTree = "<group>";
@@ -322,6 +331,7 @@
78EC226E25DBC2FC0042B775 /* HaystackApp */ = {
isa = PBXGroup;
children = (
F12D5A5E25FA79D600CBBA09 /* Bluetooth */,
78023CAC25F7775300B083EF /* Firmwares */,
78286D3A25E4017400F65511 /* Mail Plugin */,
78EC227025DBC8BB0042B775 /* Views */,
@@ -331,6 +341,7 @@
787D8AC025DECD3C00148766 /* AccessoryController.swift */,
78023CAA25F7767000B083EF /* ESP32Controller.swift */,
7821DAD025F7B2C10054DC33 /* FileManager.swift */,
F1647C1A25FF7954004144D6 /* AccessoryNearbyMonitor.swift */,
);
path = HaystackApp;
sourceTree = "<group>";
@@ -360,6 +371,15 @@
path = Views;
sourceTree = "<group>";
};
F12D5A5E25FA79D600CBBA09 /* Bluetooth */ = {
isa = PBXGroup;
children = (
F12D5A5925FA4F3500CBBA09 /* BluetoothAccessoryScanner.swift */,
F12D5A5F25FA79FA00CBBA09 /* Advertisement.swift */,
);
path = Bluetooth;
sourceTree = "<group>";
};
/* End PBXGroup section */
/* Begin PBXNativeTarget section */
@@ -575,6 +595,7 @@
78D9B80625F7CF60009B9CE8 /* ManageAccessoriesView.swift in Sources */,
78486BEF25DD711E0007ED87 /* PopUpAlertView.swift in Sources */,
78014A2925DC08580089F6D9 /* MicrobitController.swift in Sources */,
F1647C1B25FF7954004144D6 /* AccessoryNearbyMonitor.swift in Sources */,
78286D1F25E3D8B800F65511 /* ALTAnisetteData.m in Sources */,
781EB3EC25DAD7EA00FEAA19 /* DecryptReports.swift in Sources */,
78EC226C25DBC2E40042B775 /* OpenHaystackMainView.swift in Sources */,
@@ -587,10 +608,12 @@
781EB3F125DAD7EA00FEAA19 /* FindMyKeyDecoder.swift in Sources */,
787D8AC125DECD3C00148766 /* AccessoryController.swift in Sources */,
78023CAB25F7767000B083EF /* ESP32Controller.swift in Sources */,
F12D5A6025FA79FA00CBBA09 /* Advertisement.swift in Sources */,
781EB3F225DAD7EA00FEAA19 /* OpenHaystackApp.swift in Sources */,
781EB3F325DAD7EA00FEAA19 /* Models.swift in Sources */,
781EB3F425DAD7EA00FEAA19 /* FindMyController.swift in Sources */,
781EB3F525DAD7EA00FEAA19 /* BoringSSL.m in Sources */,
F12D5A5A25FA4F3500CBBA09 /* BluetoothAccessoryScanner.swift in Sources */,
78286D5625E401F000F65511 /* MailPluginManager.swift in Sources */,
);
runOnlyForDeploymentPostprocessing = 0;
@@ -609,6 +632,7 @@
buildActionMask = 2147483647;
files = (
78023CB125F7841F00B083EF /* MicrocontrollerTests.swift in Sources */,
F1647C1625FF6C61004144D6 /* BluetoothTests.swift in Sources */,
78EC226425DAE0BE0042B775 /* OpenHaystackTests.swift in Sources */,
);
runOnlyForDeploymentPostprocessing = 0;

View File

@@ -8,3 +8,35 @@
//
import Foundation
class AccessoryNearbyMonitor: BluetoothAccessoryDelegate {
var accessoryController: AccessoryController
var scanner: BluetoothAccessoryScanner
init(accessoryController: AccessoryController) {
self.accessoryController = accessoryController
self.scanner = BluetoothAccessoryScanner()
self.initScanner()
}
func initScanner() {
self.scanner.delegate = self
}
func received(_ advertisement: Advertisement) {
guard let accessory = getAccessoryForAdvertisement(advertisement) else {
return
}
accessory.isOnline = true
}
func getAccessoryForAdvertisement(_ advertisement: Advertisement) -> Accessory? {
let accessory =
try? self.accessoryController.accessories.first {
let accessoryPublicKey = try $0.getAdvertisementKey().advanced(by: 6)
return accessoryPublicKey == advertisement.publicKeyPayload
} ?? nil
return accessory
}
}

View File

@@ -0,0 +1,55 @@
//
// 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 CoreBluetooth
import Foundation
struct Advertisement {
let publicKeyPayload: Data
init?(fromAdvertisementData: [String: Any]) {
guard let manufacturerData = fromAdvertisementData[CBAdvertisementDataManufacturerDataKey] as? Data else {
return nil
}
try? self.init(fromManufacturerData: manufacturerData)
}
init?(fromManufacturerData: Data) {
guard let publicKey = Advertisement.extractPublicKeyFromPayload(fromManufacturerData) else {
return nil
}
self.publicKeyPayload = publicKey
}
static let publicKeyPayloadLength = 22
static func extractPublicKeyFromPayload(_ payload: Data) -> Data? {
guard payload.count == 29 else {
return nil
}
// Apple company ID
guard payload.subdata(in: 0..<2) == Data([0x4c, 0x00]) else {
return nil
}
// Offline finding sub type
guard payload.subdata(in: 2..<3) == Data([0x12]) else {
return nil
}
// Offline finding sub type length
guard payload.subdata(in: 3..<4) == Data([0x19]) else {
return nil
}
let publicKey = payload.subdata(in: 5..<5 + publicKeyPayloadLength)
guard publicKey.count == publicKeyPayloadLength else {
return nil
}
return publicKey
}
}

View File

@@ -0,0 +1,47 @@
//
// 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 CoreBluetooth
import Foundation
protocol BluetoothAccessoryDelegate {
func received(_ advertisement: Advertisement)
}
public class BluetoothAccessoryScanner: NSObject, CBCentralManagerDelegate {
var scanner: CBCentralManager!
var delegate: BluetoothAccessoryDelegate?
override init() {
super.init()
scanner = CBCentralManager(delegate: self, queue: DispatchQueue.main)
}
public func centralManagerDidUpdateState(_ central: CBCentralManager) {
startScanning(central)
}
private func startScanning(_ central: CBCentralManager) {
guard central.state == .poweredOn else {
return
}
let scanOptions = [
CBCentralManagerScanOptionAllowDuplicatesKey: false
]
scanner.scanForPeripherals(withServices: nil, options: scanOptions)
}
public func centralManager(_ central: CBCentralManager, didDiscover peripheral: CBPeripheral, advertisementData: [String: Any], rssi RSSI: NSNumber) {
guard let adv = Advertisement(fromAdvertisementData: advertisementData) else {
return
}
self.delegate?.received(adv)
}
}

View File

@@ -36,6 +36,7 @@ class Accessory: ObservableObject, Codable, Identifiable, Equatable, Hashable {
@Published var lastLocation: CLLocation?
@Published var locationTimestamp: Date?
@Published var isDeployed: Bool
@Published var isOnline: Bool = false
init(name: String = "New accessory", color: Color = randomColor(), iconName: String = randomIcon()) throws {
self.name = name

View File

@@ -54,6 +54,9 @@ struct AccessoryListEntry: View {
}
Spacer()
Circle()
.fill(accessory.isOnline ? Color.green : Color.red)
.frame(width: 8, height: 8)
if !accessory.isDeployed {
Button(
action: { self.deployAccessoryToMicrobit(accessory) },

View File

@@ -28,5 +28,7 @@
<true/>
<key>NSSupportsSuddenTermination</key>
<true/>
<key>NSBluetoothAlwaysUsageDescription</key>
<string>OpenHaystack uses Bluetooth to detect the presence of nearby accessories.</string>
</dict>
</plist>

View File

@@ -12,6 +12,7 @@ import SwiftUI
@main
struct OpenHaystackApp: App {
@StateObject var accessoryController: AccessoryController
var accessoryNearbyMonitor: AccessoryNearbyMonitor
init() {
let accessoryController: AccessoryController
@@ -21,6 +22,7 @@ struct OpenHaystackApp: App {
accessoryController = AccessoryController()
}
self._accessoryController = StateObject(wrappedValue: accessoryController)
self.accessoryNearbyMonitor = AccessoryNearbyMonitor(accessoryController: accessoryController)
}
var body: some Scene {

View File

@@ -7,8 +7,11 @@
// SPDX-License-Identifier: AGPL-3.0-only
//
import CoreBluetooth
import XCTest
@testable import OpenHaystack
class BluetoothTests: XCTestCase {
override func setUpWithError() throws {
@@ -19,16 +22,40 @@ class BluetoothTests: XCTestCase {
// Put teardown code here. This method is called after the invocation of each test method in the class.
}
func testExample() throws {
// This is an example of a functional test case.
// Use XCTAssert and related functions to verify your tests produce the correct results.
func testNoManufacturerData() throws {
let data: [String: Any] = [
"": Data()
]
let adv = Advertisement(fromAdvertisementData: data)
XCTAssertNil(adv)
}
func testPerformanceExample() throws {
// This is an example of a performance test case.
self.measure {
// Put the code you want to measure the time of here.
func testEmptyManufacturerData() throws {
let data: [String: Any] = [
CBAdvertisementDataManufacturerDataKey: Data()
]
let adv = Advertisement(fromAdvertisementData: data)
XCTAssertNil(adv)
}
func testCorrectAdvertisement() throws {
let publicKey = "11111111111111111111111111111111111111111111".hexaData
let data = "4c00121900111111111111111111111111111111111111111111110100".hexaData
let adv = Advertisement(fromManufacturerData: data)
XCTAssertNotNil(adv)
XCTAssertEqual(adv?.publicKeyPayload, publicKey)
}
}
extension StringProtocol {
var hexaData: Data { .init(hexa) }
var hexaBytes: [UInt8] { .init(hexa) }
private var hexa: UnfoldSequence<UInt8, Index> {
sequence(state: startIndex) { startIndex in
guard startIndex < self.endIndex else { return nil }
let endIndex = self.index(startIndex, offsetBy: 2, limitedBy: self.endIndex) ?? self.endIndex
defer { startIndex = endIndex }
return UInt8(self[startIndex..<endIndex], radix: 16)
}
}
}