23 Commits

Author SHA1 Message Date
Alexander Heinrich
ebfe7922ec Updating build workflow for Xcode 13 2022-01-04 12:59:34 +01:00
Alexander Heinrich
005d642dd8 Adding new NRF firmware which supports rotating keys 2022-01-04 12:44:45 +01:00
Alexander Heinrich
c349ffde7f Updating mail plug-in for macOS 12.1 2022-01-04 12:44:45 +01:00
Alexander Heinrich
f582d86455 Updating mail plug-in for macOS 12.1 2022-01-04 12:44:45 +01:00
Alexander Heinrich
e55a0959af Adding a class that automatically checks for updates of the app 2022-01-04 12:44:45 +01:00
Morten Harter
278fe4e30d Added support for key derivation
Added deployment for nRF52 Devices
2022-01-04 12:44:45 +01:00
Alexander Heinrich
d9a1a33b1e Updating project to be M1 (ARM) compatible 2021-11-25 11:24:14 +01:00
Alexander Heinrich
3a8e543491 Using the ESP32 v4.3 branch and not the release since the release is still failing to build 2021-11-04 12:50:10 +01:00
Alexander Heinrich
c6600b0555 Updating esp-idf version 2021-11-04 11:16:41 +01:00
Howard
9363dc0534 Clarify the nature of the key parameter used by flash_esp32.sh
The OpenHaystack UI does not expose the concept of "public key". The script `flash_esp.32` is in fact expecting the base-64 encoded advertisement key.
2021-11-04 09:40:54 +01:00
yoution
e95fe0cc32 Update README.md
fix: fix board type
2021-11-04 09:40:29 +01:00
Alexander Heinrich
599c24042d Fixing an issue with swift-format. It has changed the command line arguments and was throwing an error otherwise 2021-11-04 09:20:54 +01:00
Alexander Heinrich
204473c1cf Fixing issues with the Mail plug-in update processs 2021-11-03 18:10:34 +01:00
Sascha Mowtschan
e55aa25d9c Fixes #85
Add PluginCompatibilityUUID for MacOS 12.0.1, Mail.app 15.0
2021-11-03 18:10:34 +01:00
Alexander Heinrich
e39e328a89 Updating UUIDs for macOS 11.6 2021-09-21 17:27:34 +02:00
Alexander Heinrich
f9149cdc74 nrf52832 pin layout 2021-08-25 15:53:03 +02:00
Alexander Heinrich
206a2e7004 Copying public to clipboard as Byte array or escaped string 2021-08-25 14:39:58 +02:00
Alexander Heinrich
78fba7391c Checking if the Mail plug-in is installed in the correct version. Otherwise the new mail plug-in will be installed 2021-08-06 11:46:56 +02:00
Alexander Heinrich
aa7c0a50af Updating workflows to macOS 11 2021-08-06 11:23:47 +02:00
Alexander Heinrich
48ceb9550c Small icon changes 2021-08-06 11:19:19 +02:00
Alexander Heinrich
6105a9454a Updating preview for better control of Screenshots 2021-08-06 11:19:19 +02:00
VladutLP
71fb26da56 Added a bunch of ID's into the plist for Mail app version 14 2021-08-06 11:16:10 +02:00
Milan Stute
c7a15fe0e4 Add WiSec demo 2021-06-02 14:09:57 +02:00
34 changed files with 26115 additions and 49 deletions

View File

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

View File

@@ -18,14 +18,14 @@ defaults:
jobs:
format-swift:
runs-on: macos-latest
runs-on: macos-11
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
@@ -45,9 +45,9 @@ jobs:
steps:
- name: "Checkout code"
uses: actions/checkout@v2
- name: "Select Xcode 12"
- name: "Select Xcode 13"
uses: devbotsxyz/xcode-select@v1
with:
version: "12"
version: "13"
- name: "Archive project"
run: xcodebuild archive -scheme ${APP} -configuration release -archivePath ${APP}.xcarchive

View File

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

View File

@@ -16,7 +16,7 @@ defaults:
jobs:
build-firmware:
runs-on: macos-latest
runs-on: macos-11
steps:
- uses: actions/checkout@v2

View File

@@ -30,7 +30,7 @@ jobs:
build-and-release:
name: "Create release on GitHub"
runs-on: macos-latest
runs-on: macos-11
env:
APP: OpenHaystack
PROJECT_DIR: OpenHaystack

6
.gitignore vendored
View File

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

View File

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

View File

