Pull to refresh

Пишем анонимный мессенджер с нуля

Reading time39 min
Views22K

Предисловие

Более подробно с общей теорией анонимности можно ознакомиться тут. Познакомиться с ядром скрытой сети, с теоретически доказуемой анонимностью, тут. Со всем представленным кодом и всеми примерами можно ознакомиться тут.

HLM (Hidden Lake Messenger) - анонимный мессенджер, построенный на ядре анонимной сети HLS (Hidden Lake Service). Данная статья приводит создание HLS и HLM с нуля, показывая их примитивность и простоту. Код HLS, HLM.

Введение

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

Под сокрытием пользовательской активности мы будем понимать именно критерий ненаблюдаемости. Помимо критерия ненаблюдаемости, также существует критерий несвязываемости, который лишь "разрывает" связь между абонентами, не позволяя связать источника (отправителя) и получателя. Критерий несвязываемости уже присутствует в критерии ненаблюдаемости, а потому является более слабым выражением анонимности.

Доказательство

Критерий ненаблюдаемости уже включает в себя критерий несвязываемости. Если пойти от обратного и предположить ложность данного суждения (то-есть, отсутствие несвязываемости в ненаблюдаемости), тогда можно было бы при помощи несвязываемости определить существование субъектов информации и, тем самым, допустить нарушение ненаблюдаемости, что является противоречием для последнего.

Описание абстрактно-планируемого мессенджера

"Анонимность" представляет собой сложный, комплексный термин. В работе "Теория строения скрытых систем" было представлено развитие анонимности в сетевых коммуникациях, были выявлены критерии анонимности, базовое определение анонимности, основные методы её достижения (конструкты), а также противоречия между безопасностью и анонимностью. Всё это говорит о том, что при разработке мы должны чётко обозначить границы того, к чему мы движемся.

  1. Мессенджер будет базироваться на теоретически доказуемой анонимности.

Теоретически доказуемая анонимность

Скрытыми сетями с теоретически доказуемой анонимностью принято считать замкнутые (полностью прослушиваемые) системы, в которых становится невозможным осуществление любых пассивных атак (в том числе и при существовании глобального наблюдателя) направленных на деанонимизацию отправителя и получателя с минимальными условностями по количеству узлов неподчинённых сговору. Говоря иначе, с точки зрения пассивного атакующего, апостериорные знания (полученные вследствие наблюдений) должны оставаться равными априорным (до наблюдений), тем самым сохраняя равновероятность деанонимизации по N-ому множеству субъектов сети.

  1. Мессенджер будет связывать абонентов информации между собой.

Анонимность между абонентами

Существует несколько видов анонимизации трафика между отправителем и получателем:

1. Система разграничивает абонентов информации. В такой концепции существует три возможных случая: 1) отправитель анонимен к получателю, но получатель известен отправителю; 2) отправитель известен получателю, но получатель анонимен к отправителю; 3) отправитель и получатель анонимны друг к другу. Примером являются 1) анонимный доступ к открытому Интернет ресурсу; 2) анонимное получение информации из ботнет системы со стороны сервера-координатора; 3) анонимный доступ к скрытому ресурсу в анонимной сети.

2. Система связывает абонентов информации. В такой концепции отправитель и получатель известны друг к другу. Системы построенные на данном пункте часто ограничены в своём применении, но, так или иначе, остаются способными представлять анонимность субъектов, в том числе и на уровне критерия ненаблюдаемости.

Второй пункт является наименее привлекательным со стороны любого представленного случая из первого пункта. Тем не менее, такое действие становится оправданным со стороны простоты программной реализации и простоты теоретической доказуемости.

  1. Мессенджер будет представлять сквозное (end-to-end) шифрование

Клиент-безопасные приложения

Клиент-безопасные приложения или приложения базируемые на безопасной линии связи «клиент-клиент» представляют собой абстрагирование передаваемых / хранимых объектов от промежуточных субъектов, тем самым приводя мощность доверия |T| к своему теоретически минимально заданному значению. Частным случаем связи «клиент-клиент» становится сквозное (end-to-end или E2E) шифрование.

Мощность доверия

Мощность доверия — количество узлов, участвующих в хранении или передаче информации, представленной дли них в открытом описании. Иными словами, такие узлы способны читать, подменять и видоизменять информацию, т.к. для них она находится в предельно чистом, прозрачном, транспарентном состоянии. Чем больше мощность доверия, тем выше предполагаемый шанс компрометации отдельных узлов, а следовательно, и хранимой на них информации. Принято считать одним из узлов получателя. Таким образом, нулевая мощность доверия |T| = 0 будет возникать лишь в моменты отсутствия каких-либо связей и соединений. Если |T| = 1, это говорит о том, что связь защищена, иными словами, никто кроме отправителя и получателя информацией не владеют. Во всех других случаях |T| > 1, что говорит о групповой связи (то-есть, о существовании нескольких получателей), либо о промежуточных узлах, способных читать информацию в открытом виде.

  1. Мессенджер будет базироваться на одноранговой (peer-to-peer) децентрализованной архитектуре сети.

Peer-to-peer сети

Существует несколько видов одноранговых сетей, а именно:

  1. Централизованная одноранговая архитектура. Представляет собой существование двух ролей - клиентов и ретрансляторов. Клиент в такой системе генерируют и принимают всю информацию. Ретрансляторы служат исключительно для перенаправления клиентской информации, не содержа в себе никакой дополнительной логики.

  2. Децентрализованная одноранговая архитектура. Представляет собой "сращивание" воедино клиентов и ретрансляторов, в следствие чего каждый клиент теперь становится способным осуществлять перенаправления поступаемой ему клиентской информации извне.

  3. Распределённая одноранговая архитектура. Подвид децентрализованной одноранговой архитектуры. Зародилась в следствии "коррозии" децентрализованных форм централизованными. Иными словами, в децентрализованных архитектурах существует проблема, когда клиенты начинают выбирать малое количество стабильных клиентов для последующей ретрансляции, тем самым приводят систему к неявному виду централизации. Распределённая одноранговая архитектура переводит качество соединений на их количество.

Выбор децентрализованной архитектуры взамен распределённой был вызван вследствии пятого (f2f сети) и шестого пунктов (абстрактные анонимные сети) определения мессенджера. Данные пункты с одной стороны ограничивают количество соединений, тем самым затрудняя выстраивание распределённой системы, с другой стороны централизация (даже в явном представлении) не будет нарушать анонимизацию трафика.

  1. Мессенджер будет связывать абонентов посредством доверительных (friend-to-friend) связей.

Friend-to-friend сети

Каждый действующий субъект сети будет выстраивать связи с другими участниками, основываясь на субъективности к уровню доверия, устанавливая и редактируя белый список на своей стороне. Чтобы успешно подключиться к сети такого рода, субъекту необходимо стать доверенным узлом, то есть пользователем, которому кто-либо доверяет. Сложность исполнения атаки на подобную сеть будет сводиться также к сложности встраивания подчиняемых узлов, потому как каждый получатель информации, в конечном итоге, должен будет заранее устанавливать список возможных отправителей.

  1. Мессенджер будет представлять реализацию абстрактной анонимной сети.

Абстрактная анонимная сеть

Среди анонимных сетей можно выявить класс систем с теоретически доказуемой анонимностью и максимально разграничивающим свойством, приводящим к наибольшему разрыву связей между объектом (как информации) и его субъектами (в лице отправителя и получателя) посредством выстраивания децентрализованных соединений между всеми участниками сети.

Из-за своей специфичной архитектуры передача информации может осуществляться в любой дуплексной среде вне зависимости от расположения и связей узлов, что полностью отрывает распространение объектов от своей сетевой архитектуры и переводит маршрутизацию в этап виртуального транслирования. Анонимные сети, с вышеописанными характеристиками, будут именоваться абстрактными.

Любые комбинации связей возможны без вреда анонимности
Любые комбинации связей возможны без вреда анонимности

За счёт данной "абстрактности" комбинации и вариации расположения и связей узлов между собой никак не влияют на уровень безопасности и анонимности конечных клиентов.

Краткое описание HLS

Вкратце суть HLS сводится к следующему: предположим, что существует три участника {A, B, C}. Каждый из них соединён друг к другу (что в сравнении с DC-сетями не является обязательным критерием, но данный случай я привёл исключительно для упрощения) (P.S. имеются реализации которые позволяют в DC-сетях не соединяться друг к другу, но данные способы скорее являются хаками, нежели вариативностью). Каждый субъект устанавливает время генерации информации = T. У каждого участника имеется своё внутренее хранилище по типу FIFO (первый пришёл - первый ушёл), можно сказать имеется структура "очередь".

DC-сети (проблема обедающих криптографов)

Предположим, что участник A хочет отправить информацию по сети так, чтобы {B, C} эту информацию получили, но не смогли узнать, кто действительно является отправителем. Иными словами, для B это может быть {A, C}, а для C это {A, B} с вероятностью 50/50. Все участники начинают согласовывать общий бит со своими соседями в момент времени T. Предположим, что участники {A, B} согласовали бит = 1, {B, C} = 1, {C, A} = 0.

