9 Commits
v0.5.3 ... main

Author SHA1 Message Date
Sebastian
8d214aa5eb Use macOS 14 runner with Xcode 15.3 2024-07-09 09:19:10 +02:00
Sebastian
27d975b1f0 Add manual trigger for all workflows 2024-07-09 09:19:10 +02:00
Sebastian
173eb741fa Fix CI to use renamed GitHub Action 2024-07-09 09:19:10 +02:00
Sebastian
17410e2c00 Create a SetttingsView to manually enter Search Party Token, add error handling for expired token 2024-07-08 10:37:38 +02:00
Sebastian
3ef4280df1 Use AOSKit to generate anisette headers 2024-07-08 10:37:38 +02:00
Sebastian
1b66d15cad Fix report decryption for new report format 2024-07-08 10:37:38 +02:00
Alexander Heinrich
e8dcf61daa Adding CITATION.cff file 2024-06-11 12:38:44 +02:00
Shai Mishali
7d72fa1ac1 [fix] Derive symmetric key correctly 2023-10-16 17:19:56 +02:00
Alexander Heinrich
6eb2822632 Updating Mail Plugin to work with macOS 13.5 and 13.6 2023-10-09 13:08:46 +02:00
17 changed files with 300 additions and 38 deletions

View File

@@ -9,6 +9,7 @@ on:
branches: [ main ]
paths:
- OpenHaystack/**
workflow_dispatch:
env:
APP: OpenHaystack
@@ -18,7 +19,7 @@ defaults:
jobs:
format-swift:
runs-on: macos-12
runs-on: macos-14
steps:
- name: "Checkout code"
uses: actions/checkout@v2
@@ -28,7 +29,7 @@ jobs:
run: swift-format lint --recursive .
format-objc:
runs-on: macos-12
runs-on: macos-14
steps:
- name: "Checkout code"
uses: actions/checkout@v2
@@ -38,16 +39,16 @@ jobs:
run: clang-format -n **/*.{h,m}
build-app:
runs-on: macos-12
runs-on: macos-14
needs:
- format-swift
- format-objc
steps:
- name: "Checkout code"
uses: actions/checkout@v2
- name: "Select Xcode 13"
uses: devbotsxyz/xcode-select@v1
- name: "Select Xcode 15.3"
uses: keehun/xcode-select@v1
with:
version: "13"
version: "15.3"
- name: "Archive project"
run: xcodebuild archive -scheme ${APP} -configuration release -archivePath ${APP}.xcarchive

View File

@@ -32,7 +32,7 @@ jobs:
- name: "Checkout code"
uses: actions/checkout@v2
- name: "Select Xcode 12"
uses: devbotsxyz/xcode-select@v1
uses: keehun/xcode-select@v1
with:
version: "12"
- name: "Archive project"
@@ -47,7 +47,7 @@ jobs:
- name: "Checkout code"
uses: actions/checkout@v2
- name: "Select Xcode 12"
uses: devbotsxyz/xcode-select@v1
uses: keehun/xcode-select@v1
with:
version: "12"
- name: "Archive project"

View File

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

View File