@@ -7,6 +7,10 @@
objects = {
/* Begin PBXBuildFile section */
5A2C9089273425720044407E /* NRF in Resources */ = {isa = PBXBuildFile; fileRef = 5A2C9088273425720044407E /* NRF */; };
5A2C908B2734266A0044407E /* DataToHexExtension.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5A2C908A2734266A0044407E /* DataToHexExtension.swift */; };
5A2C908D273429360044407E /* NRFController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5A2C908C273429360044407E /* NRFController.swift */; };
5A2C908F273429540044407E /* NRFInstallSheet.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5A2C908E273429540044407E /* NRFInstallSheet.swift */; };
78014A2925DC08580089F6D9 /* MicrobitController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 78014A2725DC01220089F6D9 /* MicrobitController.swift */; };
78014A2B25DC22120089F6D9 /* sample.bin in Resources */ = {isa = PBXBuildFile; fileRef = 78014A2A25DC22110089F6D9 /* sample.bin */; };
78014A2F25DC2F100089F6D9 /* pattern_sample.bin in Resources */ = {isa = PBXBuildFile; fileRef = 78014A2E25DC2F100089F6D9 /* pattern_sample.bin */; };
@@ -29,6 +33,8 @@
781EB43125DADF2B00FEAA19 /* AnisetteDataManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 781EB40F25DADB0600FEAA19 /* AnisetteDataManager.swift */; };
7821DAD125F7B2C10054DC33 /* FileManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7821DAD025F7B2C10054DC33 /* FileManager.swift */; };
7821DAD325F7C39A0054DC33 /* ESP32InstallSheet.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7821DAD225F7C39A0054DC33 /* ESP32InstallSheet.swift */; };
782853C22755103A00B18EDE /* UpdateCheckController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 782853C12755103A00B18EDE /* UpdateCheckController.swift */; };
782853C427551B4400B18EDE /* UpdateCheckTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 782853C327551B4400B18EDE /* UpdateCheckTests.swift */; };
78286CB225E3ACE700F65511 /* OpenHaystackPluginService.m in Sources */ = {isa = PBXBuildFile; fileRef = 78286CAF25E3ACE700F65511 /* OpenHaystackPluginService.m */; };
78286D1F25E3D8B800F65511 /* ALTAnisetteData.m in Sources */ = {isa = PBXBuildFile; fileRef = 78286CB025E3ACE700F65511 /* ALTAnisetteData.m */; };
78286D2A25E3EC3200F65511 /* AppleAccountData.m in Sources */ = {isa = PBXBuildFile; fileRef = 78286D2925E3EC3200F65511 /* AppleAccountData.m */; };
@@ -108,6 +114,10 @@
025DFEDB248FED250039C718 /* DecryptReports.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DecryptReports.swift; sourceTree = "<group>"; };
0298C0C8248F9506003928FE /* AuthKit.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = AuthKit.framework; path = ../../../../../../../../../../System/Library/PrivateFrameworks/AuthKit.framework; sourceTree = "<group>"; };
116B4EEC24A913AA007BA636 /* SavePanel.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SavePanel.swift; sourceTree = "<group>"; };
5A2C9088273425720044407E /* NRF */ = {isa = PBXFileReference; lastKnownFileType = folder; path = NRF; sourceTree = "<group>"; };
5A2C908A2734266A0044407E /* DataToHexExtension.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DataToHexExtension.swift; sourceTree = "<group>"; };
5A2C908C273429360044407E /* NRFController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NRFController.swift; sourceTree = "<group>"; };
5A2C908E273429540044407E /* NRFInstallSheet.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NRFInstallSheet.swift; sourceTree = "<group>"; };
78014A2725DC01220089F6D9 /* MicrobitController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MicrobitController.swift; sourceTree = "<group>"; };
78014A2A25DC22110089F6D9 /* sample.bin */ = {isa = PBXFileReference; lastKnownFileType = archive.macbinary; path = sample.bin; sourceTree = "<group>"; };
78014A2E25DC2F100089F6D9 /* pattern_sample.bin */ = {isa = PBXFileReference; lastKnownFileType = archive.macbinary; path = pattern_sample.bin; sourceTree = "<group>"; };
@@ -127,6 +137,8 @@
781EB40F25DADB0600FEAA19 /* AnisetteDataManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AnisetteDataManager.swift; sourceTree = "<group>"; };
7821DAD025F7B2C10054DC33 /* FileManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FileManager.swift; sourceTree = "<group>"; };
7821DAD225F7C39A0054DC33 /* ESP32InstallSheet.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ESP32InstallSheet.swift; sourceTree = "<group>"; };
782853C12755103A00B18EDE /* UpdateCheckController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UpdateCheckController.swift; sourceTree = "<group>"; };
782853C327551B4400B18EDE /* UpdateCheckTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UpdateCheckTests.swift; sourceTree = "<group>"; };
78286C8E25E3AC0400F65511 /* OpenHaystackMail.mailbundle */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = OpenHaystackMail.mailbundle; sourceTree = BUILT_PRODUCTS_DIR; };
78286C9025E3AC0400F65511 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = "<group>"; };
78286CAE25E3ACE700F65511 /* OpenHaystackPluginService.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = OpenHaystackPluginService.h; sourceTree = "<group>"; };
@@ -203,6 +215,7 @@
78023CAC25F7775300B083EF /* Firmwares */ = {
isa = PBXGroup;
children = (
5A2C9088273425720044407E /* NRF */,
78023CAE25F7797400B083EF /* ESP32 */,
78023CAD25F7775A00B083EF /* Microbit */,
);
@@ -328,6 +341,7 @@
78EC226525DAE0BE0042B775 /* Info.plist */,
78023CB025F7841F00B083EF /* MicrocontrollerTests.swift */,
F1647C1525FF6C61004144D6 /* BluetoothTests.swift */,
782853C327551B4400B18EDE /* UpdateCheckTests.swift */,
);
path = OpenHaystackTests;
sourceTree = "<group>";
@@ -346,6 +360,9 @@
78023CAA25F7767000B083EF /* ESP32Controller.swift */,
7821DAD025F7B2C10054DC33 /* FileManager.swift */,
F1647C1A25FF7954004144D6 /* AccessoryNearbyMonitor.swift */,
5A2C908A2734266A0044407E /* DataToHexExtension.swift */,
5A2C908C273429360044407E /* NRFController.swift */,
782853C12755103A00B18EDE /* UpdateCheckController.swift */,
);
path = HaystackApp;
sourceTree = "<group>";
@@ -373,6 +390,7 @@
7821DAD225F7C39A0054DC33 /* ESP32InstallSheet.swift */,
78D9B80525F7CF60009B9CE8 /* ManageAccessoriesView.swift */,
F126102E2600D1D80066A859 /* Slider+LogScale.swift */,
5A2C908E273429540044407E /* NRFInstallSheet.swift */,
);
path = Views;
sourceTree = "<group>";
@@ -510,6 +528,7 @@
78023CAF25F7797400B083EF /* ESP32 in Resources */,
7899D1D625DE74EE00115740 /* firmware.bin in Resources */,
781EB3FE25DAD7EA00FEAA19 /* MapViewController.xib in Resources */,
5A2C9089273425720044407E /* NRF in Resources */,
781EB40025DAD7EA00FEAA19 /* Preview Assets.xcassets in Resources */,
781EB40225DAD7EA00FEAA19 /* Assets.xcassets in Resources */,
);
@@ -597,6 +616,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 */,
@@ -605,6 +625,7 @@
78286D8C25E5355B00F65511 /* PreviewData.swift in Sources */,
781EB3EB25DAD7EA00FEAA19 /* SavePanel.swift in Sources */,
7899D1E125DE97E200115740 /* IconSelectionView.swift in Sources */,
5A2C908F273429540044407E /* NRFInstallSheet.swift in Sources */,
78EC227725DBDB7E0042B775 /* KeychainController.swift in Sources */,
78D9B80625F7CF60009B9CE8 /* ManageAccessoriesView.swift in Sources */,
78486BEF25DD711E0007ED87 /* PopUpAlertView.swift in Sources */,
@@ -614,6 +635,7 @@
78286D1F25E3D8B800F65511 /* ALTAnisetteData.m in Sources */,
781EB3EC25DAD7EA00FEAA19 /* DecryptReports.swift in Sources */,
78EC226C25DBC2E40042B775 /* OpenHaystackMainView.swift in Sources */,
5A2C908B2734266A0044407E /* DataToHexExtension.swift in Sources */,
78EC227225DBC8CE0042B775 /* Accessory.swift in Sources */,
7821DAD125F7B2C10054DC33 /* FileManager.swift in Sources */,
78286E0225E66F9400F65511 /* AccessoryListEntry.swift in Sources */,
@@ -629,6 +651,7 @@
78F8BB4C261C50EB00D9F37F /* LargeButtonStyle.swift in Sources */,
781EB3F425DAD7EA00FEAA19 /* FindMyController.swift in Sources */,
781EB3F525DAD7EA00FEAA19 /* BoringSSL.m in Sources */,
782853C22755103A00B18EDE /* UpdateCheckController.swift in Sources */,
F12D5A5A25FA4F3500CBBA09 /* BluetoothAccessoryScanner.swift in Sources */,
78286D5625E401F000F65511 /* MailPluginManager.swift in Sources */,
);
@@ -648,6 +671,7 @@
buildActionMask = 2147483647;
files = (
78023CB125F7841F00B083EF /* MicrocontrollerTests.swift in Sources */,
782853C427551B4400B18EDE /* UpdateCheckTests.swift in Sources */,
F1647C1625FF6C61004144D6 /* BluetoothTests.swift in Sources */,
78EC226425DAE0BE0042B775 /* OpenHaystackTests.swift in Sources */,
);
@@ -796,7 +820,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)",
@@ -823,7 +847,7 @@
DEVELOPMENT_TEAM = "";
ENABLE_HARDENED_RUNTIME = NO;
ENABLE_PREVIEWS = YES;
EXCLUDED_ARCHS = "arm64e arm64";
EXCLUDED_ARCHS = "";
INFOPLIST_FILE = OpenHaystack/Info.plist;
LD_RUNPATH_SEARCH_PATHS = (
"$(inherited)",

View File

@@ -6,8 +6,8 @@
"repositoryURL": "https://github.com/apple/swift-crypto.git",
"state": {
"branch": null,
"revision": "9b9d1868601a199334da5d14f4ab2d37d4f8d0c5",
"version": "1.0.2"
"revision": "3bea268b223651c4ab7b7b9ad62ef9b2d4143eb6",
"version": "1.1.6"
}
},
{
@@ -15,8 +15,8 @@
"repositoryURL": "https://github.com/apple/swift-nio.git",
"state": {
"branch": null,
"revision": "6d3ca7e54e06a69d0f2612c2ce8bb8b7319085a4",
"version": "2.26.0"
"revision": "6aa9347d9bc5bbfe6a84983aec955c17ffea96ef",
"version": "2.33.0"
}
},
{
@@ -24,8 +24,8 @@
"repositoryURL": "https://github.com/apple/swift-nio-ssl",
"state": {
"branch": null,
"revision": "bbb38fbcbbe9dc4665b2c638dfa5681b01079bfb",
"version": "2.10.4"
"revision": "5e68c1ded15619bb281b273fa8c2d8fd7f7b2b7d",
"version": "2.16.1"
}
}
]

View File

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

View File

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

View File