Далее каждый участник сети XOR'ит (операция исключающее ИЛИ) биты со всех своих соединений: A = 1 xor 0 = 1; B = 1 xor 1 = 0; C = 0 xor 1 = 1. Данные результаты обмениваются по всей сети и XOR'ятся каждым её участником: 0 xor 1 xor 1 = 0. Это говорит о том, что участник A передал бит информации = 0. Чтобы субъект A мог передать бит = 1, ему необходимо добавить операцию НЕ в своём вычислении, то есть A = НЕ(1 xor 0) = 0. В итоге, все вычисления прийдут к такому результату: 0 xor 0 xor 1 = 1. Таким образом, становится возможным передать 1 бит информации полностью анонимно (конечно же со стороны определения теоретически доказуемой анонимности).

Проблема обедающих криптографов
Проблема обедающих криптографов

Предположим, что один из участников, либо B, либо C захочет деанонимизировать либо оставшуются сеть {A, C}, либо {B, C} соответственно (то есть узнать, кто является отправителем информации). Тогда ему потребуется узнать согласованный секрет со стороны другой линии связи, что является сложной задачей (если конечно не был произведён сговор нескольких участников). Таким образом, атака со стороны внутреннего пассивного наблюдателя становится безрезультатной. Со стороны внешнего глобального наблюдателя такая же ситуация, потому как он видит лишь переадресации зашифрованных битов (потому как используется безопасный канал связи) в один момент времени T всеми участниками сети.

Предположим, что участник A хочет отправить некую информацию одному из участников {B, C}, так, чтобы другой участник (или внешний наблюдатель) не знал что существует какой-либо факт отправления. Каждый участник в определённый индивидуальный период T генерирует сообщение. Такое сообщение может быть либо ложным (не имеющее никакого фактического содержания и никому по факту не отправляется, заполняясь случайными битами), либо истинным (запрос или ответ). Отправить раньше или позже положенного времени T никакой участник не может. Если скопилось несколько запросов одному и тому же участнику, тогда он их ложит в свою очередь сообщений и после периода T достаёт из очереди и отсылает в сеть. Таким образом, сама структура HLS есть множество последовательно выстроенных очередей.

Система на базе очередей
Система на базе очередей

Таким образом, внешний глобальный наблюдатель будет видеть лишь картину, при которой каждый участник в свой определённо заданный период времени T отправляет некое сообщение всем остальным узлам сети, что не даёт никакой информации о факте отправления, либо получения. Внутренние пассивные участники также неспособны узнать коммуниицирует ли один из участников в данный период времени с каким-либо другим, т.к. предполагается, что шифрованная информация не выдаёт никаких данных об отправителе и получателе непосредственно (на примере Bitmessage).

Bitmessage

Bitmessage - клиент-безопасное приложение, мессенджер. Сам по себе не представляет анонимность пользователей, но его уникальной особенностью является маршрутизация отправляемых данных.

  1. Информация шифруется (в качестве упрощения) публичным ключом получателя (на самом деле используется гибридная схема шифрования).

  2. Зашифрованная информация отправляется всем участникам сети (на практике конечно же текущим соединеням).

  3. Каждый пользователь, при получении шифрованной информации извне, пытается её расшифровать своим приватным ключом.

  4. Если пользователь смог расшифровать информацию, значит он является истинным её получателем.

  5. Если пользователь не смог расшифровать информацию, тогда он продолжает её распространять дальше по сети, отправляя таковую всем своим соединениям.

Уникальность такого подхода заключается в отстутствии маршрутизирующей информации, кроме как понимания факта {получатель / не получатель}. Тем не менее, Bitmessage не является анонимным мессенджером, потому что в нём отсутствует какая-либо запутывающая маршрутизация. Иными словами относительно легко определить кто является отправителем и кто получателем, при условии, если получатель всегда будет генерировать ответ на запрос инициатора.

Запутывающая маршрутизация

Маршрутизация в анонимных сетях не является примитивной и ставит эффективность распространения объектов опциональным параметром (низкие / высокие задержки), потому как главной целью становится создание запутывающего алгоритма (анонимизатора), который приводил бы к трудоёмкости анализа истинного пути от точки отправления до точки назначения.

Схематичное описание запутывающей маршрутизации по отношению к внешним и внутренним атакующим (наблюдателям)
Схематичное описание запутывающей маршрутизации по отношению к внешним и внутренним атакующим (наблюдателям)

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

Реализация HLS

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

Криптографический протокол

Протокол определяется восьмью шагами, где три шага на стороне отправителя и пять шагов на стороне получателя. Для работы протокола необходимы алгоритмы КСГПСЧ (криптографически стойкого генератора псевдослучайных чисел), ЭЦП (электронной цифровой подписи), криптографической хеш-функции, установки / подтверждения работы, симметричного и асимметричного шифров.

Участники протокола: 
  A - отправитель, 
  B - получатель.

Шаги участника A:
1. K = G( N ), R = G( N ),
  где G - функция-генератор случайных байт,
      N - количество байт для генерации,
      K - сеансовый ключ шифрования,
      R - случайный набор байт.
2. HP = H( R || P || PubKA || PubKB ),
  где HP - хеш сообщения,
      H - функция хеширования,
      P - исходное сообщение,
      PubKX - публичный ключ. 
3. CP = [ E( PubKB, K ), E( K, PubKA ), E( K, R ), E( K, P ), HP, E( K, S( PrivKA, HP ) ), W( C, HP ) ],
  где CP - зашифрованное сообщение,
      E - функция шифрования,
      S - функция подписания,
      W - функция подтверждения работы,
      C - сложность работы,
      PrivKX - приватный ключ.

Шаги участника B: 
4. W( C, HP ) = PW( C, W( C, HP ) ),
  где PW - функция проверки работы.
  Если ≠, то протокол прерывается.
5. K = D( PrivKB, E( PubKB, K ) ),
  где D - функция расшифрования.
  Если ≠, то протокол прерывается.
6. PubKA = D( K, E( K, PubKA ) ).
  Если ≠, то протокол прерывается.
7. HP = V( PubKA, D( K, E( K, S( PrivKA, HP ) ) ) ),
  где  V - функция проверки подписи.
  Если  ≠, то протокол прерывается. 
8. HP = H( D( K, E( K, R ) ) || D( K, E( K, P ) ) || PubKA || PubKB ),
  Если  ≠, то протокол прерывается. 

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

Также протокол способен игнорировать сетевую идентификацию субъектов информации, замещая её идентификацией криптографической. При таком подходе аутентификация субъектов начинает становиться сингулярной функцией, относящейся лишь и только к асимметричной криптографии, и как следствие, прикладной уровень стека TCP/IP начинает симулятивно заменять криптографический слой по способу обнаружения отправителя и получателя. Из вышеописанного также справедливо следует, что для построения полноценной информационной системы необходимым является симулятивная замена транспортного и прикладного уровня последующими криптографическими абстракциями. Под транспортным уровнем может пониматься способ передачи сообщений из внешней (анонимной сети) во внутреннюю (локальную), под прикладным — взаимодействие со внутренними сервисами.

Сеанс связи в приведённом протоколе определяется самим пакетом, или иными словами один пакет становится равен одному сеансу за счёт генерации случайного сеансового ключа. Описанный подход приводит к ненадобности сохранения фактического сеанса связи, исключает внешние долговременные связи между субъектами посредством имманентности и абстрагирования объектов, что приводит к невозможности рассекречивания всей информации, даже при компрометации одного или нескольких сеансовых ключей.

Безопасность протокола определяется в большей мере безопасностью асимметричной функции шифрования, т.к. все действия сводятся к расшифрованию сеансового ключа приватным ключом. Если приватный ключ не может расшифровать сеансовый, то это говорит о том факте, что само сообщение было зашифровано другим публичным ключом и потому получатель также есть другой субъект. Функция хеширования необходима для проверки целостности отправленных данных. Функция проверки подписи необходима для аутентификации отправителя. Функция проверки доказательства работы необходима для предотвращения спама.

Шифрование подписи сеансовым ключом является необходимым, т.к. взломщик протокола, для определения отправителя (а именно его публичного ключа) может составить список уже известных ему публичных ключей и проверять каждый на правильность подписи. Если проверка приводит к безошибочному результату, то это говорит об обнаружении отправителя.

Шифрование случайного числа (соли) также есть необходимость, потому как, если злоумышленник знает его и субъектов передаваемой информации, то он способен пройтись методом «грубой силы» по словарю часто встречаемых и распространённых текстов для выявления исходного сообщения.

Использование одной и той же пары асимметричных ключей для шифрования и подписания не является уязвимостью, если применяются разные алгоритмы кодирования или сама структура алгоритма представляет различные способы реализации. Так например, при алгоритме RSA для шифрования может использоваться алгоритм OAEP, а для подписания – PSS. В таком случае не возникает «подводных камней» связанных с возможным чередованием «шифрование-подписание». Тем не менее остаются риски связанные с компрометацией единственной пары ключей, при которой злоумышленник сможет не только расшифровывать все получаемые сообщения, но и подписывать отправляемые. Но этот критерий также является и относительным плюсом, когда личность субъекта не раздваивается и, как следствие, данный факт не приводит к запутанным ситуациям чистого отправления и скомпрометированного получения (и наоборот).

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

