В данной статье речь пойдет про Apple HomeKit Accessory Protocol (HAP): внутренности и разработку контроллера.
Apple HomeKit создан для взаимодействия контроллера (по умолчанию iOS-устройства, приложение Home) и множества устройств(аксессуаров). Протокол открыт для некоммерческого использования, загрузить его можно с сайта Apple. На основе этой версии протокола создано несколько open-source проектов, и когда говорят про HomeKit на каком-нибуль Raspberry Pi обычно подразумевают установку homebridge и плагинов для создания совместимых аксессуаров.
Обратная же задача - создание контроллера - не такая распространенная и из проектов мне удалось найти лишь pypi.org/project/homekit/.
Поставим задачу создать контроллер, например, для управления аксессуарами с Android-телефона и попробуем ее решить. Для простоты будем работать только с IP-сетями, без Bluetooth.
Как это должно работать?
- Обнаружение устройства 
Для того, чтобы начать работать с аксессуарами, их необходимо первым делом обнаружить. Устройства рекламируют себя в соответствии с протоколами Multicast DNS и DNS service discovery.
Говоря проще, можно в локальной сети обнаружить устройство, отправив multicast запрос _hap._tcp.local по адресу 224.0.0.251, и, получив ответ, распарсить DNS записи A, SRV, TXT. После этого можно подключаться к сервису, используя полученную информацию.
- Установка защищенного соединения 
Возможно два сценария: устройства уже связаны, либо связь (pairing) надо только установить. В первом случае нужно перемещаться к шагу /pair-verify, в случае же установления нового соединения, первым делом надо выполнить шаг /pair-setup.
Apple HomeKit использует протокол Stanfordʼs Secure Remote Password (SRP) с использованием пароля (пин-кода).
- Работа с аксессуарами, характеристиками и их значениями. 
/pair-setup
Коммуникация происходит по установленному TCP соединению. Все запросы в данном шаге - это обычные HTTP POST запросы с типом данных application/pairing+tlv8 и соответственно с телом в TLV-кодировке.
Далее кратко что происходит на данном этапе:
- M1: контроллер отправляет запрос на установление связи (SRP Start Request) 
- M2: аксессуар инициирует новую сессию SRP, генерирует необходимые рандомы и ключевую пару. В ответ контроллеру отправляется сгенерированный публичный ключ и соль. (SRP Start Response) 
- M3: контроллер отправляет запрос на проверку данных (SRP Verify Request). На данном шаге контроллер генерирует свою сессионную ключевую пару , спрашивает пользователя ввести пин-код, считает общий ключ SRP сессии и пруф (SRP proof). Аксессуару отправляется сгенерированный публичный ключ и пруф. 
- M4: аксессуар проверяет пруф контроллера отправляет свой пруф в ответ (SRP Verify Response). 
- M5: контроллер -> аксессуару (‘Exchange Requestʼ). Первым делом контроллер проверяет пруф аксессуара. После этого генерируется долгосрочная ключевая пара (LTPK и LTSK) на кривой ed25519. Контроллер формирует новый ключ (HKDF) из сессионного ключа, конкатенирует его с идентификатором контроллера(iOSDevicePairingID) и его публичным ключом (iOSDeviceLTPK), подписывает секретным LTSK. Идентификатор, публичный ключ и подпись записываются в TLV-сообщение, шифруются алгоритмом ChaCha20-Poly1305 с использованием общего сессионного ключа. Зашифрованное сообщение опять записывается в виде TLV-сообщения и отправляется аксессуару. 
- M6: аксессуар -> контроллер (‘Exchange Responseʼ). Здесь же аксессуар извлекает информацию (iOSDeviceLTPK, iOSDevicePairingID), проверяет подпись. Далее, аналогично, подписывает и отправляет свой идентификатор, долгосрочный публичный ключ, подпись. 
После успешного выполнения всех шагов M1-M6, контроллер и iOS устройство сохраняют идентификаторы и публичные ключи (LTPK) друг друга на долгий срок.
/pair-verify
Процедура используется каждый раз для установления защищенного соединения. Здесь же шагов уже меньше (M1-M4).
Каждый участник: и Контроллер, и Аксессуар генерируют Curve25519 ключевые пары, отправляют друг другу публичные ключи и вырабатывают симметричный общий ключ, из которого формируется сессионный ключ. Долгосрочные ключи (LTPK и LTSK) используются лишь для проверки подписей.
Защищенное соединение
После успешного завершения процедуры pair-verify соединение TCP остается открытым и все данные внутри него зашифрованы сессионным ключом. Получается, что Keep-Alive HTTP-соединение "обновляется" (аналогично вебсокетовскому Upgrade) и теперь для получения корректного HTTP данные необходимо прежде расшифровать.
Данные - точно так же HTTP запросы и ответы, но уже стандартный json.
Начало решения: выбор
Выбор остановился на Go и brutella/hap пакете. Модуль не содержит в себе реализации контроллера и планов на добавление нет, поэтому необходимо все будет сделать самому. Но это просто, учитывая то, что все криптографические процедуры реализованы для серверной части.
В пользу решения на Go говорило и то, что на нем можно писать графическую часть в том числе и для Android (fyne.io, gioui.org).
Модуль форкнут, удалено лишнего, добавлены файлы для части контроллера.
Реализация:
По реализации подробно расписывать не буду, только несколько моментов.
- При обнаружении устройств контроллер по очереди для разных ip-адресов устройства пробует подключиться по TCP. После первой удачной попытки данные сохраняюся для последующего установления постоянного соединения. 
- Поскольку все запросы - это http, то можно использовать родную для Go реализацию http.Client. Возник вопрос как заставить его работать с обычным TCP-соединением? Для этого необходимо поддержать интерфейс RoundTripper: 
func (c *conn) RoundTrip(req *http.Request) (*http.Response, error) {
  err := req.Write(c)
  if err != nil {
    return nil, err
  }
  if c.inBackground {
    res := <-c.response
    return res, nil
  }
  rd := bufio.NewReader(c)
  res, err := http.ReadResponse(rd, nil)
  if err != nil {
    return nil, err
  }
  return res, nil
}
После этого можем назначать http.Client и использовать его:
	d.httpc = &http.Client{
		Transport: c,
	}
    // использовать:
	res, err := d.httpc.Get("/accessories")
    ...- И самое интересное. Если посмотреть на код выше, то можно заметить условие на флаг inBackground. Ведь можно же было обойтись одним http.ReadResponse. И на этапе pair-setup и pair-verify это работает. Проблема возникает уже после установления безопасной сессии. Дело в том, что аксессуары могут отправлять уведомления об изменениях значений. И такие уведомления выглядят так: 
