В посленее время мир «умного дома» стал более близок для начинающих энтузиастов, благодаря наличию большого количества аппаратных решений с низким порогом вхождения (речь конечно про платформу Arduino и немалому набору модулей/сенсоров для неё) и уже готовых библиотек и фреймворков для работы с ними. Как правило они имеют настройки по-умолчанию (мак-адреса, каналы и т.п.), которые нетронутыми и остаются в руках этих самых начинающих… Например фреймворк MySensors, упоминавщийся не так давно на Хабре, имеет файл настроек «MyConfig.h», который многие (мой незадачливый сосед в частности) даже не правят.

С одной стороны мне все равно, что в многоэтажном доме кто-то сможет «подслушать» температуру на кухне (а то и позвонить с вопросом «Что готовишь?»), но с другой стороны не хотелось бы, чтобы кто-то смог (хоть бы и теоретически) управлять силовыми нагрузками (включить любимую кофе-машину, например). Хочется быть чуть более уверенным, что комманда исходит от моего управляющего устройства, а не подставного (в криптографии, это известно как «проверка подлинности»).

Реализовать такой подход сравнительно просто…

Схема взаимодействия управляющего и исполнительного устройства построена на «подписи» случайной фразы внутренним ключом, известным обоим устройствам. Наглядней это можно продемонстрировать так:

image

Реализовать можно с помощью трех функций. Одна для генерации случайной фразы:

byte alphabet[] = { 0x61, 0x62, 0x63, 0x64, 0x65, 0x66, 0x67, 0x68, 0x69, 0x6a, 0x6b, 0x6c, 0x6d, 0x6e, 0x6f, 0x70, 0x71, 0x72, 0x73, 0x74, 0x75, 0x76, 0x77, 0x78, 0x79, 0x7a }; // a, b, c, d, etc

void getPhrase(byte *phrase) {
	for (int i = 0; i < 8; i++) {
		phrase[i] = alphabet[random(0, 26)];
		phrase[i + 1] = '\0';
	}
}


Вторая собственно создание MD5-хеша (MD5 library for the Arduino):

#include <MD5.h>

byte sessionPhrase[9];
byte salt[] = { 0x4c, 0x39, 0x78, 0x36, 0x73, 0x4c, 0x39, 0x78 }; // L9x6sL9x - внутренний ключ

void signPhrase(byte *phrase, char *signedPhrase) {
	byte localPhrase[17];
	for (int i = 0; i < 16; i++) {
		if (i < 8) localPhrase[i] = salt[i];
		else localPhrase[i] = sessionPhrase[i - 8];
		localPhrase[i + 1] = '\0';
	}

	unsigned char* hash = MD5::make_hash((char *)localPhrase);
	char *md5str = MD5::make_digest(hash, 16);

	for (int i = 0; i < 16; i++) {
		signedPhrase[i] = md5str[i];
		signedPhrase[i + 1] = '\0';
	}

	free(hash);
	free(md5str);
}


Третья для сверки «подписанной» фразы инициатора с «самоподписанной» фразой исполнителя:

bool compareSignedPhrases(char *signedPhrase) {
	if (strcmp(signedPhrase, (char *)sessionMD5Phrase) == 0) {
		return true;
	}
	return false;
}


Краткий код для проверки работы генерации фразы, её подписывания и сравнения
#include <MySensor.h>
#include <SPI.h>
#include <MD5.h>

#define DEBUG 1

byte sessionPhrase[9];
char sessionMD5Phrase[17];
byte salt[] = { 0x4c, 0x39, 0x78, 0x36, 0x73, 0x4c, 0x39, 0x78 }; // L9x6sL9x — внутренний ключ
byte alphabet[] = { 0x61, 0x62, 0x63, 0x64, 0x65, 0x66, 0x67, 0x68, 0x69, 0x6a, 0x6b, 0x6c, 0x6d, 0x6e, 0x6f, 0x70, 0x71, 0x72, 0x73, 0x74, 0x75, 0x76, 0x77, 0x78, 0x79, 0x7a }; // a, b, c, d, etc