Недостатком протокола является отсутствие последовательности между несколькими пакетами. Иными словами невозможно определить нумерацию, что в некой степени переводит часть полноценного протокола на логику приложения, как например передача файлов. Это, в свою очередь, обосновывается упрощением протокола, где не требуются хранилище или база данных для хранения последовательности пакетов со стороны каждого входящего объекта. Также в некоторых приложениях последовательность сообщений не критична, как например в электронной почте или мессенджерах, где необходим лишь сам факт уже существующего дубликата (данный момент можно проверять хешем пакета).

Другим недостатком является постоянное применение функции подписания, которая считается наиболее ресурсозатрачиваемой, с практической точки зрения, операцией. При большом количестве поступаемых сообщений, возникнет и необходимость в большом количестве проверок подписания. При этом использование MAC, взамен ЭЦП, является недопустимым, потому как таковая имитовставка создаст буквально поточную связь между субъектами информации (создаст дополнительные связи между субъектами и генерируемым объектом), усложнит протокол и может привести теоретически к более чем одному возможному вектору нападения на протокол.

Для улучшения эффективности, допустим при передаче файлов, программный код можно изменить так, чтобы снизить количество проверок работы в процессе передачи, но с первоначальным доказательством работы на основе случайной строки (полученной от точки назначения), а потом и с накопленным хеш-значением из n-блоков файла, для i-ой проверки. Таким образом, минимальный контроль работы будет осуществляться лишь M/nN+1 раз, где M — размер файла, N — размер одного блока. Если доказательство не поступило или оно является неверным, то нужно считать, что файл был передан с ошибкой и тем самым запросить повреждённый или непроверенный блок заново.

Другая проблема заключается в отсутствии каких бы то ни было видимых метаданных (хеш-значения, доказательства работы), которые бы помогли в борьбе со спамом, что в свою очередь является крайне важным критерием для большинства децентрализованных систем. Таким образом, отсутствие метаданных равносильно отсутствию отказоустойчивости, что отсылает на противоречие эквивалентности полностью анализируемого и неподверженного анализу пакетам. Одним из возможных решений данной проблемы может служить использование общепринятного и стандартизированного протокола типа SSL/TLS с целью сокрытия факта использования монолитного протокола.

Программная реализация шифрования и расшифрования приведена ниже. Основным отличием программной реализации от представленного выше протокола является учёт статики пакета. Иными словами, необходимо такое свойство, чтобы один пакет был неотличим от другого по своему размеру.

Шифрование
// Принимает в качестве аргументов публичный ключ получателя
// и полезную нагрузку.
// Полезная нагрузка - это Header=uint64 и Body=[]byte.
func (client *sClient) Encrypt(receiver asymmetric.IPubKey, pl payload.IPayload) (message.IMessage, error) {
	// Для безопасности необходимо, чтобы ключи абонентов имели одинаковый размер
    // во избежание малых ключей со стороны отправителя или получателя
    if receiver.Size() != client.PubKey().Size() {
		return nil, fmt.Errorf("size of public keys sender and receiver not equal")
	}

    // fVoidMsgSize - размер пустого пакета без полезной нагрузки 
    // (здесь учитываются размеры публичного ключа, хеша, подписи и т.п. метаданных)
    // В качестве шифрованной информации принимаются байты, потому необходимо
    // их кодирование. 16-кодировка идеальное простое кодирование для точного
	// для точного понимания будущего размера информации.
    // Нам нужно учитывать реальный размер информации исходя из
    // закодированного размера, поэтому мы уменьшаем размер вдвое.
    var (
		maxMsgSize = client.Settings().GetMessageSize() >> 1 // limit of bytes without hex
		resultSize = uint64(client.fVoidMsgSize) + uint64(len(pl.ToBytes()))
	)

    // Если всё же полученный размер оказывается больше лимита,
    // тогда ничего не шифруем. В таком контексте надо подумать
    // о том, как разделить сообщение (если таковое больше положенного).
	if resultSize > maxMsgSize {
		return nil, fmt.Errorf(
			"limit of message size without hex encoding = %d bytes < current payload size with additional padding = %d bytes",
			maxMsgSize,
			resultSize,
		)
	}

    // Передаём в функцию шифрования размер дополнения до блока.
	return client.encryptWithParams(
		receiver,
		pl,
		client.Settings().GetWorkSize(),
		maxMsgSize-resultSize,
	), nil
}
// Тут используются первые три действия на инициирующей стороне
// криптографического протокола.
func (client *sClient) encryptWithParams(receiver asymmetric.IPubKey, pl payload.IPayload, workSize, addPadd uint64) message.IMessage {
	var (
		rand    = random.NewStdPRNG()
		salt    = rand.Bytes(symmetric.CAESKeySize)
		session = rand.Bytes(symmetric.CAESKeySize)
	)

    // Конкатенируем полезную нагрузку с дополнением случайных байт.
	// Образуем новую полезную нагрузку, в заголовке которой указан
    // размер настоящей информации.
    payloadBytes := pl.ToBytes()
	doublePayload := payload.NewPayload(
		uint64(len(payloadBytes)),
		bytes.Join(
			[][]byte{
				payloadBytes,
				rand.Bytes(addPadd),
			},
			[]byte{},
		),
	)

    // Хешируем всю основную информацию.
    // Соль необходима для образования разных хешей.
	hash := hashing.NewSHA256Hasher(bytes.Join(
		[][]byte{
			salt,
			client.PubKey().Bytes(),
			receiver.Bytes(),
			doublePayload.ToBytes(),
		},
		[]byte{},
	)).Bytes()

    // Шифруем все необходимые данные сеансовым ключом.
    // Сеансовый ключ шифруем публичным ключом.
	cipher := symmetric.NewAESCipher(session)
	bProof := encoding.Uint64ToBytes(puzzle.NewPoWPuzzle(workSize).Proof(hash))
	return &message.SMessage{
		FHead: message.SHeadMessage{
			FSender:  encoding.HexEncode(cipher.Encrypt(client.PubKey().Bytes())),
			FSession: encoding.HexEncode(receiver.Encrypt(session)),
			FSalt:    encoding.HexEncode(cipher.Encrypt(salt)),
		},
		FBody: message.SBodyMessage{
			FPayload: encoding.HexEncode(cipher.Encrypt(doublePayload.ToBytes())),
			FHash:    encoding.HexEncode(hash),
			FSign:    encoding.HexEncode(cipher.Encrypt(client.PrivKey().Sign(hash))),
			FProof:   encoding.HexEncode(bProof[:]),
		},
	}
}

Расшифрование
// Последние пять действий на принимающей стороне
// криптографического протокола.
func (client *sClient) Decrypt(msg message.IMessage) (asymmetric.IPubKey, payload.IPayload, error) {
	// Проверяем корректность принятого msg.
    if msg == nil {
		return nil, nil, fmt.Errorf("msg is nil")
	}

	// Проверяем размер хеша.
	if len(msg.Body().Hash()) != hashing.CSHA256Size {
		return nil, nil, fmt.Errorf("msg hash != sha256 size")
	}

	// Проверяем совершённую работу.
	diff := client.Settings().GetWorkSize()
	puzzle := puzzle.NewPoWPuzzle(diff)
	if !puzzle.Verify(msg.Body().Hash(), msg.Body().Proof()) {
		return nil, nil, fmt.Errorf("invalid proof of msg")
	}

	// Пытаемся расшифровать сеансовый ключ.
	session := client.PrivKey().Decrypt(msg.Head().Session())
	if session == nil {
		return nil, nil, fmt.Errorf("failed decrypt session key")
	}

	// Расшифровываем публичный ключ.
	cipher := symmetric.NewAESCipher(session)
	publicBytes := cipher.Decrypt(msg.Head().Sender())
	if publicBytes == nil {
		return nil, nil, fmt.Errorf("failed decrypt public key")
	}

	// Декодируем публичный ключ из байт и проверяем его размер.
	pubKey := asymmetric.LoadRSAPubKey(publicBytes)
	if pubKey == nil {
		return nil, nil, fmt.Errorf("failed load public key")
	}
	if pubKey.Size() != client.PubKey().Size() {
		return nil, nil, fmt.Errorf("invalid public key size")
	}

	// Расшифровываем и декодируем полезную нагрузку.
	doublePayloadBytes := cipher.Decrypt(msg.Body().Payload().ToBytes())
	if doublePayloadBytes == nil {
		return nil, nil, fmt.Errorf("failed decrypt double payload")
	}
	doublePayload := payload.LoadPayload(doublePayloadBytes)
	if doublePayload == nil {
		return nil, nil, fmt.Errorf("failed load double payload")
	}

    // Расшифровываем соль.
	salt := cipher.Decrypt(msg.Head().Salt())
	if salt == nil {
		return nil, nil, fmt.Errorf("failed decrypt salt")
	}

	// Проверяем корректность принятого хеша с полученным.
	check := hashing.NewSHA256Hasher(bytes.Join(
		[][]byte{
			salt,
			publicBytes,
			client.PubKey().Bytes(),
			doublePayload.ToBytes(),
		},
		[]byte{},
	)).Bytes()
	if !bytes.Equal(check, msg.Body().Hash()) {
		return nil, nil, fmt.Errorf("invalid msg hash")
	}

	// Расшифровываем подпись и проверяем её корректность.
	sign := cipher.Decrypt(msg.Body().Sign())
	if sign == nil {
		return nil, nil, fmt.Errorf("failed decrypt sign")
	}
	if !pubKey.Verify(msg.Body().Hash(), sign) {
		return nil, nil, fmt.Errorf("invalid msg sign")
	}

	// Удаляем случайные (добавочные) байты из полезной нагрузки.
	mustLen := doublePayload.Head()
	if mustLen > uint64(len(doublePayload.Body())) {
		return nil, nil, fmt.Errorf("invalid size of payload")
	}
	pld := payload.LoadPayload(doublePayload.Body()[:mustLen])
	if pld == nil {
		return nil, nil, fmt.Errorf("invalid load payload")
	}

	// Возвращаем публичный ключ отправителя и 
    // отправленную полезную нагрузку.
	return pubKey, pld, nil
}