@@ -9,6 +9,7 @@ on:
branches: [ main ]
paths:
- Firmware/Microbit_v1/**
workflow_dispatch:
defaults:
run:
@@ -16,7 +17,7 @@ defaults:
jobs:
build-firmware:
runs-on: macos-11
runs-on: macos-14
steps:
- uses: actions/checkout@v2

View File

@@ -4,6 +4,7 @@ on:
push:
tags:
- 'v*' # Push events to matching v*, i.e. v1.0, v20.15.10
workflow_dispatch:
jobs:
build-firmware-esp32:
@@ -30,7 +31,7 @@ jobs:
build-and-release:
name: "Create release on GitHub"
runs-on: macos-11
runs-on: macos-14
env:
APP: OpenHaystack
PROJECT_DIR: OpenHaystack
@@ -42,10 +43,10 @@ jobs:
steps:
- name: Checkout code
uses: actions/checkout@v2
- name: "Select Xcode 12"
uses: devbotsxyz/xcode-select@v1
- name: "Select Xcode 15.3"
uses: keehun/xcode-select@v1
with:
version: "12"
version: "15.3"
- name: "Add ESP32 firmware"
uses: actions/download-artifact@v2
with:

31
CITATION.cff Normal file
View File

@@ -0,0 +1,31 @@
# This CITATION.cff file was generated with cffinit.
# Visit https://bit.ly/cffinit to generate yours today!
cff-version: 1.2.0
title: OpenHaystack
message: 'If you use this software, please cite it as below.'
type: software
authors:
- given-names: Alexander
family-names: Heinrich
affiliation: 'SEEMOO, TU Darmstadt'
orcid: 'https://orcid.org/0000-0002-1150-1922'
- given-names: Milan
family-names: Stute
affiliation: 'SEEMOO, TU Darmstadt'
orcid: 'https://orcid.org/0000-0003-4921-8476'
- given-names: Matthias
family-names: Hollick
affiliation: 'SEEMOO, TU Darmstadt'
orcid: 'https://orcid.org/0000-0002-9163-5989'
repository-code: 'https://github.com/seemoo-lab/openhaystack'
abstract: >-
OpenHaystack is a framework for tracking personal
Bluetooth devices via Apple's massive Find My network. Use
it to create your own tracking tags that you can append to
physical objects (keyrings, backpacks, ...) or integrate
it into other Bluetooth-capable devices such as notebooks.
license: AGPL-3.0
commit: 7d72fa1ac19d2a9f6dec43011be07df8976a8b02
version: 0.5.3
date-released: '2023-10-09'

View File

@@ -3,7 +3,7 @@
archiveVersion = 1;
classes = {
};
objectVersion = 52;
objectVersion = 54;
objects = {
/* Begin PBXBuildFile section */
@@ -58,6 +58,7 @@
78EC227225DBC8CE0042B775 /* Accessory.swift in Sources */ = {isa = PBXBuildFile; fileRef = 78EC227125DBC8CE0042B775 /* Accessory.swift */; };
78EC227725DBDB7E0042B775 /* KeychainController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 78EC227625DBDB7E0042B775 /* KeychainController.swift */; };
78F8BB4C261C50EB00D9F37F /* LargeButtonStyle.swift in Sources */ = {isa = PBXBuildFile; fileRef = 78F8BB4B261C50EB00D9F37F /* LargeButtonStyle.swift */; };
9ED440A02C1605EF002574D1 /* OpenHaystackSettingsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9ED4409F2C1605EF002574D1 /* OpenHaystackSettingsView.swift */; };
F126102F2600D1D80066A859 /* Slider+LogScale.swift in Sources */ = {isa = PBXBuildFile; fileRef = F126102E2600D1D80066A859 /* Slider+LogScale.swift */; };
F12D5A5A25FA4F3500CBBA09 /* BluetoothAccessoryScanner.swift in Sources */ = {isa = PBXBuildFile; fileRef = F12D5A5925FA4F3500CBBA09 /* BluetoothAccessoryScanner.swift */; };
F12D5A6025FA79FA00CBBA09 /* Advertisement.swift in Sources */ = {isa = PBXBuildFile; fileRef = F12D5A5F25FA79FA00CBBA09 /* Advertisement.swift */; };
@@ -169,6 +170,7 @@
78EC227125DBC8CE0042B775 /* Accessory.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Accessory.swift; sourceTree = "<group>"; };
78EC227625DBDB7E0042B775 /* KeychainController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = KeychainController.swift; sourceTree = "<group>"; };
78F8BB4B261C50EB00D9F37F /* LargeButtonStyle.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LargeButtonStyle.swift; sourceTree = "<group>"; };
9ED4409F2C1605EF002574D1 /* OpenHaystackSettingsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OpenHaystackSettingsView.swift; sourceTree = "<group>"; };
F126102E2600D1D80066A859 /* Slider+LogScale.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Slider+LogScale.swift"; sourceTree = "<group>"; };
F12D5A5925FA4F3500CBBA09 /* BluetoothAccessoryScanner.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BluetoothAccessoryScanner.swift; sourceTree = "<group>"; };
F12D5A5F25FA79FA00CBBA09 /* Advertisement.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Advertisement.swift; sourceTree = "<group>"; };
@@ -380,6 +382,7 @@
isa = PBXGroup;
children = (
78F8BB4A261C50D500D9F37F /* Styles */,
9ED4409F2C1605EF002574D1 /* OpenHaystackSettingsView.swift */,
78286D7625E5114600F65511 /* ActivityIndicator.swift */,
78EC226B25DBC2E40042B775 /* OpenHaystackMainView.swift */,
78486BEE25DD711E0007ED87 /* PopUpAlertView.swift */,
@@ -644,6 +647,7 @@
7821DAD325F7C39A0054DC33 /* ESP32InstallSheet.swift in Sources */,
781EB3F125DAD7EA00FEAA19 /* FindMyKeyDecoder.swift in Sources */,
787D8AC125DECD3C00148766 /* AccessoryController.swift in Sources */,
9ED440A02C1605EF002574D1 /* OpenHaystackSettingsView.swift in Sources */,
78023CAB25F7767000B083EF /* ESP32Controller.swift in Sources */,
F12D5A6025FA79FA00CBBA09 /* Advertisement.swift in Sources */,
781EB3F225DAD7EA00FEAA19 /* OpenHaystackApp.swift in Sources */,

