Supporting ESP32 as tags for OpenHaystack (#19)

* Moving microbit firmware to a subfolder in /Firmware to prepare integration of ESP32

* Add firmware for ESP32 and update workflows

* Integrated ESP32 firmware from @fhessel to OpenHaystack App

Co-authored-by: Frank Hessel <fhessel@seemoo.tu-darmstadt.de>
This commit is contained in:
Alexander Heinrich
2021-03-09 23:57:28 +01:00
committed by GitHub
parent f88663f5e7
commit 898563ca0b
46 changed files with 2955 additions and 270 deletions

View File

@@ -0,0 +1,38 @@
name: 'Build Firmware with ESP-IDF'
description: 'Builds a firmware for the ESP32 using the ESP-IDF'
inputs:
src-dir:
description: 'Source directory for the ESP-IDF project'
required: true
out-dir:
description: 'Directory to which bin files will be written'
required: true
app-name:
description: 'Name of the IDF application/main binary'
required: true
runs:
using: "composite"
steps:
- name: Prepare ESP-IDF
shell: bash
run: |
sudo apt update
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
cd /opt/esp/esp-idf
./install.sh
- name: Build firmware
shell: bash
run: |
source /opt/esp/esp-idf/export.sh
cd ${{ inputs.src-dir }}
idf.py build
- name: Bundle output files
shell: bash
run: |
mkdir -p "${{ inputs.out-dir }}/bootloader" "${{ inputs.out-dir }}/partition_table"
cp "${{ inputs.src-dir }}/build/bootloader/bootloader.bin" "${{ inputs.out-dir }}/bootloader/bootloader.bin"
cp "${{ inputs.src-dir }}/build/partition_table/partition-table.bin" "${{ inputs.out-dir }}/partition_table/partition-table.bin"
cp "${{ inputs.src-dir }}/build/${{ inputs.app-name }}.bin" "${{ inputs.out-dir }}/${{ inputs.app-name }}.bin"

View File

@@ -0,0 +1,28 @@
name: "Build firmware (ESP32)"
on:
push:
branches: [ main ]
paths:
- Firmware/ESP32/**
pull_request:
branches: [ main ]
paths:
- Firmware/ESP32/**
jobs:
build-firmware-esp32:
runs-on: ubuntu-latest
steps:
- name: "Checkout code"
uses: actions/checkout@v2
- name: "Copy static files"
run: |
mkdir -p archive/build
cp Firmware/ESP32/flash_esp32.sh archive/
- name: "Build ESP32 firmware"
uses: ./.github/actions/build-esp-idf
with:
src-dir: Firmware/ESP32
out-dir: archive/build
app-name: openhaystack

View File

@@ -4,15 +4,15 @@ on:
push:
branches: [ main ]
paths:
- Firmware/**
- Firmware/Microbit_v1/**
pull_request:
branches: [ main ]
paths:
- Firmware/**
- Firmware/Microbit_v1/**
defaults:
run:
working-directory: Firmware
working-directory: Firmware/Microbit_v1
jobs:
build-firmware:

View File

@@ -6,6 +6,28 @@ on:
- 'v*' # Push events to matching v*, i.e. v1.0, v20.15.10
jobs:
build-firmware-esp32:
runs-on: ubuntu-latest
steps:
- name: "Checkout code"
uses: actions/checkout@v2
- name: "Copy static files"
run: |
mkdir -p archive/build
cp Firmware/ESP32/flash_esp32.sh archive/
- name: "Build ESP32 firmware"
uses: ./.github/actions/build-esp-idf
with:
src-dir: Firmware/ESP32
out-dir: archive/build
app-name: openhaystack
- name: "Create archive"
uses: actions/upload-artifact@v2
with:
name: firmware-esp32
path: archive/*
retention-days: 1
build-and-release:
name: "Create release on GitHub"
runs-on: macos-latest
@@ -15,6 +37,8 @@ jobs:
defaults:
run:
working-directory: ${{ env.PROJECT_DIR }}
needs:
- build-firmware-esp32
steps:
- name: Checkout code
uses: actions/checkout@v2
@@ -22,6 +46,11 @@ jobs:
uses: devbotsxyz/xcode-select@v1
with:
version: "12"
- name: "Add ESP32 firmware"
uses: actions/download-artifact@v2
with:
name: firmware-esp32
path: "${{ env.PROJECT_DIR }}/OpenHaystack/HaystackApp/Firmwares/ESP32"
- name: "Archive project"
run: xcodebuild archive -scheme ${APP} -configuration release -archivePath ${APP}.xcarchive
- name: "Create ZIP"

4
.gitmodules vendored
View File

@@ -1,3 +1,3 @@
[submodule "Firmware/blessed"]
path = Firmware/blessed
[submodule "Firmware/Microbit_v1/blessed"]
path = Firmware/Microbit_v1/blessed
url = https://github.com/pauloborges/blessed.git

3
Firmware/ESP32/.gitignore vendored Normal file
View File

@@ -0,0 +1,3 @@
build/**
venv/**
sdkconfig.old

View File

@@ -0,0 +1,7 @@
# The following lines of boilerplate have to be in your project's CMakeLists
# in this exact order for cmake to work correctly
cmake_minimum_required(VERSION 3.5)
set(SUPPORTED_TARGETS esp32)
include($ENV{IDF_PATH}/tools/cmake/project.cmake)
project(openhaystack)

10
Firmware/ESP32/Makefile Normal file
View File

@@ -0,0 +1,10 @@
#
# This is a project Makefile. It is assumed the directory this Makefile resides in is a
# project subdirectory.
#
PROJECT_NAME := openhaystack-esp32
COMPONENT_ADD_INCLUDEDIRS := components/include
include $(IDF_PATH)/make/project.mk

44
Firmware/ESP32/README.md Normal file
View File

@@ -0,0 +1,44 @@
# OpenHaystack Firmware for ESP32
This project contains a PoC firmware for Espressif ESP32 chips (like ESP32-WROOM or ESP32-WROVER, but _not_ ESP32-S2).
After flashing our firmware, the device sends out Bluetooth Low Energy advertisements such that it can be found by [Apple's Find My network](https://developer.apple.com/find-my/).
## Disclaimer
Note that the firmware is just a proof-of-concept and currently only implements advertising a single static key. This means that **devices running this firmware are trackable** by other devices in proximity.
## Requirements
To change and rebuild the firmware, you need Espressif's IoT Development Framework (ESP-IDF).
Installation instructions for the latest version of the ESP-IDF can be found in [its documentation](https://docs.espressif.com/projects/esp-idf/en/latest/esp32/get-started/).
The firmware is tested on version 4.2.
For deploying the firmware, you need Python 3 on your path, either as `python3` (preferred) or as `python`, and the `venv` module needs to be available.
## Build
With the ESP-IDF on your `$PATH`, you can use `idf.py` to build the application from within this directory:
```bash
idf.py build
```
This will create the following files:
- `build/bootloader/bootloader.bin` -- The second stage bootloader
- `build/partition_table/partition-table.bin` -- The partition table
- `build/openhaystack.bin` -- The application itself
These files are required for the next step: Deploy the firmware.
## 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"
```
> **Note:** You might need to reset your device after running the script before it starts sending advertisements.
For more options, see `./flash-esp32.h --help`.

139
Firmware/ESP32/flash_esp32.sh Executable file
View File

@@ -0,0 +1,139 @@
#!/bin/bash
# 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"
# Defaults: Serial port to access the ESP32
PORT=/dev/ttyS0
# Defaults: Fast baud rate
BAUDRATE=921600
# Parameter parsing
while [[ $# -gt 0 ]]; do
KEY="$1"
case "$KEY" in
-p|--port)
PORT="$2"
shift
shift
;;
-s|--slow)
BAUDRATE=115200
shift
;;
-v|--venvdir)
VENV_DIR="$2"
shift
shift
;;
-h|--help)
echo "flash_esp32.sh - Flash the OpenHaystack firmware onto an ESP32 module"
echo ""
echo " This script will create a virtual environment for the required tools."
echo ""
echo "Call: flash_esp32.sh [-p <port>] [-v <dir>] [-s] PUBKEY"
echo ""
echo "Required Arguments:"
echo " PUBKEY"
echo " The base64-encoded public key"
echo ""
echo "Optional Arguments:"
echo " -h, --help"
echo " Show this message and exit."
echo " -p, --port <port>"
echo " Specify the serial interface to which the device is connected."
echo " -s, --slow"
echo " Use 115200 instead of 921600 baud when flashing."
echo " Might be required for long/bad USB cables or slow USB-to-Serial converters."
echo " -v, --venvdir <dir>"
echo " Select Python virtual environment with esptool installed."
echo " If the directory does not exist, it will be created."
exit 1
;;
*)
if [[ -z "$PUBKEY" ]]; then
PUBKEY="$1"
shift
else
echo "Got unexpected parameter $1"
exit 1
fi
;;
esac
done
# Sanity check: Pubkey exists
if [[ -z "$PUBKEY" ]]; then
echo "Missing public key, call with --help for usage"
exit 1
fi
# Sanity check: Port
if [[ ! -e "$PORT" ]]; then
echo "$PORT does not exist, please specify a valid serial interface with the -p argument"
exit 1
fi
# Setup the virtual environment
if [[ ! -d "$VENV_DIR" ]]; then
# Create the virtual environment
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
source "$VENV_DIR/bin/activate"
pip install --upgrade pip
pip install esptool
if [[ $? != 0 ]]; then
echo "Could not install Python 3 module esptool in $VENV_DIR";
exit 1
fi
else
source "$VENV_DIR/bin/activate"
fi
# Prepare the key
KEYFILE="$SCRIPT_DIR/tmp.key"
if [[ -f "$KEYFILE" ]]; then
echo "$KEYFILE already exists, stopping here not to override files..."
exit 1
fi
echo "$PUBKEY" | python3 -m base64 -d - > "$KEYFILE"
if [[ $? != 0 ]]; then
echo "Could not parse the public key. Please provide valid base64 input"
exit 1
fi
# Call esptool.py. Errors from here on are critical
set -e
# Clear NVM
esptool.py --after no_reset \
erase_region 0x9000 0x5000
esptool.py --before no_reset --baud $BAUDRATE \
write_flash 0x1000 "$SCRIPT_DIR/build/bootloader/bootloader.bin" \
0x8000 "$SCRIPT_DIR/build/partition_table/partition-table.bin" \
0xe000 "$KEYFILE" \
0x10000 "$SCRIPT_DIR/build/openhaystack.bin"
rm "$KEYFILE"

View File

@@ -0,0 +1,3 @@
idf_component_register(SRCS "openhaystack_main.c"
INCLUDE_DIRS ".")

View File

View File

@@ -0,0 +1,4 @@
#
# "main" pseudo-component makefile.
#
# (Uses default behaviour of compiling all source files in directory, adding 'include' to include path.)

View File

@@ -0,0 +1,162 @@
#include <stdint.h>
#include <string.h>
#include <stdbool.h>
#include <stdio.h>
#include "nvs_flash.h"
#include "esp_partition.h"
#include "esp_bt.h"
#include "esp_gap_ble_api.h"
#include "esp_gattc_api.h"
#include "esp_gatt_defs.h"
#include "esp_bt_main.h"
#include "esp_bt_defs.h"
#include "esp_log.h"
#include "freertos/FreeRTOS.h"
static const char* LOG_TAG = "open_haystack";
/** Callback function for BT events */
static void esp_gap_cb(esp_gap_ble_cb_event_t event, esp_ble_gap_cb_param_t *param);
/** Random device address */
static esp_bd_addr_t rnd_addr = { 0xFF, 0xBB, 0xCC, 0xDD, 0xEE, 0xFF };
/** Advertisement payload */
static uint8_t adv_data[31] = {
0x1e, /* Length (30) */
0xff, /* Manufacturer Specific Data (type 0xff) */
0x4c, 0x00, /* Company ID (Apple) */
0x12, 0x19, /* Offline Finding type and length */
0x00, /* State */
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, /* First two bits */
0x00, /* Hint (0x00) */
};
/* https://docs.espressif.com/projects/esp-idf/en/latest/esp32/api-reference/bluetooth/esp_gap_ble.html#_CPPv420esp_ble_adv_params_t */
static esp_ble_adv_params_t ble_adv_params = {
// Advertising min interval:
// Minimum advertising interval for undirected and low duty cycle
// directed advertising. Range: 0x0020 to 0x4000 Default: N = 0x0800
// (1.28 second) Time = N * 0.625 msec Time Range: 20 ms to 10.24 sec
.adv_int_min = 0x00A0, // 100ms
// Advertising max interval:
// Maximum advertising interval for undirected and low duty cycle
// directed advertising. Range: 0x0020 to 0x4000 Default: N = 0x0800
// (1.28 second) Time = N * 0.625 msec Time Range: 20 ms to 10.24 sec
.adv_int_max = 0x0140, // 200ms
// Advertisement type
.adv_type = ADV_TYPE_NONCONN_IND,
// Use the random address
.own_addr_type = BLE_ADDR_TYPE_RANDOM,
// All channels
.channel_map = ADV_CHNL_ALL,
// Allow both scan and connection requests from anyone.
.adv_filter_policy = ADV_FILTER_ALLOW_SCAN_ANY_CON_ANY,
};
static void esp_gap_cb(esp_gap_ble_cb_event_t event, esp_ble_gap_cb_param_t *param)
{
esp_err_t err;
switch (event) {
case ESP_GAP_BLE_ADV_DATA_RAW_SET_COMPLETE_EVT:
esp_ble_gap_start_advertising(&ble_adv_params);
break;
case ESP_GAP_BLE_ADV_START_COMPLETE_EVT:
//adv start complete event to indicate adv start successfully or failed
if ((err = param->adv_start_cmpl.status) != ESP_BT_STATUS_SUCCESS) {
ESP_LOGE(LOG_TAG, "advertising start failed: %s", esp_err_to_name(err));
} else {
ESP_LOGI(LOG_TAG, "advertising has started.");
}
break;
case ESP_GAP_BLE_ADV_STOP_COMPLETE_EVT:
if ((err = param->adv_stop_cmpl.status) != ESP_BT_STATUS_SUCCESS){
ESP_LOGE(LOG_TAG, "adv stop failed: %s", esp_err_to_name(err));
}
else {
ESP_LOGI(LOG_TAG, "stop adv successfully");
}
break;
default:
break;
}
}
int load_key(uint8_t *dst, size_t size) {
const esp_partition_t *keypart = esp_partition_find_first(0x40, 0x00, "key");
if (keypart == NULL) {
ESP_LOGE(LOG_TAG, "Could not find key partition");
return 1;
}
esp_err_t status;
status = esp_partition_read(keypart, 0, dst, size);
if (status != ESP_OK) {
ESP_LOGE(LOG_TAG, "Could not read key from partition: %s", esp_err_to_name(status));
}
return status;
}
void set_addr_from_key(esp_bd_addr_t addr, uint8_t *public_key) {
addr[0] = public_key[0] | 0b11000000;
addr[1] = public_key[1];
addr[2] = public_key[2];
addr[3] = public_key[3];
addr[4] = public_key[4];
addr[5] = public_key[5];
}
void set_payload_from_key(uint8_t *payload, uint8_t *public_key) {
/* copy last 22 bytes */
memcpy(&payload[7], &public_key[6], 22);
/* append two bits of public key */
payload[29] = public_key[0] >> 6;
}
void app_main(void)
{
ESP_ERROR_CHECK(nvs_flash_init());
ESP_ERROR_CHECK(esp_bt_controller_mem_release(ESP_BT_MODE_CLASSIC_BT));
esp_bt_controller_config_t bt_cfg = BT_CONTROLLER_INIT_CONFIG_DEFAULT();
esp_bt_controller_init(&bt_cfg);
esp_bt_controller_enable(ESP_BT_MODE_BLE);
esp_bluedroid_init();
esp_bluedroid_enable();
// Load the public key from the key partition
static uint8_t public_key[28];
if (load_key(public_key, sizeof(public_key)) != ESP_OK) {
ESP_LOGE(LOG_TAG, "Could not read the key, stopping.");
return;
}
set_addr_from_key(rnd_addr, public_key);
set_payload_from_key(adv_data, public_key);
ESP_LOGI(LOG_TAG, "using device address: %02x %02x %02x %02x %02x %02x", rnd_addr[0], rnd_addr[1], rnd_addr[2], rnd_addr[3], rnd_addr[4], rnd_addr[5]);
esp_err_t status;
//register the scan callback function to the gap module
if ((status = esp_ble_gap_register_callback(esp_gap_cb)) != ESP_OK) {
ESP_LOGE(LOG_TAG, "gap register error: %s", esp_err_to_name(status));
return;
}
if ((status = esp_ble_gap_set_rand_addr(rnd_addr)) != ESP_OK) {
ESP_LOGE(LOG_TAG, "couldn't set random address: %s", esp_err_to_name(status));
return;
}
if ((esp_ble_gap_config_adv_data_raw((uint8_t*)&adv_data, sizeof(adv_data))) != ESP_OK) {
ESP_LOGE(LOG_TAG, "couldn't configure BLE adv: %s", esp_err_to_name(status));
return;
}
ESP_LOGI(LOG_TAG, "application initialized");
}

View File

@@ -0,0 +1,5 @@
# Name, Type, SubType, Offset, Size, Flags
nvs, data, nvs, 0x9000, 0x5000,
key, 0x40, 0x00, 0xe000, 0x1000,
phy_init, data, phy, 0xf000, 0x1000,
factory, app, factory, 0x10000, 1M,
1 # Name Type SubType Offset Size Flags
2 nvs data nvs 0x9000 0x5000
3 key 0x40 0x00 0xe000 0x1000
4 phy_init data phy 0xf000 0x1000
5 factory app factory 0x10000 1M

1606
Firmware/ESP32/sdkconfig Normal file

File diff suppressed because it is too large Load Diff

View File

@@ -10,6 +10,9 @@
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 */; };
78023CAB25F7767000B083EF /* ESP32Controller.swift in Sources */ = {isa = PBXBuildFile; fileRef = 78023CAA25F7767000B083EF /* ESP32Controller.swift */; };
78023CAF25F7797400B083EF /* ESP32 in Resources */ = {isa = PBXBuildFile; fileRef = 78023CAE25F7797400B083EF /* ESP32 */; };
78023CB125F7841F00B083EF /* MicrocontrollerTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 78023CB025F7841F00B083EF /* MicrocontrollerTests.swift */; };
781EB3EA25DAD7EA00FEAA19 /* ReportsFetcher.m in Sources */ = {isa = PBXBuildFile; fileRef = 78108B84248E8FDD0007E9C4 /* ReportsFetcher.m */; };
781EB3EB25DAD7EA00FEAA19 /* SavePanel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 116B4EEC24A913AA007BA636 /* SavePanel.swift */; };
781EB3EC25DAD7EA00FEAA19 /* DecryptReports.swift in Sources */ = {isa = PBXBuildFile; fileRef = 025DFEDB248FED250039C718 /* DecryptReports.swift */; };
@@ -25,6 +28,8 @@
781EB40025DAD7EA00FEAA19 /* Preview Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 78108B76248E8FB80007E9C4 /* Preview Assets.xcassets */; };
781EB40225DAD7EA00FEAA19 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 78108B73248E8FB80007E9C4 /* Assets.xcassets */; };
781EB43125DADF2B00FEAA19 /* AnisetteDataManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 781EB40F25DADB0600FEAA19 /* AnisetteDataManager.swift */; };
7821DAD125F7B2C10054DC33 /* FileManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7821DAD025F7B2C10054DC33 /* FileManager.swift */; };
7821DAD325F7C39A0054DC33 /* ESP32InstallSheet.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7821DAD225F7C39A0054DC33 /* ESP32InstallSheet.swift */; };
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 */; };
@@ -42,6 +47,7 @@
7899D1D625DE74EE00115740 /* firmware.bin in Resources */ = {isa = PBXBuildFile; fileRef = 7899D1D525DE74EE00115740 /* firmware.bin */; };
7899D1E125DE97E200115740 /* IconSelectionView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7899D1E025DE97E200115740 /* IconSelectionView.swift */; };
7899D1E925DEBF4900115740 /* AccessoryMapAnnotation.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7899D1E825DEBF4800115740 /* AccessoryMapAnnotation.swift */; };
78D9B80625F7CF60009B9CE8 /* ManageAccessoriesView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 78D9B80525F7CF60009B9CE8 /* ManageAccessoriesView.swift */; };
78EC226425DAE0BE0042B775 /* OpenHaystackTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 78EC226325DAE0BE0042B775 /* OpenHaystackTests.swift */; };
78EC226C25DBC2E40042B775 /* OpenHaystackMainView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 78EC226B25DBC2E40042B775 /* OpenHaystackMainView.swift */; };
78EC227225DBC8CE0042B775 /* Accessory.swift in Sources */ = {isa = PBXBuildFile; fileRef = 78EC227125DBC8CE0042B775 /* Accessory.swift */; };
@@ -101,6 +107,9 @@
78014A2725DC01220089F6D9 /* MicrobitController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MicrobitController.swift; sourceTree = "<group>"; };
78014A2A25DC22110089F6D9 /* sample.bin */ = {isa = PBXFileReference; lastKnownFileType = archive.macbinary; path = sample.bin; sourceTree = "<group>"; };
78014A2E25DC2F100089F6D9 /* pattern_sample.bin */ = {isa = PBXFileReference; lastKnownFileType = archive.macbinary; path = pattern_sample.bin; sourceTree = "<group>"; };
78023CAA25F7767000B083EF /* ESP32Controller.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ESP32Controller.swift; sourceTree = "<group>"; };
78023CAE25F7797400B083EF /* ESP32 */ = {isa = PBXFileReference; lastKnownFileType = folder; path = ESP32; sourceTree = "<group>"; };
78023CB025F7841F00B083EF /* MicrocontrollerTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MicrocontrollerTests.swift; sourceTree = "<group>"; };
78108B6F248E8FB50007E9C4 /* AppDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = "<group>"; };
78108B73248E8FB80007E9C4 /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = "<group>"; };
78108B76248E8FB80007E9C4 /* Preview Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = "Preview Assets.xcassets"; sourceTree = "<group>"; };
@@ -113,6 +122,8 @@
78108B90248F72AF0007E9C4 /* FindMyController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FindMyController.swift; sourceTree = "<group>"; };
781EB40825DAD7EA00FEAA19 /* OpenHaystack.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = OpenHaystack.app; sourceTree = BUILT_PRODUCTS_DIR; };
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>"; };
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>"; };
@@ -135,6 +146,7 @@
7899D1D525DE74EE00115740 /* firmware.bin */ = {isa = PBXFileReference; lastKnownFileType = archive.macbinary; path = firmware.bin; sourceTree = "<group>"; };
7899D1E025DE97E200115740 /* IconSelectionView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = IconSelectionView.swift; sourceTree = "<group>"; };
7899D1E825DEBF4800115740 /* AccessoryMapAnnotation.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AccessoryMapAnnotation.swift; sourceTree = "<group>"; };
78D9B80525F7CF60009B9CE8 /* ManageAccessoriesView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ManageAccessoriesView.swift; sourceTree = "<group>"; };
78EC226125DAE0BE0042B775 /* OpenHaystackTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = OpenHaystackTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; };
78EC226325DAE0BE0042B775 /* OpenHaystackTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OpenHaystackTests.swift; sourceTree = "<group>"; };
78EC226525DAE0BE0042B775 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = "<group>"; };
@@ -180,6 +192,23 @@
path = BoringSSL;
sourceTree = "<group>";
};
78023CAC25F7775300B083EF /* Firmwares */ = {
isa = PBXGroup;
children = (
78023CAE25F7797400B083EF /* ESP32 */,
78023CAD25F7775A00B083EF /* Microbit */,
);
path = Firmwares;
sourceTree = "<group>";
};
78023CAD25F7775A00B083EF /* Microbit */ = {
isa = PBXGroup;
children = (
7899D1D525DE74EE00115740 /* firmware.bin */,
);
path = Microbit;
sourceTree = "<group>";
};
78108B63248E8FB50007E9C4 = {
isa = PBXGroup;
children = (
@@ -291,6 +320,7 @@
78014A2A25DC22110089F6D9 /* sample.bin */,
78EC226325DAE0BE0042B775 /* OpenHaystackTests.swift */,
78EC226525DAE0BE0042B775 /* Info.plist */,
78023CB025F7841F00B083EF /* MicrocontrollerTests.swift */,
);
path = OpenHaystackTests;
sourceTree = "<group>";
@@ -298,13 +328,15 @@
78EC226E25DBC2FC0042B775 /* HaystackApp */ = {
isa = PBXGroup;
children = (
7899D1D525DE74EE00115740 /* firmware.bin */,
78023CAC25F7775300B083EF /* Firmwares */,
78286D3A25E4017400F65511 /* Mail Plugin */,
78EC227025DBC8BB0042B775 /* Views */,
78EC226F25DBC8B60042B775 /* Model */,
78EC227625DBDB7E0042B775 /* KeychainController.swift */,
78014A2725DC01220089F6D9 /* MicrobitController.swift */,
787D8AC025DECD3C00148766 /* AccessoryController.swift */,
78023CAA25F7767000B083EF /* ESP32Controller.swift */,
7821DAD025F7B2C10054DC33 /* FileManager.swift */,
);
path = HaystackApp;
sourceTree = "<group>";
@@ -328,6 +360,8 @@
7899D1E825DEBF4800115740 /* AccessoryMapAnnotation.swift */,
78286E0125E66F9400F65511 /* AccessoryListEntry.swift */,
7851F1DC25EE90FA0049480D /* AccessoryMapView.swift */,
7821DAD225F7C39A0054DC33 /* ESP32InstallSheet.swift */,
78D9B80525F7CF60009B9CE8 /* ManageAccessoriesView.swift */,
);
path = Views;
sourceTree = "<group>";
@@ -447,6 +481,7 @@
isa = PBXResourcesBuildPhase;
buildActionMask = 2147483647;
files = (
78023CAF25F7797400B083EF /* ESP32 in Resources */,
781EB3FD25DAD7EA00FEAA19 /* Main.storyboard in Resources */,
7899D1D625DE74EE00115740 /* firmware.bin in Resources */,
781EB3FE25DAD7EA00FEAA19 /* MapViewController.xib in Resources */,
@@ -583,18 +618,22 @@
781EB3EB25DAD7EA00FEAA19 /* SavePanel.swift in Sources */,
7899D1E125DE97E200115740 /* IconSelectionView.swift in Sources */,
78EC227725DBDB7E0042B775 /* KeychainController.swift in Sources */,
78D9B80625F7CF60009B9CE8 /* ManageAccessoriesView.swift in Sources */,
78486BEF25DD711E0007ED87 /* PopUpAlertView.swift in Sources */,
78014A2925DC08580089F6D9 /* MicrobitController.swift in Sources */,
78286D1F25E3D8B800F65511 /* ALTAnisetteData.m in Sources */,
781EB3EC25DAD7EA00FEAA19 /* DecryptReports.swift in Sources */,
78EC226C25DBC2E40042B775 /* OpenHaystackMainView.swift in Sources */,
78EC227225DBC8CE0042B775 /* Accessory.swift in Sources */,
7821DAD125F7B2C10054DC33 /* FileManager.swift in Sources */,
78286E0225E66F9400F65511 /* AccessoryListEntry.swift in Sources */,
781EB3EF25DAD7EA00FEAA19 /* MapViewController.swift in Sources */,
78286D7725E5114600F65511 /* ActivityIndicator.swift in Sources */,
7821DAD325F7C39A0054DC33 /* ESP32InstallSheet.swift in Sources */,
781EB3F125DAD7EA00FEAA19 /* FindMyKeyDecoder.swift in Sources */,
787D8AC125DECD3C00148766 /* AccessoryController.swift in Sources */,
781EB3F225DAD7EA00FEAA19 /* AppDelegate.swift in Sources */,
78023CAB25F7767000B083EF /* ESP32Controller.swift in Sources */,
781EB3F325DAD7EA00FEAA19 /* Models.swift in Sources */,
781EB3F425DAD7EA00FEAA19 /* FindMyController.swift in Sources */,
781EB3F525DAD7EA00FEAA19 /* BoringSSL.m in Sources */,
@@ -615,6 +654,7 @@
isa = PBXSourcesBuildPhase;
buildActionMask = 2147483647;
files = (
78023CB125F7841F00B083EF /* MicrocontrollerTests.swift in Sources */,
78EC226425DAE0BE0042B775 /* OpenHaystackTests.swift in Sources */,
);
runOnlyForDeploymentPostprocessing = 0;

View File

@@ -97,7 +97,7 @@ public class AnisetteDataManager: NSObject {
"X-Apple-I-MD": data.oneTimePassword,
"X-Apple-I-TimeZone": String(data.timeZone.abbreviation() ?? "UTC"),
"X-Apple-I-Client-Time": ISO8601DateFormatter().string(from: data.date),
"X-Apple-I-MD-RINFO": String(data.routingInfo),
"X-Apple-I-MD-RINFO": String(data.routingInfo)
] as [AnyHashable: Any])
}
}
@@ -110,8 +110,7 @@ extension AnisetteDataManager {
guard let userInfo = notification.userInfo, let requestUUID = userInfo["requestUUID"] as? String else { return }
if let archivedAnisetteData = userInfo["anisetteData"] as? Data,
let appleAccountData = try? NSKeyedUnarchiver.unarchivedObject(ofClass: AppleAccountData.self, from: archivedAnisetteData)
{
let appleAccountData = try? NSKeyedUnarchiver.unarchivedObject(ofClass: AppleAccountData.self, from: archivedAnisetteData) {
if let range = appleAccountData.deviceDescription.lowercased().range(of: "(com.apple.mail") {
var adjustedDescription = appleAccountData.deviceDescription[..<range.lowerBound]
adjustedDescription += "(com.apple.dt.Xcode/3594.4.19)>"
@@ -129,8 +128,7 @@ extension AnisetteDataManager {
guard let userInfo = notification.userInfo, let requestUUID = userInfo["requestUUID"] as? String else { return }
if let archivedAnisetteData = userInfo["anisetteData"] as? Data,
let anisetteData = try? NSKeyedUnarchiver.unarchivedObject(ofClass: ALTAnisetteData.self, from: archivedAnisetteData)
{
let anisetteData = try? NSKeyedUnarchiver.unarchivedObject(ofClass: ALTAnisetteData.self, from: archivedAnisetteData) {
if let range = anisetteData.deviceDescription.lowercased().range(of: "(com.apple.mail") {
var adjustedDescription = anisetteData.deviceDescription[..<range.lowerBound]
adjustedDescription += "(com.apple.dt.Xcode/3594.4.19)>"

View File

@@ -0,0 +1,6 @@
{
"info" : {
"author" : "xcode",
"version" : 1
}
}

View File

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

View File

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

View File

@@ -8,6 +8,7 @@
import Combine
import Foundation
import SwiftUI
import OSLog
class FindMyController: ObservableObject {
static let shared = FindMyController()
@@ -82,6 +83,34 @@ class FindMyController: ObservableObject {
}
}
func fetchReports(for accessories: [Accessory], with token: Data, completion: @escaping (Result<[FindMyDevice], Error>) -> Void) {
let findMyDevices = accessories.compactMap({ acc -> FindMyDevice? in
do {
return try acc.toFindMyDevice()
} catch {
os_log("Failed getting id for key %@", String(describing: error))
return nil
}
})
self.devices = findMyDevices
self.fetchReports(with: token) { error in
let reports = FindMyController.shared.devices.compactMap({ $0.reports }).flatMap({ $0 })
if reports.isEmpty == false {
AccessoryController.shared.updateWithDecryptedReports(devices: FindMyController.shared.devices)
}
if let error = error {
completion(.failure(error))
os_log("Error: %@", String(describing: error))
} else {
completion(.success(self.devices))
}
}
}
func fetchReports(with searchPartyToken: Data, completion: @escaping (Error?) -> Void) {
DispatchQueue.global(qos: .background).async {

View File

@@ -6,14 +6,22 @@
// SPDX-License-Identifier: AGPL-3.0-only
import Foundation
import SwiftUI
import Combine
class AccessoryController: ObservableObject {
static let shared = AccessoryController()
@Published var accessories: [Accessory]
var accessoryObserver: AnyCancellable?
init() {
self.accessories = KeychainController.loadAccessoriesFromKeychain()
self.accessoryObserver = self.accessories.publisher
.sink { _ in
try? self.save()
}
}
init(accessories: [Accessory]) {
@@ -46,4 +54,30 @@ class AccessoryController: ObservableObject {
}
}
}
func delete(accessory: Accessory) throws {
var accessories = self.accessories
guard let idx = accessories.firstIndex(of: accessory) else { return }
accessories.remove(at: idx)
withAnimation {
self.accessories = accessories
}
try self.save()
}
func addAccessory(with name: String, color: Color, icon: String) throws -> Accessory {
let accessory = try Accessory(name: name, color: color, iconName: icon)
let accessories = self.accessories + [accessory]
withAnimation {
self.accessories = accessories
}
try self.save()
return accessory
}
}

View File

@@ -0,0 +1,66 @@
//
// ESP32Controller.swift
// OpenHaystack
//
// Created by Alex - SEEMOO on 09.03.21.
// Copyright © 2021 SEEMOO - TU Darmstadt. All rights reserved.
//
import Foundation
struct ESP32Controller {
static var espFirmwareDirectory: URL? {
Bundle.main.resourceURL?.appendingPathComponent("ESP32")
}
/// Tries to find the port / path at which the ESP32 module is attached
static func findPort() -> [URL] {
// List all ports
let ports = try? FileManager.default.contentsOfDirectory(atPath: "/dev").filter({$0.contains("cu.")})
let portURLs = ports?.map({URL(fileURLWithPath: "/dev/\($0)")})
return portURLs ?? []
}
/// Runs the script to flash the firmware on an ESP32
static func flashToESP32(accessory: Accessory, port: URL, completion: @escaping (Result<Void, Error>) -> Void) throws {
// Copy firmware to a temporary directory
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 espDirectory = espFirmwareDirectory else {return}
try FileManager.default.copyFolder(from: espDirectory, to: urlTemp)
let scriptPath = urlTemp.appendingPathComponent("flash_esp32.sh")
let key = try accessory.getAdvertisementKey().base64EncodedString()
let arguments = ["-p", "\(port.path)", key]
let task = try NSUserUnixTask(url: scriptPath)
task.execute(withArguments: arguments) { e in
DispatchQueue.main.async {
if let error = e {
completion(.failure(error))
} else {
completion(.success(()))
}
// Delete the temporary folder
try? FileManager.default.removeItem(at: urlTemp)
}
}
}
}
enum FirmwareFlashError: Error {
/// Missing files for flashing
case notFound
/// Flashing / writing failed
case flashFailed
}

View File

@@ -0,0 +1,38 @@
//
// FileManager.swift
// OpenHaystack
//
// Created by Alex - SEEMOO on 09.03.21.
// Copyright © 2021 SEEMOO - TU Darmstadt. All rights reserved.
//
import Foundation
extension FileManager {
/// Copy a folder recursively.
///
/// - Parameters:
/// - from: Folder source
/// - to: Folder destination
/// - Throws: An error if copying or acessing files fails
func copyFolder(from: URL, to: URL) throws {
// Create the folder
try? FileManager.default.createDirectory(at: to, withIntermediateDirectories: false, attributes: nil)
let files = try FileManager.default.contentsOfDirectory(atPath: from.path)
for file in files {
// Check if file is a folder
var isDir: ObjCBool = .init(booleanLiteral: false)
let fileURL = from.appendingPathComponent(file)
FileManager.default.fileExists(atPath: fileURL.path, isDirectory: &isDir)
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))
}
}
}
}

View File

@@ -0,0 +1 @@
(directory will be populated in CI release workflow)

View File

@@ -0,0 +1,139 @@
#!/bin/bash
# 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"
# Defaults: Serial port to access the ESP32
PORT=/dev/ttyS0
# Defaults: Fast baud rate
BAUDRATE=921600
# Parameter parsing
while [[ $# -gt 0 ]]; do
KEY="$1"
case "$KEY" in
-p|--port)
PORT="$2"
shift
shift
;;
-s|--slow)
BAUDRATE=115200
shift
;;
-v|--venvdir)
VENV_DIR="$2"
shift
shift
;;
-h|--help)
echo "flash_esp32.sh - Flash the OpenHaystack firmware onto an ESP32 module"
echo ""
echo " This script will create a virtual environment for the required tools."
echo ""
echo "Call: flash_esp32.sh [-p <port>] [-v <dir>] [-s] PUBKEY"
echo ""
echo "Required Arguments:"
echo " PUBKEY"
echo " The base64-encoded public key"
echo ""
echo "Optional Arguments:"
echo " -h, --help"
echo " Show this message and exit."
echo " -p, --port <port>"
echo " Specify the serial interface to which the device is connected."
echo " -s, --slow"
echo " Use 115200 instead of 921600 baud when flashing."
echo " Might be required for long/bad USB cables or slow USB-to-Serial converters."
echo " -v, --venvdir <dir>"
echo " Select Python virtual environment with esptool installed."
echo " If the directory does not exist, it will be created."
exit 1
;;
*)
if [[ -z "$PUBKEY" ]]; then
PUBKEY="$1"
shift
else
echo "Got unexpected parameter $1"
exit 1
fi
;;
esac
done
# Sanity check: Pubkey exists
if [[ -z "$PUBKEY" ]]; then
echo "Missing public key, call with --help for usage"
exit 1
fi
# Sanity check: Port
if [[ ! -e "$PORT" ]]; then
echo "$PORT does not exist, please specify a valid serial interface with the -p argument"
exit 1
fi
# Setup the virtual environment
if [[ ! -d "$VENV_DIR" ]]; then
# Create the virtual environment
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
source "$VENV_DIR/bin/activate"
pip install --upgrade pip
pip install esptool
if [[ $? != 0 ]]; then
echo "Could not install Python 3 module esptool in $VENV_DIR";
exit 1
fi
else
source "$VENV_DIR/bin/activate"
fi
# Prepare the key
KEYFILE="$SCRIPT_DIR/tmp.key"
if [[ -f "$KEYFILE" ]]; then
echo "$KEYFILE already exists, stopping here not to override files..."
exit 1
fi
echo "$PUBKEY" | python3 -m base64 -d - > "$KEYFILE"
if [[ $? != 0 ]]; then
echo "Could not parse the public key. Please provide valid base64 input"
exit 1
fi
# Call esptool.py. Errors from here on are critical
set -e
# Clear NVM
esptool.py --after no_reset \
erase_region 0x9000 0x5000
esptool.py --before no_reset --baud $BAUDRATE \
write_flash 0x1000 "$SCRIPT_DIR/build/bootloader/bootloader.bin" \
0x8000 "$SCRIPT_DIR/build/partition_table/partition-table.bin" \
0xe000 "$KEYFILE" \
0x10000 "$SCRIPT_DIR/build/openhaystack.bin"
rm "$KEYFILE"

View File

@@ -17,7 +17,7 @@ struct KeychainController {
kSecAttrLabel: "FindMyAccessories",
kSecAttrService: "SEEMOO-FINDMY",
kSecMatchLimit: kSecMatchLimitOne,
kSecReturnData: true,
kSecReturnData: true
]
if test {
@@ -49,7 +49,7 @@ struct KeychainController {
kSecClass: kSecClassGenericPassword,
kSecAttrLabel: "FindMyAccessories",
kSecAttrService: "SEEMOO-FINDMY",
kSecValueData: try PropertyListEncoder().encode(accessories),
kSecValueData: try PropertyListEncoder().encode(accessories)
]
if test {
@@ -63,7 +63,7 @@ struct KeychainController {
var query: [CFString: Any] = [
kSecClass: kSecClassGenericPassword,
kSecAttrLabel: "FindMyAccessories",
kSecAttrService: "SEEMOO-FINDMY",
kSecAttrService: "SEEMOO-FINDMY"
]
if test {

View File

@@ -63,7 +63,7 @@ struct MailPluginManager {
} catch {
print(error.localizedDescription)
}
try self.copyFolder(from: localPluginURL, to: pluginURL)
try FileManager.default.copyFolder(from: localPluginURL, to: pluginURL)
self.openAppleMail()
}
@@ -73,32 +73,6 @@ struct MailPluginManager {
}
/// Copy a folder recursively.
///
/// - Parameters:
/// - from: Folder source
/// - to: Folder destination
/// - Throws: An error if copying or acessing files fails
func copyFolder(from: URL, to: URL) throws {
// Create the folder
try? FileManager.default.createDirectory(at: to, withIntermediateDirectories: false, attributes: nil)
let files = try FileManager.default.contentsOfDirectory(atPath: from.path)
for file in files {
// Check if file is a folder
var isDir: ObjCBool = .init(booleanLiteral: false)
let fileURL = from.appendingPathComponent(file)
FileManager.default.fileExists(atPath: fileURL.path, isDirectory: &isDir)
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))
}
}
}
func uninstallMailPlugin() throws {
try FileManager.default.removeItem(at: pluginURL)
}
@@ -115,7 +89,7 @@ struct MailPluginManager {
let downloadsPluginURL = downloadsFolder.appendingPathComponent(mailBundleName + ".mailbundle")
try self.copyFolder(from: localPluginURL, to: downloadsPluginURL)
try FileManager.default.copyFolder(from: localPluginURL, to: downloadsPluginURL)
}
}

View File

@@ -72,6 +72,22 @@ struct MicrobitController {
return patchedFirmware
}
static func deploy(accessory: Accessory) throws {
let microbits = try MicrobitController.findMicrobits()
guard let microBitURL = microbits.first,
let firmwareURL = Bundle.main.url(forResource: "firmware", withExtension: "bin")
else {
throw FirmwareFlashError.notFound
}
let firmware = try Data(contentsOf: firmwareURL)
let pattern = "OFFLINEFINDINGPUBLICKEYHERE!".data(using: .ascii)!
let publicKey = try accessory.getAdvertisementKey()
let patchedFirmware = try MicrobitController.patchFirmware(firmware, pattern: pattern, with: publicKey)
try MicrobitController.deployToMicrobit(microBitURL, firmwareFile: patchedFirmware)
}
}
enum PatchingError: Error {

View File

@@ -41,8 +41,7 @@ class Accessory: ObservableObject, Codable, Identifiable, Equatable {
if var colorComponents = try? container.decode([CGFloat].self, forKey: .colorComponents),
let spaceName = try? container.decode(String.self, forKey: .colorSpaceName),
let cgColor = CGColor(colorSpace: CGColorSpace(name: spaceName as CFString)!, components: &colorComponents)
{
let cgColor = CGColor(colorSpace: CGColorSpace(name: spaceName as CFString)!, components: &colorComponents) {
self.color = Color(cgColor)
} else {
self.color = Color.white
@@ -58,8 +57,7 @@ class Accessory: ObservableObject, Codable, Identifiable, Equatable {
try container.encode(self.icon, forKey: .icon)
if let colorComponents = self.color.cgColor?.components,
let colorSpace = self.color.cgColor?.colorSpace?.name
{
let colorSpace = self.color.cgColor?.colorSpace?.name {
try container.encode(colorComponents, forKey: .colorComponents)
try container.encode(colorSpace as String, forKey: .colorSpaceName)
}

View File

@@ -26,6 +26,5 @@ struct AccessoryMapView: NSViewControllerRepresentable {
nsViewController.addLastLocations(from: accessories)
nsViewController.changeMapType(mapType)
}
}

View File

@@ -0,0 +1,134 @@
//
// ESP32InstallSheet.swift
// OpenHaystack
//
// Created by Alex - SEEMOO on 09.03.21.
// Copyright © 2021 SEEMOO - TU Darmstadt. All rights reserved.
//
import SwiftUI
import OSLog
struct ESP32InstallSheet: View {
@Binding var accessory: Accessory?
@Binding var alertType: OpenHaystackMainView.AlertType?
@State var detectedPorts: [URL] = []
@State var isFlashing = false
@Environment(\.presentationMode) var presentationMode
var body: some View {
VStack {
self.portSelectionView
.padding()
.overlay(self.loadingOverlay)
.frame(minWidth: 640, minHeight: 480, alignment: .center)
}
.onAppear {
self.detectedPorts = ESP32Controller.findPort()
}
}
var portSelectionView: some View {
VStack {
Text("Flash your ESP32")
.font(.title2)
Text("Select the serial port that belongs to your ESP32 module")
.foregroundColor(.gray)
self.portList
Spacer()
HStack {
Spacer()
Button("Reload ports", action: {
self.detectedPorts = ESP32Controller.findPort()
})
Button("Cancel", action: {
self.presentationMode.wrappedValue.dismiss()
})
}
}
}
var portList: some View {
ScrollView {
VStack(spacing: 4) {
ForEach(0..<self.detectedPorts.count, id: \.self) { portIdx in
Button(action: {
if let accessory = self.accessory {
// Flash selected module
self.deployAccessoryToESP32(accessory: accessory, to: self.detectedPorts[portIdx])
}
}, label: {
HStack {
Text(self.detectedPorts[portIdx].path)
.padding(4)
Spacer()
}
.contentShape(Rectangle())
})
.buttonStyle(PlainButtonStyle())
}
}
}
}
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 deployAccessoryToESP32(accessory: Accessory, to port: URL) {
do {
self.isFlashing = true
try ESP32Controller.flashToESP32(accessory: accessory, port: port, completion: { result in
presentationMode.wrappedValue.dismiss()
self.isFlashing = false
switch result {
case .success(_):
self.alertType = .deployedSuccessfully
case .failure(let error):
os_log(.error, "Flashing to ESP32 failed %@", String(describing: error))
self.presentationMode.wrappedValue.dismiss()
self.alertType = .deployFailed
}
})
} catch {
os_log(.error, "Execution of script failed %@", String(describing: error))
self.presentationMode.wrappedValue.dismiss()
self.alertType = .deployFailed
self.isFlashing = false
}
self.accessory = nil
}
}
struct ESP32InstallSheet_Previews: PreviewProvider {
@State static var acc: Accessory? = try! Accessory(name: "Sample")
@State static var alert: OpenHaystackMainView.AlertType?
static var previews: some View {
ESP32InstallSheet(accessory: $acc, alertType: $alert)
}
}

View File

@@ -0,0 +1,133 @@
//
// ManageAccessoriesView.swift
// OpenHaystack
//
// Created by Alex - SEEMOO on 09.03.21.
// Copyright © 2021 SEEMOO - TU Darmstadt. All rights reserved.
//
import SwiftUI
struct ManageAccessoriesView: View {
@ObservedObject var accessoryController = AccessoryController.shared
var accessories: [Accessory] {
return self.accessoryController.accessories
}
// MARK: Bindings from main View
@Binding var alertType: OpenHaystackMainView.AlertType?
@Binding var focusedAccessory: Accessory?
@Binding var accessoryToDeploy: Accessory?
@Binding var showESP32DeploySheet: Bool
// MARK: View State
@State var keyName: String = ""
@State var accessoryColor: Color = Color.white
@State var selectedIcon: String = "briefcase.fill"
var body: some View {
VStack {
Text("Create a new tracking accessory")
.font(.title2)
.padding(.top)
Text("A BBC Microbit can be used to track anything you care about. Connect it over USB, name the accessory (e.g. Backpack) generate the key and deploy it")
.multilineTextAlignment(.center)
.font(.caption)
.foregroundColor(.gray)
HStack {
TextField("Name", text: self.$keyName)
ColorPicker("", selection: self.$accessoryColor)
.frame(maxWidth: 50, maxHeight: 20)
IconSelectionView(selectedImageName: self.$selectedIcon)
}
Button(
action: self.addAccessory,
label: {
Text("Generate key and deploy")
}
)
.disabled(self.keyName.isEmpty)
.padding(.bottom)
Divider()
Text("Your accessories")
.font(.title2)
.padding(.top)
if self.accessories.isEmpty {
Spacer()
Text("No accessories have been added yet. Go ahead and add one above")
.multilineTextAlignment(.center)
} else {
self.accessoryList
}
Spacer()
}
.sheet(isPresented: self.$showESP32DeploySheet, content: {
ESP32InstallSheet(accessory: self.$accessoryToDeploy, alertType: self.$alertType)
})
}
/// Accessory List view.
var accessoryList: some View {
List(self.accessories) { accessory in
AccessoryListEntry(
accessory: accessory,
alertType: self.$alertType,
delete: self.delete(accessory:),
deployAccessoryToMicrobit: self.deploy(accessory:),
zoomOn: { self.focusedAccessory = $0 })
}
.background(Color.clear)
.cornerRadius(15.0)
}
/// Delete an accessory from the list of accessories.
func delete(accessory: Accessory) {
do {
try self.accessoryController.delete(accessory: accessory)
} catch {
self.alertType = .deletionFailed
}
}
func deploy(accessory: Accessory) {
self.accessoryToDeploy = accessory
self.alertType = .selectDepoyTarget
}
/// Add an accessory with the provided details.
func addAccessory() {
let keyName = self.keyName
self.keyName = ""
do {
let accessory = try self.accessoryController.addAccessory(with: keyName, color: self.accessoryColor, icon: self.selectedIcon)
self.deploy(accessory: accessory)
} catch {
self.alertType = .keyError
}
}
}
struct ManageAccessoriesView_Previews: PreviewProvider {
@State static var accessories = PreviewData.accessories
@State static var alertType: OpenHaystackMainView.AlertType?
@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)
}
}

View File

@@ -11,10 +11,6 @@ import SwiftUI
struct OpenHaystackMainView: View {
@State var keyName: String = ""
@State var accessoryColor: Color = Color.white
@State var selectedIcon: String = "briefcase.fill"
@State var loading = false
@ObservedObject var accessoryController = AccessoryController.shared
var accessories: [Accessory] {
@@ -30,14 +26,20 @@ struct OpenHaystackMainView: View {
@State var mapType: MKMapType = .standard
@State var isLoading = false
@State var focusedAccessory: Accessory?
@State var accessoryToDeploy: Accessory?
@State var showESP32DeploySheet = false
var body: some View {
GeometryReader { geo in
ZStack {
VStack {
HStack {
self.accessoryView
.frame(width: geo.size.width * 0.5)
ManageAccessoriesView(
alertType: self.$alertType,
focusedAccessory: self.$focusedAccessory,
accessoryToDeploy: self.$accessoryToDeploy,
showESP32DeploySheet: self.$showESP32DeploySheet)
Spacer()
@@ -92,67 +94,6 @@ struct OpenHaystackMainView: View {
// MARK: Subviews
/// Left side of the view. Shows a list of accessories and the possibility to add accessories
var accessoryView: some View {
VStack {
Text("Create a new tracking accessory")
.font(.title2)
.padding(.top)
Text("A BBC Microbit can be used to track anything you care about. Connect it over USB, name the accessory (e.g. Backpack) generate the key and deploy it")
.multilineTextAlignment(.center)
.font(.caption)
.foregroundColor(.gray)
HStack {
TextField("Name", text: self.$keyName)
ColorPicker("", selection: self.$accessoryColor)
.frame(maxWidth: 50, maxHeight: 20)
IconSelectionView(selectedImageName: self.$selectedIcon)
}
Button(
action: self.addAccessory,
label: {
Text("Generate key and deploy")
}
)
.disabled(self.keyName.isEmpty)
.padding(.bottom)
Divider()
Text("Your accessories")
.font(.title2)
.padding(.top)
if self.accessories.isEmpty {
Spacer()
Text("No accessories have been added yet. Go ahead and add one above")
.multilineTextAlignment(.center)
} else {
self.accessoryList
}
Spacer()
}
}
/// Accessory List view.
var accessoryList: some View {
List(self.accessories) { accessory in
AccessoryListEntry(
accessory: accessory,
alertType: self.$alertType,
delete: self.delete(accessory:),
deployAccessoryToMicrobit: self.deployAccessoryToMicrobit(accessory:),
zoomOn: { self.focusedAccessory = $0 })
}
.background(Color.clear)
.cornerRadius(15.0)
}
/// Overlay for the map that is gray and shows an activity indicator when loading.
var mapOverlay: some View {
ZStack {
@@ -202,28 +143,25 @@ struct OpenHaystackMainView: View {
}
}
/// Add an accessory with the provided details.
func addAccessory() {
let keyName = self.keyName
self.keyName = ""
func onAppear() {
do {
let accessory = try Accessory(name: keyName, color: self.accessoryColor, iconName: self.selectedIcon)
let accessories = self.accessories + [accessory]
withAnimation {
self.accessoryController.accessories = accessories
}
try self.accessoryController.save()
self.deployAccessoryToMicrobit(accessory: accessory)
} catch {
self.errorDescription = String(describing: error)
self.showKeyError = true
/// Checks if the search party token can be fetched without the Mail Plugin. If true the plugin is not needed for this environment. (e.g. when SIP is disabled)
let reportsFetcher = ReportsFetcher()
if let token = reportsFetcher.fetchSearchpartyToken(),
let tokenString = String(data: token, encoding: .ascii) {
self.searchPartyToken = tokenString
return
}
let pluginManager = MailPluginManager()
// Check if the plugin is installed
if pluginManager.isMailPluginInstalled == false {
// Install the mail plugin
self.alertType = .activatePlugin
} else {
self.checkPluginIsRunning(nil)
}
}
/// Download the location reports for all current accessories. Shows an error if something fails, like plug-in is missing
@@ -246,75 +184,35 @@ struct OpenHaystackMainView: View {
self.isLoading = true
}
let findMyDevices = self.accessories.compactMap({ acc -> FindMyDevice? in
do {
return try acc.toFindMyDevice()
} catch {
os_log("Failed getting id for key %@", String(describing: error))
return nil
}
})
FindMyController.shared.devices = findMyDevices
FindMyController.shared.fetchReports(with: tokenData) { error in
let reports = FindMyController.shared.devices.compactMap({ $0.reports }).flatMap({ $0 })
if reports.isEmpty {
withAnimation {
self.popUpAlertType = .noReportsFound
FindMyController.shared.fetchReports(for: accessories, with: tokenData) { result in
switch result {
case .failure(let error):
os_log(.error, "Downloading reports failed %@", error.localizedDescription)
case .success(let devices):
let reports = devices.compactMap({ $0.reports }).flatMap({ $0 })
if reports.isEmpty {
withAnimation {
self.popUpAlertType = .noReportsFound
}
}
} else {
self.accessoryController.updateWithDecryptedReports(devices: FindMyController.shared.devices)
}
withAnimation {
self.isLoading = false
}
guard error != nil else { return }
os_log("Error: %@", String(describing: error))
}
}
}
/// Delete an accessory from the list of accessories.
func delete(accessory: Accessory) {
do {
var accessories = self.accessories
guard let idx = accessories.firstIndex(of: accessory) else { return }
accessories.remove(at: idx)
withAnimation {
self.accessoryController.accessories = accessories
}
try self.accessoryController.save()
} catch {
self.alertType = .deletionFailed
}
func deploy(accessory: Accessory) {
self.accessoryToDeploy = accessory
self.alertType = .selectDepoyTarget
}
/// Deploy the public key of the accessory to a BBC microbit.
func deployAccessoryToMicrobit(accessory: Accessory) {
do {
let microbits = try MicrobitController.findMicrobits()
guard let microBitURL = microbits.first,
let firmwareURL = Bundle.main.url(forResource: "firmware", withExtension: "bin")
else {
self.alertType = .deployFailed
return
}
let firmware = try Data(contentsOf: firmwareURL)
let pattern = "OFFLINEFINDINGPUBLICKEYHERE!".data(using: .ascii)!
let publicKey = try accessory.getAdvertisementKey()
let patchedFirmware = try MicrobitController.patchFirmware(firmware, pattern: pattern, with: publicKey)
try MicrobitController.deployToMicrobit(microBitURL, firmwareFile: patchedFirmware)
try MicrobitController.deploy(accessory: accessory)
} catch {
os_log("Error occurred %@", String(describing: error))
self.alertType = .deployFailed
@@ -322,28 +220,8 @@ struct OpenHaystackMainView: View {
}
self.alertType = .deployedSuccessfully
}
func onAppear() {
/// Checks if the search party token can be fetched without the Mail Plugin. If true the plugin is not needed for this environment. (e.g. when SIP is disabled)
let reportsFetcher = ReportsFetcher()
if let token = reportsFetcher.fetchSearchpartyToken(),
let tokenString = String(data: token, encoding: .ascii)
{
self.searchPartyToken = tokenString
return
}
let pluginManager = MailPluginManager()
// Check if the plugin is installed
if pluginManager.isMailPluginInstalled == false {
// Install the mail plugin
self.alertType = .activatePlugin
} else {
self.checkPluginIsRunning(nil)
}
self.accessoryToDeploy = nil
}
/// Ask to install and activate the mail plugin.
@@ -402,6 +280,7 @@ struct OpenHaystackMainView: View {
// MARK: - Alerts
// swiftlint:disable function_body_length
/// Create an alert for the given alert type.
///
/// - Parameter alertType: current alert type
@@ -465,6 +344,17 @@ struct OpenHaystackMainView: View {
action: {
self.downloadPlugin()
}), secondaryButton: .cancel())
case .selectDepoyTarget:
let microbitButton = Alert.Button.default(Text("Microbit"), action: {self.deployAccessoryToMicrobit(accessory: self.accessoryToDeploy!)})
let esp32Button = Alert.Button.default(Text("ESP32"), action: {
self.showESP32DeploySheet = true
})
return Alert(title: Text("Select target"),
message: Text("Please select to which device you want to deploy"),
primaryButton: microbitButton,
secondaryButton: esp32Button)
}
}
@@ -481,6 +371,7 @@ struct OpenHaystackMainView: View {
case noReportsFound
case activatePlugin
case pluginInstallFailed
case selectDepoyTarget
}
}
@@ -491,7 +382,7 @@ struct OpenHaystackMainView_Previews: PreviewProvider {
static var previews: some View {
OpenHaystackMainView(accessoryController: AccessoryController(accessories: accessories))
.frame(width: 640, height: 480, alignment: .center)
.frame(width: 800, height: 600, alignment: .center)
}
}

View File

@@ -0,0 +1,104 @@
//
// MicrocontrollerTests.swift
// OpenHaystackTests
//
// Created by Alex - SEEMOO on 09.03.21.
// Copyright © 2021 SEEMOO - TU Darmstadt. All rights reserved.
//
import XCTest
@testable import OpenHaystack
class MicrocontrollerTests: XCTestCase {
override func setUpWithError() throws {
// Put setup code here. This method is called before the invocation of each test method in the class.
}
override func tearDownWithError() throws {
// Put teardown code here. This method is called after the invocation of each test method in the class.
}
func testMicrobitDeploy() throws {
let urls = try MicrobitController.findMicrobits()
if let mBitURL = urls.first {
let firmware = try Data(contentsOf: Bundle(for: Self.self).url(forResource: "sample", withExtension: "bin")!)
try MicrobitController.deployToMicrobit(mBitURL, firmwareFile: firmware)
}
}
func testBinaryPatching() throws {
// Patching sample.bin should fail
do {
let firmware = try Data(contentsOf: Bundle(for: Self.self).url(forResource: "sample", withExtension: "bin")!)
let pattern = Data([0xa, 0xb, 0xc, 0xd, 0xe, 0xf, 0x0, 0x1])
let key = Data([1, 1, 1, 1, 1, 1, 1, 1])
_ = try MicrobitController.patchFirmware(firmware, pattern: pattern, with: key)
XCTFail("Should thrown an erorr before")
} catch PatchingError.patternNotFound {
// This should be thrown
} catch {
XCTFail("Unexpected error")
}
// Patching the sample should be successful
do {
let firmware = try Data(contentsOf: Bundle(for: Self.self).url(forResource: "pattern_sample", withExtension: "bin")!)
let pattern = Data([0xaa, 0xaa, 0xaa, 0xaa, 0xbb, 0xbb, 0xbb, 0xcc])
let key = Data([1, 1, 1, 1, 1, 1, 1, 1])
_ = try MicrobitController.patchFirmware(firmware, pattern: pattern, with: key)
} catch {
XCTFail("Unexpected error \(String(describing: error))")
}
// Patching key too short
// Patching the sample should be successful
do {
let firmware = try Data(contentsOf: Bundle(for: Self.self).url(forResource: "pattern_sample", withExtension: "bin")!)
let pattern = Data([0xaa, 0xaa, 0xaa, 0xaa, 0xbb, 0xbb, 0xbb, 0xcc])
let key = Data([1, 1, 1, 1, 1, 1, 1])
_ = try MicrobitController.patchFirmware(firmware, pattern: pattern, with: key)
} catch PatchingError.inequalLength {
} catch {
XCTFail("Unexpected error \(String(describing: error))")
}
// Testing with the actual firmware
do {
let firmware = try Data(contentsOf: Bundle(for: Self.self).url(forResource: "offline-finding", withExtension: "bin")!)
let pattern = "OFFLINEFINDINGPUBLICKEYHERE!".data(using: .ascii)!
let key = Data(repeating: 0xaa, count: 28)
_ = try MicrobitController.patchFirmware(firmware, pattern: pattern, with: key)
} catch PatchingError.inequalLength {
} catch {
XCTFail("Unexpected error \(String(describing: error))")
}
}
func testFindESP32Port() {
let port = ESP32Controller.findPort()
XCTAssertNotNil(port)
}
func testESP32Deploy() throws {
let accessory = try Accessory(name: "Sample")
let expect = expectation(description: "ESP32 Flash")
let port = ESP32Controller.findPort().first(where: {$0.absoluteString.contains("usb")})!
try ESP32Controller.flashToESP32(accessory: accessory, port: port) { result in
expect.fulfill()
switch result {
case .success(_):
break
case .failure(let error):
XCTFail(error.localizedDescription)
}
}
wait(for: [expect], timeout: 60.0)
}
}

View File

@@ -90,67 +90,6 @@ class OpenHaystackTests: XCTestCase {
try KeychainController.storeInKeychain(accessories: [], test: true)
}
func testMicrobitDeploy() throws {
let urls = try MicrobitController.findMicrobits()
if let mBitURL = urls.first {
let firmware = try Data(contentsOf: Bundle(for: Self.self).url(forResource: "sample", withExtension: "bin")!)
try MicrobitController.deployToMicrobit(mBitURL, firmwareFile: firmware)
}
}
func testBinaryPatching() throws {
// Patching sample.bin should fail
do {
let firmware = try Data(contentsOf: Bundle(for: Self.self).url(forResource: "sample", withExtension: "bin")!)
let pattern = Data([0xa, 0xb, 0xc, 0xd, 0xe, 0xf, 0x0, 0x1])
let key = Data([1, 1, 1, 1, 1, 1, 1, 1])
_ = try MicrobitController.patchFirmware(firmware, pattern: pattern, with: key)
XCTFail("Should thrown an erorr before")
} catch PatchingError.patternNotFound {
// This should be thrown
} catch {
XCTFail("Unexpected error")
}
// Patching the sample should be successful
do {
let firmware = try Data(contentsOf: Bundle(for: Self.self).url(forResource: "pattern_sample", withExtension: "bin")!)
let pattern = Data([0xaa, 0xaa, 0xaa, 0xaa, 0xbb, 0xbb, 0xbb, 0xcc])
let key = Data([1, 1, 1, 1, 1, 1, 1, 1])
_ = try MicrobitController.patchFirmware(firmware, pattern: pattern, with: key)
} catch {
XCTFail("Unexpected error \(String(describing: error))")
}
// Patching key too short
// Patching the sample should be successful
do {
let firmware = try Data(contentsOf: Bundle(for: Self.self).url(forResource: "pattern_sample", withExtension: "bin")!)
let pattern = Data([0xaa, 0xaa, 0xaa, 0xaa, 0xbb, 0xbb, 0xbb, 0xcc])
let key = Data([1, 1, 1, 1, 1, 1, 1])
_ = try MicrobitController.patchFirmware(firmware, pattern: pattern, with: key)
} catch PatchingError.inequalLength {
} catch {
XCTFail("Unexpected error \(String(describing: error))")
}
// Testing with the actual firmware
do {
let firmware = try Data(contentsOf: Bundle(for: Self.self).url(forResource: "offline-finding", withExtension: "bin")!)
let pattern = "OFFLINEFINDINGPUBLICKEYHERE!".data(using: .ascii)!
let key = Data(repeating: 0xaa, count: 28)
_ = try MicrobitController.patchFirmware(firmware, pattern: pattern, with: key)
} catch PatchingError.inequalLength {
} catch {
XCTFail("Unexpected error \(String(describing: error))")
}
}
func testKeyIDGeneration() throws {
// Import keys with their respective id from a plist
let plist = try Data(contentsOf: Bundle(for: Self.self).url(forResource: "sampleKeys", withExtension: "plist")!)