Пример использования шифрования и расшифрования можно продемонстрировать следующим образом. Файл "github.com/number571/go-peer/examples/modules/client/encrypt/main.go".

package main

import (
	"fmt"

	"github.com/number571/go-peer/modules/client"
	"github.com/number571/go-peer/modules/crypto/asymmetric"
	"github.com/number571/go-peer/modules/payload"
)

func main() {
	var (
		client1 = newClient()
		client2 = newClient()
	)

	msg, err := client1.Encrypt(
		client2.PubKey(),
		payload.NewPayload(0x0, []byte("hello, world!")),
	)
	if err != nil {
		panic(err)
	}

	pubKey, pld, err := client2.Decrypt(msg)
	if err != nil {
		panic(err)
	}

	fmt.Printf("Message: '%s';\nSender's public key: '%s';\n", string(pld.Body()), pubKey.String())
	fmt.Printf("Encrypted message: '%s'\n", string(msg.ToBytes()))
}

func newClient() client.IClient {
	return client.NewClient(
		client.NewSettings(&client.SSettings{
            // Размер итогового пакета.
			FMessageSize: (1 << 12), 
		}),
        // Небезопасный размер ключа. 
        // На практике лучше использовать 4096 (консервативная точка зрения).
		asymmetric.NewRSAPrivKey(1024),
	)
}
Результат выполнения

Message: 'hello, world!';
Sender's public key: 'Pub(go-peer/rsa){30818902818100CC228131C038583D6345EEFF79D5A6AD56EB3992CD1933655EC1830F66AAF8F9CAC7283F63C0E17D2C69DED57FF28F18A7C2E3905DD28466F57F3FA0F53F0EF724D109D0B120CD9CF49DAF4841EE22F86EBD6A498DF91518C52C78583E7D61509C5E3790694650A8A1891B3BA1F4FCFCF3945C93D16625432185E1677F3F0BD50203010001}';
Encrypted message: '{"head":{"salt":"86232459f3a46d6f8bb2d45d1d39898d047066b92606ff125d5c09b484c56558518c2c6b02067667939879859ba06e5533174c4cd357031a257c2f8f34631119","session":"c32d0e7e1b731f802de593104e5b062cfbda16437f89ede1c0b33a75fb008bd2b7b6b526f43116ccd5f8191ceae222d00adb2fd18ae4521c77f37da9199dad763b8a159caef9b5965527ede8b4ec4f43e16388845f41d07418b5abf3af22cad25cb546c21a1275f72ba1e5a6a7dd51139ad2ec61b07f4fe1bb0ddb108a1d51de","sender":"2b66ea223b48ae813b62e8a50eb059839f7b7f661ab67c9cebed54fe62a701a341b7bce736f9c6e29fae8d6596eea819ae00c48012c4586a3c31048085d5325db90f8c87794be38ad6b8fdc81cb2f983ead4fb57e5e231a41e740d6540c1f3f0e2012282892a31e9b8b106caa92f969a7d136edaf7cb744549d31792a260a7d6ea34805f15fc35978784f431ccd4a030b1f3b43ddd2d3fe681053c9584596f13"},"body":{"payload":"2cc55c74ecbd7fa0f75e2ce97678cd86e7920a8c919e063e43525216e18c53cf8918b0a18c3b93d57607ac1ec9e8e4f43f6df12d31bf15db56202ab63fad8e1d898a7bf9e9add90cef4d84e7e2c2cc07ac13ee823efdc4397c5c539f8ee1e32b37e851399f7f683a58b7a64e3873c6c84b9738dbab3512d05ba55e96e133926a69e6af68d8c29123802058e75db1726d164b245b1afbb4d2bdb11884f837f5a642d72260a2f660d794a2ebbc044a74662676e5b20b3ae24c32c0074d330fb55b895bd08dddbd1b04cf88d13a9fbc062312d859156f1a36967cd4f01f8c794b7844ab61c7e66ea83bb0f25595c78750131434fc024869e85eb3eb8e51fde56fa8d49df5ab41c41197712c1b38af8f31c4063626e1326e129e1ac1cb53570eb820fc2d0b6dceff3f622f88921f64405f5dbefb2452652902ed1bc211495f19a5cabe1c71cc3334ae8290feaec9018e00074ce7298ff8fc09f815eb839f2f95ce78d8f1e7dc4be785de62b616b3e7061288b07b2729f5813a3382f1ca0a5f8821436a36a94b2447d86a6d2356c61981aa095eb56df8cddae9cb6060a069d45301085a8bdea635a770f8c8a082560f40b9f4336b737acb4a14d8c5e52fef12927cc3026b01ec2d8e8d1a97c00574e078a15d89962974f659d0efe0a5dda20f36b9cd6e89a363da24fedfabe684519f3814f88b62de28f07eabb15c9711d3bc0e6f41b2508513dcc4c7a46063b0f6f29492cbe96ccf418edd0f7ab764d7517ca7feef6a33b380ac135457f139d7e00cdfe82b2a629f59fa57c7f4fc4695d0a014632b4a4ee825b4d9766aa1ba2c9377853235b18507d4a4b1990fcb691ab996624e1baa276724d2b4e67dbb294d4c54a26df90a8e8ed269e1f8cdd06f03c5677a98c22acbd8d97adc52d33cc4777600a3d553c6da1fed38f8648da4f52381369c950c39daa302c5c9232fe8838760b2fa7e21ccc218f57f7ee5c4dcaf6ef84d9db7ce5212194e87ef4acc61613b912d3d517415265a94b953c304a391221483f380e5819e903dccec1042330f04e88196f55a73ca5aaf942e36ae4cf693d42624293248d8b708b3ecc74376137f7a02275623","sign":"14438df8beb9f31a15fef4acbc644e5fd6401ca83a7e1154039564215b4682bac6cfc577c0174227c4400c419b212a028577d8db6747467e2ac386745d73c93a8ced28bc6feb14924bd9d44650ae9abd67940dde8c6aae390015e67cca723c0e574da75006d0b15f2b225d44696da834277f5c2a57a833f3fb04c46aede99d3174575b0013e7ee7baedfb2b00a20f8ea78848d8b49563fc32d24dfd8550436c2","hash":"d8873b265353fd564a95c6b122fa878579ba71243903704922036484a30c997b","proof":"0000000000001419"}}'

Сетевая коммуникация

Сетевая составляющая ядра скрытой сети будет базироваться на протоколе TCP. Необходимость в TCP над UDP заключается в понимании точной доставки всех пакетов от точки A до точки B. Необходимость в TCP над HTTP заключается в постоянном держании соединения между отправителем и получателем. В любом случае можно заменить TCP, модифицировав UDP протокол, или использовав вебсокеты на уровне HTTP, но это будет лишь усложнять систему без какой бы то ни было значительной положительной стороны.

Нужно определить базовые функции отправления информации. Таковых будет две. Broadcast является функцией распространения информации по всем текущим соединениям и потому привязан к объекту "хранителю" соединений (в нашем контексте - это узел). Request является функцией передачи информации к одному соединению с последующей целью получить от него ответ и потому привязан к объекту "соединение".

Функция Broadcast
func (node *sNode) Broadcast(pld payload.IPayload) error {
	// Сохранение хеша в памяти, чтобы предотвратить бесконечное
    // зацикливание пакета в сетевой передачи
    hash := hashing.NewSHA256Hasher(pld.ToBytes()).Bytes()
	node.inMappingWithSet(hash)

    // Берём все текущие соединения и отправляем каждому
    // узлу копию полезной нагрузки.
	var err error
	for _, conn := range node.Connections() {
		e := conn.Write(pld)
		if e != nil {
			err = e
		}
	}

	return err
}

Это конечно же метод, а не функция со стороны терминологии языка Go. Тем не менее, термин "функция" употребляется здесь и далее как некое действие, как некая процедура без привязки конкретно к языку программирования.