void setup() {
Serial.begin(115200);
}

void loop() {
/* Генерируем случайную фразу */
getPhrase(sessionPhrase);
signPhrase(sessionPhrase, sessionMD5Phrase); // Запоминаем для дальнейшего сравнения

/* Подписываем */
char signedPhrase[17];
signPhrase(sessionPhrase, signedPhrase);

#ifdef DEBUG
Serial.print(«signedPhrase: „);
Serial.println((char *)signedPhrase);
Serial.print(“copmpare: „);
Serial.println(compareSignedPhrases(signedPhrase));
Serial.println(“»);
#endif

delay(1000);
}

void getPhrase(byte *phrase) {
for (int i = 0; i < 8; i++) {
phrase[i] = alphabet[random(0, 26)];
phrase[i + 1] = '\0';
}

#ifdef DEBUG
Serial.print(«sessionPhrase: „);
Serial.println((char *)sessionPhrase);
#endif
}

void signPhrase(byte *phrase, char *signedPhrase) {
byte catPhrase[17];
for (int i = 0; i < 16; i++) {
if (i < 8) catPhrase[i] = salt[i];
else catPhrase[i] = sessionPhrase[i — 8];
catPhrase[i + 1] = '\0';
}

#ifdef DEBUG
Serial.print(“catPhrase: „);
Serial.println((char *)catPhrase);
#endif

unsigned char* hash = MD5::make_hash((char *)catPhrase);
char *md5str = MD5::make_digest(hash, 16);

for (int i = 0; i < 16; i++) {
signedPhrase[i] = md5str[i];
signedPhrase[i + 1] = '\0';
}

free(hash);
free(md5str);
}

bool compareSignedPhrases(char *signedPhrase) {
if (strcmp(signedPhrase, (char *)sessionMD5Phrase) == 0) {
return true;
}
return false;
}


Такой подход можно исполнять как в своих “велосипедах», так и в других фреймворках (таких как MySensors) и дает дополнительный плюсик к безопасности Вашего Интернета вещей.

Кстати, аутентификация будет в MySensors, на текущий момент она реализована в development-версии.

P.S.: Если будет интересно сообществу — сделаю update заметки на примере того же MySensors с примерами листингов для Serial Gateway и Relay…