EVENT/1.0 200 OK
Content-Type: application/hap+json
Content-Length: <length>
{
  ”characteristics” : [{
    ”aid” : 1,
    ”iid” : 4,
    ”value” : 23.0
  }]
}Что мы имеем? Во-первых, все данные надо читать в цикле, чтобы не пропустить уведомления. Во вторых, http.ReadResponse не может с ним справиться, поскольку EVENT - не стандартный для http заголовок.
С первым справится просто - запускаем горутину, считывающую данные:
func (c *conn) backgroundRead() {
rd := bufio.NewReader(c)
for {
	b, err := rd.Peek(len(eventHeader)) // len of EVENT string
	if err != nil {
		fmt.Println(err)
		if errors.Is(err, io.EOF) {
			return
		}
		continue
	}
	if string(b) == eventHeader {
      // обработка события
      // трансформируем событие (заменяем EVENT на HTTP)
      // читаем с res := http.ReadResponse()
      // читаем all := io.ReadAll(res.Body)
      // присваиваем res.Body = io.NopCloser(bytes.NewReader(all))
      // вызываем колбэк
    } else {
      // обработка ответа
      // читаем с res := http.ReadResponse()
      // читаем all := io.ReadAll(res.Body)
      // присваиваем res.Body = io.NopCloser(bytes.NewReader(all))
    }
  }
}Каждую итерацию проверяем заголовок на совпадение с EVENT и в таком случае - "трансформируем" - заменяем EVENT на HTTP для успешной обработки методом http.ReadResponse. Для замены пишем структуру с реализацией интерфейса io.Reader.
Следующая возникшая проблема: в некоторых случаях (длинный ответ) при итерации цикла возникала ошибка на неверный заголовок HTTP. Проблема в том, что ReadResponse возвращает ответ с полем Body, в котором данные не читаны, а значит не читаны они и в нашем соединении. Решение - прочитать полностью res.Body и только после этого можно переходить на следующую итерацию.
Графическое приложение
Для наброска графического приложение использовался модуль gioui.org. На функционал приложение на данный момент небогато - обнаружение устройств, аутентификация и установление соединения, управление аксессуарами реле и лампами (Вкл-Выкл).
Работа приложения проверялась в паре с homebridge.
PS: к сожалению, при запуске на Android, приложение не смогло обнаружить ни одно устройство.
avc: denied { bind } for scontext=u:r:untrusted_app:s0:c31,c257,c512,c768 tcontext=u:r:untrusted_app:s0:c31,c257,c512,c768 tclass=netlink_route_socket permissive=0 b/155595000 app=localhost.hkappСсылки
- github.com/hkontrol/hkontroller собственно, реализация контроллера 
- github.com/hkontrol/hkapp графический интерфейс 
Заинтересованных в open-source разработке приглашаю принять участие.
 
           
 