func (conn *sConn) Write(pld payload.IPayload) error {
    // Ставим мьютекс, чтобы нельзя было записывать параллельно
    // два разных сообщения одному пользователю (коллизии).
	conn.fMutex.Lock()
	defer conn.fMutex.Unlock()

    // Упаковываем полезную нагрузку в сообщение (добавление HMAC)
	msg := message.NewMessage(pld, []byte(conn.fSettings.GetNetworkKey()))
	// Упаковываем сообщение в пакет (добавление размера пакета)
    packBytes := message.NewPackage(msg.ToBytes()).ToBytes()
	ptr := len(packBytes)

    // Отправляем байты.
	for {
		n, err := conn.fSocket.Write(packBytes[:ptr])
		if err != nil {
			return err
		}

		ptr = ptr - n
        packBytes = packBytes[:ptr]

		if ptr == 0 {
			break
		}
	}

	return nil
}

Функция Request
func (conn *sConn) Request(pld payload.IPayload) (payload.IPayload, error) {
	var (
		chPld    = make(chan payload.IPayload)
		timeWait = conn.fSettings.GetTimeWait()
	)

    // Отправляем полезную нагрузку.
	if err := conn.Write(pld); err != nil {
		return nil, err
	}
    // Запускаем горутину и пытаемся получить ответ.
	go readPayload(conn, chPld)

	select {
	case rpld := <-chPld:
        // Принятые данные могут оказаться невалидными.
		if rpld == nil {
			return nil, fmt.Errorf("failed: read payload")
		}
		return rpld, nil
	case <-time.After(timeWait):
		return nil, fmt.Errorf("failed: time out")
	}
}
func readPayload(conn *sConn, chPld chan payload.IPayload) {
	// Результатом функции станет вывод полезной нагрузки.
    var pld payload.IPayload
	defer func() {
		chPld <- pld
	}()

	// Пытаемся прочитать блок в 64бит указывающий
    // размер принимаемых данных.
	bufLen := make([]byte, encoding.CSizeUint64)
	length, err := conn.fSocket.Read(bufLen)
	if err != nil {
		return
	}
	if length != encoding.CSizeUint64 {
		return
	}

	// mustLen = Size[u64] in uint64
	arrLen := [encoding.CSizeUint64]byte{}
	copy(arrLen[:], bufLen)

    // Сравниваем принятый размер с допустимым лимитом.
	mustLen := encoding.BytesToUint64(arrLen)
	if mustLen > conn.fSettings.GetMessageSize() {
		return
	}

    // Читаем принимаемые байты.
	msgRaw := make([]byte, 0, mustLen)
	for {
		buffer := make([]byte, mustLen)
		n, err := conn.fSocket.Read(buffer)
		if err != nil {
			return
		}

		msgRaw = bytes.Join(
			[][]byte{
				msgRaw,
				buffer[:n],
			},
			[]byte{},
		)

		mustLen -= uint64(n)
		if mustLen == 0 {
			break
		}
	}

	// Пытаемся распаковать полученные байты в структуру сообщения.
	msg := message.LoadMessage(
		msgRaw,
		[]byte(conn.fSettings.GetNetworkKey()),
	)
	if msg == nil {
		return
	}

    // Выгружаем из сообщения полезную нагрузку.
	pld = msg.Payload()
}

В функции readPayload используется упоминание Message, тем не менее это иная структура, отличная от Message в криптографическом протоколе. Общее название было взято по причине косвенного, непрямого применения в момент передачи. Иными словами, в сетевых коммуникациях передаётся не сама полезная нагрузка, а полезная нагрузка запакованная в структуру Message. Ровно по такому же сценарию в криптографическом протоколе передаётся не сама полезная нагрузка, а структура Message, в которой содержится зашифрованная полезная нагрузка.

Далее определим функцию принятия соединения и всей последующей информации от данного соединения.

Функция Handle
// Аргументы = узел принимающий информацию, соединение отправителя,
// отправленная полезная нагрузка.
type IHandlerF func(INode, conn.IConn, payload.IPayload)

func (node *sNode) Handle(head uint64, handle IHandlerF) INode {
	node.fMutex.Lock()
	defer node.fMutex.Unlock()

    // Устанавливаем функцию в качестве роутинга.
	node.fHandleRoutes[head] = handle
	return node
}
func (node *sNode) handleConn(address string, conn conn.IConn) {
	defer node.Disconnect(address)
	for {
        // Если сообщение принято валидное, тогда пытаемся
        // прочитать ещё одно сообщение от данного узла.
		ok := node.handleMessage(conn, conn.Read())
		if !ok {
            // Иначе обрываем с ним соединение.
			break
		}
	}
}

func (node *sNode) handleMessage(conn conn.IConn, pld payload.IPayload) bool {
	// Проверяем валидность принятой полезной нагрузки
	if pld == nil {
		return false
	}

	// Проверяем сообщение в маппинге (принимало ли оно ранее?)
	hash := hashing.NewSHA256Hasher(pld.ToBytes()).Bytes()
	if node.inMappingWithSet(hash) {
        // Это не есть ошибка, потому что отправитель
        // может получить пакет с разнородных узлов.
		return true
	}

	// Пытаемся получить функцию роута. Если таковой нет
    // или она неопределена, тогда считаем, что это ошибка
    // на стороне отправителя.
	f, ok := node.getFunction(pld.Head())
	if !ok || f == nil {
		return false
	}

    // Обрабатываем полезную нагрузку полученной функцией.
	f(node, conn, pld)
	return true
}
func (conn *sConn) Read() payload.IPayload {
	chPld := make(chan payload.IPayload)
	go readPayload(conn, chPld)
	return <-chPld
}

Пример взаимодействия нескольких пользователей можно показать в бесконечном цикле передачи информации от инициатора к отправителю и наоборот в игре пинг-понг (тут искусственный интелект двух ботов будет играть бесконечно, т.к. все ходы они проанализировали и предопределили). Файл "github.com/number571/go-peer/examples/modules/network/ping-pong/main.go".

package main

import (
	"fmt"
	"strconv"
	"time"

	"github.com/number571/go-peer/modules/network"
	"github.com/number571/go-peer/modules/network/conn"
	"github.com/number571/go-peer/modules/payload"
)

const (
	serviceHeader  = 0xDEADBEAF
	serviceAddress = ":8080"
)

func main() {
	var (
		service1 = network.NewNode(network.NewSettings(&network.SSettings{}))
		service2 = network.NewNode(network.NewSettings(&network.SSettings{}))
	)

	service1.Handle(serviceHeader, handler("#1"))
	service2.Handle(serviceHeader, handler("#2"))

	go service1.Listen(serviceAddress)
	time.Sleep(time.Second) // wait

	_, err := service2.Connect(serviceAddress)
	if err != nil {
		panic(err)
	}

    // Отправляем нуль в качестве инициализации 
    // бесконечных запросов-ответов.
	service2.Broadcast(payload.NewPayload(
		serviceHeader,
		[]byte("0"),
	))

	select {}
}

func handler(serviceName string) network.IHandlerF {
	return func(n network.INode, c conn.IConn, p payload.IPayload) {
		time.Sleep(time.Second) // delay for view "ping-pong" game

        // Получаем сообщение, конвертируем его в число.
		num, err := strconv.Atoi(string(p.Body()))
		if err != nil {
			panic(err)
		}

		val := "ping"
		if num%2 == 1 {
			val = "pong"
		}

		fmt.Printf("service '%s' got '%s#%d'\n", serviceName, val, num)

        // Отправляем новое сообщение в сеть.
		n.Broadcast(payload.NewPayload(
			serviceHeader,
			[]byte(fmt.Sprintf("%d", num+1)),
		))
	}
}
Результат выполнения

service '#1' got 'ping#0'
service '#2' got 'pong#1'
service '#1' got 'ping#2'
service '#2' got 'pong#3'
service '#1' got 'ping#4'
service '#2' got 'pong#5'
service '#1' got 'ping#6'
service '#2' got 'pong#7'
service '#1' got 'ping#8'
service '#2' got 'pong#9'
service '#1' got 'ping#10'
service '#2' got 'pong#11'
...

Синтез

Настал финальный этап в концепции HLS, а именно объединение сетевой коммуникации и криптографического протокола. Т.к. сам криптографический протокол достаточно легко абстрагируется от сетевых коммуникаций, то таковой способен самостоятельно и симулятивно заменять сетевую коммуникацию (идентификацию) криптографической. Иными словами, всю информацию мы будем транспортировать и маршрутизировать не на базе IP-адресов, а на основе публичных ключей. Сами публичные ключи станут сетевыми идентификаторами.

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

Функция Enqueue
func (q *sQueue) Enqueue(msg message.IMessage) error {
	q.fMutex.Lock()
	defer q.fMutex.Unlock()

    // Если очередь переполнена -> выдавать ошибку.
	if uint64(len(q.fQueue)) >= q.Settings().GetCapacity() {
		return errors.New("queue already full, need wait and retry")
	}

    // Поместить сообщение в очередь.
	go func() {
		q.fMutex.Lock()
		defer q.fMutex.Unlock()

		q.fQueue <- msg
	}()

	return nil
}