View File

@@ -1,5 +1,24 @@
{
"originHash" : "bfeb00ee66eb6db71ff8535b5ea7585725e9fe73d97f066170be55b745d346e9",
"pins" : [
{
"identity" : "swift-atomics",
"kind" : "remoteSourceControl",
"location" : "https://github.com/apple/swift-atomics.git",
"state" : {
"revision" : "cd142fd2f64be2100422d658e7411e39489da985",
"version" : "1.2.0"
}
},
{
"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",
@@ -14,8 +33,8 @@
"kind" : "remoteSourceControl",
"location" : "https://github.com/apple/swift-nio.git",
"state" : {
"revision" : "124119f0bb12384cef35aa041d7c3a686108722d",
"version" : "2.40.0"
"revision" : "9428f62793696d9a0cc1f26a63f63bb31da0516d",
"version" : "2.66.0"
}
},
{
@@ -23,10 +42,19 @@
"kind" : "remoteSourceControl",
"location" : "https://github.com/apple/swift-nio-ssl",
"state" : {
"revision" : "c30c680c78c99afdabf84805a83c8745387c4ac7",
"version" : "2.20.2"
"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" : 2
"version" : 3
}

View File

@@ -10,6 +10,20 @@
import Foundation
import OSLog
/// Uses AOSKit to get anisette headers
@objc private protocol AOSUtilitiesProtocol
{
static var machineSerialNumber: String? { get }
static var machineUDID: String? { get }
static func retrieveOTPHeadersForDSID(_ dsid: String) -> [String: Any]?
// Non-static versions used for respondsToSelector:
var machineSerialNumber: String? { get }
var machineUDID: String? { get }
func retrieveOTPHeadersForDSID(_ dsid: String) -> [String: Any]?
}
/// Uses the AltStore Mail plugin to access recent anisette data.
public class AnisetteDataManager: NSObject {
@objc static let shared = AnisetteDataManager()
@@ -28,7 +42,7 @@ public class AnisetteDataManager: NSObject {
}
func requestAnisetteData(_ completion: @escaping (Result<AppleAccountData, Error>) -> Void) {
if let accountData = self.requestAnisetteDataAuthKit() {
if let accountData = self.requestAnisetteDataAOSKit() {
os_log(.debug, "Anisette Data loaded %@", accountData.debugDescription)
completion(.success(accountData))
return
@@ -86,6 +100,61 @@ public class AnisetteDataManager: NSObject {
return accountData
}
/// Adapted from: https://github.com/altstoreio/AltStore/blob/main/AltServer/Anisette%20Data/AnisetteDataManager.swift
func requestAnisetteDataAOSKit() -> AppleAccountData? {
do
{
let aosKitURL = URL(fileURLWithPath: "/System/Library/PrivateFrameworks/AOSKit.framework")
guard let aosKit = Bundle(url: aosKitURL) else { throw AnisetteDataError.aosKitFailure }
try aosKit.loadAndReturnError()
guard let AOSUtilitiesClass = NSClassFromString("AOSUtilities"),
AOSUtilitiesClass.responds(to: #selector(AOSUtilitiesProtocol.retrieveOTPHeadersForDSID(_:))),
AOSUtilitiesClass.responds(to: #selector(getter: AOSUtilitiesProtocol.machineSerialNumber)),
AOSUtilitiesClass.responds(to: #selector(getter: AOSUtilitiesProtocol.machineUDID))
else { throw AnisetteDataError.aosKitFailure }
let AOSUtilities = unsafeBitCast(AOSUtilitiesClass, to: AOSUtilitiesProtocol.Type.self)
guard let anisetteData = AOSUtilities.retrieveOTPHeadersForDSID("-2") else { throw AnisetteDataError.aosKitFailure }
guard let machineID = anisetteData["X-Apple-MD-M"] as? String,
let otp = anisetteData["X-Apple-MD"] as? String,
let deviceId = AOSUtilities.machineUDID,
let localUserId = deviceId.data(using: .utf8)?.base64EncodedString(),
let deviceClass = NSClassFromString("AKDevice")
else {
print("Failure retrieving anisette headers from AOSKit")
throw AnisetteDataError.aosKitFailure
}
let device: AKDevice = deviceClass.current()
let routingInfo: UInt64 = 84215040
let accountData = AppleAccountData(
machineID: machineID,
oneTimePassword: otp,
localUserID: localUserId,
routingInfo: routingInfo,
deviceUniqueIdentifier: device.uniqueDeviceIdentifier(),
deviceSerialNumber: device.serialNumber(),
deviceDescription: device.serverFriendlyDescription(),
date: Date(),
locale: Locale.current,
timeZone: TimeZone.current)
/// This only works with SIP disabled
if let spToken = ReportsFetcher().fetchSearchpartyToken() {
accountData.searchPartyToken = spToken
}
return accountData
}
catch
{
return nil
}
}
@objc func requestAnisetteDataObjc(_ completion: @escaping ([AnyHashable: Any]?) -> Void) {
self.requestAnisetteData { result in
switch result {
@@ -163,4 +232,5 @@ extension AnisetteDataManager {
enum AnisetteDataError: Error {
case pluginNotFound
case invalidAnisetteData
case aosKitFailure
}

View File

@@ -20,7 +20,12 @@ struct DecryptReports {
/// - Throws: Errors if the decryption fails
/// - Returns: An decrypted location report
static func decrypt(report: FindMyReport, with key: FindMyKey) throws -> FindMyLocationReport {
let payloadData = report.payload
var payloadData = report.payload
/// Fix decryption for new report format
/// See: https://github.com/biemster/FindMy/issues/52
if payloadData.count > 88 {
payloadData.remove(at: 5)
}
let keyData = key.privateKey
let privateKey = keyData

View File

@@ -148,6 +148,10 @@ class FindMyController: ObservableObject {
} catch {
print("Failed with error \(error)")
if jsonData.isEmpty {
print("Empty response, consider updating your Search Party Token")
completion(FindMyErrors.invalidSearchPartyToken)
}
devices[deviceIndex].reports = []
}
fetchReportGroup.leave()
@@ -241,4 +245,5 @@ class FindMyController: ObservableObject {
enum FindMyErrors: Error {
case decodingPlistFailed(message: String)
case objectReleased
case invalidSearchPartyToken
}

View File

@@ -13,6 +13,7 @@ import OSLog
import SwiftUI
class AccessoryController: ObservableObject {
@AppStorage("searchPartyToken") private var searchPartyToken: String = ""
@Published var accessories: [Accessory]
var selfObserver: AnyCancellable?
var listElementsObserver = [AnyCancellable]()
@@ -244,10 +245,8 @@ class AccessoryController: ObservableObject {
case .failure(_):
completion(.failure(.activatePlugin))
case .success(let accountData):
guard let token = accountData.searchPartyToken,
token.isEmpty == false
else {
let token = accountData.searchPartyToken ?? self.searchPartyToken.data(using: .utf8) ?? Data()
if token.isEmpty {
completion(.failure(.searchPartyToken))
return
}
@@ -256,7 +255,12 @@ class AccessoryController: ObservableObject {
switch result {
case .failure(let error):
os_log(.error, "Downloading reports failed %@", error.localizedDescription)
completion(.failure(.downloadingReportsFailed))
switch error {
case FindMyErrors.invalidSearchPartyToken:
completion(.failure(.invalidSearchPartyToken))
default:
completion(.failure(.downloadingReportsFailed))
}
case .success(let devices):
let reports = devices.compactMap({ $0.reports }).flatMap({ $0 })
if reports.isEmpty {

View File

@@ -214,7 +214,7 @@ class Accessory: ObservableObject, Codable, Identifiable, Equatable, Hashable {
/// Derive FindMyKeys until we have symmetric key from one week before now
while self.lastDerivationTimestamp < Date() - TimeInterval(7 * 24 * 60 * 60) {
self.lastDerivationTimestamp.addTimeInterval(self.updateInterval)
self.oldestRelevantSymmetricKey = Accessory.kdf(inputData: self.symmetricKey, sharedInfo: "update".data(using: .ascii)!, bytesToReturn: 32)
self.oldestRelevantSymmetricKey = Accessory.kdf(inputData: self.oldestRelevantSymmetricKey, sharedInfo: "update".data(using: .ascii)!, bytesToReturn: 32)
}
/// we need to generate Keys from seven days in the past until now and 10 extra keys in case of desynchronization

View File

@@ -38,6 +38,9 @@ struct OpenHaystackMainView: View {
@State var showESP32DeploySheet = false
@AppStorage("searchPartyToken") private var settingsSPToken: String?
@AppStorage("useMailPlugin") private var settingsUseMailPlugin: Bool = false
var body: some View {
NavigationView {
@@ -135,7 +138,7 @@ struct OpenHaystackMainView: View {
Button(
action: {
if !self.mailPluginIsActive {
if self.settingsUseMailPlugin && !self.mailPluginIsActive {
self.showMailPlugInPopover.toggle()
self.checkPluginIsRunning(silent: true, nil)
} else {
@@ -174,17 +177,26 @@ struct OpenHaystackMainView: View {
return
}
let pluginManager = MailPluginManager()
// Check if the plugin is installed
if pluginManager.isMailPluginInstalled == false {
// Install the mail plugin
self.alertType = .activatePlugin
self.checkPluginIsRunning(silent: true, nil)
} else {
self.checkPluginIsRunning(nil)
/// Checks if the search party token was set in the settings. If true the plugin is also not needed
if let tokenString = self.settingsSPToken {
self.searchPartyToken = tokenString
return
}
/// Uses mail plugin if enabled in settings
if self.settingsUseMailPlugin {
let pluginManager = MailPluginManager()
// Check if the plugin is installed
if pluginManager.isMailPluginInstalled == false {
// Install the mail plugin
self.alertType = .activatePlugin
self.checkPluginIsRunning(silent: true, nil)
} else {
self.checkPluginIsRunning(nil)
}
}
}
/// Download the location reports for all current accessories. Shows an error if something fails, like plug-in is missing
@@ -308,7 +320,19 @@ struct OpenHaystackMainView: View {
title: Text("Add the search party token"),
message: Text(
"""
Please paste the search party token below after copying itfrom the macOS Keychain.
Please paste the search party token in the settings after copying it from the macOS Keychain.
The item that contains the key can be found by searching for:
com.apple.account.DeviceLocator.search-party-token
"""
),
dismissButton: Alert.Button.okay())
case .invalidSearchPartyToken:
return Alert(
title: Text("Invalid search party token"),
message: Text(
"""
The request returned an empty result, this is probably due to an invalid search party token.
Please consider updating your search party token in the settings after copying it from the macOS Keychain.
The item that contains the key can be found by searching for:
com.apple.account.DeviceLocator.search-party-token
"""
@@ -388,6 +412,7 @@ struct OpenHaystackMainView: View {
case keyError
case searchPartyToken
case invalidSearchPartyToken
case deployFailed
case nrfDeployFailed
case deployedSuccessfully

View File

@@ -0,0 +1,36 @@
//
// OpenHaystack Tracking personal Bluetooth devices via Apple's Find My network
//
// Copyright © 2024 Secure Mobile Networking Lab (SEEMOO)
// Copyright © 2024 The Open Wireless Link Project
//
// SPDX-License-Identifier: AGPL-3.0-only
//
import Foundation
import SwiftUI
struct OpenHaystackSettingsView: View {
var body: some View {
TabView {
GeneralSettingsView()
.tabItem {
Label("General", systemImage: "gear")
}
}
}
}
struct GeneralSettingsView: View {
@AppStorage("useMailPlugin") private var useMailPlugin = false
@AppStorage("searchPartyToken") private var searchPartyToken = ""
var body: some View {
Form {
Toggle("Use Apple Mail Plugin (only works on macOS 13 and lower)", isOn: $useMailPlugin)
TextField("Search Party Token", text: $searchPartyToken)
}
.padding(20)
.frame(width: 600, height: 200)
}
}

View File

@@ -44,6 +44,11 @@ struct OpenHaystackApp: App {
.commands {
SidebarCommands()
}
#if os(macOS)
Settings {
OpenHaystackSettingsView()
}
#endif
}
func checkForUpdates() {

View File

@@ -221,5 +221,50 @@
<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>