@@ -48,7 +48,6 @@
char *buf;
BIO_get_mem_data(bio, &buf);
NSLog(@"Generating shared key failed %s", buf);
free(buf);
BIO_free(bio);
}
@@ -72,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);
@@ -117,9 +116,7 @@
// Free
BN_CTX_free(ctx);
return key;
}
@@ -147,6 +144,30 @@
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;
}
return publicKeyBytes;
}
+ (NSData *_Nullable)generateNewPrivateKey {
EC_KEY *key = EC_KEY_new_by_curve_name(NID_secp224r1);
if (EC_KEY_generate_key_fips(key) == 0) {
@@ -159,6 +180,8 @@
NSMutableData *privateKeyBytes = [[NSMutableData alloc] initWithLength:keySize];
size_t size = BN_bn2bin(privateKey, privateKeyBytes.mutableBytes);
EC_KEY_free(key);
if (size == 0) {
@@ -168,6 +191,142 @@
return privateKeyBytes;
}
+ (NSData *_Nullable)internalCalculatePrivateKeyFromSharedData:(NSData *)sharedData masterBeaconPrivateKey:(NSData *)masterBeaconPrivateKey
curve:(EC_GROUP *) curve
bignum_context:(BN_CTX *) context
order:(BIGNUM *) order
u_i_bn:(BIGNUM *) u_i_bn
v_i_bn:(BIGNUM *) v_i_bn
d_0_bn:(BIGNUM *) d_0_bn
d_i_bn:(BIGNUM *) d_i_bn
tmp_bn:(BIGNUM *) tmp_bn{
// get (order of G) - 1 of our curve
int res = EC_GROUP_get_order(curve, order, context);
EC_GROUP_free(curve);
if(res != 1){
NSLog(@"Could not get Order of G for NID_secp224r1 with error: %d", res);
return nil;
}
res = BN_sub_word(order, 1);
if(res != 1){
NSLog(@"Could not calculate order - 1 (%d)", res);
return nil;
}
// get u_i and v_i as BIGNUM
NSData *u_i_data = [sharedData subdataWithRange:NSMakeRange(0, sharedData.length/2)];
NSData *v_i_data = [sharedData subdataWithRange:NSMakeRange(sharedData.length/2, sharedData.length/2)];
/*
NSLog(@"u_i_data: %@", u_i_data);
NSLog(@"v_i_data: %@", v_i_data);
*/
BN_bin2bn(u_i_data.bytes, u_i_data.length, u_i_bn);
BN_bin2bn(v_i_data.bytes, v_i_data.length, v_i_bn);
//Calculate:
//u_i = u_i (mod q-1) + 1
res = BN_mod(tmp_bn, u_i_bn, order, context);
if (res != 1){
NSLog(@"Error while calculating u_i (mod q-1) (%d)", res);
return nil;
}
BN_copy(u_i_bn, tmp_bn);
res = BN_add_word(u_i_bn, 1);
if (res != 1){
NSLog(@"Error while adding 1 to v_i (mod q-1) (%d)", res);
return nil;
}
//v_i = v_i (mod q-1) + 1
res = BN_mod(tmp_bn, v_i_bn, order, context);
if (res != 1){
NSLog(@"Error while calculating u_i (mod q-1) (%d)", res);
return nil;
}
BN_copy(v_i_bn, tmp_bn);
res = BN_add_word(v_i_bn, 1);
if (res != 1){
NSLog(@"Error while adding 1 to v_i (mod q-1) (%d)", res);
return nil;
}
/*
size_t uv_size = BN_num_bytes(u_i_bn);
NSMutableData *u_i_data2 = [[NSMutableData alloc] initWithLength:uv_size];
BN_bn2bin(u_i_bn, u_i_data2.mutableBytes);
NSLog(@"u_i_data: %@", u_i_data2);
uv_size = BN_num_bytes(u_i_bn);
NSMutableData *v_i_data2 = [[NSMutableData alloc] initWithLength:uv_size];
BN_bn2bin(v_i_bn, v_i_data2.mutableBytes);
NSLog(@"v_i_data: %@", v_i_data2);
*/
// calculate d_i = d_0_bn * u_i_bn + v_i_bn (new private key)
BN_bin2bn(masterBeaconPrivateKey.bytes, masterBeaconPrivateKey.length, d_0_bn);
res = BN_mul(tmp_bn, d_0_bn, u_i_bn, context);
if (res != 1) {
NSLog(@"Failed bignum multiplication with error: %d", res);
return nil;
}
res = BN_add(d_i_bn, tmp_bn, v_i_bn);
if (res != 1) {
NSLog(@"Failed bignum addition with error: %d", res);
return nil;
}
// normalize point to 28 bytes to have a valid scaler as private key
EC_GROUP_get_order(curve, order, context);
BN_copy(tmp_bn, d_i_bn);
res = BN_mod(d_i_bn, tmp_bn, order, context);
if(res != 1){
NSLog(@"Failed bignum modulo with error: %d", res);
}
// get private key as bytes
size_t d_i_size = BN_num_bytes(d_i_bn);
NSMutableData *privateKeyBytes = [[NSMutableData alloc] initWithLength:d_i_size];
size_t size = BN_bn2bin(d_i_bn, privateKeyBytes.mutableBytes);
if(size < 1){
return nil;
}
return privateKeyBytes;
}
+ (NSData *_Nullable)calculatePrivateKeyFromSharedData:(NSData *)sharedData masterBeaconPrivateKey:(NSData *)masterBeaconPrivateKey {
//Get the group
EC_GROUP *curve = EC_GROUP_new_by_curve_name(NID_secp224r1);
// Create big number context
BN_CTX *ctx = BN_CTX_new();
BN_CTX_start(ctx);
BIGNUM *order = BN_new();
BIGNUM *u_i_bn = BN_new();
BIGNUM *v_i_bn = BN_new();
BIGNUM *d_0_bn = BN_new();
BIGNUM *d_i_bn = BN_new();
BIGNUM *tmp_bn = BN_new();
NSData* privateKeyBytes = [self internalCalculatePrivateKeyFromSharedData:sharedData masterBeaconPrivateKey:masterBeaconPrivateKey curve:curve bignum_context:ctx order:order u_i_bn:u_i_bn v_i_bn:v_i_bn d_0_bn:d_0_bn d_i_bn:d_i_bn tmp_bn:tmp_bn];
// Free all the things
EC_GROUP_free(curve);
BN_CTX_free(ctx);
BN_free(order);
BN_free(u_i_bn);
BN_free(v_i_bn);
BN_free(d_0_bn);
BN_free(d_i_bn);
BN_free(tmp_bn);
return privateKeyBytes;
}
+ (void)printPoint:(const EC_POINT *)point withGroup:(EC_GROUP *)group {
NSMutableData *pointData = [[NSMutableData alloc] initWithLength:256];

View File

@@ -95,10 +95,13 @@ class AccessoryController: ObservableObject {
/// Export the accessories property list so it can be imported at another location.
func export(accessories: [Accessory]) throws -> URL {
let propertyList = try PropertyListEncoder().encode(accessories)
let savePanel = NSSavePanel()
savePanel.allowedFileTypes = ["plist"]
// savePanel.allowedFileTypes = ["plist", "json"]
if #available(macOS 12.0, *) {
savePanel.allowedContentTypes = [.propertyList, .json]
}
savePanel.allowedFileTypes = ["plist", "json"]
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"
@@ -106,6 +109,7 @@ class AccessoryController: ObservableObject {
savePanel.nameFieldStringValue = "openhaystack_accessories.plist"
savePanel.prompt = "Export"
savePanel.title = "Export accessories & keys"
savePanel.isExtensionHidden = false
let result = savePanel.runModal()
@@ -113,7 +117,13 @@ class AccessoryController: ObservableObject {
let url = savePanel.url
{
// Store the accessory file
try propertyList.write(to: url)
if url.pathExtension == "plist" {
let propertyList = try PropertyListEncoder().encode(accessories)
try propertyList.write(to: url)
}else if url.pathExtension == "json" {
let jsonObject = try JSONEncoder().encode(accessories)
try jsonObject.write(to: url)
}
return url
}

View File

@@ -0,0 +1,27 @@
//
// OpenHaystack Tracking personal Bluetooth devices via Apple's Find My network
//
// Copyright © 2021 Secure Mobile Networking Lab (SEEMOO)
// Copyright © 2021 The Open Wireless Link Project
//
// SPDX-License-Identifier: AGPL-3.0-only
//
import Foundation
extension Data {
/// A hexadecimal string representation of the bytes.
func hexEncodedString() -> String {
let hexDigits = Array("0123456789abcdef".utf16)
var hexChars = [UTF16.CodeUnit]()
hexChars.reserveCapacity(count * 2)
for byte in self {
let (index1, index2) = Int(byte).quotientAndRemainder(dividingBy: 16)
hexChars.append(hexDigits[index1])
hexChars.append(hexDigits[index2])
}
return String(utf16CodeUnits: hexChars, count: hexChars.count)
}
}

View File

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

View File

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

View File

@@ -0,0 +1,136 @@
#!/bin/bash
cleanup() {
echo "### done"
}
# Parameter parsing
while [[ $# -gt 0 ]]; do
KEY="$1"
case "$KEY" in
-v|--venvdir)
VENV_DIR="$2"
shift
shift
;;
-h|--help)
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_nrf.sh [-v <dir>] PUBLIC_KEY SYMMETRIC_KEY UPDATE_INTERVAL"
echo ""
echo "Required Arguments:"
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 " -v, --venvdir <dir>"
echo " Select Python virtual environment with esptool installed."
echo " If the directory does not exist, it will be created."
exit 1
;;
*)
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
fi
;;
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: 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)"
fi
if [[ -z "$PYTHON" ]]; then
echo "Could not find a Python installation, please install Python 3."
exit 1
fi
if ! ($PYTHON -V 2>&1 | grep "Python 3" > /dev/null); then
echo "Executing \"$PYTHON\" does not run Python 3, please make sure that python3 or python on your PATH points to Python 3"
exit 1
fi
if ! ($PYTHON -c "import venv" &> /dev/null); then
echo "Python 3 module \"venv\" was not found."
exit 1
fi
$PYTHON -m venv "$VENV_DIR"
if [[ $? != 0 ]]; 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 pynrfjprog && pip install intelhex
if [[ $? != 0 ]]; then
echo "Could not install Python 3 module pynrfjprog in $VENV_DIR";
exit 1
fi
else
source "$VENV_DIR/bin/activate"
fi
# Call flash_nrf.py. Errors from here on are critical
set -e
trap cleanup INT TERM EXIT
echo "### Executing python script ###"
python3 "$(dirname "$0")"/flash_nrf.py --public-key $PUBKEY --symmetric-key $SYMKEY --update-interval $UPDATE_INTERVAL --path-to-hex "$(dirname "$0")"/
echo "### Python script finished ###"

View File

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

View File

@@ -31,6 +31,11 @@ class Accessory: ObservableObject, Codable, Identifiable, Equatable, Hashable {
@Published var name: String
let id: Int
let privateKey: Data
let symmetricKey: Data
@Published var usesDerivation: Bool
@Published var oldestRelevantSymmetricKey: Data
@Published var lastDerivationTimestamp: Date
@Published var updateInterval: TimeInterval
@Published var locations: [FindMyLocationReport]?
@Published var color: Color
@Published var icon: String
@@ -41,6 +46,10 @@ class Accessory: ObservableObject, Codable, Identifiable, Equatable, Hashable {
// Reset active status if deployed
if !wasDeployed && isDeployed {
self.isActive = false
self.usesDerivation = false
} else if wasDeployed && !isDeployed {
self.usesDerivation = false
self.updateInterval = TimeInterval(60 * 60 * 24)
}
}
}
@@ -63,6 +72,14 @@ class Accessory: ObservableObject, Codable, Identifiable, Equatable, Hashable {
}
self.id = key.hashValue
self.privateKey = key
let symKey = SymmetricKey(size: .bits256)
self.symmetricKey = symKey.withUnsafeBytes {
return Data(Array($0))
}
self.usesDerivation = false
self.oldestRelevantSymmetricKey = self.symmetricKey
self.lastDerivationTimestamp = Date()
self.updateInterval = TimeInterval(60 * 60)
self.color = color
self.icon = iconName
self.isDeployed = false
@@ -73,6 +90,11 @@ class Accessory: ObservableObject, Codable, Identifiable, Equatable, Hashable {
self.name = try container.decode(String.self, forKey: .name)
self.id = try container.decode(Int.self, forKey: .id)
self.privateKey = try container.decode(Data.self, forKey: .privateKey)
self.symmetricKey = (try? container.decode(Data.self, forKey: .symmetricKey)) ?? SymmetricKey(size: .bits256).withUnsafeBytes { return Data($0) }
self.usesDerivation = (try? container.decode(Bool.self, forKey: .usesDerivation)) ?? false
self.oldestRelevantSymmetricKey = (try? container.decode(Data.self, forKey: .oldestRelevantSymmetricKey)) ?? self.symmetricKey
self.lastDerivationTimestamp = (try? container.decode(Date.self, forKey: .lastDerivationTimestamp)) ?? Date()
self.updateInterval = (try? container.decode(TimeInterval.self, forKey: .updateInterval)) ?? TimeInterval(60 * 60 * 24)
self.icon = (try? container.decode(String.self, forKey: .icon)) ?? ""
self.isDeployed = (try? container.decode(Bool.self, forKey: .isDeployed)) ?? false
self.isActive = (try? container.decode(Bool.self, forKey: .isActive)) ?? false
@@ -93,6 +115,11 @@ class Accessory: ObservableObject, Codable, Identifiable, Equatable, Hashable {
try container.encode(self.name, forKey: .name)
try container.encode(self.id, forKey: .id)
try container.encode(self.privateKey, forKey: .privateKey)
try container.encode(self.symmetricKey, forKey: .symmetricKey)
try container.encode(self.usesDerivation, forKey: .usesDerivation)
try container.encode(self.oldestRelevantSymmetricKey, forKey: .oldestRelevantSymmetricKey)
try container.encode(self.lastDerivationTimestamp, forKey: .lastDerivationTimestamp)
try container.encode(self.updateInterval, forKey: .updateInterval)
try container.encode(self.icon, forKey: .icon)
try container.encode(self.isDeployed, forKey: .isDeployed)
try container.encode(self.isActive, forKey: .isActive)
@@ -114,6 +141,15 @@ class Accessory: ObservableObject, Codable, Identifiable, Equatable, Hashable {
return publicKey
}
/// Get Uncompressed public key
/// This is needed for libraries such as mbedtls that do not support loading compressed points
func getUncompressedPublicKey() throws -> Data {
guard let publicKey = BoringSSL.deriveUncompressedPublicKey(fromPrivateKey: self.privateKey) else {
throw KeyError.keyDerivationFailed
}
return publicKey
}
func getAdvertisementKey() throws -> Data {
guard var publicKey = BoringSSL.derivePublicKey(fromPrivateKey: self.privateKey) else {
throw KeyError.keyDerivationFailed
@@ -147,30 +183,140 @@ class Accessory: ObservableObject, Codable, Identifiable, Equatable, Hashable {
return Data(digest)
}
func getNewestSymmetricKey() -> Data {
var derivationTimestamp = self.lastDerivationTimestamp
var symmetricKey = self.oldestRelevantSymmetricKey
while derivationTimestamp < Date() {
derivationTimestamp.addTimeInterval(self.updateInterval)
symmetricKey = Accessory.kdf(inputData: self.symmetricKey, sharedInfo: "update".data(using: .ascii)!, bytesToReturn: 32)
}
return symmetricKey
}
func toFindMyDevice() throws -> FindMyDevice {
let findMyKey = FindMyKey(
advertisedKey: try self.getAdvertisementKey(),
hashedKey: try self.hashedPublicKey(),
privateKey: self.privateKey,
startTime: nil,
duration: nil,
pu: nil,
yCoordinate: nil,
fullKey: nil)
var findMyKey = [FindMyKey]()
/// Always append first FindMyKey to support devices without derivation
findMyKey.append(
FindMyKey(
advertisedKey: try self.getAdvertisementKey(),
hashedKey: try self.hashedPublicKey(),
privateKey: self.privateKey,
startTime: nil,
duration: nil,
pu: nil,
yCoordinate: nil,
fullKey: nil)
)
if self.usesDerivation {
/// Derive FindMyKeys until we have symmetric key from one week before now
while self.lastDerivationTimestamp < Date() - TimeInterval(7 * 24 * 60 * 60) {
self.lastDerivationTimestamp.addTimeInterval(self.updateInterval)
self.oldestRelevantSymmetricKey = Accessory.kdf(inputData: self.symmetricKey, sharedInfo: "update".data(using: .ascii)!, bytesToReturn: 32)
}
/// we need to generate Keys from seven days in the past until now and 10 extra keys in case of desynchronization
let untilDate = Date() + TimeInterval(self.updateInterval * 11)
var derivationTimestamp = self.lastDerivationTimestamp
var derivedSymmetricKey = self.oldestRelevantSymmetricKey
print("--- Derived keys for \(self.name) ---")
print("Masterbacon symmetric key \(self.symmetricKey.hexEncodedString())")
do {
let uncompressedMasterBeaconKey = try self.getUncompressedPublicKey()
print("Masterbeacon public key (uncompressed) \(uncompressedMasterBeaconKey.hexEncodedString())")
} catch {
print("Failed to get master beacon public key (only needed for printing)")
}
while derivationTimestamp < untilDate {
/// Step 1: derive SKN_i
derivedSymmetricKey = Accessory.kdf(inputData: derivedSymmetricKey, sharedInfo: "update".data(using: .ascii)!, bytesToReturn: 32)
/// Step 2: derive u_i and v_i
let derivedAntiTrackingKeys = Accessory.kdf(inputData: derivedSymmetricKey, sharedInfo: "diversify".data(using: .ascii)!, bytesToReturn: 72)
/// Step 3 & 4: compute private and public key
guard let derivedPrivateKey = BoringSSL.calculatePrivateKey(fromSharedData: derivedAntiTrackingKeys, masterBeaconPrivateKey: self.privateKey) else {
throw KeyError.keyDerivationFailed
}
guard let derivedPublicKey = BoringSSL.derivePublicKey(fromPrivateKey: derivedPrivateKey) else {
throw KeyError.keyDerivationFailed
}
/// Drop first byte to get advertisment key
let derivedAdvertisementKey = derivedPublicKey.dropFirst()
guard derivedAdvertisementKey.count == 28 else { throw KeyError.keyDerivationFailed }
/// Get hash of advertisment key
var sha = SHA256()
sha.update(data: derivedAdvertisementKey)
let derivedAdvertisementKeyHash = Data(sha.finalize())
print("-> Derived keys for \(derivationTimestamp):")
//print("Dervided anti tracking keys \(derivedAntiTrackingKeys.hexEncodedString())")
//print("SymmetricKey \(derivedSymmetricKey.hexEncodedString())")
print("Derived public key \(derivedPublicKey.hexEncodedString())")
findMyKey.append(
FindMyKey(
advertisedKey: derivedAdvertisementKey,
hashedKey: derivedAdvertisementKeyHash,
privateKey: derivedPrivateKey,
startTime: nil,
duration: nil,
pu: nil,
yCoordinate: nil,
fullKey: nil)
)
/// Add time interval to derivation timestamp
derivationTimestamp.addTimeInterval(self.updateInterval)
}
}
return FindMyDevice(
deviceId: String(self.id),
keys: [findMyKey],
keys: findMyKey,
catalinaBigSurKeyFiles: nil,
reports: nil,
decryptedReports: nil)
}
static func kdf(inputData: Data, sharedInfo: Data, bytesToReturn: Int) -> Data {
var derivedKey = Data()
var counter: Int32 = 1
/// derive from input and shared info until we have enough data
while derivedKey.count < bytesToReturn {
var shaDigest = SHA256()
shaDigest.update(data: inputData)
let counterData = Data(Data(bytes: &counter, count: MemoryLayout.size(ofValue: counter)).reversed())
shaDigest.update(data: counterData)
shaDigest.update(data: sharedInfo)
derivedKey.append(Data(shaDigest.finalize()))
counter += 1
}
/// drop bytes which are not needed and return
derivedKey = derivedKey.dropLast(derivedKey.count - bytesToReturn)
return derivedKey
}
func resetDerivationState() {
/// reset keys and derivation time in case an accessory is reflashed with old keys
self.oldestRelevantSymmetricKey = self.symmetricKey
self.lastDerivationTimestamp = Date()
}
enum CodingKeys: String, CodingKey {
case name
case id
case privateKey
case usesDerivation
case symmetricKey
case oldestRelevantSymmetricKey
case lastDerivationTimestamp
case updateInterval
case colorComponents
case colorSpaceName
case icon

View File

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

View File

@@ -0,0 +1,72 @@
//
// OpenHaystack Tracking personal Bluetooth devices via Apple's Find My network
//
// Copyright © 2021 Secure Mobile Networking Lab (SEEMOO)
// Copyright © 2021 The Open Wireless Link Project
//
// SPDX-License-Identifier: AGPL-3.0-only
//
import Foundation
struct NRFController {
static var nrfFirmwareDirectory: URL? {
Bundle.main.resourceURL?.appendingPathComponent("NRF")
}
/// Runs the script to flash the firmware onto an nRF Device.
static func flashToNRF(accessory: Accessory, updateInterval: Int, completion: @escaping (ClosureResult) -> Void) throws {
// Copy firmware to a temporary directory
let temp = NSTemporaryDirectory() + "OpenHaystack"
let urlTemp = URL(fileURLWithPath: temp)
try? FileManager.default.removeItem(at: urlTemp)
try? FileManager.default.createDirectory(atPath: temp, withIntermediateDirectories: false, attributes: nil)
guard let nrfDirectory = nrfFirmwareDirectory else { return }
try FileManager.default.copyFolder(from: nrfDirectory, to: urlTemp)
let urlScript = urlTemp.appendingPathComponent("flash_nrf.sh")
try FileManager.default.setAttributes([FileAttributeKey.posixPermissions: 0o755], ofItemAtPath: urlScript.path)
try FileManager.default.setAttributes([FileAttributeKey.posixPermissions: 0o755], ofItemAtPath: urlTemp.appendingPathComponent("flash_nrf.py").path)
// Get public key, newest relevant symmetric key and updateInterval for flashing
let masterBeaconPublicKey = try accessory.getUncompressedPublicKey()
let masterBeaconSymmetricKey = accessory.getNewestSymmetricKey()
let arguments = [masterBeaconPublicKey.base64EncodedString(), masterBeaconSymmetricKey.base64EncodedString(), String(updateInterval)]
// Create file for logging and get file handle
let loggingFileUrl = urlTemp.appendingPathComponent("nrf_installer.log")
try "".write(to: loggingFileUrl, atomically: true, encoding: .utf8)
let loggingFileHandle = FileHandle.init(forWritingAtPath: loggingFileUrl.path)!
// Run script
let task = try NSUserUnixTask(url: urlScript)
task.standardOutput = loggingFileHandle
task.standardError = loggingFileHandle
task.execute(withArguments: arguments) { e in
DispatchQueue.main.async {
if let error = e {
completion(.failure(loggingFileUrl, error))
} else {
completion(.success(loggingFileUrl))
}
}
}
try loggingFileHandle.close()
}
}
enum ClosureResult {
case success(URL)
case failure(URL, Error)
}
enum NRFFirmwareFlashError: Error {
/// Missing files for flashing
case notFound
/// Flashing / writing failed
case flashFailed
}

View File

@@ -0,0 +1,201 @@
//
// OpenHaystack Tracking personal Bluetooth devices via Apple's Find My network
//
// Copyright © 2021 Secure Mobile Networking Lab (SEEMOO)
// Copyright © 2021 The Open Wireless Link Project
//
// SPDX-License-Identifier: AGPL-3.0-only
//
import Foundation
import AppKit
/// Can check if a new OpenHaystack version is needed and download it.
public struct UpdateCheckController {
public static func checkForNewVersion() {
// Load the GitHub Releases page
let releasesURL = URL(string: "https://github.com/seemoo-lab/openhaystack/releases")!
URLSession.shared.dataTask(with: releasesURL) { optionalData, response, error in
guard let data = optionalData,
(response as? HTTPURLResponse)?.statusCode == 200,
let htmlString = String(data:data, encoding: .utf8)
else {
return
}
guard let availableVersion = getVersion(from: htmlString) else {
return
}
//Get installed version
let version = Bundle.main.infoDictionary?["CFBundleVersionShortString"] as? String ?? "0"
let comparisonResult = compareVersions(availableVersion: availableVersion, installedVersion: version)
DispatchQueue.main.async {
if comparisonResult == .older, askToDownloadUpdate() == .alertSecondButtonReturn {
//The currently installed version is older. Install an update
self.downloadUpdate(version: availableVersion, finished: { success in
if success {
let result = successDownloadAlert()
if result == .alertSecondButtonReturn {
//Open the download folder
let downloadURL = FileManager.default.urls(for: .downloadsDirectory, in: .userDomainMask)[0]
NSWorkspace.shared.open(downloadURL)
}
}else {
if downloadFailedAlert() == .alertSecondButtonReturn {
NSWorkspace.shared.open(URL(string: "https://github.com/seemoo-lab/openhaystack/releases")!)
}
}
})
}
}
}.resume()
}
internal static func getVersion(from htmlString: String) -> String? {
guard let regex = try? NSRegularExpression(pattern: "Release (v[0-9]+(.[0-9]+)?(.[0-9]+)?)") else {
return nil
}
let htmlNSString = htmlString as NSString
let htmlRange = NSRange(location: 0, length: htmlNSString.length)
if let checkResult = regex.firstMatch(in: htmlNSString as String, options: [], range: htmlRange),
checkResult.numberOfRanges >= 2 {
//Get the latest release version range
// A result should have multiple ranges for each capture group. 1 is the capture group for the version number
let releaseVersionRange = checkResult.range(at: 1)
let releaseVersion = htmlNSString.substring(with: releaseVersionRange)
let releaseVersionNumber = releaseVersion.replacingOccurrences(of: "v", with: "")
return releaseVersionNumber
}
return nil
}
/// Compares two version strings and returns if the installed version is older, newer or the same
/// - Parameters:
/// - availableVersion: The latest available version
/// - installedVersion: The currently installed version
/// - Returns: .older when a newer version is available. .newer when the installed version is newer .same, if both versions are equal
internal static func compareVersions(availableVersion: String, installedVersion: String) -> VersionCompare {
let availableVersionSplit = availableVersion.split(separator: ".")
let installedVersionSplit = installedVersion.split(separator: ".")
for (idx, availableVersionPart) in availableVersionSplit.enumerated() {
if idx < installedVersionSplit.count {
guard let avpi = Int(availableVersionPart),
let ivpi = Int(installedVersionSplit[idx]) else {return .older}
if avpi > ivpi {
return .older
}else if ivpi > avpi {
return .newer
}
}else {
//The installed version is x.x
// The new version is x.x.y so it must be older
return .older
}
}
if installedVersionSplit.count > availableVersionSplit.count {
//The installed version has a higher sub-version. So it must be newer
return .newer
}
// All numbers were equal
return .same
}
enum VersionCompare {
case same, newer, older
}
static func downloadUpdate(version: String, finished: @escaping (Bool)->()) {
//Download the current version into a file in Downloads
let downloadURL = URL(string: "https://github.com/seemoo-lab/openhaystack/releases/download/v\(version)/OpenHaystack.zip")!
let task = URLSession.shared.downloadTask(with: downloadURL) { optionalFileURL, response, error in
guard let downloadLocation = optionalFileURL else {
finished(false)
return
}
//Move the file to the downloads folder
let downloadURL = FileManager.default.urls(for: .downloadsDirectory, in: .userDomainMask)[0]
let openHaystackURL = downloadURL.appendingPathComponent("OpenHaystack.zip")
do {
let fm = FileManager.default
if fm.fileExists(atPath: openHaystackURL.path) {
_ = try fm.replaceItemAt(openHaystackURL, withItemAt: downloadLocation)
}else {
try fm.moveItem(at: downloadLocation, to: openHaystackURL)
}
DispatchQueue.main.async {finished(true)}
}catch let error {
print(error.localizedDescription)
DispatchQueue.main.async {finished(false)}
}
}
task.resume()
}
private static func askToDownloadUpdate() -> NSApplication.ModalResponse {
let alert = NSAlert()
alert.messageText = NSLocalizedString("New version available", comment: "Alert title")
alert.informativeText = NSLocalizedString("A new version of OpenHaystack is available. Do you want to download it now?", comment: "Alert text")
alert.addButton(withTitle: "Cancel")
alert.addButton(withTitle: "Download")
return alert.runModal()
}
private static func successDownloadAlert() -> NSApplication.ModalResponse {
let alert = NSAlert()
alert.messageText = NSLocalizedString("Successfully downloaded update", comment: "Alert title")
alert.informativeText = NSLocalizedString("The new version has been downloaded successfully and it was placed in your Downloads folder.", comment: "Alert text")
alert.addButton(withTitle: "Okay")
alert.addButton(withTitle: "Open folder")
return alert.runModal()
}
private static func downloadFailedAlert() -> NSApplication.ModalResponse {
let alert = NSAlert()
alert.messageText = NSLocalizedString("Download failed", comment: "Alert title")
alert.informativeText = NSLocalizedString("To update to the newest version, please open the releases page on GitHub", comment: "Alert text")
alert.addButton(withTitle: "Cancel")
alert.addButton(withTitle: "Open")
return alert.runModal()
}
}
extension String {
func substring(from range: NSRange) -> String {
let substring = self[self.index(startIndex, offsetBy: range.lowerBound)..<self.index(startIndex, offsetBy: range.upperBound)]
return String(substring)
}
}

View File

@@ -36,6 +36,15 @@ struct AccessoryListEntry: View {
.font(.footnote)
}
func updateIntervalView() -> some View {
let intervalFormatter = DateComponentsFormatter()
intervalFormatter.unitsStyle = .abbreviated
return Group {
Text("Key derivation interval: \(intervalFormatter.string(from: accessory.updateInterval)!)")
}.font(.footnote)
}
var body: some View {
HStack {
@@ -51,6 +60,9 @@ struct AccessoryListEntry: View {
.font(.headline)
}
self.timestampView()
if accessory.usesDerivation {
self.updateIntervalView()
}
}
Spacer()
@@ -68,11 +80,22 @@ struct AccessoryListEntry: View {
.padding(EdgeInsets(top: 5, leading: 0, bottom: 5, trailing: 0))
.contextMenu {
Button("Delete", action: { self.delete(accessory) })
Divider()
Button("Rename", action: { self.editingName = true })
Menu("Key derivation options") {
Button("Toggle key derivation", action: { accessory.usesDerivation = !accessory.usesDerivation })
Button("Reset derivation state", action: { accessory.resetDerivationState() })
}
Divider()
Button("Copy advertisment key (Base64)", action: { self.copyPublicKey(of: accessory) })
Button("Copy key ID (Base64)", action: { self.copyPublicKeyHash(of: accessory) })
Menu("Copy advertisement key") {
Button("Base64", action: { self.copyAdvertisementKeyB64(of: accessory) })
Button("Byte array", action: { self.copyAdvertisementKey(escapedString: false) })
Button("Escaped string", action: { self.copyAdvertisementKey(escapedString: true) })
}
Menu("Copy symmetric and uncompressed public key") {
Button("Base64", action: { self.copySymmetricAndPublicKeyBase64(of: accessory) })
Button("Escaped string", action: { self.copySymmetricAndPublicKey(of: accessory) })
}
Divider()
Button("Mark as \(accessory.isDeployed ? "deployable" : "deployed")", action: { accessory.isDeployed.toggle() })
}
@@ -90,6 +113,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()
@@ -102,6 +137,58 @@ struct AccessoryListEntry: View {
}
}
func copyAdvertisementKey(escapedString: Bool) {
do {
let publicKey = try self.accessory.getAdvertisementKey()
let keyByteArray = [UInt8](publicKey)
if escapedString {
let string = keyByteArray.map { "\\x\(String($0, radix: 16))" }.joined()
let pasteboard = NSPasteboard.general
pasteboard.prepareForNewContents(with: .currentHostOnly)
pasteboard.setString(string, forType: .string)
} else {
let string = keyByteArray.map { "0x\(String($0, radix: 16))" }.joined(separator: ", ")
let pasteboard = NSPasteboard.general
pasteboard.prepareForNewContents(with: .currentHostOnly)
pasteboard.setString(string, forType: .string)
}
} catch {
os_log("Failed extracing public key %@", String(describing: error))
assert(false)
}
}
func copySymmetricAndPublicKey(of accessory: Accessory) {
do {
let symmetricKey = accessory.symmetricKey
let publicKey = try accessory.getUncompressedPublicKey()
let publicKeyString = [UInt8](publicKey).map { "\\x\(String($0, radix: 16))" }.joined()
let symmetricKeyString = [UInt8](symmetricKey).map { "\\x\(String($0, radix: 16))" }.joined()
let pasteboard = NSPasteboard.general
pasteboard.prepareForNewContents(with: .currentHostOnly)
pasteboard.setString("Symmetric key: \(symmetricKeyString)\n Uncompressed public key: \(publicKeyString) ", forType: .string)
} catch {
os_log("Failed extracing public key %@", String(describing: error))
assert(false)
}
}
func copySymmetricAndPublicKeyBase64(of accessory: Accessory) {
do {
let symmetricKey = accessory.symmetricKey
let publicKey = try accessory.getUncompressedPublicKey()
let pasteboard = NSPasteboard.general
pasteboard.prepareForNewContents(with: .currentHostOnly)
pasteboard.setString("Symmetric key: \(symmetricKey.base64EncodedString())\n Uncompressed public key: \(publicKey.base64EncodedString()) ", forType: .string)
} catch {
os_log("Failed extracing public key %@", String(describing: error))
assert(false)
}
}
struct AccessoryListEntry_Previews: PreviewProvider {
@StateObject static var accessory = PreviewData.accessories.first!
@State static var alertType: OpenHaystackMainView.AlertType?
@@ -122,6 +209,7 @@ struct AccessoryListEntry: View {
get: { accessory.name },
set: { accessory.name = $0 }
),
alertType: self.$alertType,
delete: { _ in () },
deployAccessoryToMicrobit: { _ in () },

View File

@@ -19,6 +19,7 @@ struct ManageAccessoriesView: View {
// MARK: Bindings from main View
@Binding var alertType: OpenHaystackMainView.AlertType?
@Binding var scriptOutput: String?
@Binding var focusedAccessory: Accessory?
@Binding var accessoryToDeploy: Accessory?
@Binding var showESP32DeploySheet: Bool
@@ -48,6 +49,8 @@ struct ManageAccessoriesView: View {
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
}
@@ -148,6 +151,13 @@ struct ManageAccessoriesView: View {
)
.buttonStyle(LargeButtonStyle())
Button(
"NRF Device",
action: {
self.sheetShown = .nrfDeviceInstall
}
).buttonStyle(LargeButtonStyle())
Button(
"Cancel",
action: {
@@ -257,6 +267,7 @@ struct ManageAccessoriesView: View {
return self.rawValue
}
case esp32Install
case nrfDeviceInstall
case deployFirmware
}
}
@@ -265,12 +276,15 @@ 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)
}
}

View File

@@ -0,0 +1,180 @@
//
// OpenHaystack Tracking personal Bluetooth devices via Apple's Find My network
//
// Copyright © 2021 Secure Mobile Networking Lab (SEEMOO)
// Copyright © 2021 The Open Wireless Link Project
//
// SPDX-License-Identifier: AGPL-3.0-only
//
import OSLog
import SwiftUI
struct NRFInstallSheet: View {
@Binding var accessory: Accessory?
@Binding var alertType: OpenHaystackMainView.AlertType?
@Binding var scriptOutput: String?
@State var isFlashing = false
@ObservedObject var days = NumbersOnly()
@ObservedObject var hours = NumbersOnly()
@ObservedObject var minutes = NumbersOnly()
@Environment(\.presentationMode) var presentationMode
var body: some View {
VStack {
self.flashView
.padding()
.overlay(self.loadingOverlay)
.frame(minWidth: 640, minHeight: 480, alignment: .center)
}
.onAppear {
}
}
var flashView: some View {
VStack {
Text("Flash your NRF Device")
.font(.title2)
Text("Fill out options for flashing firmware")
.foregroundColor(.gray)
Divider()
Text("The new NRF firmware uses rotating keys. This means that the device changes its public key after a specific number of days. This disallows ad networks to track your device over several days when you are moving around the city. Shorter update cycles then days are not supported")
self.timePicker
Text("One day is a reasonable amount of time")
.font(.footnote)
.foregroundColor(.secondary)
Spacer()
HStack {
Spacer()
Button(
"Deploy",
action: {
if let accessory = self.accessory {
var daysInt = Int(days.value) ?? 1
if daysInt < 1 {
daysInt = 1
}
let hoursInt = 0
let minutesInt = 0
let updateInterval = daysInt * 24 * 60 + hoursInt * 60 + minutesInt
//warn user if no update interval was given
if updateInterval > 0 {
deployAccessoryToNRFDevice(accessory: accessory, updateInterval: updateInterval)
} else {
}
}
})
Button(
"Cancel",
action: {
self.presentationMode.wrappedValue.dismiss()
})
}
HStack {
Spacer()
Text("Flashing from M1 Macs might fail due to missing ARM support by NRF")
.font(.footnote)
.foregroundColor(.secondary)
}
}
}
var timePicker: some View {
Group {
HStack {
TextField("", text: $days.value).textFieldStyle(RoundedBorderTextFieldStyle())
Text("Day(s)")
}
}.padding()
}
var loadingOverlay: some View {
ZStack {
if isFlashing {
Rectangle()
.fill(Color.gray)
.opacity(0.5)
VStack {
ActivityIndicator(size: .large)
Text("This can take up to 3min")
}
}
}
}
func deployAccessoryToNRFDevice(accessory: Accessory, updateInterval: Int) {
do {
self.isFlashing = true
try NRFController.flashToNRF(
accessory: accessory,
updateInterval: updateInterval,
completion: { result in
presentationMode.wrappedValue.dismiss()
self.isFlashing = false
switch result {
case .success(_):
self.alertType = .deployedSuccessfully
accessory.isDeployed = true
accessory.usesDerivation = true
accessory.updateInterval = TimeInterval(updateInterval * 60)
case .failure(let loggingFileUrl, let error):
os_log(.error, "Flashing to NRF device failed %@", String(describing: error))
self.presentationMode.wrappedValue.dismiss()
self.alertType = .nrfDeployFailed
do {
self.scriptOutput = try String(contentsOf: loggingFileUrl, encoding: .ascii)
} catch {
self.scriptOutput = "Error while trying to read log file."
}
}
})
} catch {
os_log(.error, "Preparation or execution of script failed %@", String(describing: error))
self.presentationMode.wrappedValue.dismiss()
self.alertType = .deployFailed
self.isFlashing = false
}
self.accessory = nil
}
}
struct NRFInstallSheet_Previews: PreviewProvider {
@State static var acc: Accessory? = try! Accessory(name: "Sample")
@State static var alert: OpenHaystackMainView.AlertType?
@State static var scriptOutput: String?
static var previews: some View {
NRFInstallSheet(accessory: $acc, alertType: $alert, scriptOutput: $scriptOutput)
}
}
class NumbersOnly: ObservableObject {
@Published var value = "1" {
didSet {
let filtered = value.filter { $0.isNumber }
if value != filtered {
value = filtered
}
}
}
}

View File

@@ -23,6 +23,7 @@ struct OpenHaystackMainView: View {
@State var alertType: AlertType?
@State var popUpAlertType: PopUpAlertType?
@State var errorDescription: String?
@State var scriptOutput: String?
@State var searchPartyToken: String = ""
@State var searchPartyTokenLoaded = false
@State var mapType: MKMapType = .standard
@@ -43,6 +44,7 @@ struct OpenHaystackMainView: View {
ManageAccessoriesView(
alertType: self.$alertType,
scriptOutput: self.$scriptOutput,
focusedAccessory: self.$focusedAccessory,
accessoryToDeploy: self.$accessoryToDeploy,
showESP32DeploySheet: self.$showESP32DeploySheet
@@ -135,6 +137,7 @@ struct OpenHaystackMainView: View {
action: {
if !self.mailPluginIsActive {
self.showMailPlugInPopover.toggle()
self.checkPluginIsRunning(silent: true, nil)
} else {
self.downloadLocationReports()
}
@@ -177,9 +180,11 @@ struct OpenHaystackMainView: View {
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
@@ -314,6 +319,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"),
@@ -379,6 +389,7 @@ struct OpenHaystackMainView: View {
case keyError
case searchPartyToken
case deployFailed
case nrfDeployFailed
case deployedSuccessfully
case deletionFailed
case noReportsFound

View File

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

View File

@@ -22,6 +22,24 @@
<string>Copyright © 2021 SEEMOO TU Darmstadt</string>
<key>NSPrincipalClass</key>
<string>OpenHaystackPluginService</string>
<key>Supported10.14PluginCompatibilityUUIDs</key>
<array>
<string># UUIDs for versions from 10.12 to 99.99.99</string>
<string># For mail version 10.0 (3226) on OS X Version 10.12 (build 16A319)</string>
<string>36CCB8BB-2207-455E-89BC-B9D6E47ABB5B</string>
<string># For mail version 10.1 (3251) on OS X Version 10.12.1 (build 16B2553a)</string>
<string>9054AFD9-2607-489E-8E63-8B09A749BC61</string>
<string># For mail version 10.2 (3259) on OS X Version 10.12.2 (build 16D12b)</string>
<string>1CD3B36A-0E3B-4A26-8F7E-5BDF96AAC97E</string>
<string># For mail version 10.3 (3273) on OS X Version 10.12.4 (build 16G1036)</string>
<string>21560BD9-A3CC-482E-9B99-95B7BF61EDC1</string>
<string># For mail version 11.0 (3441.0.1) on OS X Version 10.13 (build 17A315i)</string>
<string>C86CD990-4660-4E36-8CDA-7454DEB2E199</string>
<string># For mail version 12.0 (3445.100.39) on OS X Version 10.14.1 (build 18B45d)</string>
<string>A4343FAF-AE18-40D0-8A16-DFAE481AF9C1</string>
<string># For mail version 13.0 (3594.4.2) on OS X Version 10.15 (build 19A558d)</string>
<string>6EEA38FB-1A0B-469B-BB35-4C2E0EEA9053</string>
</array>
<key>Supported10.15PluginCompatibilityUUIDs</key>
<array>
<string># UUIDs for versions from 10.12 to 99.99.99</string>
@@ -44,6 +62,10 @@
<array>
<string>D985F0E4-3BBC-4B95-BBA1-12056AC4A531</string>
</array>
<key>Supported11.10PluginCompatibilityUUIDs</key>
<array>
<string>D985F0E4-3BBC-4B95-BBA1-12056AC4A531</string>
</array>
<key>Supported11.1PluginCompatibilityUUIDs</key>
<array>
<string>D985F0E4-3BBC-4B95-BBA1-12056AC4A531</string>
@@ -60,5 +82,93 @@
<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>
</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>
</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>
</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>
</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>
</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>
</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>
</array>
</dict>
</plist>

View File

@@ -0,0 +1,73 @@
//
// OpenHaystack Tracking personal Bluetooth devices via Apple's Find My network
//
// Copyright © 2021 Secure Mobile Networking Lab (SEEMOO)
// Copyright © 2021 The Open Wireless Link Project
//
// SPDX-License-Identifier: AGPL-3.0-only
//
import Foundation
import XCTest
@testable import OpenHaystack
class UpdateCheckTests: XCTestCase {
func testCompareVersions() {
let i1 = "1.0.3"
let a1 = "1.0.4"
XCTAssertEqual(UpdateCheckController.compareVersions(availableVersion: a1, installedVersion: i1), .older)
let a11 = "1.1"
XCTAssertEqual(UpdateCheckController.compareVersions(availableVersion: a11, installedVersion: i1), .older)
let a12 = "2"
XCTAssertEqual(UpdateCheckController.compareVersions(availableVersion: a12, installedVersion: i1), .older)
let a2 = "1.0.3"
XCTAssertEqual(UpdateCheckController.compareVersions(availableVersion: a2, installedVersion: i1), .same)
let a3 = "1.0.2"
XCTAssertEqual(UpdateCheckController.compareVersions(availableVersion: a3, installedVersion: i1), .newer)
let a31 = "1.0"
XCTAssertEqual(UpdateCheckController.compareVersions(availableVersion: a31, installedVersion: i1), .newer)
let a32 = "0.10.1"
XCTAssertEqual(UpdateCheckController.compareVersions(availableVersion: a32, installedVersion: i1), .newer)
let a4 = "1.1.1"
let i4 = "1.1.2"
XCTAssertEqual(UpdateCheckController.compareVersions(availableVersion: a4, installedVersion: i4), .newer)
let a41 = "1.0.2"
XCTAssertEqual(UpdateCheckController.compareVersions(availableVersion: a41, installedVersion: i1), .newer)
}
func testHTMLVersionCompare() {
let github =
"""
<h1 data-view-component="true" class="d-inline mr-3"><a href="/seemoo-lab/openhaystack/releases/tag/v0.4.1" data-view-component="true" class="Link--primary">Release v0.4.1</a></h1>
<h1 data-view-component="true" class="d-inline mr-3"><a href="/seemoo-lab/openhaystack/releases/tag/v0.4.1" data-view-component="true" class="Link--primary">Release v0.4.1</a></h1>
<a href="/seemoo-lab/openhaystack/releases/tag/v0.4.1" data-view-component="true" class="Link--primary">Release v0.4.1</a>
"""
XCTAssertEqual(UpdateCheckController.getVersion(from: github), "0.4.1")
let h1 = "<h1>Release v0.4.1</h1> <h1>Release v0.3.1</h1>"
XCTAssertEqual(UpdateCheckController.getVersion(from: h1), "0.4.1")
let h2 = "<h1>Release v0.5</h1>"
XCTAssertEqual(UpdateCheckController.getVersion(from: h2), "0.5")
let h3 = "<h1>Release v1.5</h1>"
XCTAssertEqual(UpdateCheckController.getVersion(from: h3), "1.5")
let h4 = "<h1>Release v1</h1>"
XCTAssertEqual(UpdateCheckController.getVersion(from: h4), "1")
}
func testDownload() {
let expect = expectation(description: "Update download")
UpdateCheckController.downloadUpdate(version: "0.4.1", finished: { success in
XCTAssertTrue(success)
expect.fulfill()
})
wait(for: [expect], timeout: 20.0)
}
}

View File

@@ -110,7 +110,7 @@ Feel free to port OpenHaystack to other devices that support Bluetooth Low Energ
| Platform | Tested on | Deploy via app | Comment |
|----------|-----------|:--------------:|---------|
| [Nordic nRF51](Firmware/Microbit_v1) | BBC micro:bit v1 | ✓ | Only supports nRF51288 at this time (see issue #6). |
| [Nordic nRF51](Firmware/Microbit_v1) | BBC micro:bit v1 | ✓ | Only supports nRF51822 at this time (see issue #6). |
| [Espressif ESP32](Firmware/ESP32) | SP32-WROOM, ESP32-WROVER | ✓ | Deployment can take up to 3 minutes. Requires Python 3. Thanks **@fhessel**. |
| [Linux HCI](Firmware/Linux_HCI) | Raspberry Pi 4 w/ Raspbian | | Should support any Linux machine. |
@@ -123,7 +123,8 @@ Feel free to port OpenHaystack to other devices that support Bluetooth Low Energ
## References
- Alexander Heinrich, Milan Stute, Tim Kornhuber, Matthias Hollick. **Who Can _Find My_ Devices? Security and Privacy of Apple's Crowd-Sourced Bluetooth Location Tracking System.** _Proceedings on Privacy Enhancing Technologies (PoPETs)_, 2021. [📄 Preprint](https://arxiv.org/abs/2103.02282).
- Alexander Heinrich, Milan Stute, Tim Kornhuber, Matthias Hollick. **Who Can _Find My_ Devices? Security and Privacy of Apple's Crowd-Sourced Bluetooth Location Tracking System.** _Proceedings on Privacy Enhancing Technologies (PoPETs)_, 2021. [doi:10.2478/popets-2021-0045](https://doi.org/10.2478/popets-2021-0045) [📄 Paper](https://www.petsymposium.org/2021/files/papers/issue3/popets-2021-0045.pdf) [📄 Preprint](https://arxiv.org/abs/2103.02282).
- Alexander Heinrich, Milan Stute, and Matthias Hollick. **DEMO: OpenHaystack: A Framework for Tracking Personal Bluetooth Devices via Apples Massive Find My Network.** _14th ACM Conference on Security and Privacy in Wireless and Mobile (WiSec 21)_, 2021.
- Tim Kornhuber. **Analysis of Apple's Crowd-Sourced Location Tracking System.** _Technical University of Darmstadt_, Master's thesis, 2020.
- Apple Inc. **Find My Network Accessory Specification Developer Preview Release R3.** 2020. [📄 Download](https://developer.apple.com/find-my/).

BIN
Resources/Pins-NRF52832.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 276 KiB