Функция Dequeue
func (q *sQueue) Dequeue() <-chan message.IMessage {
	time.Sleep(q.Settings().GetDuration())

	go func() {
		q.fMutex.Lock()
		defer q.fMutex.Unlock()

        // Если очередь неактивна -> остановить выполнение.
		if !q.fIsRun {
			return
		}

        // Если в очереди сообщений не существует сообщений,
        // тогда нужно взять ложное сообщение из пула сгенерированных.
		if len(q.fQueue) == 0 {
			q.fQueue <- (<-q.fMsgPull.fQueue)
		}
	}()

	return q.fQueue
}

Далее необходимым является создание обёртки над функциями отправления и принятия сообщений в сетевых коммуникациях, но изменив при этом идентификаторы. Иными словами, необходимо заменить получателя network.INode на anonymity.INode и отправителя conn.IConn на asymmetric.IPubKey.

Функция Handle
type IHandlerF func(INode, asymmetric.IPubKey, payload.IPayload) []byte

func (node *sNode) Handle(head uint32, handle IHandlerF) INode {
	node.fMutex.Lock()
	defer node.fMutex.Unlock()

	node.fHandleRoutes[head] = handle
	return node
}

Стоит заметить, что head ранее был равен 64bit, теперь 32bit. Связано это с тем, что в "обёрточной" реализации нам также необходимо создать механизм получения ответа. Данная реализация более затруднительна, чем Request в сетевых коммуникациях, потому что неизвестно насколько далеко и в какой части сети находится конечный адресат сообщения. В такой парадигме информация будет проходить несколько узлов и ответ получателя может быть принят не тем узлом, через кого первоначально отправлялось сообщение.

Поэтому здесь 64bit разбивается на две составляющие по 32bit в виде конкатенации. Левая половина отвечает за уникальный идентификатор связи между отправителем и получателем для правильного валидирования принимаемой информации и перенаправления таковой на функцию Request в качестве ответа. Правая половина отвечает за функции редиректа, как и в сетевых коммуникациях 64bit.

// Единственная роут функция из сетевых коммуникаций.
// Представляет собой обёртку над множеством роут функций
// анонимных коммуникаций.
func (node *sNode) handleWrapper() network.IHandlerF {
	go func() {
		for {
            // Если из очереди пришло сообщение,
            // то его необходимо отправить в сеть.
			msg, ok := <-node.Queue().Dequeue()
			if !ok {
				break
			}
			node.broadcast(msg)
		}
	}()

	return func(nnode network.INode, _ conn.IConn, npld payload.IPayload) {
		// Получаем сообщение из сети. Проверяем. Распаковываем.
        msg := node.initialCheck(message.LoadMessage(npld.Body()))
		if msg == nil {
			return
		}

		// Как только получили новое сообщение из сети ->
        // отправляем его всем своим соединениям.
		nnode.Broadcast(npld)
		client := node.Queue().Client()

		// Пытаемся расшифровать полученное сообщение.
		sender, pld, err := client.Decrypt(msg)
		if err != nil {
			return
		}

		// Если сообщение отправлено нам, тогда необходимо
        // проверить отправителя информации. Находится ли
        // он в нашем белом списке?
		if !node.F2F().InList(sender) {
			return
		}

		// Проверяем существования сообщения в базе данных.
  		// Если существует, то далее ничего не предпринимаем.
        // Если отсутствует, тогда заносим в БД и смотрим далее.
        hash := []byte(fmt.Sprintf("_hash_%X", msg.Body().Hash()))
		if _, err := node.KeyValueDB().Get(hash); err == nil {
			return
		}
		node.KeyValueDB().Set(hash, []byte{})

		// Получаем связь от отправителя и пытаемся сопоставить
        // со своими связями из Request. Если таковая связь 
        // существует - это значит, что данный отправитель
        // является получателем сгенерировавшим ответ.
		head := pld.Head()
		action, ok := node.getAction(
			loadHead(head).Actions(),
		)
		if ok {
            // Перенаправляем полезную нагрузку на функцию 
            // Request.
			action <- pld.Body()
			return
		}

		// Берём по роуту функцию и смотрим её существование.
		f, ok := node.getRoute(
			loadHead(head).Routes(),
		)
		if !ok || f == nil {
			return
		}

        // Выполняем действие на основе роут функции.
		resp := f(node, sender, pld)
		// Если resp пустой - это не ошибка. Может существовать
        // логика, где не требуется ответа.
        if resp == nil {
			return
		}

        // Генерируем ответ.
		respMsg, err := client.Encrypt(
			sender,
			payload.NewPayload(head, resp),
		)
		if err != nil {
			panic(err)
		}

        // Заносим ответ в очередь.
		for i := uint64(0); i <= node.Settings().GetRetryEnqueue(); i++ {
			err := node.Queue().Enqueue(respMsg)
			if err != nil {
				time.Sleep(node.Queue().Settings().GetDuration())
				continue
			}
			break
		}
	}
}
// Представляет собой низкоуровневую функцию (конечно же относительно
// действий данного пакета) и находится вне выполнения очередей.
// Такое свойство необходимо для распространения маршрутизирующей 
// информации без замедления по очередям.
// Чтобы отправить пакет без необходимости ожидать ответа,
// нужно воспользоваться конструкцией node.Queue().Enqueue(msg).
func (node *sNode) broadcast(msg message.IMessage) error {
    // Перенаправляем сообщение всем своим соединениям. 
	return node.Network().Broadcast(payload.NewPayload(
		settings.CMaskNetwork,
		msg.ToBytes(),
	))
}
// Данная функция необходима для предотвращения редиректа
// невалидной информации при помощи функции Broadcast.
func (node *sNode) initialCheck(msg message.IMessage) message.IMessage {
	// Проверяем валидность сообщения.
    if msg == nil {
		return nil
	}

    // Проверяем размер хеша.
	if len(msg.Body().Hash()) != hashing.CSHA256Size {
		return nil
	}

    // Проверяем проделанную работу.
	diff := node.Queue().Client().Settings().GetWorkSize()
	puzzle := puzzle.NewPoWPuzzle(diff)
	if !puzzle.Verify(msg.Body().Hash(), msg.Body().Proof()) {
		return nil
	}

    // Внешне структура сообщения валидна.
	return msg
}

Функция Request
func (node *sNode) Request(recv asymmetric.IPubKey, pld payload_adapter.IPayload) ([]byte, error) {
	// Если количество соединений равно нулю, то и нет смысла
    // что-либо отправлять.
    if len(node.Network().Connections()) == 0 {
		return nil, errors.New("length of connections = 0")
	}

    // Создаём 32-битную связь с отправителем и
    // заносим редирект функцию в качестве запроса к ней.
	headAction := uint32(random.NewStdPRNG().Uint64())
	headRoutes := mustBeUint32(pld.Head())

    // Создаём новую полезную нагрузку на базе нового заголовка.
	newPld = payload.NewPayload(
		joinHead(headAction, headRoutes).Uint64(),
		pld.Body(),
	)

    // Шифруем полезную нагрузку открытым ключом получателя.
	msg, err := node.Queue().Client().Encrypt(recv, newPld)
	if err != nil {
		return nil, err
	}

    // Устанавливаем связь с получателем.
	node.setAction(headAction)
	defer node.delAction(headAction)

    // Пытаемся занести сообщение в очередь.
	for i := uint64(0); i <= node.Settings().GetRetryEnqueue(); i++ {
		if err := node.Queue().Enqueue(msg); err != nil {
			time.Sleep(node.Queue().Settings().GetDuration())
			continue
		}
		break
	}

    // Получаем ответ по связи.
	return node.recv(headAction, node.Settings().GetTimeWait())
}
func (node *sNode) recv(head uint32, timeOut time.Duration) ([]byte, error) {
	// Берём "считыватель" по связи
    action, ok := node.getAction(head)
	if !ok {
		return nil, errors.New("action undefined")
	}
    // Просматриваем и получаем событие.
	select {
	case result, opened := <-action:
		if !opened {
			return nil, errors.New("chan is closed")
		}
		return result, nil
	case <-time.After(timeOut):
		return nil, errors.New("time is over")
	}
}

Примером синтеза сетевой коммуникации и криптографического протокола может быть та же игра двух сверхразумов соревнующихся в пинг-понге, но теперь анонимно (по крайней мере внешне становится невозможным определение кто получатель, а кто отправитель, даже если они в один момент решат перестать играть и просто будут поддерживать сеть).

package main

import (
	"fmt"
	"os"
	"strconv"
	"time"

	"github.com/number571/go-peer/modules/client"
	"github.com/number571/go-peer/modules/crypto/asymmetric"
	"github.com/number571/go-peer/modules/friends"
	"github.com/number571/go-peer/modules/network"
	"github.com/number571/go-peer/modules/network/anonymity"
	payload_adapter "github.com/number571/go-peer/modules/network/anonymity/adapters/payload"
	"github.com/number571/go-peer/modules/network/conn"
	"github.com/number571/go-peer/modules/payload"
	"github.com/number571/go-peer/modules/queue"
	"github.com/number571/go-peer/modules/storage/database"
)

