diff --git a/.github/workflows/build.yaml b/.github/workflows/build.yaml new file mode 100644 index 0000000..2e46cc0 --- /dev/null +++ b/.github/workflows/build.yaml @@ -0,0 +1,17 @@ +name: build + +on: [push] + +jobs: + build: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v1 + - name: Set up Python + uses: actions/setup-python@v1 + - name: Install dependencies + run: | + python -m pip install --upgrade pip + pip install platformio + - name: Run PlatformIO Build + run: platformio run --environment nrf52840_dk \ No newline at end of file diff --git a/.github/workflows/test.yaml b/.github/workflows/test.yaml new file mode 100644 index 0000000..6293b34 --- /dev/null +++ b/.github/workflows/test.yaml @@ -0,0 +1,17 @@ +name: test + +on: [push] + +jobs: + test: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v1 + - name: Set up Python + uses: actions/setup-python@v1 + - name: Install dependencies + run: | + python -m pip install --upgrade pip + pip install platformio + - name: Run PlatformIO Tests + run: platformio test --environment desktop diff --git a/.gitignore b/.gitignore index 567609b..85ad76a 100644 --- a/.gitignore +++ b/.gitignore @@ -1 +1,47 @@ build/ + +# Random seed file created by test scripts and sample programs +seedfile + +# CMake build artifacts: +CMakeCache.txt +CMakeFiles +CTestTestfile.cmake +cmake_install.cmake +Testing +# CMake generates *.dir/ folders for in-tree builds (used by MSVC projects), ignore all of those: +*.dir/ +# MSVC files generated by CMake: +/*.sln +/*.vcxproj +/*.filters + +# Test coverage build artifacts: +Coverage +*.gcno +*.gcda + +# generated by scripts/memory.sh +massif-* + +# MSVC build artifacts: +*.exe +*.pdb +*.ilk +*.lib + +# Python build artifacts: +*.pyc + +# Generated documentation: +/apidoc + +# Editor navigation files: +/GPATH +/GRTAGS +/GSYMS +/GTAGS +/TAGS +/tags +.pio +.vscode \ No newline at end of file diff --git a/CMakeLists.txt b/CMakeLists.txt deleted file mode 100644 index ec9b378..0000000 --- a/CMakeLists.txt +++ /dev/null @@ -1,13 +0,0 @@ -# SPDX-License-Identifier: Apache-2.0 - -cmake_minimum_required(VERSION 3.13.1) -find_package(Zephyr REQUIRED HINTS $ENV{ZEPHYR_BASE}) -project(bluecovid) - -target_sources(app PRIVATE src/main.c src/contacts.c src/exposure-notification.c src/gatt-service.c src/covid.c) - -zephyr_include_directories(${APPLICATION_SOURCE_DIR}/src/tls_config) - -#use for testing only, will update keys every X seconds, default is 600 -#add_definitions(-DEN_INTERVAL_LENGTH=11) -#add_definitions(-DEN_INTERVAL_LENGTH=1) diff --git a/README.md b/README.md index 1de590a..a2dc2fa 100644 --- a/README.md +++ b/README.md @@ -1,7 +1,10 @@ -# Covid Bracelet, compatibile with Covid Apps on iOS and Android +# Covid Bracelet, compatibile with Covid Apps on iOS and Android [![Actions Status: test](https://github.com/CovidBraceletPrj/CovidBracelet/workflows/test/badge.svg)](https://github.com/CovidBraceletPrj/CovidBracelet/actions) [![Actions Status: build](https://github.com/CovidBraceletPrj/CovidBracelet/workflows/build/badge.svg)](https://github.com/CovidBraceletPrj/CovidBracelet/actions) + **Contributions Welcome!** + + ## Features * Sends and receives exposure beacons as specified by Google and Apple for Covid Contact Tracing * Rolling, encrypted, anonymous beacons @@ -17,8 +20,14 @@ Builds on on Zephyr OS and NRF52 BLE SOCs. Note: as we for now do not use the flash for key storage, this currently only works on nrf52480 or you can just store a very small number of keys. Moving the keys to flash is on the TODO list and will fix this. -## Get Started: -We use Zepyhr master as we need a newer mbed TLS as the ones that ships with Zephyr 2.2. We are waiting for the Zepyhr 2.3 release. To build, please install Zephyr and compile via west. Note that Platform.io does not support Zephyr 2.3 rc / Zepyhr master at the current time. +## Get Started: +This project is based on platformIO, see: [https://platformio.org/platformio-ide](https://platformio.org/platformio-ide) + +## Testing +To run the tests for the desktop environment select the test task for desktop or run: +``` +platformio test -e desktop +``` **Note: this is a proof of concept and not ready for production** diff --git a/include/tls_config/user-tls.conf b/include/tls_config/user-tls.conf new file mode 100644 index 0000000..fc9e231 --- /dev/null +++ b/include/tls_config/user-tls.conf @@ -0,0 +1,3 @@ +#define MBEDTLS_HKDF_C +#define MBEDTLS_CIPHER_MODE_CTR +#define MBEDTLS_ENTROPY_C \ No newline at end of file diff --git a/lib/README b/lib/README new file mode 100644 index 0000000..6debab1 --- /dev/null +++ b/lib/README @@ -0,0 +1,46 @@ + +This directory is intended for project specific (private) libraries. +PlatformIO will compile them to static libraries and link into executable file. + +The source code of each library should be placed in a an own separate directory +("lib/your_library_name/[here are source files]"). + +For example, see a structure of the following two libraries `Foo` and `Bar`: + +|--lib +| | +| |--Bar +| | |--docs +| | |--examples +| | |--src +| | |- Bar.c +| | |- Bar.h +| | |- library.json (optional, custom build options, etc) https://docs.platformio.org/page/librarymanager/config.html +| | +| |--Foo +| | |- Foo.c +| | |- Foo.h +| | +| |- README --> THIS FILE +| +|- platformio.ini +|--src + |- main.c + +and a contents of `src/main.c`: +``` +#include +#include + +int main (void) +{ + ... +} + +``` + +PlatformIO Library Dependency Finder will find automatically dependent +libraries scanning project source files. + +More information about PlatformIO Library Dependency Finder +- https://docs.platformio.org/page/librarymanager/ldf.html diff --git a/lib/exposure-notification/examples/tracking.c b/lib/exposure-notification/examples/tracking.c new file mode 100644 index 0000000..e0b7ffa --- /dev/null +++ b/lib/exposure-notification/examples/tracking.c @@ -0,0 +1,52 @@ +#include "exposure-notification.h" +#include "time.h" + + +int main(int argc, char **argv) { + + // first init everything + // TODO: More randomization, we init with NULL to let en init mbdet entropy for us + en_init(NULL); + + // get the currentTime + time_t currentTime = time(NULL); + + // The periodKey changes each EN_TEK_ROLLING_PERIOD intervals + ENIntervalNumber periodInterval = en_get_interval_number_at_period_start(currentTime); + + ENPeriodKey periodKey; + ENPeriodIdentifierKey periodIdentifierKey; + ENPeriodMetadataEncryptionKey periodMetadaEncryptionKey; + + // setup period keys at the beginning + // in theory you could let them generate automatically setting periodInterval to 0 + en_generate_and_derive_period_keys(&periodKey, &periodIdentifierKey, &periodMetadaEncryptionKey); + + char metadata[128] = ""; + + while(1) { + // we check the current time to know if we actually need to regenerate anything + currentTime = time(NULL); + ENIntervalNumber currentInterval = en_get_interval_number(currentTime); + + // we check if we need to generate new keys + if (currentInterval - periodInterval >= EN_TEK_ROLLING_PERIOD) { + periodInterval = en_get_interval_number_at_period_start(currentTime); + en_generate_and_derive_period_keys(&periodKey, &periodIdentifierKey, &periodMetadaEncryptionKey); + // TODO: Store new periodKey with periodInterval + } + + // we now generate the new interval identifier and re-encrypt the metadata + ENIntervalIdentifier intervalIdentifier; + en_derive_interval_identifier(&intervalIdentifier, &periodIdentifierKey, currentInterval); + + char encryptedMetadata[sizeof(metadata)]; + en_encrypt_interval_metadata(&periodMetadaEncryptionKey, &intervalIdentifier, metadata, encryptedMetadata, sizeof(metadata)); + + // broadcast intervalIdentifier plus encryptedMetada according to specs + // TODO: receive packets and store them + // repeat for 10-20 minutes + } + + return 0; +} \ No newline at end of file diff --git a/src/exposure-notification.h b/lib/exposure-notification/include/exposure-notification.h similarity index 100% rename from src/exposure-notification.h rename to lib/exposure-notification/include/exposure-notification.h diff --git a/lib/exposure-notification/library.json b/lib/exposure-notification/library.json new file mode 100644 index 0000000..46c0954 --- /dev/null +++ b/lib/exposure-notification/library.json @@ -0,0 +1,14 @@ +{ + "description": "exposure-notification", + "frameworks": "*", + "keywords": "exposure, notification, contact, tracking, tracing", + "name": "exposure-notification", + "platforms": "*", + "version": "0.1", + "dependencies": [ + { + "name": "mbedtls", + "version": "^2.16.4" + } + ] +} \ No newline at end of file diff --git a/src/exposure-notification.c b/lib/exposure-notification/src/exposure-notification.c similarity index 96% rename from src/exposure-notification.c rename to lib/exposure-notification/src/exposure-notification.c index ebf478a..497f14c 100644 --- a/src/exposure-notification.c +++ b/lib/exposure-notification/src/exposure-notification.c @@ -4,22 +4,27 @@ * SPDX-License-Identifier: Apache-2.0 */ + +#if EN_INCLUDE_ZEPHYR_DEPS #include #include #include #include #include +#endif + #include -#include -#include -#include -#include -#include +#include "mbedtls/entropy.h" +#include "mbedtls/ctr_drbg.h" +#include "mbedtls/hkdf.h" +#include "mbedtls/sha256.h" +#include "mbedtls/aes.h" #include "exposure-notification.h" + static mbedtls_ctr_drbg_context ctr_drbg; static ENRandomBytesCallback random_bytes_callback = NULL; diff --git a/platformio.ini b/platformio.ini new file mode 100644 index 0000000..9311773 --- /dev/null +++ b/platformio.ini @@ -0,0 +1,36 @@ +; PlatformIO Project Configuration File +; +; Build options: build flags, source filter +; Upload options: custom upload port, speed and extra flags +; Library options: dependencies, extra library storages +; Advanced options: extra scripting +; +; Please visit documentation for the other options and examples +; https://docs.platformio.org/page/projectconf.html + + +[env:nrf52840_dk] +platform = nordicnrf52 +board = nrf52840_dk +framework = zephyr +monitor_speed = 115200 +test_ignore = test_desktop +build_flags = + -Iinclude/tls_config + # For testing: -DUNITY_EXCLUDE_SETJMP_H=1 + -DEN_INCLUDE_ZEPHYR_DEPS=1 + -DEN_INIT_MBEDTLS_ENTROPY=0 +lib_deps = exposure-notification + + +[env:desktop] +platform = native +test_ignore = test_embedded +lib_compat_mode = off +lib_deps = mbedtls@~2 +build_flags = + -Iinclude/desktop + -Iinclude/tls_config + -Wno-nullability-completeness + -DMBEDTLS_USER_CONFIG_FILE='"user-tls.conf"' +src_filter = --<.src/> diff --git a/src/tls_config/user-tls.conf b/src/tls_config/user-tls.conf deleted file mode 100644 index 70b4d50..0000000 --- a/src/tls_config/user-tls.conf +++ /dev/null @@ -1,5 +0,0 @@ -#define MBEDTLS_HKDF_C -#define MBEDTLS_CIPHER_MODE_CTR -#define MBEDTLS_ENTROPY_C - -#define EN_INIT_MBEDTLS_ENTROPY 0 \ No newline at end of file diff --git a/test/README b/test/README new file mode 100644 index 0000000..df5066e --- /dev/null +++ b/test/README @@ -0,0 +1,11 @@ + +This directory is intended for PIO Unit Testing and project tests. + +Unit Testing is a software testing method by which individual units of +source code, sets of one or more MCU program modules together with associated +control data, usage procedures, and operating procedures, are tested to +determine whether they are fit for use. Unit testing finds problems early +in the development cycle. + +More information about PIO Unit Testing: +- https://docs.platformio.org/page/plus/unit-testing.html diff --git a/test/test_common/test_common.cpp b/test/test_common/test_common.cpp new file mode 100644 index 0000000..a5ec95c --- /dev/null +++ b/test/test_common/test_common.cpp @@ -0,0 +1,222 @@ +#include + +#include + +#include "exposure-notification.h" + +#define TEST_ASSERT_EQUAL_KEY(k1, k2) TEST_ASSERT_EQUAL(0, memcmp(k1.b, k2.b, sizeof(k1.b))) +#define TEST_ASSERT_NOT_EQUAL_KEY(k1, k2) TEST_ASSERT_NOT_EQUAL(0, memcmp(k1.b, k2.b, sizeof(k1.b))) + + +void test_init(void) { + int ret = en_init(NULL); + TEST_ASSERT_EQUAL(0, ret); +} + +void test_get_interval_number(void) { + TEST_ASSERT_EQUAL(0, en_get_interval_number(0)); + TEST_ASSERT_EQUAL(0, en_get_interval_number(EN_INTERVAL_LENGTH-1)); + TEST_ASSERT_EQUAL(1, en_get_interval_number(EN_INTERVAL_LENGTH)); + TEST_ASSERT_EQUAL(42, en_get_interval_number(EN_INTERVAL_LENGTH*42)); +} + +void test_get_interval_number_at_period_start(void) { + TEST_ASSERT_EQUAL(0, en_get_interval_number_at_period_start(0)); + TEST_ASSERT_EQUAL(0, en_get_interval_number_at_period_start(EN_INTERVAL_LENGTH-1)); + TEST_ASSERT_EQUAL(0, en_get_interval_number_at_period_start(EN_INTERVAL_LENGTH)); + TEST_ASSERT_EQUAL(0, en_get_interval_number_at_period_start(EN_INTERVAL_LENGTH*42)); + TEST_ASSERT_EQUAL(EN_TEK_ROLLING_PERIOD, en_get_interval_number_at_period_start(EN_INTERVAL_LENGTH*EN_TEK_ROLLING_PERIOD)); + TEST_ASSERT_EQUAL(EN_TEK_ROLLING_PERIOD, en_get_interval_number_at_period_start(EN_INTERVAL_LENGTH*EN_TEK_ROLLING_PERIOD+1)); +} + +void test_generate_period_key(void) { + ENPeriodKey p1, p2; + en_generate_period_key(&p1); + en_generate_period_key(&p2); + // yes there is a minimal chance that both are the same ;) + TEST_ASSERT_NOT_EQUAL_KEY(p1, p2); +} + +void test_get_period_identifier_key(void) { + + ENPeriodKey pk; + en_generate_period_key(&pk); + + ENPeriodIdentifierKey k1, k2; + en_derive_period_identifier_key(&k1, &pk); + en_derive_period_identifier_key(&k2, &pk); + + // This time both should be the same key as the depent on the period key + TEST_ASSERT_EQUAL_KEY(k1, k2); + + // BUT: they should be different than the period key itself + TEST_ASSERT_NOT_EQUAL_KEY(k1, pk); +} + + +void test_get_interval_identifier(void) { + + ENPeriodKey pk; + en_generate_period_key(&pk); + + ENIntervalNumber iv = en_get_interval_number(0); + + ENIntervalIdentifier id1, id2; + en_derive_interval_identifier(&id1, &pk, iv); + en_derive_interval_identifier(&id2, &pk, iv); + + // identities should be the same for the same intervalnumber and period key + + TEST_ASSERT_EQUAL_KEY(id1, id2); + + iv = en_get_interval_number(EN_INTERVAL_LENGTH); + en_derive_interval_identifier(&id2, &pk, iv); + + // BUT: they should be different when we are using another interval number + TEST_ASSERT_NOT_EQUAL_KEY(id1, id2); + + // They should also be different for another period key but the the same interval number + en_generate_period_key(&pk); + en_derive_interval_identifier(&id1, &pk, iv); + + en_generate_period_key(&pk); + en_derive_interval_identifier(&id2, &pk, iv); + + TEST_ASSERT_NOT_EQUAL_KEY(id1, id2); +} + +void test_get_period_metadata_encryption_key(void) { + + ENPeriodKey pk; + en_generate_period_key(&pk); + ENPeriodMetadataEncryptionKey k1, k2; + en_derive_period_metadata_encryption_key(&k1, &pk); + en_derive_period_metadata_encryption_key(&k2, &pk); + + // This time both should be the same key as the depent on the period key + TEST_ASSERT_EQUAL_KEY(k1, k2); + + // BUT: they should be different than the period key itself + TEST_ASSERT_NOT_EQUAL_KEY(k1, pk); + + // and they should also be different than the periodIdentifierKey + ENPeriodIdentifierKey pik; + en_derive_period_identifier_key(&pik, &pk); + TEST_ASSERT_NOT_EQUAL_KEY(k1, pik); +} + + +void test_en_encrypt_interval_metadata(void) { + + ENPeriodKey pk; + en_generate_period_key(&pk); + ENPeriodMetadataEncryptionKey k1; + en_derive_period_metadata_encryption_key(&k1, &pk); + + ENIntervalNumber iv = en_get_interval_number(0); + ENIntervalIdentifier id; + en_derive_interval_identifier(&id, &pk, iv); + + unsigned char testData[42] = "This is some test data :)"; + unsigned char encryptedData[42] = ""; + + en_encrypt_interval_metadata(&k1, &id, testData, encryptedData, sizeof(testData)); + + // the original data should not be changed + TEST_ASSERT_EQUAL_CHAR_ARRAY("This is some test data :)", testData, sizeof("This is some test data :)")-1); + + // but the encrypted one should be changed + TEST_ASSERT_NOT_EQUAL(0, memcmp(testData, encryptedData, sizeof(testData))); + + unsigned char decryptedData[42] = ""; + en_decrypt_interval_metadata(&k1, &id, encryptedData, decryptedData, sizeof(testData)); + + // the decrypted metadata should be the same as the testData + TEST_ASSERT_EQUAL_CHAR_ARRAY(testData, decryptedData, sizeof(testData)); +} + +void test_identifier_generation_performance(void) { + + int runs = 1000000; + + for(int i = 0; i < runs; i++) { + ENPeriodKey pk; + en_generate_period_key(&pk); + ENPeriodIdentifierKey ik; + en_derive_period_identifier_key(&ik, &pk); + + for(int iv = 0; iv < EN_TEK_ROLLING_PERIOD; iv++) { + ENIntervalNumber intervalNumber = en_get_interval_number(iv); + ENIntervalIdentifier id; + en_derive_interval_identifier(&id, &ik, intervalNumber); + } + } +} + +void test_generate_and_derive_period_keys(void) { + // Test multiple generation + ENPeriodKey periodKey; + ENPeriodIdentifierKey periodIdentifierKey; + ENPeriodMetadataEncryptionKey periodMetadaEncryptionKey; + for(int i = 0; i < 10; i++) { + en_generate_and_derive_period_keys(&periodKey, &periodIdentifierKey, &periodMetadaEncryptionKey); + } +} + + +void test_against_fixtures(void) { + // First define base values + ENIntervalNumber intervalNumber = 2642976; + ENPeriodKey periodKey = {.b = {0x75, 0xc7, 0x34, 0xc6, 0xdd, 0x1a, 0x78, 0x2d, 0xe7, 0xa9, 0x65, 0xda, 0x5e, 0xb9, 0x31, 0x25}}; + unsigned char metadata[4] = {0x40, 0x08, 0x00, 0x00}; + + // define the expected values + ENPeriodIdentifierKey expectedPIK = {.b = {0x18, 0x5a, 0xd9, 0x1d, 0xb6, 0x9e, 0xc7, 0xdd, 0x04, 0x89, 0x60, 0xf1, 0xf3, 0xba, 0x61, 0x75}}; + ENPeriodMetadataEncryptionKey expectedPMEK = {.b = {0xd5, 0x7c, 0x46, 0xaf, 0x7a, 0x1d, 0x83, 0x96, 0x5b, 0x9b, 0xed, 0x8b, 0xd1, 0x52, 0x93, 0x6a}}; + + ENIntervalIdentifier expectedIntervalIdentifier = {.b = {0x8b, 0xe6, 0xcd, 0x37, 0x1c, 0x5c, 0x89, 0x16, 0x04, 0xbf, 0xbe, 0x49, 0xdf, 0x84, 0x50, 0x96}}; + unsigned char expectedEncryptedMetadata[4] = {0x72, 0x03, 0x38, 0x74}; + + + ENPeriodIdentifierKey pik; + en_derive_period_identifier_key(&pik, &periodKey); + TEST_ASSERT_EQUAL_KEY(expectedPIK, pik); + + ENPeriodMetadataEncryptionKey pmek; + en_derive_period_metadata_encryption_key(&pmek, &periodKey); + TEST_ASSERT_EQUAL_KEY(expectedPMEK, pmek); + + ENIntervalIdentifier intervalIdentifier; + en_derive_interval_identifier(&intervalIdentifier, &pik, intervalNumber); + TEST_ASSERT_EQUAL_KEY(expectedIntervalIdentifier, intervalIdentifier); + + + unsigned char encryptedMetadata[sizeof(metadata)] = {0}; + en_encrypt_interval_metadata(&pmek, &intervalIdentifier, metadata, encryptedMetadata, sizeof(metadata)); + TEST_ASSERT_EQUAL_CHAR_ARRAY(expectedEncryptedMetadata, encryptedMetadata, sizeof(expectedEncryptedMetadata)); +} + + +void test_exposure_notification() { + UNITY_BEGIN(); + RUN_TEST(test_init); + RUN_TEST(test_get_interval_number); + RUN_TEST(test_get_interval_number_at_period_start); + RUN_TEST(test_generate_period_key); + RUN_TEST(test_get_period_identifier_key); + RUN_TEST(test_get_interval_identifier); + RUN_TEST(test_get_period_metadata_encryption_key); + RUN_TEST(test_en_encrypt_interval_metadata); + RUN_TEST(test_generate_and_derive_period_keys); + RUN_TEST(test_against_fixtures); + //RUN_TEST(test_identifier_generation_performance); + UNITY_END(); +} + + + + +int main(int argc, char **argv) { + test_exposure_notification(); + return 0; +} \ No newline at end of file diff --git a/zephyr/CMakeLists.txt b/zephyr/CMakeLists.txt new file mode 100644 index 0000000..258e99a --- /dev/null +++ b/zephyr/CMakeLists.txt @@ -0,0 +1,6 @@ +cmake_minimum_required(VERSION 3.13.1) +include($ENV{ZEPHYR_BASE}/cmake/app/boilerplate.cmake NO_POLICY_SCOPE) +project(test) + +FILE(GLOB app_sources ../src/*.c*) +target_sources(app PRIVATE ${app_sources}) diff --git a/prj.conf b/zephyr/prj.conf similarity index 81% rename from prj.conf rename to zephyr/prj.conf index dc60d8a..cee8fc9 100644 --- a/prj.conf +++ b/zephyr/prj.conf @@ -8,8 +8,6 @@ CONFIG_BT_DEVICE_NAME="BlueCovid" CONFIG_ENTROPY_GENERATOR=y CONFIG_MBEDTLS=y -#CONFIG_MBEDTLS_USER_CONFIG_ENABLE=y -#CONFIG_MBEDTLS_CFG_FILE="mbedtls_config.h" CONFIG_MBEDTLS_USER_CONFIG_ENABLE=y CONFIG_MBEDTLS_USER_CONFIG_FILE="user-tls.conf"