├── version ├── endpoint ├── anisette-data │ └── .gitkeep ├── requirements.txt ├── data │ ├── config.ini │ └── rename_me.pem ├── updateRepo ├── Dockerfile ├── Dockerfile_dev ├── mh_config.py └── register │ └── apple_cryptography.py ├── macless_haystack ├── linux │ ├── .gitignore │ ├── main.cc │ ├── flutter │ │ ├── generated_plugin_registrant.h │ │ ├── generated_plugins.cmake │ │ ├── generated_plugin_registrant.cc │ │ └── CMakeLists.txt │ ├── my_application.h │ ├── my_application.cc │ └── CMakeLists.txt ├── web │ ├── favicon.png │ ├── icons │ │ ├── Icon-192.png │ │ ├── Icon-512.png │ │ ├── Icon-maskable-192.png │ │ └── Icon-maskable-512.png │ ├── manifest.json │ └── index.html ├── android │ ├── app │ │ ├── debug.store │ │ ├── src │ │ │ ├── main │ │ │ │ ├── res │ │ │ │ │ ├── mipmap-hdpi │ │ │ │ │ │ └── ic_launcher.png │ │ │ │ │ ├── mipmap-mdpi │ │ │ │ │ │ └── ic_launcher.png │ │ │ │ │ ├── mipmap-xhdpi │ │ │ │ │ │ └── ic_launcher.png │ │ │ │ │ ├── mipmap-xxhdpi │ │ │ │ │ │ └── ic_launcher.png │ │ │ │ │ ├── mipmap-xxxhdpi │ │ │ │ │ │ └── ic_launcher.png │ │ │ │ │ ├── drawable │ │ │ │ │ │ └── launch_background.xml │ │ │ │ │ ├── drawable-v21 │ │ │ │ │ │ └── launch_background.xml │ │ │ │ │ ├── values │ │ │ │ │ │ └── styles.xml │ │ │ │ │ └── values-night │ │ │ │ │ │ └── styles.xml │ │ │ │ ├── kotlin │ │ │ │ │ └── de │ │ │ │ │ │ └── dchristl │ │ │ │ │ │ └── headlesshaystack │ │ │ │ │ │ └── MainActivity.kt │ │ │ │ └── AndroidManifest.xml │ │ │ ├── debug │ │ │ │ └── AndroidManifest.xml │ │ │ └── profile │ │ │ │ └── AndroidManifest.xml │ │ └── build.gradle │ ├── gradle.properties │ ├── gradle │ │ └── wrapper │ │ │ └── gradle-wrapper.properties │ ├── .gitignore │ ├── build.gradle │ └── settings.gradle ├── assets │ └── OpenHaystackIcon.png ├── lib │ ├── callbacks.dart │ ├── accessory │ │ ├── accessory_battery.dart │ │ ├── accessory_list_item_placeholder.dart │ │ ├── no_accessories.dart │ │ ├── accessory_icon.dart │ │ ├── accessory_icon_model.dart │ │ ├── accessory_color_selector.dart │ │ ├── accessory_icon_selector.dart │ │ ├── accessory_dto.dart │ │ └── accessory_list_item.dart │ ├── item_management │ │ ├── loading_spinner.dart │ │ ├── refresh_action.dart │ │ ├── accessory_id_input.dart │ │ ├── accessory_name_input.dart │ │ ├── accessory_color_input.dart │ │ ├── accessory_pk_input.dart │ │ ├── accessory_icon_input.dart │ │ ├── item_management.dart │ │ ├── new_item_action.dart │ │ ├── item_import.dart │ │ └── item_creation.dart │ ├── placeholder │ │ ├── avatar_placeholder.dart │ │ └── text_placeholder.dart │ ├── deployment │ │ ├── hyperlink.dart │ │ ├── code_block.dart │ │ ├── deployment_linux_hci.dart │ │ ├── deployment_nrf51.dart │ │ ├── deployment_esp32.dart │ │ ├── deployment_details.dart │ │ └── deployment_email.dart │ ├── splashscreen.dart │ ├── history │ │ ├── days_selection_slider.dart │ │ └── location_popup.dart │ ├── dashboard │ │ ├── accessory_map_list_vert.dart │ │ └── dashboard.dart │ ├── preferences │ │ ├── user_preferences_model.dart │ │ └── preferences_page.dart │ ├── main.dart │ ├── findMy │ │ ├── reports_fetcher.dart │ │ ├── models.dart │ │ └── decrypt_reports.dart │ └── location │ │ └── location_model.dart ├── .gitignore ├── .metadata ├── analysis_options.yaml ├── README.md └── pubspec.yaml ├── firmware ├── ESP32 │ ├── .gitignore │ ├── src │ │ └── CMakeLists.txt │ ├── CMakeLists.txt │ ├── with_key.csv │ ├── Makefile │ ├── platformio.ini │ └── README.md └── nrf5x │ ├── pinout_beacon_without_case.jpg │ ├── pinout_waterproof_beacon_with_case.jpg │ ├── openocd.cfg │ ├── openhaystack.h │ ├── ble_stack.h │ ├── battery.h │ ├── simple_board.h │ ├── e104bt5032a_board.h │ ├── README.md │ ├── openhaystack.c │ ├── ble_stack.c │ ├── aliexpress_board.h │ ├── aliexpress_board_no_xtal.h │ ├── main.c │ ├── nrf51_battery.h │ └── Makefile ├── images ├── firefox_cert.png ├── history_web.png ├── accessories_web.png ├── dashboard_web.png ├── history_mobile.png ├── settings_mobile.png ├── dashboard_mobile.png ├── history_mobile_2.png ├── history_web_light.png └── accessories_mobile.png ├── .gitmodules ├── .gitignore └── generate_keys.py /version: -------------------------------------------------------------------------------- 1 | 2.7.1 2 | -------------------------------------------------------------------------------- /endpoint/anisette-data/.gitkeep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /macless_haystack/linux/.gitignore: -------------------------------------------------------------------------------- 1 | flutter/ephemeral 2 | -------------------------------------------------------------------------------- /firmware/ESP32/.gitignore: -------------------------------------------------------------------------------- 1 | build/** 2 | venv/** 3 | .vscode/** 4 | **/*.code-workspace -------------------------------------------------------------------------------- /endpoint/requirements.txt: -------------------------------------------------------------------------------- 1 | requests 2 | cryptography 3 | pbkdf2 4 | srp 5 | pycryptodome 6 | configparser -------------------------------------------------------------------------------- /images/firefox_cert.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dchristl/macless-haystack/HEAD/images/firefox_cert.png -------------------------------------------------------------------------------- /images/history_web.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dchristl/macless-haystack/HEAD/images/history_web.png -------------------------------------------------------------------------------- /images/accessories_web.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dchristl/macless-haystack/HEAD/images/accessories_web.png -------------------------------------------------------------------------------- /images/dashboard_web.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dchristl/macless-haystack/HEAD/images/dashboard_web.png -------------------------------------------------------------------------------- /images/history_mobile.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dchristl/macless-haystack/HEAD/images/history_mobile.png -------------------------------------------------------------------------------- /images/settings_mobile.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dchristl/macless-haystack/HEAD/images/settings_mobile.png -------------------------------------------------------------------------------- /images/dashboard_mobile.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dchristl/macless-haystack/HEAD/images/dashboard_mobile.png -------------------------------------------------------------------------------- /images/history_mobile_2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dchristl/macless-haystack/HEAD/images/history_mobile_2.png -------------------------------------------------------------------------------- /images/history_web_light.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dchristl/macless-haystack/HEAD/images/history_web_light.png -------------------------------------------------------------------------------- /images/accessories_mobile.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dchristl/macless-haystack/HEAD/images/accessories_mobile.png -------------------------------------------------------------------------------- /macless_haystack/web/favicon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dchristl/macless-haystack/HEAD/macless_haystack/web/favicon.png -------------------------------------------------------------------------------- /macless_haystack/android/app/debug.store: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dchristl/macless-haystack/HEAD/macless_haystack/android/app/debug.store -------------------------------------------------------------------------------- /macless_haystack/web/icons/Icon-192.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dchristl/macless-haystack/HEAD/macless_haystack/web/icons/Icon-192.png -------------------------------------------------------------------------------- /macless_haystack/web/icons/Icon-512.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dchristl/macless-haystack/HEAD/macless_haystack/web/icons/Icon-512.png -------------------------------------------------------------------------------- /.gitmodules: -------------------------------------------------------------------------------- 1 | [submodule "firmware/nrf5x/nrf5x-base"] 2 | path = firmware/nrf5x/nrf5x-base 3 | url = https://github.com/lab11/nrf5x-base.git 4 | -------------------------------------------------------------------------------- /firmware/nrf5x/pinout_beacon_without_case.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dchristl/macless-haystack/HEAD/firmware/nrf5x/pinout_beacon_without_case.jpg -------------------------------------------------------------------------------- /macless_haystack/assets/OpenHaystackIcon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dchristl/macless-haystack/HEAD/macless_haystack/assets/OpenHaystackIcon.png -------------------------------------------------------------------------------- /macless_haystack/web/icons/Icon-maskable-192.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dchristl/macless-haystack/HEAD/macless_haystack/web/icons/Icon-maskable-192.png -------------------------------------------------------------------------------- /macless_haystack/web/icons/Icon-maskable-512.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dchristl/macless-haystack/HEAD/macless_haystack/web/icons/Icon-maskable-512.png -------------------------------------------------------------------------------- /firmware/ESP32/src/CMakeLists.txt: -------------------------------------------------------------------------------- 1 | FILE(GLOB_RECURSE app_sources ${CMAKE_SOURCE_DIR}/src/*.*) 2 | 3 | idf_component_register(SRCS ${app_sources} INCLUDE_DIRS ".") -------------------------------------------------------------------------------- /firmware/nrf5x/pinout_waterproof_beacon_with_case.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dchristl/macless-haystack/HEAD/firmware/nrf5x/pinout_waterproof_beacon_with_case.jpg -------------------------------------------------------------------------------- /firmware/ESP32/CMakeLists.txt: -------------------------------------------------------------------------------- 1 | cmake_minimum_required(VERSION 3.5) 2 | 3 | set(SUPPORTED_TARGETS esp32) 4 | include($ENV{IDF_PATH}/tools/cmake/project.cmake) 5 | project(openhaystack) -------------------------------------------------------------------------------- /firmware/nrf5x/openocd.cfg: -------------------------------------------------------------------------------- 1 | #nRF51822 Target 2 | source [find interface/stlink.cfg] 3 | 4 | transport select hla_swd 5 | 6 | set WORKAREASIZE 0x4000 7 | source [find target/nrf51.cfg] 8 | -------------------------------------------------------------------------------- /macless_haystack/android/app/src/main/res/mipmap-hdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dchristl/macless-haystack/HEAD/macless_haystack/android/app/src/main/res/mipmap-hdpi/ic_launcher.png -------------------------------------------------------------------------------- /macless_haystack/android/app/src/main/res/mipmap-mdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dchristl/macless-haystack/HEAD/macless_haystack/android/app/src/main/res/mipmap-mdpi/ic_launcher.png -------------------------------------------------------------------------------- /macless_haystack/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dchristl/macless-haystack/HEAD/macless_haystack/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png -------------------------------------------------------------------------------- /macless_haystack/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dchristl/macless-haystack/HEAD/macless_haystack/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png -------------------------------------------------------------------------------- /macless_haystack/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dchristl/macless-haystack/HEAD/macless_haystack/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png -------------------------------------------------------------------------------- /macless_haystack/android/gradle.properties: -------------------------------------------------------------------------------- 1 | org.gradle.jvmargs=-Xmx4G 2 | android.useAndroidX=true 3 | android.enableJetifier=true 4 | android.nonTransitiveRClass=false 5 | android.nonFinalResIds=false 6 | -------------------------------------------------------------------------------- /macless_haystack/linux/main.cc: -------------------------------------------------------------------------------- 1 | #include "my_application.h" 2 | 3 | int main(int argc, char** argv) { 4 | g_autoptr(MyApplication) app = my_application_new(); 5 | return g_application_run(G_APPLICATION(app), argc, argv); 6 | } 7 | -------------------------------------------------------------------------------- /macless_haystack/lib/callbacks.dart: -------------------------------------------------------------------------------- 1 | 2 | import 'accessory/accessory_model.dart'; 3 | 4 | typedef LoadLocationUpdatesCallback = Future Function(Accessory? data); 5 | typedef SaveOrderUpdatesCallback = Future Function( 6 | List accessories); 7 | -------------------------------------------------------------------------------- /endpoint/data/config.ini: -------------------------------------------------------------------------------- 1 | [Settings] 2 | port=6176 3 | binding_address= 4 | anisette_url=http://anisette:6969 5 | loglevel=DEBUG 6 | 7 | appleid= 8 | appleid_pass= 9 | cert=certificate.pem 10 | priv_key=privkey.pem 11 | 12 | endpoint_user= 13 | endpoint_pass= 14 | -------------------------------------------------------------------------------- /firmware/ESP32/with_key.csv: -------------------------------------------------------------------------------- 1 | # ESP-IDF Partition Table 2 | # Name, Type, SubType, Offset, Size, Flags 3 | nvs, data, nvs, 0x9000, 0x6000, 4 | phy_init, data, phy, 0xf000, 0x1000, 5 | factory, app, factory, 0x10000, 1M, 6 | key, data, nvs_keys, 0x110000, 1M, 7 | -------------------------------------------------------------------------------- /macless_haystack/android/gradle/wrapper/gradle-wrapper.properties: -------------------------------------------------------------------------------- 1 | distributionBase=GRADLE_USER_HOME 2 | distributionPath=wrapper/dists 3 | zipStoreBase=GRADLE_USER_HOME 4 | zipStorePath=wrapper/dists 5 | distributionUrl=https\://services.gradle.org/distributions/gradle-8.13-all.zip 6 | -------------------------------------------------------------------------------- /firmware/ESP32/Makefile: -------------------------------------------------------------------------------- 1 | # 2 | # This is a project Makefile. It is assumed the directory this Makefile resides in is a 3 | # project subdirectory. 4 | # 5 | 6 | PROJECT_NAME := macless-haystack-esp32 7 | 8 | COMPONENT_ADD_INCLUDEDIRS := components/include 9 | 10 | include $(IDF_PATH)/make/project.mk 11 | -------------------------------------------------------------------------------- /endpoint/updateRepo: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | curr_dir="$(cd "$(dirname "$0")" && pwd)" 4 | cd "$curr_dir" || exit 5 | cd .. 6 | date > /tmp/last_update.log 7 | git pull origin "$(git branch --show-current)" >> /tmp/last_update.log 8 | git merge origin/"$(git branch --show-current)" --strategy-option=theirs >> /tmp/last_update.log -------------------------------------------------------------------------------- /macless_haystack/lib/accessory/accessory_battery.dart: -------------------------------------------------------------------------------- 1 | enum AccessoryBatteryStatus { 2 | ok, // Battery is currently charging 3 | medium, // Battery is currently discharging 4 | low, // Battery is fully charged 5 | criticalLow, // Battery status is unknown or not applicable 6 | unknown // Battery status is unknown or not applicable 7 | } -------------------------------------------------------------------------------- /macless_haystack/android/.gitignore: -------------------------------------------------------------------------------- 1 | gradle-wrapper.jar 2 | /.gradle 3 | /captures/ 4 | /gradlew 5 | /gradlew.bat 6 | /local.properties 7 | GeneratedPluginRegistrant.java 8 | 9 | # Remember to never publicly share your keystore. 10 | # See https://flutter.dev/docs/deployment/android#reference-the-keystore-from-the-app 11 | key.properties 12 | **/*.keystore 13 | **/*.jks 14 | -------------------------------------------------------------------------------- /macless_haystack/linux/flutter/generated_plugin_registrant.h: -------------------------------------------------------------------------------- 1 | // 2 | // Generated file. Do not edit. 3 | // 4 | 5 | // clang-format off 6 | 7 | #ifndef GENERATED_PLUGIN_REGISTRANT_ 8 | #define GENERATED_PLUGIN_REGISTRANT_ 9 | 10 | #include 11 | 12 | // Registers Flutter plugins. 13 | void fl_register_plugins(FlPluginRegistry* registry); 14 | 15 | #endif // GENERATED_PLUGIN_REGISTRANT_ 16 | -------------------------------------------------------------------------------- /macless_haystack/linux/my_application.h: -------------------------------------------------------------------------------- 1 | #ifndef FLUTTER_MY_APPLICATION_H_ 2 | #define FLUTTER_MY_APPLICATION_H_ 3 | 4 | #include 5 | 6 | G_DECLARE_FINAL_TYPE(MyApplication, my_application, MY, APPLICATION, 7 | GtkApplication) 8 | 9 | /** 10 | * my_application_new: 11 | * 12 | * Creates a new Flutter-based application. 13 | * 14 | * Returns: a new #MyApplication. 15 | */ 16 | MyApplication* my_application_new(); 17 | 18 | #endif // FLUTTER_MY_APPLICATION_H_ 19 | -------------------------------------------------------------------------------- /macless_haystack/android/app/src/main/res/drawable/launch_background.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 12 | 13 | -------------------------------------------------------------------------------- /macless_haystack/android/app/src/main/res/drawable-v21/launch_background.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 12 | 13 | -------------------------------------------------------------------------------- /firmware/ESP32/platformio.ini: -------------------------------------------------------------------------------- 1 | ; PlatformIO Project Configuration File 2 | ; 3 | ; Build options: build flags, source filter 4 | ; Upload options: custom upload port, speed and extra flags 5 | ; Library options: dependencies, extra library storages 6 | ; Advanced options: extra scripting 7 | ; 8 | ; Please visit documentation for the other options and examples 9 | ; https://docs.platformio.org/page/projectconf.html 10 | 11 | [env:esp32dev] 12 | platform = espressif32 13 | board = esp32dev 14 | framework = espidf 15 | monitor_speed = 115200 16 | board_build.partitions = with_key.csv 17 | 18 | 19 | -------------------------------------------------------------------------------- /endpoint/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM python:slim 2 | 3 | 4 | ENV TERM xterm 5 | WORKDIR /app 6 | 7 | RUN apt-get update && apt-get install -y curl nano iproute2 git cron 8 | # Clone endpoint-folder 9 | RUN git init 10 | RUN git remote add origin https://github.com/dchristl/macless-haystack.git 11 | RUN git config core.sparseCheckout true 12 | RUN echo "endpoint" >> .git/info/sparse-checkout 13 | RUN git pull origin main 14 | RUN git checkout main 15 | # Configure python 16 | RUN pip install --upgrade pip 17 | RUN pip install --no-cache-dir -r endpoint/requirements.txt 18 | # Update server files on startup 19 | CMD ["sh", "-c", "./endpoint/updateRepo && python -u endpoint/mh_endpoint.py"] -------------------------------------------------------------------------------- /macless_haystack/lib/item_management/loading_spinner.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | 3 | class LoadingSpinner extends StatelessWidget { 4 | 5 | /// Displays a centered loading spinner. 6 | const LoadingSpinner({ super.key }); 7 | 8 | @override 9 | Widget build(BuildContext context) { 10 | return Row( 11 | mainAxisAlignment: MainAxisAlignment.center, 12 | children: [Padding( 13 | padding: const EdgeInsets.only(top: 20), 14 | child: CircularProgressIndicator( 15 | color: Theme.of(context).primaryColor, 16 | semanticsLabel: 'Loading. Please wait.', 17 | ), 18 | )], 19 | ); 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /macless_haystack/lib/placeholder/avatar_placeholder.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | 3 | class AvatarPlaceholder extends StatelessWidget { 4 | final double size; 5 | 6 | /// Displays a placeholder for the actual avatar, occupying the same layout space. 7 | const AvatarPlaceholder({ 8 | super.key, 9 | this.size = 24, 10 | }); 11 | 12 | @override 13 | Widget build(BuildContext context) { 14 | return Container( 15 | width: size * 3 / 2, 16 | height: size * 3 / 2, 17 | decoration: const BoxDecoration( 18 | color: Color.fromARGB(255, 200, 200, 200), 19 | shape: BoxShape.circle, 20 | ), 21 | ); 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /endpoint/Dockerfile_dev: -------------------------------------------------------------------------------- 1 | FROM python:slim 2 | 3 | 4 | ENV TERM xterm 5 | WORKDIR /app 6 | 7 | RUN apt-get update && apt-get install -y curl nano iproute2 git cron && rm -rf /var/lib/apt/lists/* 8 | # Clone endpoint-folder 9 | RUN git init 10 | RUN git remote add origin https://github.com/dchristl/macless-haystack.git 11 | RUN git config core.sparseCheckout true 12 | RUN echo "endpoint" >> .git/info/sparse-checkout 13 | RUN git pull origin dev 14 | RUN git checkout dev 15 | # Configure python 16 | RUN pip install --upgrade pip 17 | RUN pip install --no-cache-dir -r endpoint/requirements.txt 18 | # Update server files on startup 19 | CMD ["sh", "-c", "./endpoint/updateRepo && exec python -u endpoint/mh_endpoint.py"] -------------------------------------------------------------------------------- /macless_haystack/android/build.gradle: -------------------------------------------------------------------------------- 1 | allprojects { 2 | repositories { 3 | google() 4 | mavenCentral() 5 | } 6 | } 7 | 8 | rootProject.buildDir = '../build' 9 | subprojects { 10 | project.buildDir = "${rootProject.buildDir}/${project.name}" 11 | } 12 | subprojects { 13 | afterEvaluate { project -> 14 | if (project.plugins.hasPlugin("com.android.application") || 15 | project.plugins.hasPlugin("com.android.library")) { 16 | project.android { 17 | compileSdkVersion 36 18 | } 19 | } 20 | } 21 | project.evaluationDependsOn(':app') 22 | } 23 | 24 | tasks.register("clean", Delete) { 25 | delete rootProject.buildDir 26 | } 27 | -------------------------------------------------------------------------------- /firmware/nrf5x/openhaystack.h: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | #include 4 | 5 | /* 6 | * set_addr_from_key will set the bluetooth address from the first 6 bytes of the key used to be advertised 7 | */ 8 | void set_addr_from_key(const char *key); 9 | 10 | /* 11 | * fill_adv_template_from_key will set the advertising data based on the remaining bytes from the advertised key 12 | */ 13 | void fill_adv_template_from_key(const char *key); 14 | 15 | /* 16 | * setAdvertisementKey will setup the key to be advertised 17 | * 18 | * @param[in] key public key to be advertised 19 | * @param[out] bleAddr bluetooth address to setup 20 | * @param[out] data raw data to advertise 21 | * 22 | * @returns raw data size 23 | */ 24 | uint8_t setAdvertisementKey(const char *key, uint8_t **bleAddr, uint8_t **data); 25 | -------------------------------------------------------------------------------- /macless_haystack/android/app/src/main/res/values/styles.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 7 | 13 | 16 | 17 | -------------------------------------------------------------------------------- /macless_haystack/android/settings.gradle: -------------------------------------------------------------------------------- 1 | pluginManagement { 2 | def flutterSdkPath = { 3 | def properties = new Properties() 4 | file("local.properties").withInputStream { properties.load(it) } 5 | def flutterSdkPath = properties.getProperty("flutter.sdk") 6 | assert flutterSdkPath != null, "flutter.sdk not set in local.properties" 7 | return flutterSdkPath 8 | }() 9 | 10 | includeBuild("$flutterSdkPath/packages/flutter_tools/gradle") 11 | 12 | repositories { 13 | google() 14 | mavenCentral() 15 | gradlePluginPortal() 16 | } 17 | } 18 | 19 | plugins { 20 | id "dev.flutter.flutter-plugin-loader" version "1.0.0" 21 | id "com.android.application" version '8.13.2' apply false 22 | id "org.jetbrains.kotlin.android" version "2.3.0-RC3" apply false 23 | } 24 | 25 | include ":app" -------------------------------------------------------------------------------- /macless_haystack/lib/item_management/refresh_action.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/foundation.dart'; 2 | import 'package:flutter/material.dart'; 3 | 4 | class RefreshAction extends StatefulWidget { 5 | final AsyncCallback callback; 6 | 7 | /// A new accessory can be created or an existing one imported manually. 8 | const RefreshAction({super.key, required this.callback}); 9 | 10 | @override 11 | State createState() { 12 | return _RefreshingWidgetState(); 13 | } 14 | } 15 | 16 | class _RefreshingWidgetState extends State { 17 | @override 18 | Widget build(BuildContext context) { 19 | return FloatingActionButton( 20 | heroTag: null, 21 | onPressed: () { 22 | widget.callback.call(); 23 | }, 24 | tooltip: 'Refresh', 25 | child: const Icon(Icons.refresh)); 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /macless_haystack/android/app/src/main/kotlin/de/dchristl/headlesshaystack/MainActivity.kt: -------------------------------------------------------------------------------- 1 | package de.dchristl.headlesshaystack 2 | 3 | import android.os.Build 4 | import android.os.Bundle 5 | import androidx.core.view.WindowCompat 6 | import io.flutter.embedding.android.FlutterActivity 7 | 8 | class MainActivity: FlutterActivity() { 9 | 10 | override fun onCreate(savedInstanceState: Bundle?) { 11 | // Aligns the Flutter view vertically with the window. 12 | WindowCompat.setDecorFitsSystemWindows(getWindow(), false) 13 | 14 | if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) { 15 | // Disable the Android splash screen fade out animation to avoid 16 | // a flicker before the similar frame is drawn in Flutter. 17 | splashScreen.setOnExitAnimationListener { splashScreenView -> splashScreenView.remove() } 18 | } 19 | 20 | super.onCreate(savedInstanceState) 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /macless_haystack/lib/accessory/accessory_list_item_placeholder.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:macless_haystack/accessory/accessory_list_item.dart'; 3 | import 'package:macless_haystack/placeholder/avatar_placeholder.dart'; 4 | import 'package:macless_haystack/placeholder/text_placeholder.dart'; 5 | 6 | class AccessoryListItemPlaceholder extends StatelessWidget { 7 | 8 | /// A placeholder for an [AccessoryListItem] showing a loading animation. 9 | const AccessoryListItemPlaceholder({ 10 | super.key, 11 | }); 12 | 13 | @override 14 | Widget build(BuildContext context) { 15 | // Uses a similar layout to the actual accessory list item 16 | return const ListTile( 17 | title: TextPlaceholder(), 18 | subtitle: TextPlaceholder(), 19 | dense: true, 20 | leading: AvatarPlaceholder(), 21 | trailing: TextPlaceholder(width: 60), 22 | ); 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /macless_haystack/lib/deployment/hyperlink.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:url_launcher/url_launcher.dart'; 3 | 4 | class Hyperlink extends StatelessWidget { 5 | /// The target url to open. 6 | final String target; 7 | 8 | /// The display text of the hyperlink. Default is [target]. 9 | final String _text; 10 | 11 | /// Displays a hyperlink that can be opened by a tap. 12 | const Hyperlink({ 13 | super.key, 14 | required this.target, 15 | text, 16 | }) : _text = text ?? target; 17 | 18 | @override 19 | Widget build(BuildContext context) { 20 | return InkWell( 21 | child: Text( 22 | _text, 23 | style: const TextStyle( 24 | color: Colors.blue, 25 | decoration: TextDecoration.underline, 26 | ), 27 | ), 28 | onTap: () { 29 | launchUrl(Uri.parse((target))); 30 | }, 31 | ); 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /macless_haystack/lib/splashscreen.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | 3 | class Splashscreen extends StatelessWidget { 4 | 5 | /// Display a fullscreen splashscreen to cover loading times. 6 | const Splashscreen({ super.key }); 7 | 8 | @override 9 | Widget build(BuildContext context) { 10 | Size screenSize = MediaQuery.of(context).size; 11 | Orientation orientation = MediaQuery.of(context).orientation; 12 | 13 | var maxScreen = orientation == Orientation.portrait ? screenSize.width : screenSize.height; 14 | var maxSize = maxScreen * 0.4; 15 | 16 | return Scaffold( 17 | body: Center( 18 | child: Container( 19 | constraints: BoxConstraints(maxWidth: maxSize, maxHeight: maxSize), 20 | child: const Image( 21 | width: 1800, 22 | image: AssetImage('assets/OpenHaystackIcon.png')), 23 | ), 24 | ), 25 | ); 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /macless_haystack/.gitignore: -------------------------------------------------------------------------------- 1 | # Miscellaneous 2 | *.class 3 | *.log 4 | *.pyc 5 | *.swp 6 | .DS_Store 7 | .atom/ 8 | .buildlog/ 9 | .history 10 | .svn/ 11 | 12 | # IntelliJ related 13 | *.iml 14 | *.ipr 15 | *.iws 16 | .idea/ 17 | 18 | # The .vscode folder contains launch configuration and tasks you configure in 19 | # VS Code which you may wish to be included in version control, so this line 20 | # is commented out by default. 21 | #.vscode/ 22 | 23 | # Flutter/Dart/Pub related 24 | **/doc/api/ 25 | **/ios/Flutter/.last_build_id 26 | .dart_tool/ 27 | .flutter-plugins 28 | .flutter-plugins-dependencies 29 | .packages 30 | .pub-cache/ 31 | .pub/ 32 | /build/ 33 | pubspec.lock 34 | 35 | # Web related 36 | 37 | # Symbolication related 38 | app.*.symbols 39 | 40 | # Obfuscation related 41 | app.*.map.json 42 | 43 | # Android Studio will place build artifacts here 44 | /android/app/debug 45 | /android/app/profile 46 | /android/app/release 47 | -------------------------------------------------------------------------------- /macless_haystack/linux/flutter/generated_plugins.cmake: -------------------------------------------------------------------------------- 1 | # 2 | # Generated file, do not edit. 3 | # 4 | 5 | list(APPEND FLUTTER_PLUGIN_LIST 6 | flutter_secure_storage_linux 7 | maps_launcher 8 | url_launcher_linux 9 | ) 10 | 11 | list(APPEND FLUTTER_FFI_PLUGIN_LIST 12 | ) 13 | 14 | set(PLUGIN_BUNDLED_LIBRARIES) 15 | 16 | foreach(plugin ${FLUTTER_PLUGIN_LIST}) 17 | add_subdirectory(flutter/ephemeral/.plugin_symlinks/${plugin}/linux plugins/${plugin}) 18 | target_link_libraries(${BINARY_NAME} PRIVATE ${plugin}_plugin) 19 | list(APPEND PLUGIN_BUNDLED_LIBRARIES $) 20 | list(APPEND PLUGIN_BUNDLED_LIBRARIES ${${plugin}_bundled_libraries}) 21 | endforeach(plugin) 22 | 23 | foreach(ffi_plugin ${FLUTTER_FFI_PLUGIN_LIST}) 24 | add_subdirectory(flutter/ephemeral/.plugin_symlinks/${ffi_plugin}/linux plugins/${ffi_plugin}) 25 | list(APPEND PLUGIN_BUNDLED_LIBRARIES ${${ffi_plugin}_bundled_libraries}) 26 | endforeach(ffi_plugin) 27 | -------------------------------------------------------------------------------- /macless_haystack/android/app/src/debug/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 2 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | -------------------------------------------------------------------------------- /macless_haystack/android/app/src/profile/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 2 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | -------------------------------------------------------------------------------- /macless_haystack/lib/accessory/no_accessories.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:macless_haystack/item_management/new_item_action.dart'; 3 | 4 | class NoAccessoriesPlaceholder extends StatelessWidget { 5 | 6 | /// Displays a message that no accessories are present. 7 | /// 8 | /// Allows the user to quickly add a new accessory. 9 | const NoAccessoriesPlaceholder({ super.key }); 10 | 11 | @override 12 | Widget build(BuildContext context) { 13 | return const Center( 14 | child: Column( 15 | mainAxisAlignment: MainAxisAlignment.center, 16 | children: [ 17 | Text( 18 | 'There\'s Nothing Here Yet\nAdd an accessory to get started.', 19 | style: TextStyle( 20 | fontSize: 20, 21 | color: Colors.grey, 22 | ), 23 | textAlign: TextAlign.center, 24 | ), 25 | NewKeyAction(mini: true), 26 | ], 27 | ), 28 | ); 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /macless_haystack/lib/item_management/accessory_id_input.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | 3 | class AccessoryIdInput extends StatelessWidget { 4 | final ValueChanged changeListener; 5 | 6 | /// Displays an input field with validation for an accessory ID. 7 | const AccessoryIdInput({ 8 | super.key, 9 | required this.changeListener, 10 | }); 11 | 12 | @override 13 | Widget build(BuildContext context) { 14 | return Padding( 15 | padding: const EdgeInsets.symmetric(horizontal: 16.0, vertical: 4.0), 16 | child: TextFormField( 17 | decoration: const InputDecoration( 18 | labelText: 'ID', 19 | ), 20 | validator: (value) { 21 | if (value == null) { 22 | return 'ID must be provided.'; 23 | } 24 | int? parsed = int.tryParse(value); 25 | if (parsed == null) { 26 | return 'ID must be an integer value.'; 27 | } 28 | return null; 29 | }, 30 | onSaved: changeListener, 31 | ), 32 | ); 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /macless_haystack/web/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "Macless Haystack", 3 | "short_name": "Haystack", 4 | "start_url": ".", 5 | "display": "standalone", 6 | "background_color": "#0175C2", 7 | "theme_color": "#0175C2", 8 | "description": "Macless Haystack.", 9 | "orientation": "portrait-primary", 10 | "prefer_related_applications": false, 11 | "icons": [ 12 | { 13 | "src": "icons/Icon-192.png", 14 | "sizes": "192x192", 15 | "type": "image/png" 16 | }, 17 | { 18 | "src": "icons/Icon-512.png", 19 | "sizes": "512x512", 20 | "type": "image/png" 21 | }, 22 | { 23 | "src": "icons/Icon-maskable-192.png", 24 | "sizes": "192x192", 25 | "type": "image/png", 26 | "purpose": "maskable" 27 | }, 28 | { 29 | "src": "icons/Icon-maskable-512.png", 30 | "sizes": "512x512", 31 | "type": "image/png", 32 | "purpose": "maskable" 33 | } 34 | ] 35 | } 36 | -------------------------------------------------------------------------------- /macless_haystack/.metadata: -------------------------------------------------------------------------------- 1 | # This file tracks properties of this Flutter project. 2 | # Used by Flutter tool to assess capabilities and perform upgrades etc. 3 | # 4 | # This file should be version controlled and should not be manually edited. 5 | 6 | version: 7 | revision: "4cf269e36de2573851eaef3c763994f8f9be494d" 8 | channel: "stable" 9 | 10 | project_type: app 11 | 12 | # Tracks metadata for the flutter migrate command 13 | migration: 14 | platforms: 15 | - platform: root 16 | create_revision: 4cf269e36de2573851eaef3c763994f8f9be494d 17 | base_revision: 4cf269e36de2573851eaef3c763994f8f9be494d 18 | - platform: web 19 | create_revision: 4cf269e36de2573851eaef3c763994f8f9be494d 20 | base_revision: 4cf269e36de2573851eaef3c763994f8f9be494d 21 | 22 | # User provided section 23 | 24 | # List of Local paths (relative to this file) that should be 25 | # ignored by the migrate tool. 26 | # 27 | # Files that are not part of the templates will be ignored by default. 28 | unmanaged_files: 29 | - 'lib/main.dart' 30 | - 'ios/Runner.xcodeproj/project.pbxproj' 31 | -------------------------------------------------------------------------------- /macless_haystack/android/app/src/main/res/values-night/styles.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 9 | 15 | 18 | 19 | -------------------------------------------------------------------------------- /firmware/nrf5x/ble_stack.h: -------------------------------------------------------------------------------- 1 | #include "softdevice_handler.h" 2 | #include "boards.h" 3 | #include "ble.h" 4 | #include "ble_gap.h" 5 | #include "app_error.h" 6 | 7 | #ifndef uint8_t 8 | typedef __uint8_t uint8_t; 9 | typedef __uint16_t uint16_t; 10 | typedef __uint32_t uint32_t; 11 | typedef __uint64_t uint64_t; 12 | #endif 13 | 14 | /* 15 | * init_ble will initialize the ble stack, it will use the crystal definition based on NRF_CLOCK_LFCLKSRC. 16 | * In devices with no external crystal you should use the internal rc. You can look at the e104bt5032a_board.h file 17 | */ 18 | void init_ble(); 19 | 20 | /** 21 | * setMacAddress will set the bluetooth address 22 | */ 23 | void setMacAddress(uint8_t *addr); 24 | 25 | /** 26 | * setAdvertisementData will set the data to be advertised 27 | */ 28 | void setAdvertisementData(uint8_t *data, uint8_t dlen); 29 | 30 | /** 31 | * Start advertising at the specified interval 32 | * 33 | * @param[in] interval advertising interval in milliseconds 34 | */ 35 | void startAdvertisement(int interval); 36 | 37 | /** 38 | * Function for the Power manager. 39 | */ 40 | void power_manage(void); -------------------------------------------------------------------------------- /macless_haystack/lib/accessory/accessory_icon.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | 3 | class AccessoryIcon extends StatelessWidget { 4 | /// The icon to display. 5 | final IconData icon; 6 | /// The color of the surrounding ring. 7 | final Color color; 8 | /// The size of the icon. 9 | final double size; 10 | 11 | /// Displays the icon in a colored ring. 12 | /// 13 | /// The default size can be adjusted by setting the [size] parameter. 14 | const AccessoryIcon({ 15 | super.key, 16 | this.icon = Icons.help, 17 | this.color = Colors.grey, 18 | this.size = 24, 19 | }); 20 | 21 | @override 22 | Widget build(BuildContext context) { 23 | return Container( 24 | decoration: BoxDecoration( 25 | color: Theme.of(context).colorScheme.surface, 26 | shape: BoxShape.circle, 27 | border: Border.all(width: size / 6, color: color), 28 | ), 29 | child: Padding( 30 | padding: EdgeInsets.all(size / 12), 31 | child: Icon( 32 | icon, 33 | size: size, 34 | color: Theme.of(context).colorScheme.onSurface, 35 | ), 36 | ), 37 | ); 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /macless_haystack/linux/flutter/generated_plugin_registrant.cc: -------------------------------------------------------------------------------- 1 | // 2 | // Generated file. Do not edit. 3 | // 4 | 5 | // clang-format off 6 | 7 | #include "generated_plugin_registrant.h" 8 | 9 | #include 10 | #include 11 | #include 12 | 13 | void fl_register_plugins(FlPluginRegistry* registry) { 14 | g_autoptr(FlPluginRegistrar) flutter_secure_storage_linux_registrar = 15 | fl_plugin_registry_get_registrar_for_plugin(registry, "FlutterSecureStorageLinuxPlugin"); 16 | flutter_secure_storage_linux_plugin_register_with_registrar(flutter_secure_storage_linux_registrar); 17 | g_autoptr(FlPluginRegistrar) maps_launcher_registrar = 18 | fl_plugin_registry_get_registrar_for_plugin(registry, "MapsLauncherPlugin"); 19 | maps_launcher_plugin_register_with_registrar(maps_launcher_registrar); 20 | g_autoptr(FlPluginRegistrar) url_launcher_linux_registrar = 21 | fl_plugin_registry_get_registrar_for_plugin(registry, "UrlLauncherPlugin"); 22 | url_launcher_plugin_register_with_registrar(url_launcher_linux_registrar); 23 | } 24 | -------------------------------------------------------------------------------- /macless_haystack/lib/item_management/accessory_name_input.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | 3 | class AccessoryNameInput extends StatelessWidget { 4 | final ValueChanged? onSaved; 5 | final ValueChanged? onChanged; 6 | /// The initial accessory name 7 | final String? initialValue; 8 | 9 | /// Displays an input field with validation for an accessory name. 10 | const AccessoryNameInput({ 11 | super.key, 12 | this.onSaved, 13 | this.initialValue, 14 | this.onChanged, 15 | }); 16 | 17 | @override 18 | Widget build(BuildContext context) { 19 | return Padding( 20 | padding: const EdgeInsets.symmetric(horizontal: 16.0, vertical: 4.0), 21 | child: TextFormField( 22 | decoration: const InputDecoration( 23 | labelText: 'Name', 24 | ), 25 | validator: (value) { 26 | if (value == null) { 27 | return 'Name must be provided.'; 28 | } 29 | if (value.isEmpty || value.length > 30) { 30 | return 'Name must be a non empty string of max length 30.'; 31 | } 32 | return null; 33 | }, 34 | onSaved: onSaved, 35 | onChanged: onChanged, 36 | initialValue: initialValue, 37 | ), 38 | ); 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /macless_haystack/lib/item_management/accessory_color_input.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:macless_haystack/accessory/accessory_color_selector.dart'; 3 | 4 | class AccessoryColorInput extends StatelessWidget { 5 | /// The inititial color value 6 | final Color color; 7 | /// Callback called when the color is changed. Parameter is null 8 | /// if color did not change 9 | final ValueChanged changeListener; 10 | 11 | /// Displays a color selection input that previews the current selection. 12 | const AccessoryColorInput({ 13 | super.key, 14 | required this.color, 15 | required this.changeListener, 16 | }); 17 | 18 | @override 19 | Widget build(BuildContext context) { 20 | return ListTile( 21 | title: Row( 22 | children: [ 23 | const Text('Color: '), 24 | Icon( 25 | Icons.circle, 26 | color: color, 27 | ), 28 | const Spacer(), 29 | OutlinedButton( 30 | child: const Text('Change'), 31 | onPressed: () async { 32 | Color? selectedColor = await AccessoryColorSelector 33 | .showColorSelection(context, color); 34 | changeListener(selectedColor); 35 | }, 36 | ), 37 | ], 38 | ), 39 | ); 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /macless_haystack/lib/deployment/code_block.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:flutter/services.dart'; 3 | 4 | class CodeBlock extends StatelessWidget { 5 | final String text; 6 | 7 | /// Displays a code block that can easily copied by the user. 8 | const CodeBlock({ 9 | super.key, 10 | required this.text, 11 | }); 12 | 13 | @override 14 | Widget build(BuildContext context) { 15 | return Padding( 16 | padding: const EdgeInsets.symmetric(vertical: 8.0), 17 | child: Stack( 18 | children: [ 19 | Container( 20 | width: double.infinity, 21 | constraints: const BoxConstraints(minHeight: 50), 22 | decoration: BoxDecoration( 23 | borderRadius: const BorderRadius.all(Radius.circular(10)), 24 | color: Theme.of(context).colorScheme.surface, 25 | ), 26 | padding: const EdgeInsets.all(5), 27 | child: SelectableText(text), 28 | ), 29 | Positioned( 30 | top: 0, 31 | right: 5, 32 | child: OutlinedButton( 33 | child: const Text('Copy'), 34 | onPressed: () { 35 | Clipboard.setData(ClipboardData(text: text)); 36 | }, 37 | ), 38 | ), 39 | ], 40 | ), 41 | ); 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /macless_haystack/web/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | Macless Haystack 33 | 34 | 35 | 36 | 37 | 38 | 39 | -------------------------------------------------------------------------------- /firmware/ESP32/README.md: -------------------------------------------------------------------------------- 1 | ## Macless Haystack Firmware for ESP32 2 | 3 | This project contains a battery-optimzed PoC firmware for Espressif ESP32 chips (like ESP32-WROOM or ESP32-WROVER, but _not_ ESP32-S2). 4 | 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/). 5 | This firmware consumes slightly more power when more than 1 key is used. The controller wakes up every 30 minutes and switches the key. 6 | 7 | ### Requirements 8 | 9 | - [Esptool](https://docs.espressif.com/projects/esptool/en/latest/esp32/installation.html) installed *or* 10 | - [Espressif's Flash Download Tools](https://www.espressif.com/en/support/download/other-tools) installed if you prefer a graphical way 11 | 12 | ### Deploy the Firmware 13 | 14 | - Download and unpack the firmware 15 | - Copy your previously generated PREFIX_keyfile in the same folder 16 | 17 | ```bash 18 | esptool.py write_flash 0x1000 bootloader.bin \ 19 | 0x8000 partitions.bin \ 20 | 0x10000 firmware.bin \ 21 | 0x110000 PREFIX_keyfile 22 | ``` 23 | 24 | If any problem occurs, erase flash manually before flashing: 25 | 26 | ```bash 27 | esptool.py erase_flash 28 | ``` 29 | 30 | > [!NOTE] 31 | > You might need to reset your device after running the script before it starts sending advertisements. 32 | -------------------------------------------------------------------------------- /firmware/nrf5x/battery.h: -------------------------------------------------------------------------------- 1 | #define STATUS_FLAG_BATTERY_MASK 0b11000000 2 | #define STATUS_FLAG_COUNTER_MASK 0b00111111 3 | #define STATUS_FLAG_MEDIUM_BATTERY 0b01000000 4 | #define STATUS_FLAG_LOW_BATTERY 0b10000000 5 | #define STATUS_FLAG_CRITICALLY_LOW_BATTERY 0b11000000 6 | #define STATUS_FLAG_BATTERY_UPDATES_SUPPORT 0b00100000 7 | 8 | #ifdef S130 9 | #include "nrf51_battery.h" 10 | #else 11 | uint8_t get_current_level() {return 100;}; 12 | #endif 13 | 14 | void updateBatteryLevel(uint8_t * data) 15 | { 16 | uint8_t * status_flag_ptr = data + 6; 17 | #ifdef S130 // If the board supports battery updates 18 | *status_flag_ptr |= STATUS_FLAG_BATTERY_UPDATES_SUPPORT; 19 | #endif 20 | 21 | /* 22 | static uint16_t battery_counter = BATTERY_COUNTER_THRESHOLD; 23 | if((++battery_counter) < BATTERY_COUNTER_THRESHOLD){ 24 | return; 25 | } 26 | battery_counter = 0; 27 | */ 28 | 29 | uint8_t battery_level = get_current_level(); 30 | 31 | *status_flag_ptr &= (~STATUS_FLAG_BATTERY_MASK); 32 | if(battery_level > 80){ 33 | // do nothing 34 | }else if(battery_level > 50){ 35 | *status_flag_ptr |= STATUS_FLAG_MEDIUM_BATTERY; 36 | }else if(battery_level > 30){ 37 | *status_flag_ptr |= STATUS_FLAG_LOW_BATTERY; 38 | }else{ 39 | *status_flag_ptr |= STATUS_FLAG_CRITICALLY_LOW_BATTERY; 40 | } 41 | } -------------------------------------------------------------------------------- /macless_haystack/lib/item_management/accessory_pk_input.dart: -------------------------------------------------------------------------------- 1 | import 'dart:convert'; 2 | 3 | import 'package:flutter/material.dart'; 4 | 5 | class AccessoryPrivateKeyInput extends StatelessWidget { 6 | final ValueChanged changeListener; 7 | 8 | /// Displays an input field with validation for a Base64 encoded accessory private key. 9 | const AccessoryPrivateKeyInput({ 10 | super.key, 11 | required this.changeListener, 12 | }); 13 | 14 | @override 15 | Widget build(BuildContext context) { 16 | return Padding( 17 | padding: const EdgeInsets.symmetric(horizontal: 16.0, vertical: 4.0), 18 | child: TextFormField( 19 | decoration: const InputDecoration( 20 | hintText: 'SGVsbG8gV29ybGQhCg==', 21 | labelText: 'Private Key (Base64)', 22 | ), 23 | validator: (value) { 24 | if (value == null || value.isEmpty) { 25 | return 'Private key must be provided.'; 26 | } 27 | try { 28 | var removeEscaping = value 29 | .replaceAll('\\', '').replaceAll('\n', ''); 30 | base64Decode(removeEscaping); 31 | } catch (e) { 32 | return 'Value must be valid base64 key.'; 33 | } 34 | return null; 35 | }, 36 | onSaved: (newValue) => 37 | changeListener(newValue?.replaceAll('\\', '').replaceAll('\n', '')), 38 | ), 39 | ); 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /macless_haystack/lib/item_management/accessory_icon_input.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:macless_haystack/accessory/accessory_icon_selector.dart'; 3 | 4 | class AccessoryIconInput extends StatelessWidget { 5 | /// The initial icon 6 | final IconData initialIcon; 7 | /// The original icon name 8 | final String iconString; 9 | /// The color of the icon 10 | final Color color; 11 | /// Callback called when the icon is changed. Parameter is null 12 | /// if icon did not change 13 | final ValueChanged changeListener; 14 | 15 | /// Displays an icon selection input that previews the current selection. 16 | const AccessoryIconInput({ 17 | super.key, 18 | required this.initialIcon, 19 | required this.iconString, 20 | required this.color, 21 | required this.changeListener, 22 | }); 23 | 24 | @override 25 | Widget build(BuildContext context) { 26 | return ListTile( 27 | title: Row( 28 | children: [ 29 | const Text('Icon: '), 30 | Icon(initialIcon), 31 | const Spacer(), 32 | OutlinedButton( 33 | child: const Text('Change'), 34 | onPressed: () async { 35 | String? selectedIcon = await AccessoryIconSelector 36 | .showIconSelection(context, iconString, color); 37 | changeListener(selectedIcon); 38 | }, 39 | ), 40 | ], 41 | ), 42 | ); 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /macless_haystack/lib/accessory/accessory_icon_model.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | 3 | class AccessoryIconModel { 4 | /// A list of all available icons 5 | static const List icons = [ 6 | "creditcard.fill", "briefcase.fill", "case.fill", "latch.2.case.fill", 7 | "key.fill", "mappin", "globe", "crown.fill", 8 | "gift.fill", "car.fill", "bicycle", "figure.walk", 9 | "heart.fill", "hare.fill", "tortoise.fill", "eye.fill", 10 | ]; 11 | 12 | /// A mapping from the cupertino icon names to the material icon names. 13 | /// 14 | /// If the icons do not match, so a similar replacement is used. 15 | static const iconMapping = { 16 | 'creditcard.fill': Icons.credit_card, 17 | 'briefcase.fill': Icons.business_center, 18 | 'case.fill': Icons.work, 19 | 'latch.2.case.fill': Icons.business_center, 20 | 'key.fill': Icons.vpn_key, 21 | 'mappin': Icons.place, 22 | // 'pushpin': Icons.push_pin, 23 | 'globe': Icons.language, 24 | 'crown.fill': Icons.school, 25 | 'gift.fill': Icons.redeem, 26 | 'car.fill': Icons.directions_car, 27 | 'bicycle': Icons.pedal_bike, 28 | 'figure.walk': Icons.directions_walk, 29 | 'heart.fill': Icons.favorite, 30 | 'hare.fill': Icons.pets, 31 | 'tortoise.fill': Icons.bug_report, 32 | 'eye.fill': Icons.visibility, 33 | }; 34 | 35 | /// Looks up the equivalent material icon for the cupertino icon [iconName]. 36 | static IconData? mapIcon(String iconName) { 37 | return iconMapping[iconName]; 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /macless_haystack/analysis_options.yaml: -------------------------------------------------------------------------------- 1 | # This file configures the analyzer, which statically analyzes Dart code to 2 | # check for errors, warnings, and lints. 3 | # 4 | # The issues identified by the analyzer are surfaced in the UI of Dart-enabled 5 | # IDEs (https://dart.dev/tools#ides-and-editors). The analyzer can also be 6 | # invoked from the command line by running `flutter analyze`. 7 | 8 | # The following line activates a set of recommended lints for Flutter apps, 9 | # packages, and plugins designed to encourage good coding practices. 10 | include: package:flutter_lints/flutter.yaml 11 | 12 | linter: 13 | # The lint rules applied to this project can be customized in the 14 | # section below to disable rules from the `package:flutter_lints/flutter.yaml` 15 | # included above or to enable additional rules. A list of all available lints 16 | # and their documentation is published at 17 | # https://dart-lang.github.io/linter/lints/index.html. 18 | # 19 | # Instead of disabling a lint rule for the entire project in the 20 | # section below, it can also be suppressed for a single line of code 21 | # or a specific dart file by using the `// ignore: name_of_lint` and 22 | # `// ignore_for_file: name_of_lint` syntax on the line or in the file 23 | # producing the lint. 24 | rules: 25 | # avoid_print: false # Uncomment to disable the `avoid_print` rule 26 | # prefer_single_quotes: true # Uncomment to enable the `prefer_single_quotes` rule 27 | 28 | # Additional information about this file can be found at 29 | # https://dart.dev/guides/language/analysis-options 30 | -------------------------------------------------------------------------------- /macless_haystack/lib/accessory/accessory_color_selector.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:flutter_colorpicker/flutter_colorpicker.dart'; 3 | 4 | class AccessoryColorSelector extends StatelessWidget { 5 | 6 | /// This shows a color selector. 7 | /// 8 | /// The color can be selected via a color field or by inputing explicit 9 | /// RGB values. 10 | const AccessoryColorSelector({ super.key }); 11 | 12 | /// Displays the color selector with the [initialColor] preselected. 13 | /// 14 | /// The selected color is returned if the user selects the save option. 15 | /// Otherwise the selection is discarded with a null return value. 16 | static Future showColorSelection(BuildContext context, Color initialColor) async { 17 | Color currentColor = initialColor; 18 | return await showDialog( 19 | context: context, 20 | builder: (BuildContext context) { 21 | return AlertDialog( 22 | title: const Text('Pick a color'), 23 | content: SingleChildScrollView( 24 | child: ColorPicker( 25 | hexInputBar: true, 26 | pickerColor: currentColor, 27 | onColorChanged: (Color newColor) { 28 | currentColor = newColor; 29 | }, 30 | ) 31 | ), 32 | actions: [ 33 | ElevatedButton( 34 | child: const Text('Save'), 35 | onPressed: () { 36 | Navigator.pop(context, currentColor); 37 | }, 38 | ), 39 | ], 40 | ); 41 | }, 42 | ); 43 | } 44 | 45 | @override 46 | Widget build(BuildContext context) { 47 | throw UnimplementedError(); 48 | } 49 | 50 | } 51 | -------------------------------------------------------------------------------- /macless_haystack/lib/history/days_selection_slider.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | 3 | class DaysSelectionSlider extends StatefulWidget { 4 | /// The number of days currently selected. 5 | final double numberOfDays; 6 | 7 | /// A callback listening for value changes. 8 | final ValueChanged onChanged; 9 | 10 | /// Display a slider that allows to define how many days to go back 11 | /// (range 1 to 7). 12 | const DaysSelectionSlider({ 13 | super.key, 14 | required this.numberOfDays, 15 | required this.onChanged, 16 | }); 17 | 18 | @override 19 | State createState() { 20 | return _DaysSelectionSliderState(); 21 | } 22 | } 23 | 24 | class _DaysSelectionSliderState extends State { 25 | @override 26 | Widget build(BuildContext context) { 27 | return Padding( 28 | padding: const EdgeInsets.all(12.0), 29 | child: Column( 30 | children: [ 31 | const Center( 32 | child: Text( 33 | 'How many days back?', 34 | style: TextStyle(fontSize: 20), 35 | ), 36 | ), 37 | Row( 38 | children: [ 39 | const Text('1', style: TextStyle(fontWeight: FontWeight.bold)), 40 | Expanded( 41 | 42 | child: Slider( 43 | value: widget.numberOfDays, 44 | min: 1, 45 | max: 7, 46 | label: '${widget.numberOfDays.round()}', 47 | divisions: 6, 48 | onChanged: widget.onChanged, 49 | ), 50 | ), 51 | const Text('7', style: TextStyle(fontWeight: FontWeight.bold)), 52 | ], 53 | ), 54 | ], 55 | ), 56 | ); 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /endpoint/mh_config.py: -------------------------------------------------------------------------------- 1 | import logging 2 | import os 3 | import configparser 4 | import sys 5 | 6 | sys.path.append(os.getcwd()) 7 | 8 | CONFIG_PATH = "data" 9 | CONFIG_FILE = "auth.json" 10 | CERT_FILE = "certificate.pem" # optional 11 | KEY_FILE = "privkey.pem" # optional 12 | 13 | 14 | def getConfigPath(): 15 | script_path = os.path.abspath(__file__) 16 | return CONFIG_PATH if os.path.isabs(CONFIG_PATH) else os.path.dirname(script_path) + '/' + CONFIG_PATH 17 | 18 | 19 | config = configparser.ConfigParser() 20 | config.read(getConfigPath() + '/config.ini') 21 | 22 | 23 | def getAnisetteServer(): 24 | return config.get('Settings', 'anisette_url', fallback='http://anisette:6969') 25 | 26 | 27 | def getPort(): 28 | return int(config.get('Settings', 'port', fallback='6176')) 29 | 30 | 31 | def getBindingAddress(): 32 | return config.get('Settings', 'binding_address', fallback='0.0.0.0') 33 | 34 | 35 | def getUser(): 36 | return config.get('Settings', 'appleid', fallback=None) 37 | 38 | 39 | def getPass(): 40 | return config.get('Settings', 'appleid_pass', fallback=None) 41 | 42 | 43 | def getConfigFile(): 44 | return getConfigPath() + '/' + CONFIG_FILE 45 | 46 | 47 | def getCertFile(): 48 | return getConfigPath() + '/' + config.get('Settings', 'cert', fallback=CERT_FILE) 49 | 50 | 51 | def getKeyFile(): 52 | return getConfigPath() + '/' + config.get('Settings', 'priv_key', fallback=KEY_FILE) 53 | 54 | 55 | def getEndpointUser(): 56 | return config.get('Settings', 'endpoint_user', fallback=None) 57 | 58 | 59 | def getEndpointPass(): 60 | return config.get('Settings', 'endpoint_pass', fallback=None) 61 | 62 | 63 | def getLogLevel(): 64 | logLevel = config.get('Settings', 'loglevel', fallback='INFO') 65 | return logging.getLevelName(logLevel) 66 | 67 | 68 | logging.basicConfig(level=getLogLevel(), 69 | format='%(asctime)s - %(levelname)s - %(message)s') 70 | # Suppress http-log 71 | logging.getLogger('urllib3').setLevel(logging.INFO) 72 | -------------------------------------------------------------------------------- /macless_haystack/lib/deployment/deployment_linux_hci.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:macless_haystack/deployment/code_block.dart'; 3 | import 'package:macless_haystack/deployment/deployment_details.dart'; 4 | import 'package:macless_haystack/deployment/hyperlink.dart'; 5 | 6 | class DeploymentInstructionsLinux extends StatelessWidget { 7 | final String advertisementKey; 8 | 9 | /// Displays a deployment guide for the generic Linux HCI platform. 10 | const DeploymentInstructionsLinux({ 11 | super.key, 12 | this.advertisementKey = '', 13 | }); 14 | 15 | @override 16 | Widget build(BuildContext context) { 17 | return DeploymentDetails( 18 | title: 'Linux HCI Deployment', 19 | steps: [ 20 | const Step( 21 | title: Text('Requirements'), 22 | content: Text('Install the hcitool software on a Bluetooth ' 23 | 'Low Energy Linux device, for example a Raspberry Pi. ' 24 | 'Additionally Pyhton 3 needs to be installed.'), 25 | ), 26 | const Step( 27 | title: Text('Download'), 28 | content: Column( 29 | children: [ 30 | Text('Next download the python script that ' 31 | 'configures the HCI tool to send out BLE advertisements.'), 32 | Hyperlink( 33 | target: 34 | 'https://raw.githubusercontent.com/seemoo-lab/openhaystack/main/Firmware/Linux_HCI/HCI.py'), 35 | CodeBlock( 36 | text: 37 | 'curl -o HCI.py https://raw.githubusercontent.com/seemoo-lab/openhaystack/main/Firmware/Linux_HCI/HCI.py'), 38 | ], 39 | ), 40 | ), 41 | Step( 42 | title: const Text('Usage'), 43 | content: Column( 44 | children: [ 45 | const Text('To start the BLE advertisements run the script.'), 46 | CodeBlock(text: 'sudo python3 HCI.py --key $advertisementKey'), 47 | ], 48 | ), 49 | ), 50 | ], 51 | ); 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /macless_haystack/lib/history/location_popup.dart: -------------------------------------------------------------------------------- 1 | 2 | import 'package:universal_io/io.dart'; 3 | import 'package:flutter/material.dart'; 4 | import 'package:flutter_map/flutter_map.dart'; 5 | import 'package:intl/intl.dart'; 6 | import 'package:latlong2/latlong.dart'; 7 | 8 | class LocationPopup extends Marker { 9 | /// The location to display. 10 | final LatLng location; 11 | 12 | /// The time stamp the location was recorded. 13 | final DateTime time; 14 | final DateTime end; 15 | final BuildContext ctx; 16 | 17 | /// Displays a small popup window with the coordinates at [location] and 18 | /// the [time] in a human readable format. 19 | LocationPopup( 20 | {super.key, 21 | required this.location, 22 | required this.time, 23 | required this.end, 24 | required this.ctx}) 25 | : super( 26 | width: 250, 27 | height: 150, 28 | point: location, 29 | child: Padding( 30 | padding: const EdgeInsets.only(bottom: 80), 31 | child: InkWell( 32 | onTap: () { 33 | /* NOOP */ 34 | }, 35 | child: Card( 36 | child: Column( 37 | crossAxisAlignment: CrossAxisAlignment.center, 38 | children: [ 39 | Expanded( 40 | child: Center( 41 | child: Text( 42 | '${DateFormat.Md(Platform.localeName).format(time)} ${DateFormat.jm(Platform.localeName).format(time)} - ${DateFormat.Md(Platform.localeName).format(end)} ${DateFormat.jm(Platform.localeName).format(end)}', 43 | ))), 44 | Expanded( 45 | child: Center( 46 | child: Text( 47 | 'Lat: ${location.round(decimals: 2).latitude}, ' 48 | 'Lng: ${location.round(decimals: 2).longitude}', 49 | ))), 50 | ], 51 | ), 52 | ), 53 | ), 54 | ), 55 | rotate: true, 56 | ); 57 | } 58 | -------------------------------------------------------------------------------- /macless_haystack/android/app/build.gradle: -------------------------------------------------------------------------------- 1 | plugins { 2 | id "com.android.application" 3 | id "kotlin-android" 4 | id "dev.flutter.flutter-gradle-plugin" 5 | } 6 | 7 | 8 | def localProperties = new Properties() 9 | def localPropertiesFile = rootProject.file('local.properties') 10 | if (localPropertiesFile.exists()) { 11 | localPropertiesFile.withReader('UTF-8') { reader -> 12 | localProperties.load(reader) 13 | } 14 | } 15 | 16 | 17 | def flutterVersionCode = localProperties.getProperty('flutter.versionCode') 18 | if (flutterVersionCode == null) { 19 | flutterVersionCode = '1' 20 | } 21 | 22 | def flutterVersionName = localProperties.getProperty('flutter.versionName') 23 | if (flutterVersionName == null) { 24 | flutterVersionName = '1.0' 25 | } 26 | 27 | 28 | android { 29 | compileSdk 36 30 | 31 | compileOptions { 32 | sourceCompatibility JavaVersion.VERSION_17 33 | targetCompatibility JavaVersion.VERSION_17 34 | } 35 | 36 | kotlinOptions { 37 | jvmTarget = '17' 38 | } 39 | 40 | sourceSets { 41 | main.java.srcDirs += 'src/main/kotlin' 42 | } 43 | 44 | defaultConfig { 45 | applicationId "de.dchristl.headlesshaystack" 46 | // You can update the following values to match your application needs. 47 | // For more information, see: https://docs.flutter.dev/deployment/android#reviewing-the-gradle-build-configuration. 48 | minSdkVersion 24 49 | targetSdkVersion 36 50 | versionCode flutterVersionCode.toInteger() 51 | versionName flutterVersionName 52 | } 53 | signingConfigs { 54 | debug { 55 | keyAlias 'androiddebugkey' 56 | keyPassword 'android' 57 | storeFile file('debug.store') 58 | storePassword 'android' 59 | } 60 | } 61 | buildTypes { 62 | release { 63 | // Signing with the debug keys for now, so `flutter run --release` works. 64 | signingConfig signingConfigs.debug 65 | 66 | } 67 | } 68 | namespace 'de.dchristl.headlesshaystack' 69 | } 70 | 71 | flutter { 72 | source '../..' 73 | } 74 | 75 | -------------------------------------------------------------------------------- /macless_haystack/lib/dashboard/accessory_map_list_vert.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:flutter_map/flutter_map.dart'; 3 | 4 | import 'package:provider/provider.dart'; 5 | import 'package:macless_haystack/accessory/accessory_list.dart'; 6 | import 'package:macless_haystack/accessory/accessory_registry.dart'; 7 | import 'package:macless_haystack/location/location_model.dart'; 8 | import 'package:macless_haystack/map/map.dart'; 9 | import 'package:latlong2/latlong.dart'; 10 | 11 | import '../callbacks.dart'; 12 | 13 | class AccessoryMapListVertical extends StatefulWidget { 14 | final LoadLocationUpdatesCallback loadLocationUpdates; 15 | final SaveOrderUpdatesCallback saveOrderUpdatesCallback; 16 | 17 | /// Displays a map view and the accessory list in a vertical alignment. 18 | const AccessoryMapListVertical({ 19 | super.key, 20 | required this.loadLocationUpdates, 21 | required this.saveOrderUpdatesCallback, 22 | }); 23 | 24 | @override 25 | State createState() => 26 | _AccessoryMapListVerticalState(); 27 | } 28 | 29 | class _AccessoryMapListVerticalState extends State { 30 | final MapController _mapController = MapController(); 31 | 32 | void _centerPoint(LatLng point) { 33 | _mapController 34 | .fitCamera(CameraFit.bounds(bounds: LatLngBounds.fromPoints([point]))); 35 | } 36 | 37 | @override 38 | Widget build(BuildContext context) { 39 | return Consumer2( 40 | builder: (BuildContext context, AccessoryRegistry accessoryRegistry, 41 | LocationModel locationModel, Widget? child) { 42 | return Column( 43 | children: [ 44 | Flexible( 45 | fit: FlexFit.tight, 46 | child: AccessoryMap( 47 | mapController: _mapController, 48 | ), 49 | ), 50 | Flexible( 51 | fit: FlexFit.tight, 52 | child: AccessoryList( 53 | loadLocationUpdates: widget.loadLocationUpdates, 54 | saveOrderUpdatesCallback: widget.saveOrderUpdatesCallback, 55 | centerOnPoint: _centerPoint, 56 | ), 57 | ), 58 | ], 59 | ); 60 | }, 61 | ); 62 | } 63 | } 64 | -------------------------------------------------------------------------------- /macless_haystack/lib/item_management/item_management.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:provider/provider.dart'; 3 | import 'package:macless_haystack/accessory/accessory_detail.dart'; 4 | import 'package:macless_haystack/accessory/accessory_icon.dart'; 5 | import 'package:macless_haystack/accessory/no_accessories.dart'; 6 | import 'package:macless_haystack/item_management/item_export.dart'; 7 | import 'package:macless_haystack/accessory/accessory_registry.dart'; 8 | import 'package:intl/intl.dart'; 9 | 10 | class KeyManagement extends StatelessWidget { 11 | /// Displays a list of all accessories. 12 | /// 13 | /// Each accessory can be exported and is linked to a detail page. 14 | const KeyManagement({ 15 | super.key, 16 | }); 17 | 18 | @override 19 | Widget build(BuildContext context) { 20 | return Consumer( 21 | builder: (context, accessoryRegistry, child) { 22 | var accessories = accessoryRegistry.accessories; 23 | 24 | if (accessories.isEmpty) { 25 | return const NoAccessoriesPlaceholder(); 26 | } 27 | 28 | return Scrollbar( 29 | child: ListView( 30 | children: accessories.map((accessory) { 31 | String lastSeen = accessory.datePublished != null && 32 | accessory.datePublished != DateTime(1970) 33 | ? DateFormat('dd.MM.yyyy kk:mm') 34 | .format(accessory.datePublished!) 35 | : 'Never'; 36 | return Material( 37 | child: ListTile( 38 | onTap: () { 39 | Navigator.push( 40 | context, 41 | MaterialPageRoute( 42 | builder: (context) => AccessoryDetail( 43 | accessory: accessory, 44 | )), 45 | ); 46 | }, 47 | dense: true, 48 | title: Text(accessory.name), 49 | subtitle: Text('Last seen: $lastSeen'), 50 | leading: AccessoryIcon( 51 | icon: accessory.icon, 52 | color: accessory.color, 53 | ), 54 | trailing: ItemExportMenu(accessory: accessory), 55 | )); 56 | }).toList(), 57 | ), 58 | ); 59 | }, 60 | ); 61 | } 62 | } 63 | -------------------------------------------------------------------------------- /macless_haystack/lib/preferences/user_preferences_model.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/foundation.dart'; 2 | import 'package:shared_preferences/shared_preferences.dart'; 3 | 4 | const introductionShownKey = 'INTRODUCTION_SHOWN'; 5 | const locationPreferenceKnownKey = 'LOCATION_PREFERENCE_KNOWN'; 6 | const locationAccessWantedKey = 'LOCATION_PREFERENCE_WANTED'; 7 | const fetchLocationOnStartupKey = 'FETCH_LOCATION_ON_STARTUP'; 8 | const endpointUrl = 'HAYSTACK_URL'; 9 | const String endpointUser = 'HAYSTACK_USER'; 10 | const String endpointPass = 'HAYSTACK_PASS'; 11 | const String numberOfDaysToFetch = 'NUMBER_OF_DAYS'; 12 | 13 | class UserPreferences extends ChangeNotifier { 14 | /// If these settings are initialized. 15 | bool initialized = false; 16 | 17 | /// The shared preferences storage. 18 | SharedPreferences? _prefs; 19 | 20 | /// Manages information about the users preferences. 21 | UserPreferences() { 22 | _initializeAsync(); 23 | } 24 | 25 | /// Initialize shared preferences access 26 | void _initializeAsync() async { 27 | _prefs = await SharedPreferences.getInstance(); 28 | 29 | // For Debugging: 30 | // await prefs.clear(); 31 | 32 | initialized = true; 33 | notifyListeners(); 34 | } 35 | 36 | /// Returns if the introduction should be shown. 37 | bool? shouldShowIntroduction() { 38 | if (_prefs == null) { 39 | return null; 40 | } else { 41 | if (!_prefs!.containsKey(introductionShownKey)) { 42 | return true; // Initial start of the app 43 | } 44 | return _prefs?.getBool(introductionShownKey); 45 | } 46 | } 47 | 48 | /// Returns if the user's locaiton preference is known. 49 | bool? get locationPreferenceKnown { 50 | return _prefs?.getBool(locationPreferenceKnownKey) ?? false; 51 | } 52 | 53 | /// Returns if the user desires location access. 54 | bool? get locationAccessWanted { 55 | return _prefs?.getBool(locationAccessWantedKey); 56 | } 57 | 58 | /// Updates the location access preference of the user. 59 | Future setLocationPreference(bool locationAccessWanted) async { 60 | _prefs ??= await SharedPreferences.getInstance(); 61 | var success = await _prefs!.setBool(locationPreferenceKnownKey, true); 62 | if (!success) { 63 | return Future.value(false); 64 | } else { 65 | var result = 66 | await _prefs!.setBool(locationAccessWantedKey, locationAccessWanted); 67 | notifyListeners(); 68 | return result; 69 | } 70 | } 71 | } 72 | -------------------------------------------------------------------------------- /macless_haystack/lib/main.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:macless_haystack/dashboard/dashboard.dart'; 3 | import 'package:provider/provider.dart'; 4 | import 'package:macless_haystack/accessory/accessory_registry.dart'; 5 | import 'package:macless_haystack/location/location_model.dart'; 6 | import 'package:macless_haystack/preferences/user_preferences_model.dart'; 7 | import 'package:macless_haystack/splashscreen.dart'; 8 | import 'package:flutter_settings_screens/flutter_settings_screens.dart'; 9 | import 'package:intl/date_symbol_data_local.dart'; 10 | 11 | void main() { 12 | Settings.init(); 13 | initializeDateFormatting(); 14 | runApp(const MyApp()); 15 | } 16 | 17 | class MyApp extends StatelessWidget { 18 | const MyApp({super.key}); 19 | 20 | @override 21 | Widget build(BuildContext context) { 22 | return MultiProvider( 23 | providers: [ 24 | ChangeNotifierProvider(create: (ctx) => AccessoryRegistry()), 25 | ChangeNotifierProvider(create: (ctx) => UserPreferences()), 26 | ChangeNotifierProvider(create: (ctx) => LocationModel()), 27 | ], 28 | child: MaterialApp( 29 | title: 'Macless Haystack', 30 | theme: ThemeData(primarySwatch: Colors.blue), 31 | darkTheme: ThemeData.dark(), 32 | home: const AppLayout(), 33 | ), 34 | ); 35 | } 36 | } 37 | 38 | class AppLayout extends StatefulWidget { 39 | const AppLayout({super.key}); 40 | 41 | @override 42 | State createState() => _AppLayoutState(); 43 | } 44 | 45 | class _AppLayoutState extends State { 46 | @override 47 | initState() { 48 | super.initState(); 49 | 50 | var accessoryRegistry = 51 | Provider.of(context, listen: false); 52 | accessoryRegistry.loadAccessories(); 53 | } 54 | 55 | @override 56 | void dispose() { 57 | super.dispose(); 58 | } 59 | 60 | @override 61 | void didChangeDependencies() { 62 | // Precache logo for faster load times (e.g. on the splash screen) 63 | precacheImage(const AssetImage('assets/OpenHaystackIcon.png'), context); 64 | super.didChangeDependencies(); 65 | } 66 | 67 | @override 68 | Widget build(BuildContext context) { 69 | bool isInitialized = context.watch().initialized; 70 | bool isLoading = context.watch().loading; 71 | if (!isInitialized || isLoading) { 72 | return const Splashscreen(); 73 | } 74 | 75 | return const Dashboard(); 76 | } 77 | } 78 | -------------------------------------------------------------------------------- /macless_haystack/README.md: -------------------------------------------------------------------------------- 1 | # OpenHaystack Mobile 2 | Porting OpenHaystack to Mobile 3 | 4 | # About OpenHaystack 5 | OpenHaystack is a project that allows location tracking of Bluetooth Low Energy (BLE) devices over Apples Find My Network. 6 | 7 | # Development 8 | This project is written in [Dart](https://dart.dev/), using the cross platform development framework [Flutter](https://flutter.dev/). This allows the creation of apps for all major platforms using a single code base. 9 | 10 | ## Requisites 11 | To develop and build the project the following tools are needed and should be installed. 12 | 13 | - [Flutter SDK](https://docs.flutter.dev/get-started/install) 14 | - [Xcode](https://developer.apple.com/xcode/) (for iOS) 15 | - [Android SDK / Studio](https://developer.android.com/studio/) (for Android) 16 | - (optional) IDE Plugin (e.g. for [VS Code](https://marketplace.visualstudio.com/items?itemName=Dart-Code.flutter)) 17 | 18 | To check the installation run `flutter doctor`. Before continuing review all displayed errors. 19 | 20 | 21 | ## Getting Started 22 | First the necessary dependencies need to be installed. The IDE plugin may take care of this automatically. 23 | ```bash 24 | $ flutter pub get 25 | ``` 26 | 27 | Then set the location proxy server URL in [reports_fetcher.dart](lib/findMy/reports_fetcher.dart) (replace `https://add-your-proxy-server-here/getLocationReports` with your custom URL). 28 | 29 | To run the debug version of the app start a supported emulator and run 30 | ```bash 31 | $ flutter run 32 | ``` 33 | 34 | When the app is running a new key pair can be created / imported in the app. 35 | 36 | ## Project Structure 37 | The project follows the default structure for flutter applications. The `android`, `linux` and `web` folders contain native projects for the specified platform. Native code can be added here for example to access special APIs. 38 | 39 | The business logic and UI can be found in the `lib` folder. This folder is furthermore separated into modules containing code regarding a common aspect. 40 | The business logic for accessing and decrypting the location reports is separated in the `findMy` folder for easier reuse. 41 | 42 | ## Building 43 | This project currently supports Android, Linux and web targets. 44 | If you are building the project for the first time, you need to run 45 | ```bash 46 | $ flutter pub run flutter_launcher_icons:main 47 | ``` 48 | to create the icons and then, to create a distributable application package run 49 | ```bash 50 | $ flutter build [linux|apk|web] 51 | ``` 52 | The resulting build artifacts can be found in the `build` folder. To deploy the artifacts to a device consult the platform specific documentation. 53 | -------------------------------------------------------------------------------- /macless_haystack/lib/placeholder/text_placeholder.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | 3 | class TextPlaceholder extends StatefulWidget { 4 | final double maxWidth; 5 | final double? width; 6 | final double? height; 7 | final bool animated; 8 | 9 | /// Displays a placeholder for the actual text, occupying the same layout space. 10 | /// 11 | /// An optional loading animation is provided. 12 | const TextPlaceholder({ 13 | super.key, 14 | this.maxWidth = double.infinity, 15 | this.width, 16 | this.height = 10, 17 | this.animated = true, 18 | }); 19 | @override 20 | State createState() { 21 | return _TextPlaceholderState(); 22 | } 23 | } 24 | 25 | class _TextPlaceholderState extends State 26 | with SingleTickerProviderStateMixin { 27 | late Animation animation; 28 | late AnimationController controller; 29 | 30 | @override 31 | void initState() { 32 | super.initState(); 33 | 34 | controller = AnimationController( 35 | vsync: this, 36 | duration: const Duration(seconds: 1), 37 | ); 38 | animation = Tween(begin: 0, end: 1).animate(controller) 39 | ..addListener(() { 40 | setState(() {}); // Trigger UI update with current value 41 | }) 42 | ..addStatusListener((status) { 43 | if (status == AnimationStatus.completed) { 44 | controller.reverse(); 45 | } else if (status == AnimationStatus.dismissed) { 46 | controller.forward(); 47 | } 48 | }); 49 | 50 | controller.forward(); 51 | } 52 | 53 | @override 54 | void dispose() { 55 | controller.dispose(); 56 | super.dispose(); 57 | } 58 | 59 | @override 60 | Widget build(BuildContext context) { 61 | return Container( 62 | constraints: BoxConstraints(maxWidth: widget.maxWidth), 63 | height: widget.height, 64 | width: widget.width, 65 | decoration: BoxDecoration( 66 | gradient: widget.animated 67 | ? LinearGradient( 68 | begin: Alignment.centerLeft, 69 | end: Alignment.centerRight, 70 | stops: [0.0, animation.value, 1.0], 71 | colors: const [ 72 | Color.fromARGB(255, 200, 200, 200), 73 | Color.fromARGB(255, 230, 230, 230), 74 | Color.fromARGB(255, 200, 200, 200) 75 | ], 76 | ) 77 | : null, 78 | color: 79 | widget.animated ? null : const Color.fromARGB(255, 200, 200, 200), 80 | borderRadius: const BorderRadius.all(Radius.circular(8)), 81 | ), 82 | ); 83 | } 84 | } 85 | -------------------------------------------------------------------------------- /endpoint/register/apple_cryptography.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | 3 | import hashlib 4 | import json 5 | import logging 6 | import os 7 | import struct 8 | import sys 9 | 10 | from cryptography.hazmat.backends import default_backend 11 | from cryptography.hazmat.primitives.ciphers import Cipher 12 | 13 | from endpoint import mh_config 14 | from .pypush_gsa_icloud import icloud_login_mobileme 15 | 16 | logger = logging.getLogger() 17 | 18 | 19 | def sha256(data): 20 | digest = hashlib.new("sha256") 21 | digest.update(data) 22 | return digest.digest() 23 | 24 | 25 | def decrypt(enc_data, algorithm_dkey, mode): 26 | decryptor = Cipher(algorithm_dkey, mode, default_backend()).decryptor() 27 | return decryptor.update(enc_data) + decryptor.finalize() 28 | 29 | 30 | def decode_tag(data): 31 | latitude = struct.unpack(">i", data[0:4])[0] / 10000000.0 32 | longitude = struct.unpack(">i", data[4:8])[0] / 10000000.0 33 | confidence = int.from_bytes(data[8:9]) 34 | status = int.from_bytes(data[9:10]) 35 | return {'lat': latitude, 'lon': longitude, 'conf': confidence, 'status': status} 36 | 37 | 38 | def getAuth(regenerate=False): 39 | if os.path.exists(mh_config.getConfigFile()) and not regenerate: 40 | with open(mh_config.getConfigFile(), "r") as f: 41 | j = json.load(f) 42 | else: 43 | logger.info('Trying to login') 44 | mobileme = icloud_login_mobileme( 45 | username=mh_config.getUser(), password=mh_config.getPass()) 46 | 47 | logger.debug('Answer from icloud login') 48 | logger.debug(mobileme) 49 | status = mobileme['delegates']['com.apple.mobileme']['status'] 50 | if status == 0: 51 | j = {'dsid': mobileme['dsid'], 'searchPartyToken': mobileme['delegates'] 52 | ['com.apple.mobileme']['service-data']['tokens']['searchPartyToken']} 53 | with open(mh_config.getConfigFile(), "w") as f: 54 | json.dump(j, f) 55 | else: 56 | msg = mobileme['delegates']['com.apple.mobileme']['status-message'] 57 | logger.error('Invalid status: ' + str(status)) 58 | logger.error('Error message: ' + msg) 59 | if 'blocking' in msg: 60 | logger.error( 61 | 'It seems your account score is not high enough. Log in to https://appleid.apple.com/ and add your credit card (nothing will be charged) or additional data to increase it.') 62 | logger.error('Unable to proceed, program will be terminated.') 63 | 64 | sys.exit() 65 | return (j['dsid'], j['searchPartyToken']) 66 | 67 | 68 | def registerDevice(): 69 | 70 | logger.info(f'Trying to register new device.') 71 | getAuth(regenerate=True) 72 | -------------------------------------------------------------------------------- /macless_haystack/lib/deployment/deployment_nrf51.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:macless_haystack/deployment/code_block.dart'; 3 | import 'package:macless_haystack/deployment/deployment_details.dart'; 4 | import 'package:macless_haystack/deployment/hyperlink.dart'; 5 | 6 | class DeploymentInstructionsNRF51 extends StatelessWidget { 7 | final String advertisementKey; 8 | 9 | /// Displays a deployment guide for the NRF51 platform. 10 | const DeploymentInstructionsNRF51({ 11 | super.key, 12 | this.advertisementKey = '', 13 | }); 14 | 15 | @override 16 | Widget build(BuildContext context) { 17 | return DeploymentDetails( 18 | title: 'nRF51822 Deployment', 19 | steps: [ 20 | const Step( 21 | title: Text('Requirements'), 22 | content: Text('To build the firmware the GNU Arm Embedded ' 23 | 'Toolchain is required.'), 24 | ), 25 | const Step( 26 | title: Text('Download'), 27 | content: Column( 28 | children: [ 29 | Text('Download the firmware source code from GitHub ' 30 | 'and navigate to the given folder.'), 31 | Hyperlink(target: 'https://github.com/seemoo-lab/openhaystack'), 32 | CodeBlock( 33 | text: 34 | 'git clone https://github.com/seemoo-lab/openhaystack.git && cd openhaystack/Firmware/Microbit_v1'), 35 | ], 36 | ), 37 | ), 38 | Step( 39 | title: const Text('Build'), 40 | content: Column( 41 | children: [ 42 | const Text('Replace the public_key in main.c (initially ' 43 | 'OFFLINEFINEINGPUBLICKEYHERE!) with the actual ' 44 | 'advertisement key. Then execute make to create the ' 45 | 'firmware.'), 46 | CodeBlock( 47 | text: 'static char public_key[28] = "$advertisementKey";'), 48 | const CodeBlock(text: 'make'), 49 | ], 50 | ), 51 | ), 52 | const Step( 53 | title: Text('Firmware Deployment'), 54 | content: Column( 55 | children: [ 56 | Text('If the firmware is built successfully it can ' 57 | 'be deployed to the microcontroller with the following ' 58 | 'command.'), 59 | Text( 60 | 'Please fill in the volume of your microcontroller.', 61 | style: TextStyle( 62 | fontWeight: FontWeight.bold, 63 | ), 64 | ), 65 | CodeBlock(text: 'make install DEPLOY_PATH=/Volumes/MICROBIT'), 66 | ], 67 | ), 68 | ), 69 | ], 70 | ); 71 | } 72 | } 73 | -------------------------------------------------------------------------------- /macless_haystack/lib/deployment/deployment_esp32.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:macless_haystack/deployment/code_block.dart'; 3 | import 'package:macless_haystack/deployment/deployment_details.dart'; 4 | import 'package:macless_haystack/deployment/hyperlink.dart'; 5 | 6 | class DeploymentInstructionsESP32 extends StatelessWidget { 7 | final String advertisementKey; 8 | 9 | /// Displays a deployment guide for the ESP32 platform. 10 | const DeploymentInstructionsESP32({ 11 | super.key, 12 | this.advertisementKey = '', 13 | }); 14 | 15 | @override 16 | Widget build(BuildContext context) { 17 | return DeploymentDetails( 18 | title: 'ESP32 Deployment', 19 | steps: [ 20 | const Step( 21 | title: Text('Requirements'), 22 | content: Text('To build the firmware for the ESP32 Espressif\'s ' 23 | 'IoT Development Framework (ESP-IDF) is required. Additionally ' 24 | 'Python 3 and the venv module need to be installed.'), 25 | ), 26 | const Step( 27 | title: Text('Download'), 28 | content: Column( 29 | children: [ 30 | Text('Download the firmware source code from GitHub ' 31 | 'and navigate to the given folder.'), 32 | Hyperlink(target: 'https://github.com/seemoo-lab/openhaystack'), 33 | CodeBlock( 34 | text: 35 | 'git clone https://github.com/seemoo-lab/openhaystack.git && cd openhaystack/Firmware/ESP32'), 36 | ], 37 | ), 38 | ), 39 | const Step( 40 | title: Text('Build'), 41 | content: Column( 42 | children: [ 43 | Text( 44 | 'Execute the ESP-IDF build command to create the ESP32 firmware.'), 45 | CodeBlock(text: 'idf.py build'), 46 | ], 47 | ), 48 | ), 49 | Step( 50 | title: const Text('Firmware Deployment'), 51 | content: Column( 52 | children: [ 53 | const Text('If the firmware is built successfully it can ' 54 | 'be flashed onto the ESP32. This action is performed by ' 55 | 'the flash_esp32.sh script that is provided with the ' 56 | 'advertisement key of the newly created accessory.'), 57 | const Text( 58 | 'Please fill in the serial port of your microcontroller.', 59 | style: TextStyle( 60 | fontWeight: FontWeight.bold, 61 | ), 62 | ), 63 | CodeBlock( 64 | text: 65 | './flash_esp32.sh -p /dev/yourSerialPort "$advertisementKey"'), 66 | ], 67 | ), 68 | ), 69 | ], 70 | ); 71 | } 72 | } 73 | -------------------------------------------------------------------------------- /macless_haystack/lib/accessory/accessory_icon_selector.dart: -------------------------------------------------------------------------------- 1 | import 'dart:math'; 2 | 3 | import 'package:flutter/material.dart'; 4 | import 'package:macless_haystack/accessory/accessory_icon_model.dart'; 5 | 6 | typedef IconChangeListener = void Function(String? newValue); 7 | 8 | class AccessoryIconSelector extends StatelessWidget { 9 | /// The existing icon used previously. 10 | final String icon; 11 | /// The existing color used previously. 12 | final Color color; 13 | /// A callback being called when the icon changes. 14 | final IconChangeListener iconChanged; 15 | 16 | /// This show an icon selector. 17 | /// 18 | /// The icon can be selected from a list of available icons. 19 | /// The icons are handled by the cupertino icon names. 20 | const AccessoryIconSelector({ 21 | super.key, 22 | required this.icon, 23 | required this.color, 24 | required this.iconChanged, 25 | }); 26 | 27 | /// Displays the icon selector with the [currentIcon] preselected in the [highlighColor]. 28 | /// 29 | /// The selected icon as a cupertino icon name is returned if the user selects an icon. 30 | /// Otherwise the selection is discarded and a null value is returned. 31 | static Future showIconSelection(BuildContext context, String currentIcon, Color highlighColor) async { 32 | return await showDialog( 33 | context: context, 34 | builder: (BuildContext context) { 35 | return LayoutBuilder( 36 | builder: (context, constraints) => Dialog( 37 | child: GridView.count( 38 | primary: false, 39 | padding: const EdgeInsets.all(20), 40 | crossAxisSpacing: 10, 41 | mainAxisSpacing: 10, 42 | shrinkWrap: true, 43 | crossAxisCount: min((constraints.maxWidth / 80).floor(), 8), 44 | semanticChildCount: AccessoryIconModel.icons.length, 45 | children: AccessoryIconModel.icons 46 | .map((value) => IconButton( 47 | icon: Icon(AccessoryIconModel.mapIcon(value)), 48 | color: value == currentIcon ? highlighColor : null, 49 | onPressed: () { Navigator.pop(context, value); }, 50 | )).toList(), 51 | ), 52 | ), 53 | ); 54 | } 55 | ); 56 | } 57 | 58 | @override 59 | Widget build(BuildContext context) { 60 | return Container( 61 | decoration: const BoxDecoration( 62 | color: Color.fromARGB(255, 200, 200, 200), 63 | shape: BoxShape.circle, 64 | ), 65 | child: IconButton( 66 | onPressed: () async { 67 | String? selectedIcon = await showIconSelection(context, icon, color); 68 | if (selectedIcon != null) { 69 | iconChanged(selectedIcon); 70 | } 71 | }, 72 | icon: Icon(AccessoryIconModel.mapIcon(icon)), 73 | ), 74 | ); 75 | } 76 | } 77 | -------------------------------------------------------------------------------- /firmware/nrf5x/simple_board.h: -------------------------------------------------------------------------------- 1 | /* Copyright (c) 2016 Nordic Semiconductor. All Rights Reserved. 2 | * 3 | * The information contained herein is property of Nordic Semiconductor ASA. 4 | * Terms and conditions of usage are described in detail in NORDIC 5 | * SEMICONDUCTOR STANDARD SOFTWARE LICENSE AGREEMENT. 6 | * 7 | * Licensees are granted free, non-transferable use of the information. NO 8 | * WARRANTY of ANY KIND is provided. This heading must NOT be removed from 9 | * the file. 10 | * 11 | */ 12 | #ifndef SIMPLE_BOARD_H 13 | #define SIMPLE_BOARD_H 14 | 15 | #ifdef __cplusplus 16 | extern "C" { 17 | #endif 18 | 19 | #define NRF_CLOCK_LFCLKSRC {.source = NRF_CLOCK_LF_SRC_RC, \ 20 | .rc_ctiv = 16, \ 21 | .rc_temp_ctiv = 2, \ 22 | .xtal_accuracy = 1} 23 | 24 | // LEDs definitions 25 | #define LEDS_NUMBER 0 26 | #define LEDS_ACTIVE_STATE 0 27 | 28 | #define LEDS_LIST { } 29 | 30 | #define LEDS_INV_MASK 0 31 | 32 | #define BUTTONS_NUMBER 0 33 | 34 | #define BUTTON_PULL NRF_GPIO_PIN_PULLDOWN 35 | 36 | #define BUTTONS_ACTIVE_STATE 0 37 | 38 | #define BUTTONS_LIST { } 39 | 40 | #define CTS_PIN_NUMBER UART_PIN_DISCONNECTED 41 | #define RTS_PIN_NUMBER UART_PIN_DISCONNECTED 42 | #define HWFC false 43 | 44 | // Arduino board mappings 45 | #define ARDUINO_SCL_PIN 27 // SCL signal pin 46 | #define ARDUINO_SDA_PIN 26 // SDA signal pin 47 | #define ARDUINO_AREF_PIN 2 // Aref pin 48 | #define ARDUINO_13_PIN 25 // Digital pin 13 49 | #define ARDUINO_12_PIN 24 // Digital pin 12 50 | #define ARDUINO_11_PIN 23 // Digital pin 11 51 | #define ARDUINO_10_PIN 22 // Digital pin 10 52 | #define ARDUINO_9_PIN 20 // Digital pin 9 53 | #define ARDUINO_8_PIN 19 // Digital pin 8 54 | 55 | #define ARDUINO_7_PIN 18 // Digital pin 7 56 | #define ARDUINO_6_PIN 17 // Digital pin 6 57 | #define ARDUINO_5_PIN 16 // Digital pin 5 58 | #define ARDUINO_4_PIN 15 // Digital pin 4 59 | #define ARDUINO_3_PIN 14 // Digital pin 3 60 | #define ARDUINO_2_PIN 13 // Digital pin 2 61 | #define ARDUINO_1_PIN 12 // Digital pin 1 62 | #define ARDUINO_0_PIN 11 // Digital pin 0 63 | 64 | #define ARDUINO_A0_PIN 3 // Analog channel 0 65 | #define ARDUINO_A1_PIN 4 // Analog channel 1 66 | #define ARDUINO_A2_PIN 28 // Analog channel 2 67 | #define ARDUINO_A3_PIN 29 // Analog channel 3 68 | #define ARDUINO_A4_PIN 30 // Analog channel 4 69 | #define ARDUINO_A5_PIN 31 // Analog channel 5 70 | 71 | 72 | #ifdef __cplusplus 73 | } 74 | #endif 75 | 76 | #endif // ARDUINO_PRIMO_H 77 | -------------------------------------------------------------------------------- /firmware/nrf5x/e104bt5032a_board.h: -------------------------------------------------------------------------------- 1 | /* Copyright (c) 2016 Nordic Semiconductor. All Rights Reserved. 2 | * 3 | * The information contained herein is property of Nordic Semiconductor ASA. 4 | * Terms and conditions of usage are described in detail in NORDIC 5 | * SEMICONDUCTOR STANDARD SOFTWARE LICENSE AGREEMENT. 6 | * 7 | * Licensees are granted free, non-transferable use of the information. NO 8 | * WARRANTY of ANY KIND is provided. This heading must NOT be removed from 9 | * the file. 10 | * 11 | */ 12 | #ifndef CUSTOM_BOARD_H 13 | #define CUSTOM_BOARD_H 14 | 15 | #ifdef __cplusplus 16 | extern "C" { 17 | #endif 18 | 19 | #define NRF_CLOCK_LFCLKSRC {.source = NRF_CLOCK_LF_SRC_RC, \ 20 | .rc_ctiv = 16, \ 21 | .rc_temp_ctiv = 2, \ 22 | .xtal_accuracy = 1} 23 | 24 | // LEDs definitions 25 | #define LEDS_NUMBER 0 26 | #define LEDS_ACTIVE_STATE 0 27 | 28 | #define LEDS_LIST { } 29 | 30 | #define LEDS_INV_MASK 0 31 | 32 | #define BUTTONS_NUMBER 0 33 | 34 | #define BUTTON_PULL NRF_GPIO_PIN_PULLDOWN 35 | 36 | #define BUTTONS_ACTIVE_STATE 0 37 | 38 | #define BUTTONS_LIST { } 39 | 40 | #define CTS_PIN_NUMBER UART_PIN_DISCONNECTED 41 | #define RTS_PIN_NUMBER UART_PIN_DISCONNECTED 42 | #define HWFC false 43 | 44 | // Arduino board mappings 45 | #define ARDUINO_SCL_PIN 27 // SCL signal pin 46 | #define ARDUINO_SDA_PIN 26 // SDA signal pin 47 | #define ARDUINO_AREF_PIN 2 // Aref pin 48 | #define ARDUINO_13_PIN 25 // Digital pin 13 49 | #define ARDUINO_12_PIN 24 // Digital pin 12 50 | #define ARDUINO_11_PIN 23 // Digital pin 11 51 | #define ARDUINO_10_PIN 22 // Digital pin 10 52 | #define ARDUINO_9_PIN 20 // Digital pin 9 53 | #define ARDUINO_8_PIN 19 // Digital pin 8 54 | 55 | #define ARDUINO_7_PIN 18 // Digital pin 7 56 | #define ARDUINO_6_PIN 17 // Digital pin 6 57 | #define ARDUINO_5_PIN 16 // Digital pin 5 58 | #define ARDUINO_4_PIN 15 // Digital pin 4 59 | #define ARDUINO_3_PIN 14 // Digital pin 3 60 | #define ARDUINO_2_PIN 13 // Digital pin 2 61 | #define ARDUINO_1_PIN 12 // Digital pin 1 62 | #define ARDUINO_0_PIN 11 // Digital pin 0 63 | 64 | #define ARDUINO_A0_PIN 3 // Analog channel 0 65 | #define ARDUINO_A1_PIN 4 // Analog channel 1 66 | #define ARDUINO_A2_PIN 28 // Analog channel 2 67 | #define ARDUINO_A3_PIN 29 // Analog channel 3 68 | #define ARDUINO_A4_PIN 30 // Analog channel 4 69 | #define ARDUINO_A5_PIN 31 // Analog channel 5 70 | 71 | 72 | #ifdef __cplusplus 73 | } 74 | #endif 75 | 76 | #endif // ARDUINO_PRIMO_H 77 | -------------------------------------------------------------------------------- /endpoint/data/rename_me.pem: -------------------------------------------------------------------------------- 1 | -----BEGIN CERTIFICATE----- 2 | MIIDGTCCAgGgAwIBAgIUMoCTzYoASTKRvlYk8R6StkETVJ8wDQYJKoZIhvcNAQEL 3 | BQAwHDEaMBgGA1UEAwwRaGVhZGxlc3NfaGF5c3RhY2swHhcNMjMwNzAzMTEwMjI3 4 | WhcNMzMwNjMwMTEwMjI3WjAcMRowGAYDVQQDDBFoZWFkbGVzc19oYXlzdGFjazCC 5 | ASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBALPAPB1uf9RdgAjsmRNGe4nh 6 | oNDTiGj7/V9Sn3p2NtG2YV8uD+V8zOeFrv1hEh3LqgCkaXA+8xu8y+CcA1stvlC3 7 | 6PfGHCEGTqZj3NQ96BBkRDuPepKHecwsWJrUDcFGrjEot9jImgMPTqocQHDQdiEe 8 | VN/0Ndh5ZOSf3gVJAitkrVbTiAKq6n3fmxh+vW28hQDVu/MKpSIQtk75d4nxq3SN 9 | gAvIRSXtg7q+weIYivnFGD2R6+DuB1Y+OXqBHhMXWZHt3DXkJHNDKzeQoyWpV/VR 10 | Rn9iw4iWVC1aMyjEXt0EL87tv5Y9A8AzQrU+5bUk1w//Vx54hh1lQQLn6rxTYLMC 11 | AwEAAaNTMFEwHQYDVR0OBBYEFES0Por2w1dhSw2nZTql8Pmdkco4MB8GA1UdIwQY 12 | MBaAFES0Por2w1dhSw2nZTql8Pmdkco4MA8GA1UdEwEB/wQFMAMBAf8wDQYJKoZI 13 | hvcNAQELBQADggEBADp8/v6LMsVwI6qRZv2kDt/WnWdKuj+qd9+RdIU8MyfIOwhy 14 | L4r1kLMK/bU6zYAKGs4a1E3CkH44oVz3z/+14gxjXikgxLKWlwZg3BLOlcUhcEcI 15 | 4LcqHpFbVgj8zKZJZykutxlOemApWm64qdCnP7tmrFiC64m3rgOzlzu4Wp35c02l 16 | W9D+kmqEXziHTMwBFUA+Z78DzuyndlmDP8vrJiP6ehPboKspSW70uW8k/dr+uQVj 17 | 4tkjQhMap1px/Go7sr/M5EVt/Z/cOq3NT3OgSP+8j30LLz5+4Ln/MHUBe974NUYb 18 | UL5AWi3WwG0oZY66eQsjMLgEVC7WcJcMUdtWIqk= 19 | -----END CERTIFICATE----- 20 | -----BEGIN PRIVATE KEY----- 21 | MIIEvAIBADANBgkqhkiG9w0BAQEFAASCBKYwggSiAgEAAoIBAQCzwDwdbn/UXYAI 22 | 7JkTRnuJ4aDQ04ho+/1fUp96djbRtmFfLg/lfMznha79YRIdy6oApGlwPvMbvMvg 23 | nANbLb5Qt+j3xhwhBk6mY9zUPegQZEQ7j3qSh3nMLFia1A3BRq4xKLfYyJoDD06q 24 | HEBw0HYhHlTf9DXYeWTkn94FSQIrZK1W04gCqup935sYfr1tvIUA1bvzCqUiELZO 25 | +XeJ8at0jYALyEUl7YO6vsHiGIr5xRg9kevg7gdWPjl6gR4TF1mR7dw15CRzQys3 26 | kKMlqVf1UUZ/YsOIllQtWjMoxF7dBC/O7b+WPQPAM0K1PuW1JNcP/1ceeIYdZUEC 27 | 5+q8U2CzAgMBAAECggEAHo2AylHjaXHhFuM8W6Hh0rxmrMgfyhR26zJsiBC63ad9 28 | 4oM+UXgGjh3Tch8DSMw637QSdfFFOkQ7dTife2qJjEfOUfs7AcQEL1UPDoZdQRgB 29 | 3wvSvkBHzBQHlX2QvvboGdP9d9wVDYsXCGQX8fUE9zkkHRLx6hMsOo4P218e/sfr 30 | njMcr9D4szi4+nIHPm7i/rbc9Fpda5z/YJdZisYiec0bJGCfuM1FmaahtQTc3wE8 31 | nmkCmIsa7cvP5RHj9T83D5vugIO7SkXiETMTxLIcYR/DvpJSiMeSoxf4vl6BA6sB 32 | rJNm9yqzdEas2LYP96P8tWiFiLbZhin614VKuPEBOQKBgQDZnbhU++XbxJBErTOv 33 | PKaD1VcPAqyp/tZWiRovx+q8v4q8JUb3XTftBGCxy1KyzlJPFVcnbCEWxlTIqiCJ 34 | /gad2uMC6IUuvnL8mfpkedA8yljinEMGamlpyWN93P5aJobuuZcIT7WMHSqpfj05 35 | nnQHwEnWskl19y0hWXbpiQ2g+wKBgQDTdL4+HdbeHTA3YQgp//60BPhPxJGl7Gtn 36 | p0pV5mnz+5Wd74rIBe5+HNjd2TFgWS28VIjVrfNLzNRojg7m/t96MBuiVQ2Ew8Iw 37 | rpb4LnEmCFh9S/l75QewFUjnjFhgnG2SuPPE4PuIEamaQGthzAaIkN38CJL67+/2 38 | Wl8KCu9hqQKBgE/OGWWBI2CfC1FEO2oOUfqS6GRm1K9a93uwt9vB8wHZNKWe/hGF 39 | LBdNvbA2IlPUejbqWpXof5H8lecpDNnOQNrvBMVyRDVKPp0IUt06FvXUNxiTubjG 40 | mXXkFwp3WwfwjRdLFGpF4QxLPfP+ibFxvJeDGxETPQF37iMGzicCze61AoGAIX9m 41 | +9QiS8F39+3pKy4gnUgERi6vnAdd5Ge+AOmlcz8xrnlaFOHLrDLJsCtOBSNbeXAy 42 | 1Rbfaeyi4YVPmwxZPrQMTKUIpTWVt7yQsfQ5fHrp+b9lYFkh5KUajYPQJE3jldCy 43 | 3Ud+0UlrAsKdwDpf9pZsdBavog0MiIw8bgzZazECgYBtsWVF39D9BZPAYs3UgZqL 44 | IzyxI34JcGDeOj/iiU7F1d0CjCEOYJsZBBhHhJfU2EBfF97h9tbJAFt6AdNTFOIB 45 | J94eSsCTgOq5qNBWKvx5mVXAqBaWC8v1BRghnoMXolc1kVJk0K0m7/GdAOl4ARTS 46 | xM4RbRkKVlks0jj7K2ZFig== 47 | -----END PRIVATE KEY----- 48 | -------------------------------------------------------------------------------- /firmware/nrf5x/README.md: -------------------------------------------------------------------------------- 1 | ## Macless Haystack Firmware for NRF51 and NRF52 2 | 3 | This project contains an battery-optimized firmware for the Nordic NRF5x chips from [acalatrava](https://github.com/acalatrava/openhaystack-firmware). So all credits goes to him. 4 | 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/). 5 | This firmware consumes more power when more than 1 key is used. The controller wakes up every 30 minutes and switches the key. 6 | 7 | > [!WARNING] 8 | > Currently, only the NRF51 build has been tested, and the NRF52 build has not been tested yet, but it should work. Feedback on this is welcome. It has been tested with [this](https://www.aliexpress.com/item/1005003671695188.html?spm=a2g0o.order_list.order_list_main.55.72491802ZTaXKp) or [this](https://de.aliexpress.com/item/32860266105.html?spm=a2g0o.order_list.order_list_main.50.72491802ZTaXKp&gatewayAdapt=glo2deu) beacon from Aliexpress. 9 | 10 | ### Deploy the Firmware 11 | 12 | - Download firmware for your device 13 | - Copy your previously generated PREFIX_keyfile in the same folder 14 | - Patch the firmware with your keyfile (Change the path if necessary!) 15 | 16 | Note that, in the commands below, you only have to change the `PREFIX_keyfile` part, the `OFFLINEFINDINGPUBLICKEYHERE` should stay as it is, as that's a marker for the binary. 17 | 18 | ```bash 19 | # For the nrf51 20 | export LC_CTYPE=C 21 | xxd -p -c 100000 PREFIX_keyfile | xxd -r -p | dd of=nrf51_firmware.bin skip=1 bs=1 seek=$(grep -oba OFFLINEFINDINGPUBLICKEYHERE! nrf51_firmware.bin | cut -d ':' -f 1) conv=notrunc 22 | ``` 23 | 24 | or 25 | 26 | ```bash 27 | # For the nrf52 28 | export LC_CTYPE=C 29 | xxd -p -c 100000 PREFIX_keyfile | xxd -r -p | dd of=nrf52_firmware.bin skip=1 bs=1 seek=$(grep -oba OFFLINEFINDINGPUBLICKEYHERE! nrf52_firmware.bin | cut -d ':' -f 1) conv=notrunc 30 | ``` 31 | 32 | The output should be something like this, depending on the count of your keys (in this example 3 keys => 3*28=84 Bytes): 33 | 34 | ```bash 35 | 84+0 records in 36 | 84+0 records out 37 | 84 bytes copied, 0.00024581 s, 346 kB/s 38 | ``` 39 | 40 | - Patch the changed firmware file your firmware, i.e with openocd: 41 | 42 | ```bash 43 | openocd -f openocd.cfg -c "init; halt; nrf51 mass_erase; program nrf51_firmware.bin; reset; exit" 44 | ``` 45 | (Hint: If needed, the file openocd.cfg is in the root of this folder) 46 | 47 | > [!NOTE] 48 | > You might need to reset your device after running the script before it starts sending advertisements. 49 | 50 | ### Misc 51 | 52 | If you want to compile the firmware for yourself or need further informations have a look at [project documentation](https://github.com/acalatrava/openhaystack-firmware/blob/main/apps/openhaystack-alternative/README.md) 53 | 54 | A detailed step-by-step for beginners can be found [here](https://github.com/acalatrava/openhaystack-firmware/blob/main/apps/openhaystack-alternative/iBeacon%20StepByStep.md) 55 | -------------------------------------------------------------------------------- /macless_haystack/android/app/src/main/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 14 | 15 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | -------------------------------------------------------------------------------- /firmware/nrf5x/openhaystack.c: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | #include 4 | #include 5 | #include "openhaystack.h" 6 | #include 7 | 8 | static uint8_t addr[6] = {0xFF, 0xBB, 0xCC, 0xDD, 0xEE, 0xFF}; 9 | 10 | static uint8_t offline_finding_adv_template[] = { 11 | 0x1e, /* Length (30) */ 12 | 0xff, /* Manufacturer Specific Data (type 0xff) */ 13 | 0x4c, 0x00, /* Company ID (Apple) */ 14 | 0x12, 0x19, /* Offline Finding type and length */ 15 | 0x00, /* State */ 16 | 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 17 | 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 18 | 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 19 | 0x00, /* First two bits */ 20 | 0x00, /* Hint (0x00) */ 21 | }; 22 | 23 | /* 24 | * set_addr_from_key will set the bluetooth address from the first 6 bytes of the key used to be advertised 25 | */ 26 | void set_addr_from_key(const char *key) 27 | { 28 | /* copy first 6 bytes */ 29 | addr[5] = key[0] | 0b11000000; 30 | addr[4] = key[1]; 31 | addr[3] = key[2]; 32 | addr[2] = key[3]; 33 | addr[1] = key[4]; 34 | addr[0] = key[5]; 35 | } 36 | 37 | /* 38 | * fill_adv_template_from_key will set the advertising data based on the remaining bytes from the advertised key 39 | */ 40 | void fill_adv_template_from_key(const char *key) 41 | { 42 | 43 | size_t key_size = 28; 44 | char key_hex[28 * 5 + 1]; 45 | 46 | // Ausgabe des key-Arrays als Hexadezimalwerte 47 | for (size_t i = 0; i < key_size; i++) 48 | { 49 | snprintf(&key_hex[i * 5], 6, "0x%02X,", (unsigned char)key[i]); 50 | } 51 | 52 | memcpy(&offline_finding_adv_template[7], &key[6], 22); 53 | /* append two bits of public key */ 54 | 55 | size_t offline_finding_adv_template_size = sizeof(offline_finding_adv_template); 56 | 57 | // Erstellen eines String-Puffers, der groß genug ist, um das Array aufzunehmen 58 | char string_buffer[offline_finding_adv_template_size * 5 + 1]; // Jeder Wert benötigt bis zu 4 Zeichen und ein Nullterminator 59 | 60 | // Umwandeln des offline_finding_adv_template-Arrays in einen String 61 | for (size_t i = 0; i < offline_finding_adv_template_size; i++) 62 | { 63 | snprintf(&string_buffer[i * 5], 6, "0x%02X,", offline_finding_adv_template[i]); 64 | } 65 | 66 | printf("%s\n", string_buffer); 67 | offline_finding_adv_template[29] = key[0] >> 6; 68 | } 69 | 70 | /* 71 | * setAdvertisementKey will setup the key to be advertised 72 | * 73 | * @param[in] key public key to be advertised 74 | * @param[out] bleAddr bluetooth address to setup 75 | * @param[out] data raw data to advertise 76 | * 77 | * @returns raw data size 78 | */ 79 | uint8_t setAdvertisementKey(const char *key, uint8_t **bleAddr, uint8_t **data) 80 | { 81 | set_addr_from_key(key); 82 | fill_adv_template_from_key(key); 83 | 84 | *bleAddr = malloc(sizeof(addr)); 85 | memcpy(*bleAddr, addr, sizeof(addr)); 86 | 87 | *data = malloc(sizeof(offline_finding_adv_template)); 88 | memcpy(*data, offline_finding_adv_template, sizeof(offline_finding_adv_template)); 89 | 90 | return sizeof(offline_finding_adv_template); 91 | } 92 | -------------------------------------------------------------------------------- /firmware/nrf5x/ble_stack.c: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | #include 4 | #include "ble_stack.h" 5 | 6 | /******************************************************************************* 7 | * BLE stack specific functions 8 | ******************************************************************************/ 9 | 10 | /* 11 | * init_ble will initialize the ble stack, it will use the crystal definition based on NRF_CLOCK_LFCLKSRC. 12 | * In devices with no external crystal you should use the internal rc. You can look at the e104bt5032a_board.h file 13 | */ 14 | void init_ble() 15 | { 16 | uint32_t err_code; 17 | nrf_clock_lf_cfg_t clock_lf_cfg = NRF_CLOCK_LFCLKSRC; 18 | 19 | // Initialize the SoftDevice handler module. 20 | SOFTDEVICE_HANDLER_INIT(&clock_lf_cfg, NULL); 21 | 22 | ble_enable_params_t ble_enable_params; 23 | err_code = softdevice_enable_get_default_config(CENTRAL_LINK_COUNT, // central link count 24 | PERIPHERAL_LINK_COUNT, // peripheral link count 25 | &ble_enable_params); 26 | ble_enable_params.common_enable_params.vs_uuid_count = BLE_UUID_VS_COUNT_MIN; 27 | APP_ERROR_CHECK(err_code); 28 | 29 | // Check the ram settings against the used number of links 30 | CHECK_RAM_START_ADDR(CENTRAL_LINK_COUNT, PERIPHERAL_LINK_COUNT); 31 | 32 | // Enable BLE stack. 33 | err_code = softdevice_enable(&ble_enable_params); 34 | APP_ERROR_CHECK(err_code); 35 | 36 | // Use max power. 37 | sd_ble_gap_tx_power_set(4); 38 | } 39 | 40 | /** 41 | * setMacAddress will set the bluetooth address 42 | */ 43 | void setMacAddress(uint8_t *addr) 44 | { 45 | ble_gap_addr_t gap_addr; 46 | uint32_t err_code; 47 | 48 | memcpy(gap_addr.addr, addr, sizeof(gap_addr.addr)); 49 | gap_addr.addr_type = BLE_GAP_ADDR_TYPE_PUBLIC; 50 | err_code = sd_ble_gap_address_set(BLE_GAP_ADDR_CYCLE_MODE_NONE, &gap_addr); 51 | APP_ERROR_CHECK(err_code); 52 | } 53 | 54 | /** 55 | * setAdvertisementData will set the data to be advertised 56 | */ 57 | void setAdvertisementData(uint8_t *data, uint8_t dlen) 58 | { 59 | uint32_t err_code; 60 | 61 | err_code = sd_ble_gap_adv_data_set(data, dlen, NULL, 0); 62 | APP_ERROR_CHECK(err_code); 63 | } 64 | 65 | /** 66 | * Start advertising at the specified interval 67 | * 68 | * @param[in] interval advertising interval in milliseconds 69 | */ 70 | void startAdvertisement(int interval) 71 | { 72 | ble_gap_adv_params_t m_adv_params; 73 | memset(&m_adv_params, 0, sizeof(m_adv_params)); 74 | m_adv_params.type = BLE_GAP_ADV_TYPE_ADV_NONCONN_IND; 75 | m_adv_params.p_peer_addr = NULL; 76 | m_adv_params.fp = BLE_GAP_ADV_FP_ANY; 77 | m_adv_params.interval = MSEC_TO_UNITS(interval, UNIT_0_625_MS); 78 | m_adv_params.timeout = 0; 79 | sd_ble_gap_adv_start(&m_adv_params); 80 | } 81 | 82 | /** 83 | * Function for the Power manager. 84 | */ 85 | void power_manage(void) 86 | { 87 | uint32_t err_code = sd_app_evt_wait(); 88 | APP_ERROR_CHECK(err_code); 89 | } -------------------------------------------------------------------------------- /macless_haystack/lib/deployment/deployment_details.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | 3 | class DeploymentDetails extends StatefulWidget { 4 | /// The steps required to deploy on this target. 5 | final List steps; 6 | 7 | /// The name of the deployment target. 8 | final String title; 9 | 10 | /// Describes a generic step-by-step deployment for a special hardware target. 11 | /// 12 | /// The actual steps depend on the target platform and are provided in [steps]. 13 | const DeploymentDetails({ 14 | super.key, 15 | required this.title, 16 | required this.steps, 17 | }); 18 | 19 | @override 20 | State createState() { 21 | return _DeploymentDetailsState(); 22 | } 23 | } 24 | 25 | class _DeploymentDetailsState extends State { 26 | /// The index of the currently displayed step. 27 | int _index = 0; 28 | 29 | @override 30 | Widget build(BuildContext context) { 31 | var stepCount = widget.steps.length; 32 | return Scaffold( 33 | appBar: AppBar( 34 | title: Text(widget.title), 35 | ), 36 | body: SafeArea( 37 | child: Stepper( 38 | currentStep: _index, 39 | controlsBuilder: (BuildContext context, ControlsDetails details) { 40 | String continueText = 41 | _index < stepCount - 1 ? 'CONTINUE' : 'FINISH'; 42 | return Row( 43 | children: [ 44 | ElevatedButton( 45 | style: ElevatedButton.styleFrom( 46 | shape: RoundedRectangleBorder( 47 | borderRadius: BorderRadius.circular(1))), 48 | onPressed: details.onStepContinue, 49 | child: Text(continueText), 50 | ), 51 | if (_index > 0) 52 | TextButton( 53 | onPressed: details.onStepCancel, 54 | child: const Text('BACK'), 55 | ), 56 | ], 57 | ); 58 | }, 59 | onStepCancel: () { 60 | // Back button clicked 61 | if (_index == 0) { 62 | // Cancel deployment and return 63 | Navigator.pop(context); 64 | } else if (_index > 0) { 65 | setState(() { 66 | _index -= 1; 67 | }); 68 | } 69 | }, 70 | onStepContinue: () { 71 | // Continue button clicked 72 | if (_index == stepCount - 1) { 73 | // TODO: Mark accessory as deployed 74 | // Deployment finished 75 | Navigator.pop(context); 76 | Navigator.pop(context); 77 | } else { 78 | setState(() { 79 | _index += 1; 80 | }); 81 | } 82 | }, 83 | onStepTapped: (int index) { 84 | setState(() { 85 | _index = index; 86 | }); 87 | }, 88 | steps: widget.steps, 89 | ), 90 | ), 91 | ); 92 | } 93 | } 94 | -------------------------------------------------------------------------------- /macless_haystack/linux/flutter/CMakeLists.txt: -------------------------------------------------------------------------------- 1 | cmake_minimum_required(VERSION 3.10) 2 | 3 | set(EPHEMERAL_DIR "${CMAKE_CURRENT_SOURCE_DIR}/ephemeral") 4 | 5 | # Configuration provided via flutter tool. 6 | include(${EPHEMERAL_DIR}/generated_config.cmake) 7 | 8 | # TODO: Move the rest of this into files in ephemeral. See 9 | # https://github.com/flutter/flutter/issues/57146. 10 | 11 | # Serves the same purpose as list(TRANSFORM ... PREPEND ...), 12 | # which isn't available in 3.10. 13 | function(list_prepend LIST_NAME PREFIX) 14 | set(NEW_LIST "") 15 | foreach(element ${${LIST_NAME}}) 16 | list(APPEND NEW_LIST "${PREFIX}${element}") 17 | endforeach(element) 18 | set(${LIST_NAME} "${NEW_LIST}" PARENT_SCOPE) 19 | endfunction() 20 | 21 | # === Flutter Library === 22 | # System-level dependencies. 23 | find_package(PkgConfig REQUIRED) 24 | pkg_check_modules(GTK REQUIRED IMPORTED_TARGET gtk+-3.0) 25 | pkg_check_modules(GLIB REQUIRED IMPORTED_TARGET glib-2.0) 26 | pkg_check_modules(GIO REQUIRED IMPORTED_TARGET gio-2.0) 27 | 28 | set(FLUTTER_LIBRARY "${EPHEMERAL_DIR}/libflutter_linux_gtk.so") 29 | 30 | # Published to parent scope for install step. 31 | set(FLUTTER_LIBRARY ${FLUTTER_LIBRARY} PARENT_SCOPE) 32 | set(FLUTTER_ICU_DATA_FILE "${EPHEMERAL_DIR}/icudtl.dat" PARENT_SCOPE) 33 | set(PROJECT_BUILD_DIR "${PROJECT_DIR}/build/" PARENT_SCOPE) 34 | set(AOT_LIBRARY "${PROJECT_DIR}/build/lib/libapp.so" PARENT_SCOPE) 35 | 36 | list(APPEND FLUTTER_LIBRARY_HEADERS 37 | "fl_basic_message_channel.h" 38 | "fl_binary_codec.h" 39 | "fl_binary_messenger.h" 40 | "fl_dart_project.h" 41 | "fl_engine.h" 42 | "fl_json_message_codec.h" 43 | "fl_json_method_codec.h" 44 | "fl_message_codec.h" 45 | "fl_method_call.h" 46 | "fl_method_channel.h" 47 | "fl_method_codec.h" 48 | "fl_method_response.h" 49 | "fl_plugin_registrar.h" 50 | "fl_plugin_registry.h" 51 | "fl_standard_message_codec.h" 52 | "fl_standard_method_codec.h" 53 | "fl_string_codec.h" 54 | "fl_value.h" 55 | "fl_view.h" 56 | "flutter_linux.h" 57 | ) 58 | list_prepend(FLUTTER_LIBRARY_HEADERS "${EPHEMERAL_DIR}/flutter_linux/") 59 | add_library(flutter INTERFACE) 60 | target_include_directories(flutter INTERFACE 61 | "${EPHEMERAL_DIR}" 62 | ) 63 | target_link_libraries(flutter INTERFACE "${FLUTTER_LIBRARY}") 64 | target_link_libraries(flutter INTERFACE 65 | PkgConfig::GTK 66 | PkgConfig::GLIB 67 | PkgConfig::GIO 68 | ) 69 | add_dependencies(flutter flutter_assemble) 70 | 71 | # === Flutter tool backend === 72 | # _phony_ is a non-existent file to force this command to run every time, 73 | # since currently there's no way to get a full input/output list from the 74 | # flutter tool. 75 | add_custom_command( 76 | OUTPUT ${FLUTTER_LIBRARY} ${FLUTTER_LIBRARY_HEADERS} 77 | ${CMAKE_CURRENT_BINARY_DIR}/_phony_ 78 | COMMAND ${CMAKE_COMMAND} -E env 79 | ${FLUTTER_TOOL_ENVIRONMENT} 80 | "${FLUTTER_ROOT}/packages/flutter_tools/bin/tool_backend.sh" 81 | ${FLUTTER_TARGET_PLATFORM} ${CMAKE_BUILD_TYPE} 82 | VERBATIM 83 | ) 84 | add_custom_target(flutter_assemble DEPENDS 85 | "${FLUTTER_LIBRARY}" 86 | ${FLUTTER_LIBRARY_HEADERS} 87 | ) 88 | -------------------------------------------------------------------------------- /macless_haystack/lib/accessory/accessory_dto.dart: -------------------------------------------------------------------------------- 1 | /// This class is used for de-/serializing data to the JSON transfer format. 2 | class AccessoryDTO { 3 | int id; 4 | List colorComponents; 5 | String name; 6 | double? lastDerivationTimestamp; 7 | String? symmetricKey; 8 | int? updateInterval; 9 | String privateKey; 10 | String icon; 11 | String? oldestRelevantSymmetricKey; 12 | bool isActive; 13 | List? additionalKeys; 14 | 15 | /// Creates a transfer object to serialize to the JSON export format. 16 | /// 17 | /// This implements the [toJson] method used by the Dart JSON serializer. 18 | /// ```dart 19 | /// var accessoryDTO = AccessoryDTO(...); 20 | /// jsonEncode(accessoryDTO); 21 | /// ``` 22 | AccessoryDTO( 23 | {required this.id, 24 | required this.colorComponents, 25 | required this.name, 26 | this.lastDerivationTimestamp, 27 | this.symmetricKey, 28 | this.updateInterval, 29 | required this.privateKey, 30 | required this.icon, 31 | this.oldestRelevantSymmetricKey, 32 | required this.isActive, 33 | this.additionalKeys}); 34 | 35 | /// Creates a transfer object from deserialized JSON data. 36 | /// 37 | /// The data is only decoded and not processed further. 38 | /// 39 | /// Typically used with JSON decoder. 40 | /// ```dart 41 | /// String json = '...'; 42 | /// var accessoryDTO = AccessoryDTO.fromJSON(jsonDecode(json)); 43 | /// ``` 44 | /// 45 | /// This implements the [toJson] method used by the Dart JSON serializer. 46 | /// ```dart 47 | /// var accessoryDTO = AccessoryDTO(...); 48 | /// jsonEncode(accessoryDTO); 49 | /// ``` 50 | AccessoryDTO.fromJson(Map json) 51 | : id = json['id'], 52 | colorComponents = List.from(json['colorComponents']) 53 | .map((val) => double.parse(val.toString())) 54 | .toList(), 55 | name = json['name'], 56 | lastDerivationTimestamp = json['lastDerivationTimestamp'] ?? 0, 57 | symmetricKey = json['symmetricKey'] ?? '', 58 | updateInterval = json['updateInterval'] ?? 0, 59 | privateKey = json['privateKey'], 60 | icon = json['icon'], 61 | oldestRelevantSymmetricKey = json['oldestRelevantSymmetricKey'] ?? '', 62 | /*isDeployed is only for migration an can be removed in the future*/ 63 | isActive = json['isDeployed'] ?? json['isActive'], 64 | additionalKeys = json['additionalKeys']?.cast() ?? List.empty(); 65 | 66 | /// Creates a JSON map of the serialized transfer object. 67 | /// 68 | /// Typically used by JSON encoder. 69 | /// ```dart 70 | /// var accessoryDTO = AccessoryDTO(...); 71 | /// jsonEncode(accessoryDTO); 72 | /// ``` 73 | Map toJson() => { 74 | // Without derivation (skip rolling key params) 75 | 'id': id, 76 | 'colorComponents': colorComponents, 77 | 'name': name, 78 | 'privateKey': privateKey, 79 | 'icon': icon, 80 | 'isActive': isActive, 81 | 'additionalKeys': additionalKeys 82 | }; 83 | } 84 | -------------------------------------------------------------------------------- /firmware/nrf5x/aliexpress_board.h: -------------------------------------------------------------------------------- 1 | /* Copyright (c) 2016 Nordic Semiconductor. All Rights Reserved. 2 | * 3 | * The information contained herein is property of Nordic Semiconductor ASA. 4 | * Terms and conditions of usage are described in detail in NORDIC 5 | * SEMICONDUCTOR STANDARD SOFTWARE LICENSE AGREEMENT. 6 | * 7 | * Licensees are granted free, non-transferable use of the information. NO 8 | * WARRANTY of ANY KIND is provided. This heading must NOT be removed from 9 | * the file. 10 | * 11 | */ 12 | #ifndef ALIEXPRESS_BOARD_H 13 | #define ALIEXPRESS_BOARD_H 14 | 15 | #ifdef __cplusplus 16 | extern "C" { 17 | #endif 18 | 19 | // LEDs definitions 20 | #define LEDS_NUMBER 1 21 | 22 | #define LED_1 29 23 | 24 | #define LEDS_ACTIVE_STATE 0 25 | 26 | #define LEDS_LIST { LED_1} 27 | 28 | #define BSP_LED_0 LED_1 29 | 30 | #define LEDS_INV_MASK 0 31 | 32 | #define BUTTONS_NUMBER 1 33 | 34 | #define BUTTON_START 28 35 | #define BUTTON_1 28 36 | #define BUTTON_STOP 28 37 | #define BUTTON_PULL NRF_GPIO_PIN_PULLUP 38 | 39 | #define BUTTONS_ACTIVE_STATE 0 40 | 41 | #define BUTTONS_LIST { BUTTON_1 } 42 | 43 | #define BSP_BUTTON_0 BUTTON_1 44 | 45 | #define CTS_PIN_NUMBER UART_PIN_DISCONNECTED 46 | #define RTS_PIN_NUMBER UART_PIN_DISCONNECTED 47 | #define HWFC false 48 | 49 | // Arduino board mappings 50 | #define ARDUINO_SCL_PIN 27 // SCL signal pin 51 | #define ARDUINO_SDA_PIN 26 // SDA signal pin 52 | #define ARDUINO_AREF_PIN 2 // Aref pin 53 | #define ARDUINO_13_PIN 25 // Digital pin 13 54 | #define ARDUINO_12_PIN 24 // Digital pin 12 55 | #define ARDUINO_11_PIN 23 // Digital pin 11 56 | #define ARDUINO_10_PIN 22 // Digital pin 10 57 | #define ARDUINO_9_PIN 20 // Digital pin 9 58 | #define ARDUINO_8_PIN 19 // Digital pin 8 59 | 60 | #define ARDUINO_7_PIN 18 // Digital pin 7 61 | #define ARDUINO_6_PIN 17 // Digital pin 6 62 | #define ARDUINO_5_PIN 16 // Digital pin 5 63 | #define ARDUINO_4_PIN 15 // Digital pin 4 64 | #define ARDUINO_3_PIN 14 // Digital pin 3 65 | #define ARDUINO_2_PIN 13 // Digital pin 2 66 | #define ARDUINO_1_PIN 12 // Digital pin 1 67 | #define ARDUINO_0_PIN 11 // Digital pin 0 68 | 69 | #define ARDUINO_A0_PIN 3 // Analog channel 0 70 | #define ARDUINO_A1_PIN 4 // Analog channel 1 71 | #define ARDUINO_A2_PIN 28 // Analog channel 2 72 | #define ARDUINO_A3_PIN 29 // Analog channel 3 73 | #define ARDUINO_A4_PIN 30 // Analog channel 4 74 | #define ARDUINO_A5_PIN 31 // Analog channel 5 75 | 76 | // Low frequency clock source to be used by the SoftDevice 77 | #define NRF_CLOCK_LFCLKSRC {.source = NRF_CLOCK_LF_SRC_XTAL, \ 78 | .rc_ctiv = 0, \ 79 | .rc_temp_ctiv = 0, \ 80 | .xtal_accuracy = NRF_CLOCK_LF_XTAL_ACCURACY_20_PPM} 81 | 82 | 83 | #ifdef __cplusplus 84 | } 85 | #endif 86 | 87 | #endif // ARDUINO_PRIMO_H 88 | -------------------------------------------------------------------------------- /macless_haystack/lib/findMy/reports_fetcher.dart: -------------------------------------------------------------------------------- 1 | import 'dart:convert'; 2 | import 'dart:io'; 3 | 4 | import 'package:http/http.dart' as http; 5 | import 'package:logger/logger.dart'; 6 | import 'package:flutter/foundation.dart' show kIsWeb; 7 | 8 | class ReportsFetcher { 9 | /// Fetches the location reports corresponding to the given hashed advertisement 10 | /// key. 11 | /// Throws [Exception] if no answer was received. 12 | /// 13 | static var logger = Logger( 14 | printer: PrettyPrinter(methodCount: 0), 15 | ); 16 | 17 | static Future fetchLocationReports( 18 | Iterable hashedAdvertisementKeys, 19 | int daysToFetch, 20 | String url, 21 | String user, 22 | String pass) async { 23 | var keys = hashedAdvertisementKeys.toList(growable: false); 24 | logger.i('Using ${keys.length} key(s) to ask webservice'); 25 | 26 | String? credentials; 27 | if (user.trim().isNotEmpty || pass.trim().isNotEmpty) { 28 | credentials = 'Basic ${base64.encode(utf8.encode("$user:$pass"))}'; 29 | } 30 | 31 | if (kIsWeb) { 32 | Map requestHeaders = { 33 | "Content-Type": "application/json", 34 | }; 35 | if (credentials != null) { 36 | requestHeaders['Authorization'] = credentials; 37 | } 38 | 39 | final response = await http.post(Uri.parse(url), 40 | headers: requestHeaders, 41 | body: jsonEncode({ 42 | "ids": keys, 43 | "days": daysToFetch, 44 | })); 45 | if (response.statusCode == 401) { 46 | throw Exception("Authentication failure. User/password wrong"); 47 | } 48 | if (response.statusCode == 200) { 49 | var out = await jsonDecode(response.body)["results"]; 50 | logger.i('Found ${out.length} reports'); 51 | return out; 52 | } else { 53 | throw Exception( 54 | "Failed to fetch location reports with statusCode:${response.statusCode}\n\n Response:\n$response"); 55 | } 56 | } else { 57 | var httpClient = HttpClient(); 58 | /*Ignore certificate errors*/ 59 | httpClient.badCertificateCallback = 60 | (X509Certificate cert, String host, int port) => true; 61 | 62 | final request = await httpClient.postUrl(Uri.parse(url)); 63 | var body = jsonEncode({ 64 | "ids": keys, 65 | "days": daysToFetch, 66 | }); 67 | request.headers.set(HttpHeaders.contentTypeHeader, "application/json"); 68 | if (credentials != null) { 69 | request.headers.set(HttpHeaders.authorizationHeader, credentials); 70 | } 71 | 72 | request.headers 73 | .set(HttpHeaders.contentLengthHeader, utf8.encode(body).length); 74 | request.write(body); 75 | final response = await request.close(); 76 | if (response.statusCode == 401) { 77 | throw Exception("Authentication failure. User/password wrong"); 78 | } 79 | if (response.statusCode == 200) { 80 | String body = await response.transform(utf8.decoder).join(); 81 | var out = await jsonDecode(body)["results"]; 82 | return out; 83 | } else { 84 | throw Exception( 85 | "Failed to fetch location reports with statusCode:${response.statusCode}\n\n Response:\n$response"); 86 | } 87 | } 88 | } 89 | } 90 | -------------------------------------------------------------------------------- /firmware/nrf5x/aliexpress_board_no_xtal.h: -------------------------------------------------------------------------------- 1 | /* Copyright (c) 2016 Nordic Semiconductor. All Rights Reserved. 2 | * 3 | * The information contained herein is property of Nordic Semiconductor ASA. 4 | * Terms and conditions of usage are described in detail in NORDIC 5 | * SEMICONDUCTOR STANDARD SOFTWARE LICENSE AGREEMENT. 6 | * 7 | * Licensees are granted free, non-transferable use of the information. NO 8 | * WARRANTY of ANY KIND is provided. This heading must NOT be removed from 9 | * the file. 10 | * 11 | */ 12 | #ifndef ALIEXPRESS_BOARD_NO_XTAL_H 13 | #define ALIEXPRESS_BOARD_NO_XTAL_H 14 | 15 | #ifdef __cplusplus 16 | extern "C" { 17 | #endif 18 | 19 | // LEDs definitions 20 | #define LEDS_NUMBER 1 21 | 22 | #define LED_1 29 23 | 24 | #define LEDS_ACTIVE_STATE 0 25 | 26 | #define LEDS_LIST { LED_1} 27 | 28 | #define BSP_LED_0 LED_1 29 | 30 | #define LEDS_INV_MASK 0 31 | 32 | #define BUTTONS_NUMBER 1 33 | 34 | #define BUTTON_START 28 35 | #define BUTTON_1 28 36 | #define BUTTON_STOP 28 37 | #define BUTTON_PULL NRF_GPIO_PIN_PULLUP 38 | 39 | #define BUTTONS_ACTIVE_STATE 0 40 | 41 | #define BUTTONS_LIST { BUTTON_1 } 42 | 43 | #define BSP_BUTTON_0 BUTTON_1 44 | 45 | #define RX_PIN_NUMBER 11 46 | #define TX_PIN_NUMBER 12 47 | #define CTS_PIN_NUMBER UART_PIN_DISCONNECTED 48 | #define RTS_PIN_NUMBER UART_PIN_DISCONNECTED 49 | #define HWFC false 50 | 51 | // Arduino board mappings 52 | #define ARDUINO_SCL_PIN 27 // SCL signal pin 53 | #define ARDUINO_SDA_PIN 26 // SDA signal pin 54 | #define ARDUINO_AREF_PIN 2 // Aref pin 55 | #define ARDUINO_13_PIN 25 // Digital pin 13 56 | #define ARDUINO_12_PIN 24 // Digital pin 12 57 | #define ARDUINO_11_PIN 23 // Digital pin 11 58 | #define ARDUINO_10_PIN 22 // Digital pin 10 59 | #define ARDUINO_9_PIN 20 // Digital pin 9 60 | #define ARDUINO_8_PIN 19 // Digital pin 8 61 | 62 | #define ARDUINO_7_PIN 18 // Digital pin 7 63 | #define ARDUINO_6_PIN 17 // Digital pin 6 64 | #define ARDUINO_5_PIN 16 // Digital pin 5 65 | #define ARDUINO_4_PIN 15 // Digital pin 4 66 | #define ARDUINO_3_PIN 14 // Digital pin 3 67 | #define ARDUINO_2_PIN 13 // Digital pin 2 68 | #define ARDUINO_1_PIN 12 // Digital pin 1 69 | #define ARDUINO_0_PIN 11 // Digital pin 0 70 | 71 | #define ARDUINO_A0_PIN 3 // Analog channel 0 72 | #define ARDUINO_A1_PIN 4 // Analog channel 1 73 | #define ARDUINO_A2_PIN 28 // Analog channel 2 74 | #define ARDUINO_A3_PIN 29 // Analog channel 3 75 | #define ARDUINO_A4_PIN 30 // Analog channel 4 76 | #define ARDUINO_A5_PIN 31 // Analog channel 5 77 | 78 | // Low frequency clock source to be used by the SoftDevice 79 | #define NRF_CLOCK_LFCLKSRC {.source = NRF_CLOCK_LF_SRC_RC, \ 80 | .rc_ctiv = 16, \ 81 | .rc_temp_ctiv = 2, \ 82 | .xtal_accuracy = 1} 83 | 84 | 85 | #ifdef __cplusplus 86 | } 87 | #endif 88 | 89 | #endif // ARDUINO_PRIMO_H 90 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | 2 | # Created by https://www.toptal.com/developers/gitignore/api/xcode,swift 3 | # Edit at https://www.toptal.com/developers/gitignore?templates=xcode,swift 4 | 5 | ## macOS ## 6 | 7 | .DS_Store 8 | 9 | ### Swift ### 10 | # Xcode 11 | # 12 | # gitignore contributors: remember to update Global/Xcode.gitignore, Objective-C.gitignore & Swift.gitignore 13 | 14 | ## User settings 15 | xcuserdata/ 16 | 17 | ## compatibility with Xcode 8 and earlier (ignoring not required starting Xcode 9) 18 | *.xcscmblueprint 19 | *.xccheckout 20 | 21 | ## compatibility with Xcode 3 and earlier (ignoring not required starting Xcode 4) 22 | build/ 23 | DerivedData/ 24 | *.moved-aside 25 | *.pbxuser 26 | !default.pbxuser 27 | *.mode1v3 28 | !default.mode1v3 29 | *.mode2v3 30 | !default.mode2v3 31 | *.perspectivev3 32 | !default.perspectivev3 33 | 34 | ## Obj-C/Swift specific 35 | *.hmap 36 | 37 | ## App packaging 38 | *.ipa 39 | *.dSYM.zip 40 | *.dSYM 41 | 42 | ## Playgrounds 43 | timeline.xctimeline 44 | playground.xcworkspace 45 | 46 | # Swift Package Manager 47 | # Add this line if you want to avoid checking in source code from Swift Package Manager dependencies. 48 | # Packages/ 49 | # Package.pins 50 | # Package.resolved 51 | # *.xcodeproj 52 | # Xcode automatically generates this directory with a .xcworkspacedata file and xcuserdata 53 | # hence it is not needed unless you have added a package configuration file to your project 54 | # .swiftpm 55 | 56 | .build/ 57 | 58 | # CocoaPods 59 | # We recommend against adding the Pods directory to your .gitignore. However 60 | # you should judge for yourself, the pros and cons are mentioned at: 61 | # https://guides.cocoapods.org/using/using-cocoapods.html#should-i-check-the-pods-directory-into-source-control 62 | # Pods/ 63 | # Add this line if you want to avoid checking in source code from the Xcode workspace 64 | # *.xcworkspace 65 | 66 | # Carthage 67 | # Add this line if you want to avoid checking in source code from Carthage dependencies. 68 | # Carthage/Checkouts 69 | 70 | Carthage/Build/ 71 | 72 | # Accio dependency management 73 | Dependencies/ 74 | .accio/ 75 | 76 | # fastlane 77 | # It is recommended to not store the screenshots in the git repo. 78 | # Instead, use fastlane to re-generate the screenshots whenever they are needed. 79 | # For more information about the recommended setup visit: 80 | # https://docs.fastlane.tools/best-practices/source-control/#source-control 81 | 82 | fastlane/report.xml 83 | fastlane/Preview.html 84 | fastlane/screenshots/**/*.png 85 | fastlane/test_output 86 | 87 | # Code Injection 88 | # After new code Injection tools there's a generated folder /iOSInjectionProject 89 | # https://github.com/johnno1962/injectionforxcode 90 | 91 | iOSInjectionProject/ 92 | 93 | ### Xcode ### 94 | # Xcode 95 | # gitignore contributors: remember to update Global/Xcode.gitignore, Objective-C.gitignore & Swift.gitignore 96 | 97 | 98 | 99 | 100 | ## Gcc Patch 101 | /*.gcno 102 | 103 | ### Xcode Patch ### 104 | *.xcodeproj/* 105 | !*.xcodeproj/project.pbxproj 106 | !*.xcodeproj/xcshareddata/ 107 | !*.xcworkspace/contents.xcworkspacedata 108 | **/xcshareddata/WorkspaceSettings.xcsettings 109 | 110 | # End of https://www.toptal.com/developers/gitignore/api/xcode,swift 111 | 112 | # Exports folder 113 | Exports/ 114 | 115 | 116 | openssl/release 117 | 118 | **/output 119 | **/.pio 120 | **/.vscode 121 | **/.dart_tool 122 | **/*.code-workspace 123 | notes 124 | **/osx-serial-generator 125 | *.pyc 126 | 127 | 128 | firmware/nrf5x/_build 129 | firmware/nrf5x/compiled 130 | endpoint/data/*.json 131 | endpoint/venv/* 132 | *.iml 133 | .idea 134 | /macless_haystack/android/app/.cxx/** 135 | -------------------------------------------------------------------------------- /macless_haystack/lib/preferences/preferences_page.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:provider/provider.dart'; 3 | import 'package:macless_haystack/location/location_model.dart'; 4 | import 'package:macless_haystack/preferences/user_preferences_model.dart'; 5 | import 'package:flutter_settings_screens/flutter_settings_screens.dart'; 6 | 7 | class PreferencesPage extends StatefulWidget { 8 | /// Displays this preferences page with information about the app. 9 | const PreferencesPage({super.key}); 10 | 11 | @override 12 | State createState() { 13 | return _PreferencesPageState(); 14 | } 15 | } 16 | 17 | class _PreferencesPageState extends State { 18 | @override 19 | Widget build(BuildContext context) { 20 | return Scaffold( 21 | appBar: AppBar( 22 | title: const Text('Settings'), 23 | ), 24 | body: Center( 25 | child: Column( 26 | children: [ 27 | getLocationTile(), 28 | getFetchOnStartupTile(), 29 | getUrlTile(), 30 | getUserTile(), 31 | getPassTile(), 32 | getNumberofDaysTile(), 33 | ListTile( 34 | title: getAbout(), 35 | ), 36 | ], 37 | ), 38 | ), 39 | ); 40 | } 41 | 42 | getLocationTile() { 43 | return SwitchSettingsTile( 44 | settingKey: locationAccessWantedKey, 45 | title: 'Show this devices location', 46 | onChange: (showLocation) { 47 | var locationModel = Provider.of(context, listen: false); 48 | if (showLocation) { 49 | locationModel.requestLocationUpdates(); 50 | } else { 51 | locationModel.cancelLocationUpdates(); 52 | } 53 | }, 54 | ); 55 | } 56 | 57 | getNumberofDaysTile() { 58 | return const DropDownSettingsTile( 59 | title: 'Number of days to fetch location', 60 | settingKey: numberOfDaysToFetch, 61 | values: { 62 | 0: "latest location only", 63 | 1: "1", 64 | 2: "2", 65 | 3: "3", 66 | 4: "4", 67 | 5: "5", 68 | 6: "6", 69 | 7: "7", 70 | }, 71 | selected: 7, 72 | ); 73 | } 74 | 75 | getUrlTile() { 76 | return TextInputSettingsTile( 77 | initialValue: 'http://localhost:6176', 78 | settingKey: endpointUrl, 79 | title: 'Url to macless haystack endpoint', 80 | validator: (String? url) { 81 | if (url != null && 82 | url.startsWith(RegExp('http[s]?://', caseSensitive: false))) { 83 | return null; 84 | } 85 | return "Invalid Url"; 86 | }, 87 | ); 88 | } 89 | 90 | getUserTile() { 91 | return const TextInputSettingsTile( 92 | initialValue: '', 93 | settingKey: endpointUser, 94 | title: 'Username for endpoint', 95 | ); 96 | } 97 | 98 | getPassTile() { 99 | return const TextInputSettingsTile( 100 | obscureText: true, 101 | initialValue: '', 102 | settingKey: endpointPass, 103 | title: 'Password for endpoint', 104 | ); 105 | } 106 | 107 | getAbout() { 108 | return TextButton( 109 | style: ButtonStyle( 110 | padding: 111 | WidgetStateProperty.all(const EdgeInsets.all(10)), 112 | foregroundColor: WidgetStateProperty.resolveWith( 113 | (Set states) { 114 | return Colors.white; 115 | }, 116 | ), 117 | backgroundColor: WidgetStateProperty.resolveWith( 118 | (Set states) { 119 | return Colors.indigo; 120 | }, 121 | )), 122 | child: const Text('About'), 123 | onPressed: () => showAboutDialog( 124 | context: context, 125 | )); 126 | } 127 | 128 | getFetchOnStartupTile() { 129 | return SwitchSettingsTile( 130 | settingKey: fetchLocationOnStartupKey, 131 | defaultValue: true, 132 | title: 'Fetch locations on startup', 133 | ); 134 | } 135 | } 136 | -------------------------------------------------------------------------------- /macless_haystack/linux/my_application.cc: -------------------------------------------------------------------------------- 1 | #include "my_application.h" 2 | 3 | #include 4 | #ifdef GDK_WINDOWING_X11 5 | #include 6 | #endif 7 | 8 | #include "flutter/generated_plugin_registrant.h" 9 | 10 | struct _MyApplication { 11 | GtkApplication parent_instance; 12 | char** dart_entrypoint_arguments; 13 | }; 14 | 15 | G_DEFINE_TYPE(MyApplication, my_application, GTK_TYPE_APPLICATION) 16 | 17 | // Implements GApplication::activate. 18 | static void my_application_activate(GApplication* application) { 19 | MyApplication* self = MY_APPLICATION(application); 20 | GtkWindow* window = 21 | GTK_WINDOW(gtk_application_window_new(GTK_APPLICATION(application))); 22 | 23 | // Use a header bar when running in GNOME as this is the common style used 24 | // by applications and is the setup most users will be using (e.g. Ubuntu 25 | // desktop). 26 | // If running on X and not using GNOME then just use a traditional title bar 27 | // in case the window manager does more exotic layout, e.g. tiling. 28 | // If running on Wayland assume the header bar will work (may need changing 29 | // if future cases occur). 30 | gboolean use_header_bar = TRUE; 31 | #ifdef GDK_WINDOWING_X11 32 | GdkScreen* screen = gtk_window_get_screen(window); 33 | if (GDK_IS_X11_SCREEN(screen)) { 34 | const gchar* wm_name = gdk_x11_screen_get_window_manager_name(screen); 35 | if (g_strcmp0(wm_name, "GNOME Shell") != 0) { 36 | use_header_bar = FALSE; 37 | } 38 | } 39 | #endif 40 | if (use_header_bar) { 41 | GtkHeaderBar* header_bar = GTK_HEADER_BAR(gtk_header_bar_new()); 42 | gtk_widget_show(GTK_WIDGET(header_bar)); 43 | gtk_header_bar_set_title(header_bar, "openhaystack_mobile"); 44 | gtk_header_bar_set_show_close_button(header_bar, TRUE); 45 | gtk_window_set_titlebar(window, GTK_WIDGET(header_bar)); 46 | } else { 47 | gtk_window_set_title(window, "openhaystack_mobile"); 48 | } 49 | 50 | gtk_window_set_default_size(window, 1280, 720); 51 | gtk_widget_show(GTK_WIDGET(window)); 52 | 53 | g_autoptr(FlDartProject) project = fl_dart_project_new(); 54 | fl_dart_project_set_dart_entrypoint_arguments(project, self->dart_entrypoint_arguments); 55 | 56 | FlView* view = fl_view_new(project); 57 | gtk_widget_show(GTK_WIDGET(view)); 58 | gtk_container_add(GTK_CONTAINER(window), GTK_WIDGET(view)); 59 | 60 | fl_register_plugins(FL_PLUGIN_REGISTRY(view)); 61 | 62 | gtk_widget_grab_focus(GTK_WIDGET(view)); 63 | } 64 | 65 | // Implements GApplication::local_command_line. 66 | static gboolean my_application_local_command_line(GApplication* application, gchar*** arguments, int* exit_status) { 67 | MyApplication* self = MY_APPLICATION(application); 68 | // Strip out the first argument as it is the binary name. 69 | self->dart_entrypoint_arguments = g_strdupv(*arguments + 1); 70 | 71 | g_autoptr(GError) error = nullptr; 72 | if (!g_application_register(application, nullptr, &error)) { 73 | g_warning("Failed to register: %s", error->message); 74 | *exit_status = 1; 75 | return TRUE; 76 | } 77 | 78 | g_application_activate(application); 79 | *exit_status = 0; 80 | 81 | return TRUE; 82 | } 83 | 84 | // Implements GObject::dispose. 85 | static void my_application_dispose(GObject* object) { 86 | MyApplication* self = MY_APPLICATION(object); 87 | g_clear_pointer(&self->dart_entrypoint_arguments, g_strfreev); 88 | G_OBJECT_CLASS(my_application_parent_class)->dispose(object); 89 | } 90 | 91 | static void my_application_class_init(MyApplicationClass* klass) { 92 | G_APPLICATION_CLASS(klass)->activate = my_application_activate; 93 | G_APPLICATION_CLASS(klass)->local_command_line = my_application_local_command_line; 94 | G_OBJECT_CLASS(klass)->dispose = my_application_dispose; 95 | } 96 | 97 | static void my_application_init(MyApplication* self) {} 98 | 99 | MyApplication* my_application_new() { 100 | return MY_APPLICATION(g_object_new(my_application_get_type(), 101 | "application-id", APPLICATION_ID, 102 | "flags", G_APPLICATION_NON_UNIQUE, 103 | nullptr)); 104 | } 105 | -------------------------------------------------------------------------------- /macless_haystack/lib/item_management/new_item_action.dart: -------------------------------------------------------------------------------- 1 | 2 | import 'package:file_picker/file_picker.dart'; 3 | import 'package:flutter/material.dart'; 4 | import 'package:macless_haystack/item_management/item_creation.dart'; 5 | import 'package:macless_haystack/item_management/item_file_import.dart'; 6 | import 'package:macless_haystack/item_management/item_import.dart'; 7 | import 'dart:io'; 8 | 9 | class NewKeyAction extends StatelessWidget { 10 | /// If the action button is small. 11 | final bool mini; 12 | 13 | /// Displays a floating button used to access the accessory creation menu. 14 | /// 15 | /// A new accessory can be created or an existing one imported manually. 16 | const NewKeyAction({ 17 | super.key, 18 | this.mini = false, 19 | }); 20 | 21 | /// Display a bottom sheet with creation options. 22 | void showCreationSheet(BuildContext context) { 23 | showModalBottomSheet( 24 | context: context, 25 | builder: (BuildContext context) { 26 | return SafeArea( 27 | child: ListView( 28 | shrinkWrap: true, 29 | children: [ 30 | ListTile( 31 | title: const Text('Import Accessory'), 32 | leading: const Icon(Icons.import_export), 33 | onTap: () { 34 | Navigator.pushReplacement( 35 | context, 36 | MaterialPageRoute( 37 | builder: (context) => const AccessoryImport()), 38 | ); 39 | }, 40 | ), 41 | ListTile( 42 | title: const Text('Import from JSON File'), 43 | leading: const Icon(Icons.description), 44 | onTap: () async { 45 | FilePickerResult? result = 46 | await FilePicker.platform.pickFiles( 47 | allowMultiple: false, 48 | type: FileType.custom, 49 | allowedExtensions: ['json'], 50 | dialogTitle: 'Select accessory configuration', 51 | ); 52 | 53 | if (result != null) { 54 | var uploadfile = result.files.single.bytes; 55 | if (uploadfile != null && context.mounted) { 56 | Navigator.pushReplacement( 57 | context, 58 | MaterialPageRoute( 59 | builder: (context) => 60 | ItemFileImport(bytes: uploadfile), 61 | )); 62 | } else if (result.paths.isNotEmpty) { 63 | String? filePath = result.paths[0]; 64 | if (filePath != null) { 65 | var fileAsBytes = await File(filePath).readAsBytes(); 66 | if (context.mounted) { 67 | Navigator.pushReplacement( 68 | context, 69 | MaterialPageRoute( 70 | builder: (context) => 71 | ItemFileImport(bytes: fileAsBytes), 72 | )); 73 | } 74 | } 75 | } 76 | } 77 | }, 78 | ), 79 | ListTile( 80 | title: const Text('Create new Accessory'), 81 | leading: const Icon(Icons.add_box), 82 | onTap: () { 83 | Navigator.pushReplacement( 84 | context, 85 | MaterialPageRoute( 86 | builder: (context) => const AccessoryGeneration()), 87 | ); 88 | }, 89 | ), 90 | ], 91 | ), 92 | ); 93 | }); 94 | } 95 | 96 | @override 97 | Widget build(BuildContext context) { 98 | return FloatingActionButton( 99 | mini: mini, 100 | heroTag: null, 101 | onPressed: () { 102 | showCreationSheet(context); 103 | }, 104 | tooltip: 'Create', 105 | child: const Icon(Icons.add), 106 | ); 107 | } 108 | } 109 | -------------------------------------------------------------------------------- /firmware/nrf5x/main.c: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | #include 4 | #include 5 | #include 6 | #include "ble_stack.h" 7 | #include "openhaystack.h" 8 | #include "app_timer.h" 9 | #include "battery.h" 10 | 11 | 12 | #define ADVERTISING_INTERVAL 5000 // advertising interval in milliseconds 13 | #define KEY_CHANGE_INTERVAL_MINUTES 30 // how often to rotate to new key in minutes 14 | #define KEY_CHANGE_INTERVAL_DAYS 14 // how often to update battery status in days 15 | #define MAX_KEYS 20 // maximum number of keys to rotate through 16 | 17 | #define KEY_CHANGE_INTERVAL_MS (KEY_CHANGE_INTERVAL_MINUTES * 60 * 1000) 18 | #define BATTERY_STATUS_UPDATES_INTERVAL_MS (KEY_CHANGE_INTERVAL_DAYS * 24 * 60 * 60 * 1000) 19 | 20 | #define APP_TIMER_PRESCALER 0 21 | #define APP_TIMER_MAX_TIMERS 2 22 | #define KEY_CHANGE_TIMER_TICKS APP_TIMER_TICKS(KEY_CHANGE_INTERVAL_MS, APP_TIMER_PRESCALER) 23 | #define BATTERY_STATUS_UPDATE_TIMER_TICKS APP_TIMER_TICKS(BATTERY_STATUS_UPDATES_INTERVAL_MS, APP_TIMER_PRESCALER) 24 | #define APP_TIMER_OP_QUEUE_SIZE 4 25 | 26 | int last_filled_index = -1; 27 | int current_index = 0; 28 | 29 | APP_TIMER_DEF(m_key_change_timer_id); 30 | APP_TIMER_DEF(m_battery_status_timer_id); 31 | 32 | // Create space for MAX_KEYS public keys 33 | static char public_key[MAX_KEYS][28] = { 34 | "OFFLINEFINDINGPUBLICKEYHERE!", 35 | }; 36 | 37 | uint8_t *raw_data; // Initialized by setAndAdvertiseNextKey() -> setAdvertisementKey() 38 | 39 | void setAndAdvertiseNextKey() 40 | { 41 | // Variable to hold the data to advertise 42 | uint8_t *ble_address; 43 | uint8_t data_len; 44 | 45 | // Disable advertising 46 | sd_ble_gap_adv_stop(); 47 | sd_ble_gap_adv_data_set(NULL, 0, NULL, 0); 48 | 49 | // Update key index for next advertisement...Back to zero if out of range 50 | current_index = (current_index + 1) % (last_filled_index + 1); 51 | 52 | // Set key to be advertised 53 | data_len = setAdvertisementKey(public_key[current_index], &ble_address, &raw_data); 54 | 55 | // Set bluetooth address 56 | setMacAddress(ble_address); 57 | 58 | // Update battery information 59 | updateBatteryLevel(raw_data); 60 | 61 | // Set advertisement data 62 | setAdvertisementData(raw_data, data_len); 63 | 64 | // Start advertising 65 | startAdvertisement(ADVERTISING_INTERVAL); 66 | } 67 | 68 | void key_change_timeout_handler(void *p_context) 69 | { 70 | setAndAdvertiseNextKey(); 71 | } 72 | 73 | void battery_status_update_timeout_handler(void *p_context) 74 | { 75 | updateBatteryLevel(raw_data); 76 | } 77 | 78 | 79 | static void key_change_timer_config(void) 80 | { 81 | uint32_t err_code; 82 | 83 | APP_TIMER_INIT(APP_TIMER_PRESCALER, APP_TIMER_OP_QUEUE_SIZE, NULL); 84 | 85 | // Create timer 86 | err_code = app_timer_create(&m_key_change_timer_id, APP_TIMER_MODE_REPEATED, key_change_timeout_handler); 87 | APP_ERROR_CHECK(err_code); 88 | 89 | // Set timer interval 90 | err_code = app_timer_start(m_key_change_timer_id, KEY_CHANGE_TIMER_TICKS, NULL); 91 | APP_ERROR_CHECK(err_code); 92 | } 93 | 94 | static void battery_status_update_timer_config(void) 95 | { 96 | uint32_t err_code; 97 | 98 | APP_TIMER_INIT(APP_TIMER_PRESCALER, APP_TIMER_OP_QUEUE_SIZE, NULL); 99 | 100 | // Create timer 101 | err_code = app_timer_create(&m_battery_status_timer_id, APP_TIMER_MODE_REPEATED, battery_status_update_timeout_handler); 102 | APP_ERROR_CHECK(err_code); 103 | 104 | // Set timer interval 105 | err_code = app_timer_start(m_battery_status_timer_id, BATTERY_STATUS_UPDATE_TIMER_TICKS, NULL); 106 | APP_ERROR_CHECK(err_code); 107 | } 108 | 109 | /** 110 | * main function 111 | */ 112 | int main(void) { 113 | 114 | // Find the last filled index 115 | for (int i = MAX_KEYS - 1; i >= 0; i--) 116 | { 117 | if (strlen(public_key[i]) > 0) 118 | { 119 | last_filled_index = i; 120 | break; 121 | } 122 | } 123 | 124 | // Init BLE stack and softdevice 125 | init_ble(); 126 | 127 | // Only use the app_timer to rotate keys if we need to 128 | if (last_filled_index > 0){ 129 | key_change_timer_config(); 130 | } 131 | 132 | if (last_filled_index >= 0) { 133 | setAndAdvertiseNextKey(); 134 | battery_status_update_timer_config(); 135 | } 136 | 137 | while (1){ 138 | power_manage(); 139 | } 140 | } 141 | -------------------------------------------------------------------------------- /macless_haystack/lib/location/location_model.dart: -------------------------------------------------------------------------------- 1 | import 'dart:async'; 2 | 3 | import 'package:flutter/material.dart'; 4 | import 'package:flutter/services.dart'; 5 | import 'package:geocoding/geocoding.dart' as geocode; 6 | import 'package:latlong2/latlong.dart'; 7 | import 'package:location/location.dart'; 8 | import 'package:logger/logger.dart'; 9 | 10 | class LocationModel extends ChangeNotifier { 11 | LatLng? here; 12 | geocode.Placemark? herePlace; 13 | StreamSubscription? locationStream; 14 | bool initialLocationSet = false; 15 | Location location = Location(); 16 | 17 | var logger = Logger( 18 | printer: PrettyPrinter(methodCount: 0), 19 | ); 20 | 21 | /// Requests access to the device location from the user. 22 | /// 23 | /// Initializes the location services and requests location 24 | /// access from the user if not granged. 25 | /// Returns if location access was granted. 26 | Future requestLocationAccess() async { 27 | bool serviceEnabled; 28 | PermissionStatus permissionGranted; 29 | 30 | serviceEnabled = await location.serviceEnabled(); 31 | if (!serviceEnabled) { 32 | serviceEnabled = await location.requestService(); 33 | if (!serviceEnabled) { 34 | return false; 35 | } 36 | } 37 | 38 | permissionGranted = await location.requestPermission(); 39 | if (permissionGranted == PermissionStatus.denied) { 40 | permissionGranted = await location.requestPermission(); 41 | if (permissionGranted != PermissionStatus.granted) { 42 | return false; 43 | } 44 | } 45 | return true; 46 | } 47 | 48 | /// Requests location updates from the platform. 49 | /// 50 | /// Listeners will be notified about location changes. 51 | Future requestLocationUpdates() async { 52 | var permissionGranted = await requestLocationAccess(); 53 | if (permissionGranted) { 54 | // Handle future location updates 55 | locationStream ??= location.onLocationChanged.listen(_updateLocation); 56 | 57 | var locationData = await location.getLocation(); 58 | // Fetch the current location 59 | _updateLocation(locationData); 60 | } else { 61 | initialLocationSet = true; 62 | if (locationStream != null) { 63 | locationStream?.cancel(); 64 | locationStream = null; 65 | } 66 | _removeCurrentLocation(); 67 | notifyListeners(); 68 | } 69 | } 70 | 71 | /// Updates the current location if new location data is available. 72 | /// 73 | /// Additionally updates the current address information to match 74 | /// the new location. 75 | void _updateLocation(LocationData locationData) { 76 | if (locationData.latitude != null && locationData.longitude != null) { 77 | logger.d( 78 | 'Location here: ${locationData.latitude!}, ${locationData.longitude!}'); 79 | here = LatLng(locationData.latitude!, locationData.longitude!); 80 | initialLocationSet = true; 81 | getAddress(here!).then((value) { 82 | herePlace = value; 83 | notifyListeners(); 84 | }); 85 | } else { 86 | logger.e('Received invalid location data: $locationData'); 87 | } 88 | notifyListeners(); 89 | } 90 | 91 | /// Cancels the listening for location updates. 92 | void cancelLocationUpdates() { 93 | if (locationStream != null) { 94 | locationStream?.cancel(); 95 | locationStream = null; 96 | } 97 | _removeCurrentLocation(); 98 | notifyListeners(); 99 | } 100 | 101 | /// Resets the currently stored location and address information 102 | void _removeCurrentLocation() { 103 | here = null; 104 | herePlace = null; 105 | } 106 | 107 | /// Returns the address for a given geolocation (latitude & longitude). 108 | /// 109 | /// Only works on mobile platforms with their local APIs. 110 | Future getAddress(LatLng? location) async { 111 | if (location == null) { 112 | return null; 113 | } 114 | double lat = location.latitude; 115 | double lng = location.longitude; 116 | 117 | try { 118 | if (geocode.GeocodingPlatform.instance != null) { 119 | List placemarks = 120 | await geocode.placemarkFromCoordinates(lat, lng); 121 | return placemarks.first; 122 | } 123 | } on MissingPluginException { 124 | return null; 125 | } on PlatformException { 126 | return null; 127 | } 128 | return null; 129 | } 130 | } 131 | -------------------------------------------------------------------------------- /macless_haystack/linux/CMakeLists.txt: -------------------------------------------------------------------------------- 1 | cmake_minimum_required(VERSION 3.10) 2 | project(runner LANGUAGES CXX) 3 | 4 | set(BINARY_NAME "openhaystack_mobile") 5 | set(APPLICATION_ID "de.seemoo.linux.openhaystack") 6 | 7 | cmake_policy(SET CMP0063 NEW) 8 | 9 | set(CMAKE_INSTALL_RPATH "$ORIGIN/lib") 10 | 11 | # Root filesystem for cross-building. 12 | if(FLUTTER_TARGET_PLATFORM_SYSROOT) 13 | set(CMAKE_SYSROOT ${FLUTTER_TARGET_PLATFORM_SYSROOT}) 14 | set(CMAKE_FIND_ROOT_PATH ${CMAKE_SYSROOT}) 15 | set(CMAKE_FIND_ROOT_PATH_MODE_PROGRAM NEVER) 16 | set(CMAKE_FIND_ROOT_PATH_MODE_PACKAGE ONLY) 17 | set(CMAKE_FIND_ROOT_PATH_MODE_LIBRARY ONLY) 18 | set(CMAKE_FIND_ROOT_PATH_MODE_INCLUDE ONLY) 19 | endif() 20 | 21 | # Configure build options. 22 | if(NOT CMAKE_BUILD_TYPE AND NOT CMAKE_CONFIGURATION_TYPES) 23 | set(CMAKE_BUILD_TYPE "Debug" CACHE 24 | STRING "Flutter build mode" FORCE) 25 | set_property(CACHE CMAKE_BUILD_TYPE PROPERTY STRINGS 26 | "Debug" "Profile" "Release") 27 | endif() 28 | 29 | # Compilation settings that should be applied to most targets. 30 | function(APPLY_STANDARD_SETTINGS TARGET) 31 | target_compile_features(${TARGET} PUBLIC cxx_std_14) 32 | target_compile_options(${TARGET} PRIVATE -Wall -Werror) 33 | target_compile_options(${TARGET} PRIVATE "$<$>:-O3>") 34 | target_compile_definitions(${TARGET} PRIVATE "$<$>:NDEBUG>") 35 | endfunction() 36 | 37 | set(FLUTTER_MANAGED_DIR "${CMAKE_CURRENT_SOURCE_DIR}/flutter") 38 | 39 | # Flutter library and tool build rules. 40 | add_subdirectory(${FLUTTER_MANAGED_DIR}) 41 | 42 | # System-level dependencies. 43 | find_package(PkgConfig REQUIRED) 44 | pkg_check_modules(GTK REQUIRED IMPORTED_TARGET gtk+-3.0) 45 | 46 | add_definitions(-DAPPLICATION_ID="${APPLICATION_ID}") 47 | 48 | # Application build 49 | add_executable(${BINARY_NAME} 50 | "main.cc" 51 | "my_application.cc" 52 | "${FLUTTER_MANAGED_DIR}/generated_plugin_registrant.cc" 53 | ) 54 | apply_standard_settings(${BINARY_NAME}) 55 | target_link_libraries(${BINARY_NAME} PRIVATE flutter) 56 | target_link_libraries(${BINARY_NAME} PRIVATE PkgConfig::GTK) 57 | add_dependencies(${BINARY_NAME} flutter_assemble) 58 | # Only the install-generated bundle's copy of the executable will launch 59 | # correctly, since the resources must in the right relative locations. To avoid 60 | # people trying to run the unbundled copy, put it in a subdirectory instead of 61 | # the default top-level location. 62 | set_target_properties(${BINARY_NAME} 63 | PROPERTIES 64 | RUNTIME_OUTPUT_DIRECTORY "${CMAKE_BINARY_DIR}/intermediates_do_not_run" 65 | ) 66 | 67 | # Generated plugin build rules, which manage building the plugins and adding 68 | # them to the application. 69 | include(flutter/generated_plugins.cmake) 70 | 71 | 72 | # === Installation === 73 | # By default, "installing" just makes a relocatable bundle in the build 74 | # directory. 75 | set(BUILD_BUNDLE_DIR "${PROJECT_BINARY_DIR}/bundle") 76 | if(CMAKE_INSTALL_PREFIX_INITIALIZED_TO_DEFAULT) 77 | set(CMAKE_INSTALL_PREFIX "${BUILD_BUNDLE_DIR}" CACHE PATH "..." FORCE) 78 | endif() 79 | 80 | # Start with a clean build bundle directory every time. 81 | install(CODE " 82 | file(REMOVE_RECURSE \"${BUILD_BUNDLE_DIR}/\") 83 | " COMPONENT Runtime) 84 | 85 | set(INSTALL_BUNDLE_DATA_DIR "${CMAKE_INSTALL_PREFIX}/data") 86 | set(INSTALL_BUNDLE_LIB_DIR "${CMAKE_INSTALL_PREFIX}/lib") 87 | 88 | install(TARGETS ${BINARY_NAME} RUNTIME DESTINATION "${CMAKE_INSTALL_PREFIX}" 89 | COMPONENT Runtime) 90 | 91 | install(FILES "${FLUTTER_ICU_DATA_FILE}" DESTINATION "${INSTALL_BUNDLE_DATA_DIR}" 92 | COMPONENT Runtime) 93 | 94 | install(FILES "${FLUTTER_LIBRARY}" DESTINATION "${INSTALL_BUNDLE_LIB_DIR}" 95 | COMPONENT Runtime) 96 | 97 | if(PLUGIN_BUNDLED_LIBRARIES) 98 | install(FILES "${PLUGIN_BUNDLED_LIBRARIES}" 99 | DESTINATION "${INSTALL_BUNDLE_LIB_DIR}" 100 | COMPONENT Runtime) 101 | endif() 102 | 103 | # Fully re-copy the assets directory on each build to avoid having stale files 104 | # from a previous install. 105 | set(FLUTTER_ASSET_DIR_NAME "flutter_assets") 106 | install(CODE " 107 | file(REMOVE_RECURSE \"${INSTALL_BUNDLE_DATA_DIR}/${FLUTTER_ASSET_DIR_NAME}\") 108 | " COMPONENT Runtime) 109 | install(DIRECTORY "${PROJECT_BUILD_DIR}/${FLUTTER_ASSET_DIR_NAME}" 110 | DESTINATION "${INSTALL_BUNDLE_DATA_DIR}" COMPONENT Runtime) 111 | 112 | # Install the AOT library on non-Debug builds only. 113 | if(NOT CMAKE_BUILD_TYPE MATCHES "Debug") 114 | install(FILES "${AOT_LIBRARY}" DESTINATION "${INSTALL_BUNDLE_LIB_DIR}" 115 | COMPONENT Runtime) 116 | endif() 117 | -------------------------------------------------------------------------------- /macless_haystack/lib/deployment/deployment_email.dart: -------------------------------------------------------------------------------- 1 | class DeploymentEmail { 2 | static const _mailtoLink = 3 | 'mailto:?subject=Open%20Haystack%20Deplyoment%20Instructions&body='; 4 | 5 | static const _welcomeMessage = 'OpenHaystack Deployment Guide\n\n' 6 | 'This is the deployment guide for your recently created OpenHaystack accessory. ' 7 | 'The next step is to deploy the generated cryptographic key to a compatible ' 8 | 'Bluetooth device.\n\n'; 9 | 10 | static const _finishedMessage = 11 | '\n\nThe device now sends out Bluetooth advertisements. ' 12 | 'It can take up to an hour for the location updates to appear in the app.\n'; 13 | 14 | static String getMicrobitDeploymentEmail(String advertisementKey) { 15 | String mailContent = 'nRF51822 Deployment:\n\n' 16 | 'Requirements\n' 17 | 'To build the firmware the GNU Arm Embedded Toolchain is required.\n\n' 18 | 'Download\n' 19 | 'Download the firmware source code from GitHub and navigate to the ' 20 | 'given folder.\n' 21 | 'https://github.com/seemoo-lab/openhaystack\n' 22 | 'git clone https://github.com/seemoo-lab/openhaystack.git && ' 23 | 'cd openhaystack/Firmware/Microbit_v1\n\n' 24 | 'Build\n' 25 | 'Replace the public_key in main.c (initially ' 26 | 'OFFLINEFINEINGPUBLICKEYHERE!) with the actual advertisement key. ' 27 | 'Then execute make to create the firmware. You can export your ' 28 | 'advertisement key directly from the OpenHaystack app.\n' 29 | 'static char public_key[28] = $advertisementKey;\n' 30 | 'make\n\n' 31 | 'Firmware Deployment\n' 32 | 'If the firmware is built successfully it can be deployed to the ' 33 | 'microcontroller with the following command. (Please fill in the ' 34 | 'volume of your microcontroller) \n' 35 | 'make install DEPLOY_PATH=/Volumes/MICROBIT'; 36 | 37 | return _mailtoLink + 38 | Uri.encodeComponent(_welcomeMessage) + 39 | Uri.encodeComponent(mailContent) + 40 | Uri.encodeComponent(_finishedMessage); 41 | } 42 | 43 | static String getESP32DeploymentEmail(String advertisementKey) { 44 | String mailContent = 'Espressif ESP32 Deployment: \n\n' 45 | 'Requirements\n' 46 | 'To build the firmware for the ESP32 Espressif\'s IoT Development ' 47 | 'Framework (ESP-IDF) is required. Additionally Python 3 and the venv ' 48 | 'module need to be installed.\n\n' 49 | 'Download\n' 50 | 'Download the firmware source code from GitHub and navigate to the ' 51 | 'given folder.\n' 52 | 'https://github.com/seemoo-lab/openhaystack\n' 53 | 'git clone https://github.com/seemoo-lab/openhaystack.git ' 54 | '&& cd openhaystack/Firmware/ESP32\n\n' 55 | 'Build\n' 56 | 'Execute the ESP-IDF build command to create the ESP32 firmware.\n' 57 | 'idf.py build\n\n' 58 | 'Firmware Deployment\n' 59 | 'If the firmware is built successfully it can be flashed onto the ' 60 | 'ESP32. This action is performed by the flash_esp32.sh script that ' 61 | 'is provided with the advertisement key of the newly created accessory.\n' 62 | 'Please fill in the serial port of your microcontroller.\n' 63 | 'You can export your advertisement key directly from the ' 64 | 'OpenHaystack app.\n' 65 | './flash_esp32.sh -p /dev/yourSerialPort $advertisementKey'; 66 | 67 | return _mailtoLink + 68 | Uri.encodeComponent(_welcomeMessage) + 69 | Uri.encodeComponent(mailContent) + 70 | Uri.encodeComponent(_finishedMessage); 71 | } 72 | 73 | static String getLinuxHCIDeploymentEmail(String advertisementKey) { 74 | String mailContent = 'Linux HCI Deployment:\n\n' 75 | 'Requirements\n' 76 | 'Install the hcitool software on a Bluetooth Low Energy Linux device, ' 77 | 'for example a Raspberry Pi. Additionally Pyhton 3 needs to be ' 78 | 'installed.\n\n' 79 | 'Download\n' 80 | 'Next download the python script that configures the HCI tool to ' 81 | 'send out BLE advertisements.\n' 82 | 'https://raw.githubusercontent.com/seemoo-lab/openhaystack/main/Firmware/Linux_HCI/HCI.py\n' 83 | 'curl -o HCI.py https://raw.githubusercontent.com/seemoo-lab/openhaystack/main/Firmware/Linux_HCI/HCI.py\n\n' 84 | 'Usage\n' 85 | 'To start the BLE advertisements run the script.\n' 86 | 'You can export your advertisement key directly from the ' 87 | 'OpenHaystack app.\n' 88 | 'sudo python3 HCI.py --key $advertisementKey'; 89 | 90 | return _mailtoLink + 91 | Uri.encodeComponent(_welcomeMessage) + 92 | Uri.encodeComponent(mailContent) + 93 | Uri.encodeComponent(_finishedMessage); 94 | } 95 | } 96 | -------------------------------------------------------------------------------- /firmware/nrf5x/nrf51_battery.h: -------------------------------------------------------------------------------- 1 | #ifdef S130 2 | #ifndef BLE_BAS_H__ 3 | #define BLE_BAS_H__ 4 | 5 | #include 6 | #include 7 | #include 8 | #include "ble.h" 9 | #include "ble_srv_common.h" 10 | #include "nrf_log.h" 11 | #include "nrf51_bitfields.h" 12 | #include "nrf51.h" 13 | 14 | #ifdef __cplusplus 15 | extern "C" { 16 | #endif 17 | 18 | /**@brief Battery Service event type. */ 19 | typedef enum 20 | { 21 | BLE_BAS_EVT_NOTIFICATION_ENABLED, /**< Battery value notification enabled event. */ 22 | BLE_BAS_EVT_NOTIFICATION_DISABLED /**< Battery value notification disabled event. */ 23 | } ble_bas_evt_type_t; 24 | 25 | /**@brief Battery Service event. */ 26 | typedef struct 27 | { 28 | ble_bas_evt_type_t evt_type; /**< Type of event. */ 29 | } ble_bas_evt_t; 30 | 31 | // Forward declaration of the ble_bas_t type. 32 | typedef struct ble_bas_s ble_bas_t; 33 | 34 | /**@brief Battery Service event handler type. */ 35 | typedef void (*ble_bas_evt_handler_t) (ble_bas_t * p_bas, ble_bas_evt_t * p_evt); 36 | 37 | /**@brief Battery Service init structure. This contains all options and data needed for 38 | * initialization of the service.*/ 39 | typedef struct 40 | { 41 | ble_bas_evt_handler_t evt_handler; /**< Event handler to be called for handling events in the Battery Service. */ 42 | bool support_notification; /**< TRUE if notification of Battery Level measurement is supported. */ 43 | ble_srv_report_ref_t * p_report_ref; /**< If not NULL, a Report Reference descriptor with the specified value will be added to the Battery Level characteristic */ 44 | uint8_t initial_batt_level; /**< Initial battery level */ 45 | ble_srv_cccd_security_mode_t battery_level_char_attr_md; /**< Initial security level for battery characteristics attribute */ 46 | ble_gap_conn_sec_mode_t battery_level_report_read_perm; /**< Initial security level for battery report read attribute */ 47 | } ble_bas_init_t; 48 | 49 | /**@brief Battery Service structure. This contains various status information for the service. */ 50 | struct ble_bas_s 51 | { 52 | ble_bas_evt_handler_t evt_handler; /**< Event handler to be called for handling events in the Battery Service. */ 53 | uint16_t service_handle; /**< Handle of Battery Service (as provided by the BLE stack). */ 54 | ble_gatts_char_handles_t battery_level_handles; /**< Handles related to the Battery Level characteristic. */ 55 | ble_gatts_char_handles_t battery_voltage_handles; /**< Handles related to the Battery Level characteristic. */ 56 | uint16_t report_ref_handle; /**< Handle of the Report Reference descriptor. */ 57 | uint8_t battery_level_last; /**< Last Battery Level measurement passed to the Battery Service. */ 58 | uint16_t conn_handle; /**< Handle of the current connection (as provided by the BLE stack, is BLE_CONN_HANDLE_INVALID if not in a connection). */ 59 | bool is_notification_supported; /**< TRUE if notification of Battery Level is supported. */ 60 | }; 61 | 62 | /**@brief Function for initializing the Battery Service. 63 | * 64 | * @param[out] p_bas Battery Service structure. This structure will have to be supplied by 65 | * the application. It will be initialized by this function, and will later 66 | * be used to identify this particular service instance. 67 | * @param[in] p_bas_init Information needed to initialize the service. 68 | * 69 | * @return NRF_SUCCESS on successful initialization of service, otherwise an error code. 70 | */ 71 | uint32_t ble_bas_init(ble_bas_t * p_bas, const ble_bas_init_t * p_bas_init); 72 | 73 | /**@brief Function for handling the Application's BLE Stack events. 74 | * 75 | * @details Handles all events from the BLE stack of interest to the Battery Service. 76 | * 77 | * @note For the requirements in the BAS specification to be fulfilled, 78 | * ble_bas_battery_level_update() must be called upon reconnection if the 79 | * battery level has changed while the service has been disconnected from a bonded 80 | * client. 81 | * 82 | * @param[in] p_bas Battery Service structure. 83 | * @param[in] p_ble_evt Event received from the BLE stack. 84 | */ 85 | void ble_bas_on_ble_evt(ble_bas_t * p_bas, ble_evt_t * p_ble_evt); 86 | 87 | uint8_t get_current_level (void); 88 | 89 | #ifdef __cplusplus 90 | } 91 | #endif 92 | 93 | #endif // BLE_BAS_H__ 94 | 95 | /** @} */ 96 | #endif -------------------------------------------------------------------------------- /macless_haystack/lib/accessory/accessory_list_item.dart: -------------------------------------------------------------------------------- 1 | 2 | import 'package:universal_io/io.dart'; 3 | 4 | import 'package:flutter/material.dart'; 5 | import 'package:geocoding/geocoding.dart'; 6 | import 'package:macless_haystack/accessory/accessory_icon.dart'; 7 | import 'package:macless_haystack/accessory/accessory_model.dart'; 8 | import 'package:intl/intl.dart'; 9 | 10 | import 'accessory_battery.dart'; 11 | 12 | class AccessoryListItem extends StatefulWidget { 13 | final Accessory accessory; 14 | final Widget? distance; 15 | final Placemark? herePlace; 16 | final VoidCallback onTap; 17 | final VoidCallback? onLongPress; 18 | 19 | const AccessoryListItem({ 20 | super.key, 21 | required this.accessory, 22 | required this.onTap, 23 | this.onLongPress, 24 | this.distance, 25 | this.herePlace, 26 | }); 27 | 28 | @override 29 | AccessoryListItemState createState() => AccessoryListItemState(); 30 | } 31 | 32 | class AccessoryListItemState extends State { 33 | Color _tileColor = Colors.transparent; 34 | 35 | @override 36 | Widget build(BuildContext context) { 37 | var hasChanged = widget.accessory.hasChangedFlag; 38 | if (hasChanged) { 39 | _tileColor = widget.accessory.color.withAlpha(50); 40 | Future.delayed(const Duration(seconds: 1), () { 41 | if (mounted) { 42 | widget.accessory.hasChangedFlag = false; 43 | setState(() { 44 | _tileColor = Colors.transparent; 45 | }); 46 | } 47 | }); 48 | } 49 | return FutureBuilder( 50 | future: widget.accessory.place, 51 | builder: (BuildContext context, AsyncSnapshot snapshot) { 52 | String locationString = widget.accessory.lastLocation != null 53 | ? '${widget.accessory.lastLocation!.latitude.toStringAsFixed(4)}, ${widget.accessory.lastLocation!.longitude.toStringAsFixed(4)}' 54 | : 'Unknown'; 55 | 56 | if (snapshot.hasData && snapshot.data != null) { 57 | Placemark place = snapshot.data!; 58 | locationString = '${place.locality}, ${place.administrativeArea}'; 59 | if (widget.herePlace != null && 60 | widget.herePlace!.country != place.country) { 61 | locationString = '${place.locality}, ${place.country}'; 62 | } 63 | } 64 | // Format published date in a human readable way 65 | String? dateString = widget.accessory.datePublished != null && 66 | widget.accessory.datePublished != DateTime(1970) 67 | ? '\n${DateFormat.yMMMd(Platform.localeName).format(widget.accessory.datePublished!)} ${DateFormat.jm(Platform.localeName).format(widget.accessory.datePublished!)}' 68 | : ''; 69 | 70 | return AnimatedContainer( 71 | duration: const Duration(milliseconds: 300), // Sanfter Übergang 72 | color: _tileColor, 73 | child: ListTile( 74 | onTap: widget.onTap, 75 | title: Row( 76 | crossAxisAlignment: CrossAxisAlignment.start, 77 | children: [ 78 | Text( 79 | widget.accessory.name + 80 | (widget.accessory.isActive ? '' : ' (inactive)'), 81 | style: TextStyle( 82 | color: widget.accessory.isActive 83 | ? Theme.of(context).colorScheme.onSurface 84 | : Theme.of(context).disabledColor, 85 | ), 86 | ), 87 | const SizedBox(width: 5), 88 | _buildIcon(), 89 | ], 90 | ), 91 | subtitle: SingleChildScrollView( 92 | scrollDirection: Axis.vertical, 93 | child: Text(locationString + dateString), 94 | ), 95 | trailing: widget.distance, 96 | dense: true, 97 | leading: GestureDetector( 98 | onLongPress: widget.onLongPress, 99 | child: AccessoryIcon( 100 | icon: widget.accessory.icon, 101 | color: widget.accessory.color, 102 | ), 103 | ), 104 | )); 105 | }, 106 | ); 107 | } 108 | 109 | Widget _buildIcon() { 110 | switch (widget.accessory.lastBatteryStatus) { 111 | case AccessoryBatteryStatus.ok: 112 | return const Icon(Icons.battery_full, color: Colors.green, size: 15); 113 | case AccessoryBatteryStatus.medium: 114 | return const Icon(Icons.battery_3_bar, color: Colors.orange, size: 15); 115 | case AccessoryBatteryStatus.low: 116 | return const Icon(Icons.battery_1_bar, color: Colors.red, size: 15); 117 | case AccessoryBatteryStatus.criticalLow: 118 | return const Icon(Icons.battery_alert, color: Colors.red, size: 15); 119 | default: 120 | return const SizedBox(width: 15); 121 | } 122 | } 123 | } 124 | -------------------------------------------------------------------------------- /macless_haystack/pubspec.yaml: -------------------------------------------------------------------------------- 1 | name: macless_haystack 2 | description: Macless Haystack 3 | 4 | # The following line prevents the package from being accidentally published to 5 | # pub.dev using `flutter pub publish`. This is preferred for private packages. 6 | publish_to: 'none' # Remove this line if you wish to publish to pub.dev 7 | 8 | # The following defines the version and build number for your application. 9 | # A version number is three numbers separated by dots, like 1.2.43 10 | # followed by an optional build number separated by a +. 11 | # Both the version and the builder number may be overridden in flutter 12 | # build by specifying --build-name and --build-number, respectively. 13 | # In Android, build-name is used as versionName while build-number used as versionCode. 14 | # Read more about Android versioning at https://developer.android.com/studio/publish/versioning 15 | # In iOS, build-name is used as CFBundleShortVersionString while build-number used as CFBundleVersion. 16 | # Read more about iOS versioning at 17 | # https://developer.apple.com/library/archive/documentation/General/Reference/InfoPlistKeyReference/Articles/CoreFoundationKeys.html 18 | version: 1.1.0 19 | 20 | environment: 21 | sdk: ">=3.4.0 <4.0.0" 22 | 23 | # Dependencies specify other packages that your package needs in order to work. 24 | # To automatically upgrade your package dependencies to the latest versions 25 | # consider running `flutter pub upgrade --major-versions`. Alternatively, 26 | # dependencies can be manually updated by changing the version numbers below to 27 | # the latest version available on pub.dev. To see which dependencies have newer 28 | # versions available, run `flutter pub outdated`. 29 | dependencies: 30 | flutter: 31 | sdk: flutter 32 | 33 | # UI 34 | flutter_colorpicker: ^1.1.0 35 | flutter_slidable: ^4.0.3 36 | marquee: ^2.3.0 37 | universal_io: ^2.3.1 38 | 39 | # Networking 40 | http: ^1.6.0 41 | universal_html: ^2.3.0 42 | 43 | # Cryptography 44 | # latest version of pointy castle for crypto functions 45 | pointycastle: ^4.0.0 46 | 47 | # State Management 48 | provider: ^6.1.5+1 49 | 50 | # Location 51 | flutter_map: ^8.2.2 52 | location: ^8.0.1 53 | geocoding: ^4.0.0 54 | 55 | # Storage 56 | shared_preferences: ^2.5.4 57 | flutter_secure_storage: ^10.0.0 58 | file_picker: ^10.3.7 59 | 60 | # Sharing 61 | share_plus: ^12.0.1 62 | url_launcher: ^6.3.2 63 | path_provider: ^2.1.5 64 | maps_launcher: ^3.0.0+1 65 | 66 | 67 | flutter_settings_screens: ^0.3.4 68 | latlong2: ^0.9.1 69 | intl: ^0.20.2 70 | logger: ^2.6.2 71 | 72 | 73 | dev_dependencies: 74 | 75 | dependency_validator: ^5.0.3 76 | # The "flutter_lints" package below contains a set of recommended lints to 77 | # encourage good coding practices. The lint set provided by the package is 78 | # activated in the `analysis_options.yaml` file located at the root of your 79 | # package. See that file for information about deactivating specific lint 80 | # rules and activating additional ones. 81 | flutter_lints: ^6.0.0 # Do not update 82 | test: ^1.28.0 83 | mockito: ^5.6.1 84 | 85 | # Configuration for flutter_launcher_icons 86 | flutter_icons: 87 | android: true 88 | ios: false 89 | image_path: "assets/OpenHaystackIcon.png" 90 | web: 91 | generate: true 92 | image_path: "assets/OpenHaystackIcon.png" 93 | background_color: "#0175C2" 94 | theme_color: "#0175C2" 95 | 96 | 97 | # For information on the generic Dart part of this file, see the 98 | # following page: https://dart.dev/tools/pub/pubspec 99 | 100 | # The following section is specific to Flutter. 101 | flutter: 102 | 103 | # The following line ensures that the Material Icons font is 104 | # included with your application, so that you can use the icons in 105 | # the material Icons class. 106 | uses-material-design: true 107 | 108 | # To add assets to your application, add an assets section, like this: 109 | # assets: 110 | # - images/a_dot_burr.jpeg 111 | # - images/a_dot_ham.jpeg 112 | assets: 113 | - assets/ 114 | 115 | # An image asset can refer to one or more resolution-specific "variants", see 116 | # https://flutter.dev/assets-and-images/#resolution-aware. 117 | 118 | # For details regarding adding assets from package dependencies, see 119 | # https://flutter.dev/assets-and-images/#from-packages 120 | 121 | # To add custom fonts to your application, add a fonts section here, 122 | # in this "flutter" section. Each entry in this list should have a 123 | # "family" key with the font family name, and a "fonts" key with a 124 | # list giving the asset and other descriptors for the font. For 125 | # example: 126 | # fonts: 127 | # - family: Schyler 128 | # fonts: 129 | # - asset: fonts/Schyler-Regular.ttf 130 | # - asset: fonts/Schyler-Italic.ttf 131 | # style: italic 132 | # - family: Trajan Pro 133 | # fonts: 134 | # - asset: fonts/TrajanPro.ttf 135 | # - asset: fonts/TrajanPro_Bold.ttf 136 | # weight: 700 137 | # 138 | # For details regarding fonts from package dependencies, 139 | # see https://flutter.dev/custom-fonts/#from-packages 140 | -------------------------------------------------------------------------------- /macless_haystack/lib/findMy/models.dart: -------------------------------------------------------------------------------- 1 | import 'dart:convert'; 2 | import 'dart:typed_data'; 3 | 4 | import 'package:flutter/foundation.dart'; 5 | import 'package:flutter/services.dart'; 6 | import 'package:logger/logger.dart'; 7 | import 'package:pointycastle/ecc/api.dart'; 8 | 9 | // ignore: implementation_imports 10 | import 'package:pointycastle/src/utils.dart' as pc_utils; 11 | import 'package:macless_haystack/findMy/find_my_controller.dart'; 12 | import 'package:macless_haystack/findMy/decrypt_reports.dart'; 13 | import 'package:macless_haystack/accessory/accessory_battery.dart'; 14 | 15 | 16 | /// Represents a decrypted FindMyReport. 17 | class FindMyLocationReport { 18 | static final logger = Logger( 19 | printer: PrettyPrinter(methodCount: 0), 20 | ); 21 | static const pointCorrection = 0xFFFFFFFF / 10000000; 22 | double? latitude; 23 | double? longitude; 24 | int? accuracy; 25 | DateTime? published; 26 | DateTime? timestamp; 27 | int? confidence; 28 | AccessoryBatteryStatus? batteryStatus; 29 | dynamic result; 30 | 31 | String? base64privateKey; 32 | 33 | String? id; 34 | String? hash; 35 | 36 | FindMyLocationReport(this.latitude, this.longitude, this.accuracy, 37 | this.published, this.timestamp, this.confidence, this.batteryStatus); 38 | 39 | FindMyLocationReport.withHash( 40 | this.latitude, this.longitude, this.timestamp, this.hash) { 41 | accuracy = 50; 42 | } 43 | 44 | FindMyLocationReport.decrypted(this.result, this.base64privateKey, this.id) { 45 | hash = result['payload']; 46 | } 47 | 48 | Location get location => Location(latitude!, longitude!); 49 | 50 | bool isEncrypted() { 51 | return latitude == null; 52 | } 53 | 54 | String? getId() { 55 | return id; 56 | } 57 | 58 | Future decrypt() async { 59 | { 60 | await Future.delayed(const Duration( 61 | milliseconds: 1)); //Is needed otherwise is executed synchron 62 | if (isEncrypted()) { 63 | final unixTimestampInMillis = result["datePublished"]; 64 | final datePublished = 65 | DateTime.fromMillisecondsSinceEpoch(unixTimestampInMillis); 66 | FindMyReport report = FindMyReport(datePublished, 67 | base64Decode(result["payload"]), id!, result["statusCode"]); 68 | 69 | FindMyLocationReport decryptedReport = 70 | await DecryptReports.decryptReport( 71 | report, base64Decode(base64privateKey!)); 72 | latitude = correctCoordinate(decryptedReport.latitude!, 90); 73 | longitude = correctCoordinate(decryptedReport.longitude!, 180); 74 | accuracy = decryptedReport.accuracy; 75 | timestamp = decryptedReport.timestamp; 76 | confidence = decryptedReport.confidence; 77 | result = null; 78 | base64privateKey = null; 79 | batteryStatus = decryptedReport.batteryStatus; 80 | } 81 | } 82 | } 83 | 84 | /// Correction caused by overflow, when point is outside range 85 | double correctCoordinate(double coordinate, int threshold) { 86 | if (coordinate > threshold) { 87 | coordinate = coordinate - pointCorrection; 88 | } 89 | if (coordinate < -threshold) { 90 | coordinate = coordinate + pointCorrection; 91 | } 92 | return coordinate; 93 | } 94 | } 95 | 96 | class Location { 97 | double latitude; 98 | double longitude; 99 | 100 | Location(this.latitude, this.longitude); 101 | } 102 | 103 | /// FindMy report returned by the FindMy Network 104 | class FindMyReport { 105 | DateTime datePublished; 106 | Uint8List payload; 107 | String id; 108 | int statusCode; 109 | 110 | int? confidence; 111 | DateTime? timestamp; 112 | 113 | FindMyReport(this.datePublished, this.payload, this.id, this.statusCode); 114 | 115 | FindMyReport.completeInit(this.datePublished, this.payload, this.id, 116 | this.statusCode, this.confidence, this.timestamp); 117 | } 118 | 119 | class FindMyKeyPair { 120 | final ECPublicKey _publicKey; 121 | final ECPrivateKey _privateKey; 122 | final String hashedPublicKey; 123 | String? privateKeyBase64; 124 | 125 | /// Time when this key was used to send BLE advertisements 126 | DateTime startTime; 127 | 128 | /// Duration from start time how long the key was used to send BLE advertisements 129 | double duration; 130 | 131 | FindMyKeyPair(this._publicKey, this.hashedPublicKey, this._privateKey, 132 | this.startTime, this.duration); 133 | 134 | String getBase64PublicKey() { 135 | return base64Encode(_publicKey.Q!.getEncoded(false)); 136 | } 137 | 138 | String getBase64PrivateKey() { 139 | return base64Encode(pc_utils.encodeBigIntAsUnsigned(_privateKey.d!)); 140 | } 141 | 142 | String getBase64AdvertisementKey() { 143 | return base64Encode(_getAdvertisementKey()); 144 | } 145 | 146 | Uint8List _getAdvertisementKey() { 147 | var pkBytes = _publicKey.Q!.getEncoded(true); 148 | //Drop first byte to get the 28byte version 149 | var key = pkBytes.sublist(1, pkBytes.length); 150 | return key; 151 | } 152 | 153 | String getHashedAdvertisementKey() { 154 | var key = _getAdvertisementKey(); 155 | return FindMyController.getHashedPublicKey(publicKeyBytes: key); 156 | } 157 | } 158 | -------------------------------------------------------------------------------- /firmware/nrf5x/Makefile: -------------------------------------------------------------------------------- 1 | PROJECT_NAME = $(shell basename "$(realpath ./)") 2 | 3 | APPLICATION_SRCS = $(notdir $(wildcard ./*.c)) 4 | APPLICATION_SRCS += softdevice_handler.c 5 | APPLICATION_SRCS += app_util_platform.c 6 | APPLICATION_SRCS += nrf_drv_common.c 7 | APPLICATION_SRCS += nrf_delay.c 8 | APPLICATION_SRCS += app_error.c 9 | APPLICATION_SRCS += app_error_weak.c 10 | APPLICATION_SRCS += nrf_drv_clock.c 11 | APPLICATION_SRCS += app_timer.c 12 | 13 | USE_NRF52 := $(filter nrf52,$(MAKECMDGOALS)) 14 | 15 | SDK_VERSION = 11 16 | 17 | SHELL:=/bin/bash 18 | 19 | # BOARD_SIMPLE is the default board with no external crystal to maximize compatibility 20 | # BOARD_E104BT5032A is for E104-BT5032A board https://www.aliexpress.com/item/4000538644215.html 21 | # BOARD_ALIEXPRESS is for this "AliExpress beacon" https://www.aliexpress.com/item/32826502025.html 22 | # ADV_KEYS_FILE the advertisment keys file, generated by macless haystack 23 | # BOARD_ALIEXPRESS_NO_XTAL is for AliExpress beacons without an XTAL 24 | 25 | ADV_KEYS_FILE ?= 26 | BOARD ?= BOARD_SIMPLE 27 | 28 | ADV_KEY_BASE64 ?= 29 | NEW_ADV_KEY_HEX := $(if $(ADV_KEY_BASE64),$(shell base64 -d <<< $(ADV_KEY_BASE64) | xxd -p),) 30 | 31 | # Compile for nrf52 by default, use "NRF_MODEL=nrf51 make" to compile for nrf51 platform 32 | ifeq ($(NRF_MODEL), nrf51) 33 | SOFTDEVICE_MODEL = s130 34 | else 35 | SOFTDEVICE_MODEL = s132 36 | NRF_MODEL = nrf52 37 | endif 38 | 39 | 40 | default: all 41 | 42 | 43 | buildDebug: clean debug 44 | mkdir -p compiled 45 | ifeq ($(NRF_MODEL), nrf51) 46 | mergehex -m nrf5x-base/sdk/nrf51_sdk_11.0.0/components/softdevice/s130/hex/s130_nrf51_2.0.0_softdevice.hex _build/$(PROJECT_NAME)_s130.hex -o compiled/$(NRF_MODEL)_firmware.hex 47 | else 48 | mergehex -m nrf5x-base/sdk/nrf51_sdk_11.0.0/components/softdevice/s132/hex/s132_nrf52_2.0.0_softdevice.hex _build/$(PROJECT_NAME)_s132.hex -o compiled/$(NRF_MODEL)_firmware.hex 49 | endif 50 | ifneq (, $(shell which objcopy)) 51 | objcopy --input-target=ihex --output-target=binary compiled/$(NRF_MODEL)_firmware.hex compiled/$(NRF_MODEL)_firmware.bin 52 | else ifneq (, $(shell which gobjcopy)) 53 | gobjcopy --input-target=ihex --output-target=binary compiled/$(NRF_MODEL)_firmware.hex compiled/$(NRF_MODEL)_firmware.bin 54 | else ifneq (, $(shell which hex2bin.py)) 55 | hex2bin.py compiled/$(NRF_MODEL)_firmware.hex compiled/$(NRF_MODEL)_firmware.bin 56 | else 57 | $(error Unable to find (g)objcopy or hex2bin.py. Please install binutils or hex2bin.py to generate a compiled binary.) 58 | endif 59 | 60 | build: clean all 61 | mkdir -p compiled 62 | ifeq ($(NRF_MODEL), nrf51) 63 | mergehex -m nrf5x-base/sdk/nrf51_sdk_11.0.0/components/softdevice/s130/hex/s130_nrf51_2.0.0_softdevice.hex _build/$(PROJECT_NAME)_s130.hex -o compiled/$(NRF_MODEL)_firmware.hex 64 | else 65 | mergehex -m nrf5x-base/sdk/nrf51_sdk_11.0.0/components/softdevice/s132/hex/s132_nrf52_2.0.0_softdevice.hex _build/$(PROJECT_NAME)_s132.hex -o compiled/$(NRF_MODEL)_firmware.hex 66 | endif 67 | ifneq (, $(shell which objcopy)) 68 | objcopy --input-target=ihex --output-target=binary compiled/$(NRF_MODEL)_firmware.hex compiled/$(NRF_MODEL)_firmware.bin 69 | else ifneq (, $(shell which gobjcopy)) 70 | gobjcopy --input-target=ihex --output-target=binary compiled/$(NRF_MODEL)_firmware.hex compiled/$(NRF_MODEL)_firmware.bin 71 | else ifneq (, $(shell which hex2bin.py)) 72 | hex2bin.py compiled/$(NRF_MODEL)_firmware.hex compiled/$(NRF_MODEL)_firmware.bin 73 | else 74 | $(error Unable to find (g)objcopy or hex2bin.py. Please install binutils or hex2bin.py to generate a compiled binary.) 75 | endif 76 | 77 | 78 | # I had some troubles to flashing the E104-BT5032A module, still not sure why, but I created this script that at least works better 79 | # to me than "make flash". It works by trying to recover the device on a loop which it will erase all the flash contents and then 80 | # it will programming it. You may need to unplug and plug VCC while executing this loop 81 | e104install: 82 | mergehex -m nrf5x-base/sdk/nrf51_sdk_11.0.0/components/softdevice/s132/hex/s132_nrf52_2.0.0_softdevice.hex _build/*.hex -o _build/full_firmware.bin 83 | nrfjprog -f nrf52 --recover; while [ "$$?" -ne "0" ]; do echo "****** IMPORTANT: If you see this message for too long, please try to disconnect and connect VCC ******" && nrfjprog -f nrf52 --recover; done 84 | nrfjprog -f nrf52 --program _build/full_firmware.bin --reset 85 | 86 | patch: 87 | ifneq ($(wildcard $(ADV_KEYS_FILE)),) 88 | cp compiled/$(NRF_MODEL)_firmware.bin compiled/$(NRF_MODEL)_firmware_patched.bin 89 | xxd -p -c 100000 $(ADV_KEYS_FILE) | xxd -r -p | \ 90 | dd of=compiled/$(NRF_MODEL)_firmware_patched.bin skip=1 bs=1 seek=$(shell grep -oba OFFLINEFINDINGPUBLICKEYHERE! compiled/$(NRF_MODEL)_firmware.bin | cut -d ':' -f 1) conv=notrunc 91 | else 92 | $(error The file $(ADV_KEYS_FILE) does not exist!) 93 | endif 94 | 95 | 96 | patch_old: 97 | ifneq ($(NEW_ADV_KEY_HEX),) 98 | xxd -p -c 1000000 < compiled/$(NRF_MODEL)_firmware.bin | \ 99 | sed 's/$(shell echo -n 'OFFLINEFINDINGPUBLICKEYHERE!' | xxd -p)/$(NEW_ADV_KEY_HEX)/' | \ 100 | xxd -r -p > compiled/$(NRF_MODEL)_firmware_patched.bin 101 | endif 102 | 103 | LIBRARY_PATHS += . 104 | #SOURCE_PATHS += . 105 | 106 | NRF_BASE_PATH ?= nrf5x-base 107 | include $(NRF_BASE_PATH)/make/Makefile 108 | -------------------------------------------------------------------------------- /macless_haystack/lib/item_management/item_import.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:provider/provider.dart'; 3 | import 'package:macless_haystack/accessory/accessory_model.dart'; 4 | import 'package:macless_haystack/accessory/accessory_registry.dart'; 5 | import 'package:macless_haystack/findMy/find_my_controller.dart'; 6 | import 'package:macless_haystack/item_management/accessory_color_input.dart'; 7 | import 'package:macless_haystack/item_management/accessory_icon_input.dart'; 8 | import 'package:macless_haystack/item_management/accessory_id_input.dart'; 9 | import 'package:macless_haystack/item_management/accessory_name_input.dart'; 10 | import 'package:macless_haystack/item_management/accessory_pk_input.dart'; 11 | 12 | class AccessoryImport extends StatefulWidget { 13 | /// Displays an input form to manually import an accessory. 14 | const AccessoryImport({super.key}); 15 | 16 | @override 17 | State createState() => _AccessoryImportState(); 18 | } 19 | 20 | class _AccessoryImportState extends State { 21 | /// Stores the properties of the accessory to import. 22 | Accessory newAccessory = Accessory( 23 | id: '', 24 | name: '', 25 | hashedPublicKey: '', 26 | datePublished: DateTime.now(), 27 | hashesWithTS: {}, 28 | locationHistory: [], 29 | lastBatteryStatus: null, 30 | additionalKeys: List.empty()); 31 | String privateKey = ''; 32 | 33 | final _formKey = GlobalKey(); 34 | 35 | /// Imports the private key to the key store. 36 | Future importKey(BuildContext context) async { 37 | if (_formKey.currentState != null) { 38 | if (_formKey.currentState!.validate()) { 39 | _formKey.currentState!.save(); 40 | try { 41 | var keyPair = await FindMyController.importKeyPair(privateKey); 42 | newAccessory.hashedPublicKey = keyPair.hashedPublicKey; 43 | } catch (e) { 44 | if (context.mounted) { 45 | ScaffoldMessenger.of(context).showSnackBar( 46 | const SnackBar( 47 | content: 48 | Text('Key import failed. Check if private key is correct.'), 49 | ), 50 | ); 51 | } 52 | } 53 | var keyPair = await FindMyController.importKeyPair(privateKey); 54 | newAccessory.hashedPublicKey = keyPair.hashedPublicKey; 55 | 56 | if (context.mounted) { 57 | AccessoryRegistry accessoryRegistry = 58 | Provider.of(context, listen: false); 59 | accessoryRegistry.addAccessory(newAccessory); 60 | Navigator.pop(context); 61 | } 62 | } 63 | } 64 | } 65 | 66 | @override 67 | Widget build(BuildContext context) { 68 | return Scaffold( 69 | appBar: AppBar( 70 | title: const Text('Import Accessory'), 71 | ), 72 | body: SingleChildScrollView( 73 | child: Form( 74 | key: _formKey, 75 | child: Column( 76 | children: [ 77 | const ListTile( 78 | title: Text( 79 | 'Please enter the accessory parameters. They can be found in the exported accessory file.'), 80 | ), 81 | AccessoryIdInput( 82 | changeListener: (id) => setState(() { 83 | newAccessory.id = id!; 84 | }), 85 | ), 86 | AccessoryNameInput( 87 | onSaved: (name) => setState(() { 88 | newAccessory.name = name!; 89 | }), 90 | ), 91 | AccessoryIconInput( 92 | initialIcon: newAccessory.icon, 93 | iconString: newAccessory.rawIcon, 94 | color: newAccessory.color, 95 | changeListener: (String? selectedIcon) { 96 | if (selectedIcon != null) { 97 | setState(() { 98 | newAccessory.setIcon(selectedIcon); 99 | }); 100 | } 101 | }, 102 | ), 103 | AccessoryColorInput( 104 | color: newAccessory.color, 105 | changeListener: (Color? selectedColor) { 106 | if (selectedColor != null) { 107 | setState(() { 108 | newAccessory.color = selectedColor; 109 | }); 110 | } 111 | }, 112 | ), 113 | AccessoryPrivateKeyInput( 114 | changeListener: (String? privateKeyVal) async { 115 | if (privateKeyVal != null) { 116 | setState(() { 117 | privateKey = privateKeyVal; 118 | }); 119 | } 120 | }, 121 | ), 122 | SwitchListTile( 123 | value: newAccessory.isActive, 124 | title: const Text('Is Active'), 125 | onChanged: (checked) { 126 | setState(() { 127 | newAccessory.isActive = checked; 128 | }); 129 | }, 130 | ), 131 | ListTile( 132 | title: ElevatedButton( 133 | child: const Text('Import'), 134 | onPressed: () => importKey(context), 135 | ), 136 | ), 137 | ], 138 | ), 139 | ), 140 | ), 141 | ); 142 | } 143 | } 144 | -------------------------------------------------------------------------------- /macless_haystack/lib/item_management/item_creation.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:provider/provider.dart'; 3 | import 'package:macless_haystack/accessory/accessory_model.dart'; 4 | import 'package:macless_haystack/accessory/accessory_registry.dart'; 5 | import 'package:macless_haystack/findMy/find_my_controller.dart'; 6 | import 'package:macless_haystack/item_management/accessory_color_input.dart'; 7 | import 'package:macless_haystack/item_management/accessory_icon_input.dart'; 8 | import 'package:macless_haystack/item_management/accessory_name_input.dart'; 9 | import 'package:macless_haystack/deployment/deployment_instructions.dart'; 10 | 11 | class AccessoryGeneration extends StatefulWidget { 12 | /// Displays a page to create a new accessory. 13 | /// 14 | /// The parameters of the new accessory can be input in text fields. 15 | const AccessoryGeneration({super.key}); 16 | @override 17 | State createState() { 18 | return _AccessoryGenerationState(); 19 | } 20 | } 21 | 22 | class _AccessoryGenerationState extends State { 23 | /// Stores the properties of the new accessory. 24 | Accessory newAccessory = Accessory( 25 | id: '', 26 | name: '', 27 | hashedPublicKey: '', 28 | datePublished: DateTime.now(), 29 | hashesWithTS: {}, 30 | locationHistory: [], 31 | lastBatteryStatus: null, 32 | additionalKeys: List.empty()); 33 | 34 | /// Stores the advertisement key of the newly created accessory. 35 | String? advertisementKey; 36 | 37 | final _formKey = GlobalKey(); 38 | 39 | /// Creates a new accessory with a new key pair. 40 | Future createAccessory(BuildContext context) async { 41 | if (_formKey.currentState != null) { 42 | if (_formKey.currentState!.validate()) { 43 | _formKey.currentState!.save(); 44 | 45 | var keyPair = await FindMyController.generateKeyPair(); 46 | advertisementKey = keyPair.getBase64AdvertisementKey(); 47 | newAccessory.hashedPublicKey = keyPair.hashedPublicKey; 48 | if (context.mounted) { 49 | AccessoryRegistry accessoryRegistry = 50 | Provider.of(context, listen: false); 51 | accessoryRegistry.addAccessory(newAccessory); 52 | } 53 | return true; 54 | } 55 | } 56 | return false; 57 | } 58 | 59 | @override 60 | Widget build(BuildContext context) { 61 | return Scaffold( 62 | appBar: AppBar( 63 | title: const Text('Create new Accessory'), 64 | ), 65 | body: SingleChildScrollView( 66 | child: Form( 67 | key: _formKey, 68 | child: Column( 69 | children: [ 70 | AccessoryNameInput( 71 | onSaved: (name) => setState(() { 72 | newAccessory.name = name!; 73 | }), 74 | ), 75 | AccessoryIconInput( 76 | initialIcon: newAccessory.icon, 77 | iconString: newAccessory.rawIcon, 78 | color: newAccessory.color, 79 | changeListener: (String? selectedIcon) { 80 | if (selectedIcon != null) { 81 | setState(() { 82 | newAccessory.setIcon(selectedIcon); 83 | }); 84 | } 85 | }, 86 | ), 87 | AccessoryColorInput( 88 | color: newAccessory.color, 89 | changeListener: (Color? selectedColor) { 90 | if (selectedColor != null) { 91 | setState(() { 92 | newAccessory.color = selectedColor; 93 | }); 94 | } 95 | }, 96 | ), 97 | const ListTile( 98 | title: Text( 99 | 'A secure key pair will be generated for you automatically.'), 100 | ), 101 | SwitchListTile( 102 | value: newAccessory.isActive, 103 | title: const Text('Is Active'), 104 | onChanged: (checked) { 105 | setState(() { 106 | newAccessory.isActive = checked; 107 | }); 108 | }, 109 | ), 110 | ListTile( 111 | title: OutlinedButton( 112 | child: const Text('Create only'), 113 | onPressed: () async { 114 | var created = await createAccessory(context); 115 | if (created && context.mounted) { 116 | Navigator.pop(context); 117 | } 118 | }, 119 | ), 120 | ), 121 | ListTile( 122 | title: ElevatedButton( 123 | child: const Text('Create and Deploy'), 124 | onPressed: () async { 125 | var created = await createAccessory(context); 126 | if (created && context.mounted) { 127 | Navigator.pushReplacement( 128 | context, 129 | MaterialPageRoute( 130 | builder: (context) => DeploymentInstructions( 131 | advertisementKey: 132 | advertisementKey ?? '', 133 | )), 134 | ); 135 | } 136 | }, 137 | ), 138 | ), 139 | ], 140 | ), 141 | ), 142 | ), 143 | ); 144 | } 145 | } 146 | -------------------------------------------------------------------------------- /generate_keys.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | import sys 3 | import base64 4 | import hashlib 5 | import random 6 | from cryptography.hazmat.primitives.asymmetric import ec 7 | from cryptography.hazmat.backends import default_backend 8 | import argparse 9 | import shutil 10 | import os 11 | import string 12 | from string import Template 13 | import struct 14 | 15 | OUTPUT_FOLDER = 'output/' 16 | TEMPLATE = Template('{' 17 | '\"id\": $id,' 18 | '\"colorComponents\": [' 19 | ' 0,' 20 | ' 1,' 21 | ' 0,' 22 | ' 1' 23 | '],' 24 | '\"name\": \"$name\",' 25 | '\"privateKey\": \"$privateKey\",' 26 | '\"icon\": \"\",' 27 | '\"isActive\": true,' 28 | '\"additionalKeys\": [$additionalKeys]' 29 | '}') 30 | 31 | 32 | def int_to_bytes(n, length, endianess='big'): 33 | h = '%x' % n 34 | s = ('0'*(len(h) % 2) + h).zfill(length*2).decode('hex') 35 | return s if endianess == 'big' else s[::-1] 36 | 37 | 38 | def to_C_byte_array(adv_key, isV3): 39 | out = '{' 40 | for element in range(0, len(adv_key)): 41 | e = adv_key[element] if isV3 else ord(adv_key[element]) 42 | out = out + "0x{:02x}".format(e) 43 | if element != len(adv_key)-1: 44 | out = out + ',' 45 | 46 | out = out + '}' 47 | return out 48 | 49 | 50 | def sha256(data): 51 | digest = hashlib.new("sha256") 52 | digest.update(data) 53 | return digest.digest() 54 | 55 | 56 | parser = argparse.ArgumentParser() 57 | parser.add_argument( 58 | '-n', '--nkeys', help='number of keys to generate', type=int, default=1) 59 | parser.add_argument('-p', '--prefix', help='prefix of the keyfiles') 60 | parser.add_argument( 61 | '-y', '--yaml', help='yaml file where to write the list of generated keys') 62 | parser.add_argument( 63 | '-v', '--verbose', help='print keys as they are generated', action="store_true") 64 | parser.add_argument( 65 | 66 | '-tinfs', '--thisisnotforstalking', help=argparse.SUPPRESS) 67 | 68 | args = parser.parse_args() 69 | 70 | MAX_KEYS = 1 71 | 72 | if (args.thisisnotforstalking == 'i_agree'): 73 | MAX_KEYS = 50 74 | 75 | 76 | if args.nkeys < 1 or args.nkeys > MAX_KEYS: 77 | raise argparse.ArgumentTypeError( 78 | "Number of keys out of range (between 1 and " + str(MAX_KEYS) + ")") 79 | 80 | 81 | current_directory = os.getcwd() 82 | final_directory = os.path.join(current_directory, OUTPUT_FOLDER) 83 | 84 | if os.path.exists(OUTPUT_FOLDER): 85 | shutil.rmtree(OUTPUT_FOLDER) 86 | 87 | os.mkdir(final_directory) 88 | 89 | 90 | prefix = '' 91 | 92 | if args.prefix is None: 93 | prefix = ''.join(random.choice(string.ascii_uppercase + 94 | string.digits) for _ in range(6)) 95 | else: 96 | prefix = args.prefix 97 | 98 | if args.yaml: 99 | yaml = open(OUTPUT_FOLDER + prefix + '_' + args.yaml + '.yaml', 'w') 100 | yaml.write(' keys:\n') 101 | 102 | 103 | keyfile = open(OUTPUT_FOLDER + prefix + '_keyfile', 'wb') 104 | 105 | keyfile.write(struct.pack("B", args.nkeys)) 106 | 107 | devices = open(OUTPUT_FOLDER + prefix + '_devices.json', 'w') 108 | devices.write('[\n') 109 | 110 | fname = '%s.keys' % (prefix) 111 | keys = open(OUTPUT_FOLDER + fname, 'w') 112 | 113 | 114 | isV3 = sys.version_info.major > 2 115 | print('Using python3' if isV3 else 'Using python2') 116 | print(f'Output will be written to {OUTPUT_FOLDER}') 117 | additionalKeys = [] 118 | i = 0 119 | while i < args.nkeys: 120 | priv = random.getrandbits(224) 121 | adv = ec.derive_private_key(priv, ec.SECP224R1( 122 | ), default_backend()).public_key().public_numbers().x 123 | if isV3: 124 | priv_bytes = priv.to_bytes(28, 'big') 125 | adv_bytes = adv.to_bytes(28, 'big') 126 | else: 127 | priv_bytes = int_to_bytes(priv, 28) 128 | adv_bytes = int_to_bytes(adv, 28) 129 | 130 | priv_b64 = base64.b64encode(priv_bytes).decode("ascii") 131 | adv_b64 = base64.b64encode(adv_bytes).decode("ascii") 132 | s256_b64 = base64.b64encode(sha256(adv_bytes)).decode("ascii") 133 | 134 | if '/' in s256_b64[:7]: 135 | print( 136 | 'Key skipped and regenerated, because there was a / in the b64 of the hashed pubkey :(') 137 | continue 138 | else: 139 | i += 1 140 | 141 | keyfile.write(base64.b64decode(adv_b64)) 142 | 143 | if i < args.nkeys: 144 | additionalKeys.append(priv_b64) # The last one is the leading one 145 | 146 | if args.verbose: 147 | print('%d)' % (i+1)) 148 | print('Private key: %s' % priv_b64) 149 | print('Advertisement key: %s' % adv_b64) 150 | print('Hashed adv key: %s' % s256_b64) 151 | 152 | if '/' in s256_b64[:7]: 153 | print( 154 | 'no key file written, there was a / in the b64 of the hashed pubkey :(') 155 | else: 156 | keys.write('Private key: %s\n' % priv_b64) 157 | keys.write('Advertisement key: %s\n' % adv_b64) 158 | keys.write('Hashed adv key: %s\n' % s256_b64) 159 | if args.yaml: 160 | yaml.write(' - "%s"\n' % adv_b64) 161 | 162 | addKeysS = '' 163 | if (len(additionalKeys) > 0): 164 | addKeysS = "\"" + "\",\"".join(additionalKeys) + "\"" 165 | 166 | 167 | devices.write(TEMPLATE.substitute(name=prefix, 168 | id=str(random.choice( 169 | range(0, 10000000))), 170 | privateKey=priv_b64, 171 | additionalKeys=addKeysS 172 | )) 173 | 174 | devices.write(']') 175 | -------------------------------------------------------------------------------- /macless_haystack/lib/findMy/decrypt_reports.dart: -------------------------------------------------------------------------------- 1 | import 'dart:typed_data'; 2 | 3 | import 'package:pointycastle/export.dart'; 4 | 5 | // ignore: implementation_imports 6 | import 'package:pointycastle/src/utils.dart' as pc_utils; 7 | import 'package:macless_haystack/findMy/models.dart'; 8 | import 'package:macless_haystack/accessory/accessory_battery.dart'; 9 | 10 | class DecryptReports { 11 | /// Decrypts a given [FindMyReport] with the given private key. 12 | static Future decryptReport( 13 | FindMyReport report, Uint8List key) async { 14 | final curveDomainParam = ECCurve_secp224r1(); 15 | var payloadData = report.payload; 16 | if (payloadData.length > 88) { 17 | final modifiedData = Uint8List(payloadData.length - 1); 18 | modifiedData.setRange(0, 4, payloadData); 19 | modifiedData.setRange(4, modifiedData.length, payloadData, 5); 20 | payloadData = modifiedData; 21 | } 22 | 23 | final ephemeralKeyBytes = payloadData.sublist(5, 62); 24 | final encData = payloadData.sublist(62, 72); 25 | final tag = payloadData.sublist(72, payloadData.length); 26 | 27 | _decodeTimeAndConfidence(payloadData, report); 28 | 29 | final privateKey = 30 | ECPrivateKey(pc_utils.decodeBigIntWithSign(1, key), curveDomainParam); 31 | 32 | final decodePoint = curveDomainParam.curve.decodePoint(ephemeralKeyBytes); 33 | final ephemeralPublicKey = ECPublicKey(decodePoint, curveDomainParam); 34 | 35 | final Uint8List sharedKeyBytes = _ecdh(ephemeralPublicKey, privateKey); 36 | final Uint8List derivedKey = _kdf(sharedKeyBytes, ephemeralKeyBytes); 37 | 38 | final decryptedPayload = _decryptPayload(encData, derivedKey, tag); 39 | final locationReport = _decodePayload(decryptedPayload, report); 40 | 41 | return locationReport; 42 | } 43 | 44 | /// Decodes the unencrypted timestamp and confidence 45 | static void _decodeTimeAndConfidence( 46 | Uint8List payloadData, FindMyReport report) { 47 | final seenTimeStamp = 48 | payloadData.sublist(0, 4).buffer.asByteData().getInt32(0, Endian.big); 49 | final timestamp = 50 | DateTime.utc(2001).add(Duration(seconds: seenTimeStamp)).toLocal(); 51 | final confidence = payloadData.elementAt(4); 52 | report.timestamp = timestamp; 53 | report.confidence = confidence; 54 | } 55 | 56 | /// Performs an Elliptic Curve Diffie-Hellman with the given keys. 57 | /// Returns the derived raw key data. 58 | static Uint8List _ecdh( 59 | ECPublicKey ephemeralPublicKey, ECPrivateKey privateKey) { 60 | final sharedKey = ephemeralPublicKey.Q! * privateKey.d; 61 | 62 | final bytes = sharedKey!.x! 63 | .toBigInteger()! 64 | .toUnsigned(28 * 8) 65 | .toRadixString(16) 66 | .padLeft(28 * 2, '0'); 67 | return Uint8List.fromList(List.generate( 68 | 28, (i) => int.parse(bytes.substring(i * 2, i * 2 + 2), radix: 16))); 69 | } 70 | 71 | /// Decodes the raw decrypted payload and constructs and returns 72 | /// the resulting [FindMyLocationReport]. 73 | static FindMyLocationReport _decodePayload( 74 | Uint8List payload, FindMyReport report) { 75 | final latitude = payload.buffer.asByteData(0, 4).getUint32(0, Endian.big); 76 | final longitude = payload.buffer.asByteData(4, 4).getUint32(0, Endian.big); 77 | final accuracy = payload.buffer.asByteData(8, 1).getUint8(0); 78 | final status = payload.buffer.asByteData(9, 1).getUint8(0); 79 | 80 | AccessoryBatteryStatus? batteryStatus; 81 | //STATUS_FLAG_BATTERY_UPDATES_SUPPORT is set (macless firmware) or status is not zero (pix firmware) 82 | if (status & 00100000 != 0 || status > 0) { 83 | switch (status >> 6) { 84 | // get highest 2 bits 85 | case 0: 86 | batteryStatus = AccessoryBatteryStatus.ok; 87 | break; 88 | case 1: 89 | batteryStatus = AccessoryBatteryStatus.medium; 90 | break; 91 | case 2: 92 | batteryStatus = AccessoryBatteryStatus.low; 93 | break; 94 | case 3: 95 | batteryStatus = AccessoryBatteryStatus.criticalLow; 96 | break; 97 | default: 98 | batteryStatus = null; 99 | } 100 | } 101 | final latitudeDec = latitude / 10000000.0; 102 | final longitudeDec = longitude / 10000000.0; 103 | 104 | return FindMyLocationReport( 105 | latitudeDec, 106 | longitudeDec, 107 | accuracy, 108 | report.datePublished, 109 | report.timestamp, 110 | report.confidence, 111 | batteryStatus); 112 | } 113 | 114 | /// Decrypts the given cipher text with the key data using an AES-GCM block cipher. 115 | /// Returns the decrypted raw data. 116 | static Uint8List _decryptPayload( 117 | Uint8List cipherText, Uint8List symmetricKey, Uint8List tag) { 118 | final decryptionKey = symmetricKey.sublist(0, 16); 119 | final iv = symmetricKey.sublist(16, symmetricKey.length); 120 | 121 | final aesGcm = GCMBlockCipher(AESEngine()) 122 | ..init( 123 | false, 124 | AEADParameters( 125 | KeyParameter(decryptionKey), tag.lengthInBytes * 8, iv, tag)); 126 | 127 | final plainText = Uint8List(cipherText.length); 128 | var offset = 0; 129 | while (offset < cipherText.length) { 130 | offset += aesGcm.processBlock(cipherText, offset, plainText, offset); 131 | } 132 | 133 | assert(offset == cipherText.length); 134 | return plainText; 135 | } 136 | 137 | /// ANSI X.963 key derivation to calculate the actual (symmetric) advertisement 138 | /// key and returns the raw key data. 139 | static Uint8List _kdf(Uint8List secret, Uint8List ephemeralKey) { 140 | var shaDigest = SHA256Digest(); 141 | shaDigest.update(secret, 0, secret.length); 142 | 143 | var counter = 1; 144 | var counterData = ByteData(4)..setUint32(0, counter); 145 | var counterDataBytes = counterData.buffer.asUint8List(); 146 | shaDigest.update(counterDataBytes, 0, counterDataBytes.lengthInBytes); 147 | 148 | shaDigest.update(ephemeralKey, 0, ephemeralKey.lengthInBytes); 149 | 150 | Uint8List out = Uint8List(shaDigest.digestSize); 151 | shaDigest.doFinal(out, 0); 152 | 153 | return out; 154 | } 155 | } 156 | -------------------------------------------------------------------------------- /macless_haystack/lib/dashboard/dashboard.dart: -------------------------------------------------------------------------------- 1 | 2 | import 'package:flutter/material.dart'; 3 | import 'package:flutter_settings_screens/flutter_settings_screens.dart'; 4 | import 'package:logger/logger.dart'; 5 | import 'package:macless_haystack/item_management/refresh_action.dart'; 6 | import 'package:provider/provider.dart'; 7 | import 'package:macless_haystack/accessory/accessory_registry.dart'; 8 | import 'package:macless_haystack/dashboard/accessory_map_list_vert.dart'; 9 | import 'package:macless_haystack/item_management/item_management.dart'; 10 | import 'package:macless_haystack/item_management/new_item_action.dart'; 11 | import 'package:macless_haystack/location/location_model.dart'; 12 | import 'package:macless_haystack/preferences/preferences_page.dart'; 13 | import 'package:macless_haystack/preferences/user_preferences_model.dart'; 14 | 15 | import '../accessory/accessory_model.dart'; 16 | 17 | class Dashboard extends StatefulWidget { 18 | /// Displays the layout for the mobile view of the app. 19 | /// 20 | /// The layout is optimized for a vertically aligned small screens. 21 | /// The functionality is structured in a bottom tab bar for easy access 22 | /// on mobile devices. 23 | const Dashboard({super.key}); 24 | 25 | @override 26 | State createState() { 27 | return _DashboardState(); 28 | } 29 | } 30 | 31 | class _DashboardState extends State { 32 | /// A list of the tabs displayed in the bottom tab bar. 33 | late final List> _tabs = [ 34 | { 35 | 'title': 'My Accessories', 36 | 'body': (ctx) => AccessoryMapListVertical( 37 | loadLocationUpdates: loadLocationUpdates, 38 | saveOrderUpdatesCallback: saveAccessories, 39 | ), 40 | 'icon': Icons.place, 41 | 'label': 'Map', 42 | 'actionButton': (ctx) => RefreshAction( 43 | callback: () async { 44 | await loadLocationUpdates(null); 45 | }, 46 | ), 47 | }, 48 | { 49 | 'title': 'My Accessories', 50 | 'body': (ctx) => const KeyManagement(), 51 | 'icon': Icons.style, 52 | 'label': 'Accessories', 53 | 'actionButton': (ctx) => const NewKeyAction(), 54 | }, 55 | ]; 56 | 57 | @override 58 | void initState() { 59 | super.initState(); 60 | 61 | // Initialize models and preferences 62 | var userPreferences = Provider.of(context, listen: false); 63 | var locationModel = Provider.of(context, listen: false); 64 | var locationPreferenceKnown = 65 | userPreferences.locationPreferenceKnown ?? false; 66 | var locationAccessWanted = userPreferences.locationAccessWanted ?? false; 67 | if (!locationPreferenceKnown || locationAccessWanted) { 68 | locationModel.requestLocationUpdates(); 69 | } 70 | // Load new location reports on app start 71 | if (Settings.getValue(fetchLocationOnStartupKey, 72 | defaultValue: true)!) { 73 | loadLocationUpdates(null); 74 | } 75 | } 76 | 77 | var logger = Logger( 78 | printer: PrettyPrinter(), 79 | ); 80 | 81 | /// Fetch location updates for all accessories. 82 | Future loadLocationUpdates(Accessory? accessory) async { 83 | var accessoryRegistry = 84 | Provider.of(context, listen: false); 85 | var inactive = 0; 86 | Iterable accessories; 87 | if (accessory == null) { 88 | accessories = accessoryRegistry.accessories; 89 | inactive = accessories.where((a) => !a.isActive).length; 90 | } else { 91 | accessories = [accessory]; 92 | } 93 | try { 94 | var count = await accessoryRegistry 95 | .loadLocationReports(accessories.where((a) => a.isActive)); 96 | if (mounted && accessories.isNotEmpty) { 97 | ScaffoldMessenger.of(context).showSnackBar( 98 | SnackBar( 99 | backgroundColor: Theme.of(context).colorScheme.primary, 100 | content: Text( 101 | 'Fetched $count location(s).${inactive > 0 ? '$inactive inactive accessories skipped' : ''}', 102 | style: TextStyle( 103 | color: Theme.of(context).colorScheme.onPrimary, 104 | ), 105 | ), 106 | ), 107 | ); 108 | } 109 | } catch (e, stacktrace) { 110 | logger.e('Error on fetching', error: e, stackTrace: stacktrace); 111 | if (mounted) { 112 | ScaffoldMessenger.of(context).showSnackBar( 113 | SnackBar( 114 | backgroundColor: Theme.of(context).colorScheme.error, 115 | content: Text( 116 | 'Could not find location reports. Try again later. Error: ${e.toString()}', 117 | style: TextStyle( 118 | color: Theme.of(context).colorScheme.onError, 119 | ), 120 | ), 121 | ), 122 | ); 123 | } 124 | } 125 | } 126 | 127 | /// The selected tab index. 128 | int _selectedIndex = 0; 129 | 130 | /// Updates the currently displayed tab to [index]. 131 | void _onItemTapped(int index) { 132 | setState(() { 133 | _selectedIndex = index; 134 | }); 135 | } 136 | 137 | @override 138 | Widget build(BuildContext context) { 139 | return Scaffold( 140 | appBar: AppBar( 141 | title: const Text('My Accessories'), 142 | actions: [ 143 | IconButton( 144 | onPressed: () { 145 | Navigator.push( 146 | context, 147 | MaterialPageRoute( 148 | builder: (context) => const PreferencesPage()), 149 | ); 150 | }, 151 | icon: const Icon(Icons.settings), 152 | ), 153 | ], 154 | ), 155 | body: _tabs[_selectedIndex]['body'](context), 156 | bottomNavigationBar: BottomNavigationBar( 157 | items: _tabs 158 | .map((tab) => BottomNavigationBarItem( 159 | icon: Icon(tab['icon']), 160 | label: tab['label'], 161 | )) 162 | .toList(), 163 | currentIndex: _selectedIndex, 164 | unselectedItemColor: Theme.of(context).secondaryHeaderColor, 165 | onTap: _onItemTapped, 166 | ), 167 | floatingActionButton: 168 | _tabs[_selectedIndex]['actionButton']?.call(context), 169 | floatingActionButtonLocation: FloatingActionButtonLocation.endDocked); 170 | } 171 | 172 | Future saveAccessories(List accessories) async { 173 | var accessoryRegistry = 174 | Provider.of(context, listen: false); 175 | accessoryRegistry.saveOrderUpdates(accessories); 176 | } 177 | } 178 | --------------------------------------------------------------------------------