const (
	serviceHeader  = 0xDEADBEAF
	serviceAddress = ":8080"
)

const (
	dbPath1 = "database1.db"
	dbPath2 = "database2.db"
)

func deleteDBs() {
	os.RemoveAll(dbPath1)
	os.RemoveAll(dbPath2)
}

func main() {
	deleteDBs()
	defer deleteDBs()

	var (
		service1 = newNode(dbPath1)
		service2 = newNode(dbPath2)
	)

	service1.Handle(serviceHeader, handler("#1"))
	service2.Handle(serviceHeader, handler("#2"))

    // Пользователи добавляют в друзья друг друга (простите за тавтологию)
	service1.F2F().Append(service2.Queue().Client().PubKey())
	service2.F2F().Append(service1.Queue().Client().PubKey())

    // Запускаются очереди.
	if err := service1.Run(); err != nil {
		panic(err)
	}
	if err := service2.Run(); err != nil {
		panic(err)
	}

	go service1.Network().Listen(serviceAddress)
	time.Sleep(time.Second)

	if _, err := service2.Network().Connect(serviceAddress); err != nil {
		panic(err)
	}

    // Генерируется сообщение.
    // Число нуль в качестве инициализации.
	msg, err := service2.Queue().Client().Encrypt(
		service1.Queue().Client().PubKey(),
		payload_adapter.NewPayload(
			serviceHeader,
			[]byte("0"),
		),
	)
	if err != nil {
		panic(err)
	}

    // Сообщение кладётся в очередь для будущего отправления.
	if err := service2.Queue().Enqueue(msg); err != nil {
		panic(err)
	}

	select {}
}

func handler(serviceName string) anonymity.IHandlerF {
	return func(node anonymity.INode, pubKey asymmetric.IPubKey, pld payload.IPayload) []byte {
		num, err := strconv.Atoi(string(pld.Body()))
		if err != nil {
			panic(err)
		}

		val := "ping"
		if num%2 == 1 {
			val = "pong"
		}

		fmt.Printf("service '%s' got '%s#%d'\n", serviceName, val, num)

        // Редактируем и создаём новое сообщение.
		msg, err := node.Queue().Client().Encrypt(
			pubKey,
			payload_adapter.NewPayload(
				serviceHeader,
				[]byte(fmt.Sprintf("%d", num+1)),
			),
		)
		if err != nil {
			panic(err)
		}

        // Отправляем новое сообщение.
		if err := node.Queue().Enqueue(msg); err != nil {
			panic(err)
		}
		return nil
	}
}

func newNode(dbPath string) anonymity.INode {
	return anonymity.NewNode(
		anonymity.NewSettings(&anonymity.SSettings{}),
		database.NewLevelDB(
			database.NewSettings(&database.SSettings{
				FPath: dbPath,
			}),
		),
		network.NewNode(
			network.NewSettings(&network.SSettings{
				FConnSettings: conn.NewSettings(&conn.SSettings{}),
			}),
		),
		queue.NewQueue(
			queue.NewSettings(&queue.SSettings{}),
			client.NewClient(
				client.NewSettings(&client.SSettings{}),
				asymmetric.NewRSAPrivKey(1024),
			),
		),
		friends.NewF2F(),
	)
}
Результат выполнения

Ровно такой же как в примере кода сетевых коммуникаций.

Осталось лишь единственная деталь для того, чтобы ядро скрытой сети могло называться действительно ядром - предоставление API сторонним приложениям.

1. GET/POST/DELETE /api/config/connects
2. GET/POST/DELETE /api/config/friends
3. GET/DELETE      /api/network/online
4. POST/PUT        /api/network/push
5. GET             /api/node/pubkey

Данных функций достаточно большое количество для предоставления в самой статье (иначе статья просто станет клоном репозитория). Поэтому, если интересно само API, то с таковым можете ознакомиться в README или просто по коду.

В качестве примера итогового результата можно обратиться к директории examples/cmd/echo_service. В данном примере разворачивается несколько узлов, а именно - один отправитель, один получатель (сервис) и один промежуточный узел (созданный исключительно для маршрутизации.

$ cd examples/cmd/echo_service
$ make 
$ ./request.sh
> HTTP/1.1 200 OK
> Content-Type: application/json
> Date: Fri, 25 Nov 2022 08:02:51 GMT
> Content-Length: 97

> {"result":"7b226563686f223a2268656c6c6f2c20776f726c6421222c2272657475726e223a317d0a","return":1}
Скрипт request.sh
#!/bin/bash

str2hex() {
    local str=${1:-""}
    local fmt="%02X"
    local chr
    local -i i
    for i in `seq 0 $((${#str}-1))`; do
        chr=${str:i:1}
        printf "${fmt}" "'${chr}"
    done
}

JSON_DATA='{
        "method":"POST",
        "host":"hidden-echo-service",
        "path":"/echo",
        "head":{
            "Accept": "application/json"
        },
        "body":"aGVsbG8sIHdvcmxkIQ=="
}';

PUSH_FORMAT="{
        \"receiver\":\"Pub(go-peer/rsa){3082020A0282020100B752D35E81F4AEEC1A9C42EDED16E8924DD4D359663611DE2DCCE1A9611704A697B26254DD2AFA974A61A2CF94FAD016450FEF22F218CA970BFE41E6340CE3ABCBEE123E35A9DCDA6D23738DAC46AF8AC57902DDE7F41A03EB00A4818137E1BF4DFAE1EEDF8BB9E4363C15FD1C2278D86F2535BC3F395BE9A6CD690A5C852E6C35D6184BE7B9062AEE2AFC1A5AC81E7D21B7252A56C62BB5AC0BBAD36C7A4907C868704985E1754BAA3E8315E775A51B7BDC7ACB0D0675D29513D78CB05AB6119D3CA0A810A41F78150E3C5D9ACAFBE1533FC3533DECEC14387BF7478F6E229EB4CC312DC22436F4DB0D4CC308FB6EEA612F2F9E00239DE7902DE15889EE71370147C9696A5E7B022947ABB8AFBBC64F7840BED4CE69592CAF4085A1074475E365ED015048C89AE717BC259C42510F15F31DA3F9302EAD8F263B43D14886B2335A245C00871C041CBB683F1F047573F789673F9B11B6E6714C2A3360244757BB220C7952C6D3D9D65AA47511A63E2A59706B7A70846C930DCFB3D8CAFB3BD6F687CACF5A708692C26B363C80C460F54E59912D41D9BB359698051ABC049A0D0CFD7F23DC97DA940B1EDEAC6B84B194C8F8A56A46CE69EE7A0AEAA11C99508A368E64D27756AD0BA7146A6ADA3D5FA237B3B4EDDC84B71C27DE3A9F26A42197791C7DC09E2D7C4A7D8FCDC8F9A5D4983BB278FCE9513B1486D18F8560C3F31CC70203010001}\",
        \"hex_data\":\"$(str2hex "$JSON_DATA")\"
}";

curl -i -X POST -H 'Accept: application/json' http://localhost:7572/api/network/push --data "${PUSH_FORMAT}"

В данном контексте мы воспользовались HLS для отправления запроса с целью получить ответ от сервиса, расположенным за нодой, публичный ключ которой = Pub(go-peer/rsa){3082020A0282020100B752D35E81F4...8560C3F31CC70203010001}.

Мы отправили "hello, world". Получили hex-кодировку. Если раскодируем, то получим такой ответ: `{"echo":"hello, world!","return":1}`. HLS возвращает ответ (от функции Request) всегда в hex кодировке при успешном return'e = 1.

Сам сервис был написан таким образом.

Пример сервиса (echo)
package main

import (
	"encoding/json"
	"io"
	"net/http"
)

type sResponse struct {
	FEcho   string `json:"echo"`
	FReturn int    `json:"return"`
}

func main() {
	http.HandleFunc("/echo", echoPage)
	http.ListenAndServe(":8080", nil)
}

func echoPage(w http.ResponseWriter, r *http.Request) {
	if r.Method != "POST" {
		response(w, 2, "failed: incorrect method")
		return
	}
	res, err := io.ReadAll(r.Body)
	if err != nil {
		response(w, 3, "failed: read body")
		return
	}
	response(w, 1, string(res))
}

func response(w http.ResponseWriter, ret int, res string) {
	w.Header().Set("Content-Type", "application/json")
	json.NewEncoder(w).Encode(&sResponse{
		FEcho:   res,
		FReturn: ret,
	})
}

Перенаправление HLS на сервисы реализовано достаточно просто.

package handler

import (
	"bytes"
	"fmt"
	"io"
	"net/http"

	"github.com/number571/go-peer/cmd/hls/config"
	hls_network "github.com/number571/go-peer/cmd/hls/network"
	hls_settings "github.com/number571/go-peer/cmd/hls/settings"
	"github.com/number571/go-peer/modules/crypto/asymmetric"
	"github.com/number571/go-peer/modules/network/anonymity"
	"github.com/number571/go-peer/modules/payload"
)