Комментарии (10)


  1. vedenin1980
    08.06.2015 18:06
    +1

    Но md5 для целей криптографии не рекомендуют использовать уже давным давно, так как он достаточно легко вскрывается перебором. Я бы на вашем месте лучше попробовал прикрутить библиотеку с SHA-2 или чем-то вроде того.


    1. zazhim103 Автор
      08.06.2015 18:12
      +2

      Воспримите это как некий вектор движения начинающего в сторону уменьшения паранои. Да и в плейграунде проекта Arduino одной из первых указана именно эта библиотека.


    1. akirsanov
      09.06.2015 08:55
      +1

      md5 плох для хранения паролей, в случае с подписью, при достаточной длине ключа, — вполне уместен.


  1. Ivan_83
    08.06.2015 19:09
    +5

    Ну кто же так пишет!?

    " for (int i = 0; i < 16; i++) {
    if (i < 8) localPhrase[i] = salt[i];
    else localPhrase[i] = sessionPhrase[i — 8];
    localPhrase[i + 1] = '\0';
    }" — это memcpy два раза.
    «localPhrase[i + 1] = '\0';» — выносится за цикл, те два memcpy.
    memcpy(localPhrase, salt, 8);
    memcpy((localPhrase + 8), sessionPhrase, 8);
    localPhrase[16] = 0;

    «for (int i = 0; i < 16; i++) {
    signedPhrase[i] = md5str[i];
    signedPhrase[i + 1] = '\0';
    }» — опять такое же порно: memcpy и зануление после него.

    «if (strcmp(signedPhrase, (char *)sessionMD5Phrase) == 0)» — МЕГАПОРНО!
    1. с чего вы взяли что там будет ноль на конце?
    2. нужно было хотя бы memcmp() заюзать с фиксированным размером, тайминг атака осталось бы, зато оно бы не падало.
    3. нужно защищатся от тайминг атак: cryptocoding.net/index.php/Coding_rules#Compare_secret_strings_in_constant_time

    По крипте.
    Я не большой спец, но дописывать одно к другому не очень хорошая идея, примерно так работают атаки на расширение.
    Чтобы не парится придумали hmac поверх хэш функций.

    По протоколу.
    Порно!
    Не нужно рассчитывать что кто то будет присылать строки, про строковые функции вообще нужно забыть.
    Правильный вариант будет: подключаемся, сразу от сервера=исполнительного девайса прилетает рандом байт на 16-64 и хэш.
    Мы загоняем в hmac и убеждаемся что это реально наш сервер/девайс.
    Берём нашу команду и этот рандом (как данные), загоняем их в hmac, и этому же hmac скармливаем наш секретный общий ключ.
    Получившееся (команду + хэш) отправляем на сервер.
    Сервер получает команду и хэш. К команду+рандом и секретный ключ скармливает в hmac и получает хэш, сравнивает его с тем что в пакете.

    Тут лежат мои md5, sha1, sha2 (и chacha20, ECDSA+ГОСТ Р34.10-2001, ГОСТ Р34.10-2012): netlab.linkpc.net/download/software/SDK/core/include
    там и hmac варианты и селфтесты есть.
    Достаточно один раз заинклюдить чтобы начать пользоваться.

    А заметке надо делать не апдейт а переписывать с нуля :)


    1. zazhim103 Автор
      08.06.2015 20:01

      Спасибо за дельные советы! Многим нравится именно обсуждения. Это и есть формат Хабра!

      Не нужно рассчитывать что кто то будет присылать строки

      Возможно Иван не совсем правильно понял, но общение между Инициатором и Исполнителем происходит по Радио-каналу именно по строковым данным (в заметке упоминается не один раз фраймворк MySensors), именно в связи с этим такой алгоритм действий, который можно применить и к другим системам.

      К тому же на каждое обращение генерируется новая «случайная» фраза, что позволит обойти «перебор», описанный в комментарии выше…


      1. areht
        10.06.2015 01:51

        > К тому же на каждое обращение генерируется новая «случайная» фраза, что позволит обойти «перебор»

        Это всё замечательно, только у вас защиты от подмены сообщений нет. Достаточно «включи свет в ванной» на «убей всех человеков» заменить и дальше само всё подпишется.

        Вообще, криптография — это не просто пару хешей посчитать. Писать самому… С тем же эффектом (и гораздо проще) прогнать XORом сообщение по ключу


        1. zazhim103 Автор
          10.06.2015 11:15

          Достаточно «включи свет в ванной» на «убей всех человеков» заменить...

          Естественно, исполнитель имеет свой ограниченный функционал и игнорирует команды, которые в него не заложены. Например в том же MySensors можно проверять тип входящего сообщения (message.type == V_LIGHT) и адресность конкретного сенсора (message.sensor == PRIMARY_CHILD_ID)…


          1. areht
            10.06.2015 13:58

            > и игнорирует команды, которые в него не заложены

            естественно я не рассчитываю, что лампочка будет варить кофе


  1. Vendict
    10.06.2015 06:38
    +1

    Но тут ещё одна проблема. К примеру, на Arduino, использование простейшего md5 добавляет 13К к коду, плюс столько же библиотека взаимодействия с устройством и в итоге нам останется всего 10К для нашей программы. При реализации взаимодействия с Arduino через EtherCard я думал об использовании простейшей подписи, но когда увидел размеры кода для подписи — отказался от данного решения. Нужно придумывать свою упрощённую реализацию со скидкой на «Неуловимого Джо».


    1. zazhim103 Автор
      10.06.2015 11:25

      Да, конечно, здесь по большей части обозначен «вектор движения». Можно экпериментировать, например с DES and 3DES Encryption Library.