mirror of
https://github.com/seemoo-lab/openhaystack.git
synced 2026-02-14 17:49:54 +00:00
Added support for key derivation
Added deployment for nRF52 Devices
This commit is contained in:
committed by
Alexander Heinrich
parent
d9a1a33b1e
commit
278fe4e30d
@@ -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 */; };
|
||||
@@ -108,6 +112,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>"; };
|
||||
@@ -203,6 +211,7 @@
|
||||
78023CAC25F7775300B083EF /* Firmwares */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
5A2C9088273425720044407E /* NRF */,
|
||||
78023CAE25F7797400B083EF /* ESP32 */,
|
||||
78023CAD25F7775A00B083EF /* Microbit */,
|
||||
);
|
||||
@@ -346,6 +355,8 @@
|
||||
78023CAA25F7767000B083EF /* ESP32Controller.swift */,
|
||||
7821DAD025F7B2C10054DC33 /* FileManager.swift */,
|
||||
F1647C1A25FF7954004144D6 /* AccessoryNearbyMonitor.swift */,
|
||||
5A2C908A2734266A0044407E /* DataToHexExtension.swift */,
|
||||
5A2C908C273429360044407E /* NRFController.swift */,
|
||||
);
|
||||
path = HaystackApp;
|
||||
sourceTree = "<group>";
|
||||
@@ -373,6 +384,7 @@
|
||||
7821DAD225F7C39A0054DC33 /* ESP32InstallSheet.swift */,
|
||||
78D9B80525F7CF60009B9CE8 /* ManageAccessoriesView.swift */,
|
||||
F126102E2600D1D80066A859 /* Slider+LogScale.swift */,
|
||||
5A2C908E273429540044407E /* NRFInstallSheet.swift */,
|
||||
);
|
||||
path = Views;
|
||||
sourceTree = "<group>";
|
||||
@@ -510,6 +522,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 +610,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 +619,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 +629,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 */,
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -48,7 +48,6 @@
|
||||
char *buf;
|
||||
BIO_get_mem_data(bio, &buf);
|
||||
NSLog(@"Generating shared key failed %s", buf);
|
||||
free(buf);
|
||||
BIO_free(bio);
|
||||
}
|
||||
|
||||
@@ -145,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) {
|
||||
@@ -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];
|
||||
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
120
OpenHaystack/OpenHaystack/HaystackApp/Firmwares/NRF/flash_nrf.py
Executable file
120
OpenHaystack/OpenHaystack/HaystackApp/Firmwares/NRF/flash_nrf.py
Executable file
@@ -0,0 +1,120 @@
|
||||
#!/bin/python3
|
||||
from pynrfjprog import LowLevel
|
||||
from intelhex import IntelHex
|
||||
from base64 import b64decode
|
||||
import argparse
|
||||
|
||||
|
||||
def flash_openhaystack_fw(public_key, symmetric_key, update_interval, hex_path, snr=None):
|
||||
"""
|
||||
Flash openhaystack firmware to device
|
||||
@param (optional) int snr: Specify serial number of DK to run example on.
|
||||
"""
|
||||
# Check if paramters are valid
|
||||
if len(public_key) != 57:
|
||||
pk_len = len(public_key)
|
||||
print(f'[!] Public key should be 57 bytes but is {pk_len} bytes')
|
||||
exit(-1)
|
||||
|
||||
if len(symmetric_key) != 32:
|
||||
sk_len = len(symmetric_key)
|
||||
print(f'[!] Symmetric key should be 32 bytes but is {sk_len} bytes')
|
||||
exit(-1)
|
||||
|
||||
if not 0 < update_interval < 4294967295:
|
||||
print(f'[!] Update interval is {update_interval}, but must be bigger than 0 but smaller than 4294967295 (0xFFFFFFFF)')
|
||||
exit(-1)
|
||||
|
||||
# Detect the device family of your device. Initialize an API object with UNKNOWN family and read the device's
|
||||
# family. This step is performed so this example can be run in all devices without customer input.
|
||||
print('[*] Opening API with device family UNKNOWN, reading the device family.')
|
||||
with LowLevel.API(
|
||||
# Using with construction so there is no need to open or close the API class.
|
||||
LowLevel.DeviceFamily.UNKNOWN) as api:
|
||||
if snr is not None:
|
||||
api.connect_to_emu_with_snr(snr)
|
||||
else:
|
||||
api.connect_to_emu_without_snr()
|
||||
device_family = api.read_device_family()
|
||||
|
||||
print(f'[*] Opening API with device family {device_family}, reading the device version.')
|
||||
with LowLevel.API(device_family) as api:
|
||||
# Open the loaded DLL and connect to an emulator probe. If several are connected a pop up will appear.
|
||||
if snr is not None:
|
||||
api.connect_to_emu_with_snr(snr)
|
||||
else:
|
||||
api.connect_to_emu_without_snr()
|
||||
device_version = api.read_device_version()
|
||||
|
||||
print(f'[*] Device version {device_version}')
|
||||
# Select hex file according to device family and device version
|
||||
hex_file_path = f'{hex_path}{device_family}_{device_version.split("_")[0]}_openHayStack.hex'
|
||||
|
||||
print(f'[*] Patching hex file \'{hex_file_path}\' with supplied keys')
|
||||
|
||||
# Open hex file and patch cryptographic keys
|
||||
ih = IntelHex(hex_file_path)
|
||||
|
||||
sk_address = ih.find(b'OFFLINEFINDINGSYMMETRICKEYHERE!')
|
||||
print(f'[*] SK address in hex file is {sk_address}')
|
||||
ih.puts(sk_address, symmetric_key)
|
||||
|
||||
pk_address = ih.find(b'OFFLINEFINDINGUNCOMPRESSEDPUBLICKEYHERE!AAAAAAAAAAAAAAAAA')
|
||||
print(f'[*] PK address in hex file is {pk_address}')
|
||||
ih.puts(pk_address, public_key)
|
||||
|
||||
update_interval_address = ih.find(b'\x37\x33\x33\x31')
|
||||
if update_interval_address - pk_address != 60:
|
||||
print(f'[!] {update_interval_address - pk_address} bytes between update interval and private key, but should be 60 bytes')
|
||||
exit(-1)
|
||||
print(f'[*] Update Interval address in hex file is {update_interval_address}')
|
||||
update_interval_hex = (update_interval).to_bytes(4, byteorder='little')
|
||||
ih.puts(update_interval_address, update_interval_hex)
|
||||
|
||||
# Initialize an API object with the target family. This will load nrfjprog.dll with the proper target family.
|
||||
api = LowLevel.API(device_family)
|
||||
# Open the loaded DLL and connect to an emulator probe. If several are connected a pop up will appear.
|
||||
api.open()
|
||||
try:
|
||||
if snr is not None:
|
||||
api.connect_to_emu_with_snr(snr)
|
||||
else:
|
||||
api.connect_to_emu_without_snr()
|
||||
|
||||
# Just for info
|
||||
device_version = api.read_device_version()
|
||||
print(f'[*] Device version {device_version}')
|
||||
|
||||
# Erase all the flash of the device
|
||||
print('[*] Erasing all flash in the microcontroller.')
|
||||
api.erase_all()
|
||||
|
||||
# Program the parsed hex into the device's memory
|
||||
print(f'[*] Writing patched {hex_file_path} to device.')
|
||||
for segment in ih.segments():
|
||||
api.write(segment[0], ih.gets(segment[0], segment[1] - segment[0]), True)
|
||||
|
||||
# Reset the device and run.
|
||||
api.sys_reset()
|
||||
api.go()
|
||||
print('[*] Program started')
|
||||
|
||||
# Close the loaded DLL to free resources.
|
||||
api.close()
|
||||
|
||||
print('[*] Flashed openHayStack Firmware successfully')
|
||||
|
||||
except LowLevel.APIError:
|
||||
api.close()
|
||||
raise
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
# Parse arguments given when calling the script via command line
|
||||
parser = argparse.ArgumentParser()
|
||||
parser.add_argument('-pk', '--public-key', help="Base64 encoded Public key (29 bytes)", required=True)
|
||||
parser.add_argument('-sk', '--symmetric-key', help="Base64 encoded Symmetric key (32 bytes)", required=True)
|
||||
parser.add_argument('-ui', '--update-interval', help="Update interval for key derivation in minutes", required=True, type=int)
|
||||
parser.add_argument('-ph', '--path-to-hex', help="Path to hexfile, defaults to script folder", default="")
|
||||
args = vars(parser.parse_args())
|
||||
flash_openhaystack_fw(public_key=b64decode(args['public_key']), symmetric_key=b64decode(args['symmetric_key']), update_interval=args['update_interval'], hex_path=args['path_to_hex'])
|
||||
136
OpenHaystack/OpenHaystack/HaystackApp/Firmwares/NRF/flash_nrf.sh
Executable file
136
OpenHaystack/OpenHaystack/HaystackApp/Firmwares/NRF/flash_nrf.sh
Executable 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 ###"
|
||||
@@ -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
|
||||
@@ -146,10 +182,24 @@ 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(
|
||||
|
||||
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,
|
||||
@@ -158,19 +208,116 @@ class Accessory: ObservableObject, Codable, Identifiable, Equatable, Hashable {
|
||||
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
|
||||
|
||||
@@ -8,6 +8,7 @@
|
||||
//
|
||||
|
||||
import Foundation
|
||||
import CoreLocation
|
||||
import SwiftUI
|
||||
import CoreLocation
|
||||
|
||||
|
||||
73
OpenHaystack/OpenHaystack/HaystackApp/NRFController.swift
Normal file
73
OpenHaystack/OpenHaystack/HaystackApp/NRFController.swift
Normal 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
|
||||
|
||||
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
|
||||
}
|
||||
@@ -35,6 +35,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 {
|
||||
|
||||
@@ -51,6 +60,9 @@ struct AccessoryListEntry: View {
|
||||
.font(.headline)
|
||||
}
|
||||
self.timestampView()
|
||||
if accessory.usesDerivation {
|
||||
self.updateIntervalView()
|
||||
}
|
||||
}
|
||||
|
||||
Spacer()
|
||||
@@ -68,8 +80,11 @@ 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 key ID (Base64)", action: { self.copyPublicKeyHash(of: accessory) })
|
||||
Menu("Copy advertisement key") {
|
||||
@@ -77,6 +92,10 @@ struct AccessoryListEntry: View {
|
||||
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() })
|
||||
}
|
||||
@@ -139,6 +158,36 @@ struct AccessoryListEntry: View {
|
||||
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!
|
||||
@@ -160,6 +209,7 @@ struct AccessoryListEntry: View {
|
||||
get: { accessory.name },
|
||||
set: { accessory.name = $0 }
|
||||
),
|
||||
|
||||
alertType: self.$alertType,
|
||||
delete: { _ in () },
|
||||
deployAccessoryToMicrobit: { _ in () },
|
||||
|
||||
@@ -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,13 @@ 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)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,172 @@
|
||||
//
|
||||
// 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("Put key update time:")
|
||||
self.timePicker
|
||||
|
||||
Spacer()
|
||||
|
||||
HStack {
|
||||
Spacer()
|
||||
|
||||
Button(
|
||||
"Deploy",
|
||||
action: {
|
||||
if let accessory = self.accessory {
|
||||
let daysInt = Int(days.value) ?? 0
|
||||
let hoursInt = Int(hours.value) ?? 0
|
||||
let minutesInt = Int(minutes.value) ?? 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()
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
var timePicker: some View {
|
||||
Group{
|
||||
HStack{
|
||||
TextField("", text: $days.value).textFieldStyle(RoundedBorderTextFieldStyle())
|
||||
Text("Day(s)")
|
||||
TextField("", text: $hours.value).textFieldStyle(RoundedBorderTextFieldStyle())
|
||||
Text("Hour(s)")
|
||||
TextField("", text: $minutes.value).textFieldStyle(RoundedBorderTextFieldStyle())
|
||||
Text("Minute(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 = "" {
|
||||
didSet {
|
||||
let filtered = value.filter { $0.isNumber }
|
||||
|
||||
if value != filtered {
|
||||
value = filtered
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
@@ -317,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"),
|
||||
@@ -382,6 +389,7 @@ struct OpenHaystackMainView: View {
|
||||
case keyError
|
||||
case searchPartyToken
|
||||
case deployFailed
|
||||
case nrfDeployFailed
|
||||
case deployedSuccessfully
|
||||
case deletionFailed
|
||||
case noReportsFound
|
||||
|
||||
Reference in New Issue
Block a user