Svbakulin
Интересно, спасибо. Использую HomeKit как напрямую так и (в основном) через Home Assistant где есть контроллер и бридж. Hass всем управляет (интеграция, автоматики), и задача iOS контроллеров сводится к интеграции с Сири чтобы можно было управлять всем чего угодно голосом или через Home App. Нужно это редко, но довольно удобно когда можно включить\выключить вентилятор голосом не вылезая из ванной :)
Вообще интересное что яблоки пока единственные пожалуй из крупных кто сделал домашнюю автоматизацию правильно. Google, Alexa - эти работают через облака и фактически весь дом зависит от интернета и внешних сервисов что на мой взгляд совершенно нелогично. Это ненужная задержка плюс риски безопасности и доступности. SmartThings вроде поубивало много своих девайсов (на днях видел их очиститель воздуха с бешеной скидкой - подозреваю что он просто кирпич), или как Tuya вырубила сберовские девайсы.
Хоть это и требует локального девайса, контроллер должен быть локальным, но с управлением и бакапами в облаках, как это собственно и сделано в HomeKit (с оговорками, без яблочных облаков это все равно не будет работать). Потому никакой Tuya и все что на ней сделано, Гугла или Алексы у меня дома нет и не будет.
Хорошо что последнее время китайцы похоже начали понимать "как надо" (или даже скорее у них что то еще есть о чем я не знаю) и начали появляться девайсы с поддержкой HomeKit что мне сразу говорит о том что с этим можно работать. Дело не в сертификации а в архитектуре. Даже если интеграции напрямую с Home Assistant нет, то всегда можно интегрироваться через HomeKit controller. Aqara\Xiaomi добавила поддержку в своих хабах (не то чтобы мне надо но все равно большой шаг, у них много хорошего железа), девайсы на базе CozyLife app поддерживают хомут кит (как и двух других) и у них есть то чего нет у аквары или других вендоров, есть немного девайсов с опенсорсной прошивкой для HomeKit как то athom.tech (есть у меня их LED controller - работает исключительно хорошо через HomeAssistant's HomeKit controller). несертифицированные девайсы конечно риск но пока все работало.
bobalus Автор
Да, Apple правильно и грамотно сделали.
Из других проектов без облаков - W3C Web Of Things или Mozilla Webthings.
Тоже dnssd для обнаружения, REST API для управления.
Но насчет безопасности стандарт молчит на данный момент и во всех примерах предлагается создавать аксессуары без всякой защиты.
В HAP определены так же процедуры работы со связями (/pairings). Можно добавить еще один публичный ключ LTPK с правами админа или пользователя, удалить, перечислить. В приложении Дом это часть функционала пригласить/удалить участника. В приложении Дом это работает через iCloud, но все-равно есть возможность добавить локально, либо через свой сервис.
Насчет сертифицированной версии: я так понимаю разницы в протоколе нет, просто сертификация обязует тебя пройти тесты совместимости и раньше использовался криптографический чип от Apple (https://www.reddit.com/r/HomeKit/comments/lprtuk/apple_homekit_chip/).