func HandleServiceTCP(cfg config.IConfig) anonymity.IHandlerF {
	return func(node anonymity.INode, sender asymmetric.IPubKey, pld payload.IPayload) []byte {
		// Получаем запрос из полезной нагрузки.
		requestBytes := pld.Body()
		request := hls_network.LoadRequest(requestBytes)
		if request == nil {
			return nil
		}

		// Смотрим существует ли такой хост в наших сервисах.
		address, ok := cfg.Service(request.Host())
		if !ok {
			return nil
		}

		// Если существует, тогда создаём новый запрос.
        // Здесь HLS находится в роли прокси сервера.
		req, err := http.NewRequest(
			request.Method(),
			fmt.Sprintf("http://%s%s", address, request.Path()),
			bytes.NewReader(request.Body()),
		)
		if err != nil {
			return nil
		}

		// Обновляем заголовки и добавляем всегда один уникальный
        // --> публичный ключ отправителя.
		req.Header.Add(hls_settings.CHeaderPubKey, sender.String())
		for key, val := range request.Head() {
			if key == hls_settings.CHeaderPubKey {
				continue
			}
			req.Header.Add(key, val)
		}

		// Отправляем запрос.
		resp, err := http.DefaultClient.Do(req)
		if err != nil {
			return nil
		}
		defer resp.Body.Close()

        // Читаем ответ.
		data, err := io.ReadAll(resp.Body)
		if err != nil {
			return nil
		}

		// Отправляем ответ от сервера.
		return data
	}
}

Кратко HLS можно изобразить следующим образом.

Анонимизатором, обработчиком исходящих и входящих сообщений является сам HLS.  Приложение и сервисы (как надстройки) уже можно представлять как HLM.
Анонимизатором, обработчиком исходящих и входящих сообщений является сам HLS. Приложение и сервисы (как надстройки) уже можно представлять как HLM.

Таким образом, HLS представляет собой достаточно простую и лёгкую архитектуру ядра анонимной сети с теоретически доказуемой анонимностью. Несомненно минусы HLS существуют и даже в некой степени более решающие в практическом неиспользовании рядовыми пользователями. Тем не менее, целью являлась реализация простого, минималистичного ядра и рассмотрение возможностей в последующем его применении. Из этого породилась реализация мессенджера.

Реализация HLM

Теперь давайте приступать к реализации самого мессенджера. Скажу сразу я далёк от фронтенда, а потому во всей своей реализации вслепую, втупую применял bootstrap и jquery, а также их шаблоны. Также всё GUI представление реализовано через браузер. Впринципе что-то даже из этого и получалось.

Страница /about
Страница /about

По большей части анонимный мессенджер, сам мессенджер, будет являться лишь интерфейсом, а точнее GUI для реализованного ранее HLS с привязкой к его API. Поэтому думаю мало кому интересно наблюдать и читать чисто про то как я брал bootstrap код из сайтов-шаблонов, вставлял в редактор кода, перезагружал страницу и говорил "Ого, а это даже выглядит не как куча ***" (объективно GUI выглядит всё же как ***, но я пытался делать минималистично, без нагромаждений и дополнительных сложностей).

Страница /settings
Страница /settings

Противоречиво, но повествование о реализации конкретно HLM проще показать с финала, когда уже существует интерфейс. Я думаю такое легче понимать визуально, когда мы уже что-то имеем. Лишь после осмотра можно уже углубляться внутрь. Поэтому сейчас уже показываются даже какие-то скриншоты.

Страница /friends
Страница /friends

Все действия мессенджера - это все действия HLS. Получение, добавление, удаление друзей - это функции API HLS `/api/config/friends`. Получение, добавление, удаление соединений - это также функции API HLS `/api/config/connects`. Отправление сообщений - это функция POST `/api/network/push`. Даже получение публичного ключа - это функция `/api/node/pubkey`.

Страница /friends/chat?alias_name=Alice
Страница /friends/chat?alias_name=Alice

Единственная самостоятельная часть в HLM - это база данных, которая отлична от базы HLS, потому как первой необходимо хранить сообщения, второй же только хеши при передаче различных сообщений.

Для получения сообщений извне HLM создаёт отдельный сервис для перенаправления сообщений с HLS. Код его хендлера показан ниже.

func HandleIncomigHTTP(db database.IKeyValueDB) http.HandlerFunc {
	return func(w http.ResponseWriter, r *http.Request) {
		if r.Method != "POST" {
			response(w, hls_settings.CErrorMethod, "failed: incorrect method")
			return
		}

        // Читаем всё с HLS.
		msgBytes, err := io.ReadAll(r.Body)
		if err != nil {
			response(w, hls_settings.CErrorResponse, "failed: response message")
			return
		}

		msg := strings.TrimSpace(string(msgBytes))
		if len(msg) == 0 {
			response(w, hls_settings.CErrorResponse, "failed: message is null")
			return
		}

        // Читаем публичный ключ отправителя. 
        // HLS всегда обязан отправлять публичный ключ (поэтому стоит panica)
		pubKey := asymmetric.LoadRSAPubKey(r.Header.Get(hls_settings.CHeaderPubKey))
		if pubKey == nil {
			panic("public key is null (receive from hls)!")
		}

        // Сохраняем сообщение в БД.
		if err := db.Push(pubKey, database.NewMessage(true, msg)); err != nil {
			response(w, hls_settings.CErrorPubKey, "failed: push message to database")
			return
		}

        // Отправляем сообщение по вебсокету, чтобы таковое
        // отобразилось сразу, без перезагрузки страницы.
		gChatWS <- &sChatWS{pubKey.Address().String(), msg}
		response(w, hls_settings.CErrorNone, settings.CTitlePattern)
	}
}

А в конфигах HLS и HLM указывается следующее (пример).

Конфиги

HLS:

{
	"address": {
		"http": "localhost:8572"
	},
	"services": {
		"go-peer/hidden-lake-messenger": "localhost:8081"
	},
	"connections": [
		"localhost:9571"
	],
	"friends": {
		"Alice": "Pub(go-peer/rsa){3082020A0282020100C17B6FA53983050B0339A0AB60D20A8A5FF5F8210564464C45CD2FAC2F266E8DDBA3B36C6F356AE57D1A71EED7B612C4CBC808557E4FCBAF6EDCFCECE37494144F09D65C7533109CE2F9B9B31D754453CA636A4463594F2C38303AE1B7BFFE738AC57805C782193B4854FF3F3FACA2C6BF9F75428DF6C583FBC29614C0B3329DF50F7B6399E1CC1F12BED77F29F885D7137ADFADE74A43451BB97A32F2301BE8EA866AFF34D6C7ED7FF1FAEA11FFB5B1034602B67E7918E42CA3D20E3E68AA700BE1B55A78C73A1D60D0A3DED3A6E5778C0BA68BAB9C345462131B9DC554D1A189066D649D7E167621815AB5B93905582BF19C28BCA6018E0CD205702968885E92A3B1E3DB37A25AC26FA4D2A47FF024ECD401F79FA353FEF2E4C2183C44D1D44B44938D32D8DBEDDAF5C87D042E4E9DAD671BE9C10DD8B3FE0A7C29AFE20843FE268C6A8F14949A04FF25A3EEE1EBE0027A99CE1C4DC561697297EA9FD9E23CF2E190B58CA385B66A235290A23CBB3856108EFFDD775601B3DE92C06C9EA2695C2D25D7897FD9D43C1AE10016E51C46C67F19AC84CD25F47DE2962A48030BCD8A0F14FFE4135A2893F62AC3E15CC61EC2E4ACADE0736C9A8DBC17D439248C42C5C0C6E08612414170FBE5AA6B52AE64E4CCDAE6FD3066BED5C200E07DBB0167D74A9FAD263AF253DFA870F44407F8EF3D9F12B8D910C4D803AD82ABA136F93F0203010001}"
	}
}

HLM:

{
	"address": {
		"web_local": "localhost:8080",
		"incoming": "localhost:8081"
	},
	"connection": "localhost:8572"
}

Чтобы посмотреть работоспособность мессенджера, можно перейти в директорию `examples/cmd/anon_messenger`. В данном примере разворачивается три узла - два участника чата и один промежуточный (созданный исключительно для маршрутизации).

$ cd examples/cmd/anon_messenger
$ make
> # Откроется два HTTP порта :7070, :8080;

Остаётся поговорить лишь о самом графическом интерфейсе, но тут на самом деле мало что говорить, т.к. в начале уже большая часть была описана. Если есть интерес именно по графической составляющей, по HTML, CSS, JS коду и т.д., то можете посмотреть здесь.

Заключение

Таким образом, мы реализовали анонимный мессенджер построенный на теоретически доказуемой анонимности. Сам по себе мессенджер крайне минималистичен, потому как сам также сводится к примитивнейшему ядру анонимной сети. Нельзя сказать, что мессенджер завершён, потому как ему предстоит ещё многое пройти. Как минимум необходимо реализовать логин/регистрацию по которой будет выполняться два действия - расшифрование приватного ключа для HLS и расшифрование сообщение в БД переписок. Тем не менее, даже с этими недочётами и недостатками, HLM может представлять собой интересное приложение для вечерних изучений.

Tags:
Hubs:
Total votes 18: ↑17 and ↓1+19
Comments10

Articles