Анонимный обмен файлами в реалиях глобального наблюдателя

Предисловие

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

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

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

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

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

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

Ни мне, ни вам, читатель, не запрещено обедать на золотом сервизе; к сожалению, ни у вас, ни у меня нет и, вероятно, никогда не будет средства для удовлетворения этой изящной идеи; потому я откровенно говорю, что нимало не дорожу своим правом иметь золотой сервиз и готов продать это право за один рубль серебром или даже дешевле.

Н.Г. Чернышевский. Борьба партий во Франции, 1858 год.

Введение в анонимность

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

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

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

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

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

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

Сетевая анонимность

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

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

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

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

  • Скрытие сетевого адреса получателя часто является одной из форм анонимности для сопутствующего обхода блокировок со стороны провайдера связи,

  • Скрытие сетевого адреса отправителя часто является одной из форм анонимности для сопутствующего обхода блокировок со стороны сервиса связи,

  • Государства становятся внешними, и зачастую глобальными наблюдателями (получателями) всего генерируемого трафика,

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

  • При отсутствии глобальных наблюдателей применяются сети с более слабой моделью угроз на базе принципа федеративности.

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

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

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

На данный момент времени известно лишь три задачи на базе которых могут формироваться теоретически доказуемые анонимные сети:

  1. DC-сети: Проблема обедающих криптографов (анонимные сети: Herbivore, Dissent)

  2. QB-сети: Задача на базе очередей (анонимные сети: Hidden Lake, M-A)

  3. EI-сети: Задача на базе увеличения энтропии (анонимные сети: отсутствуют)

Как было показано в статье «Можно ли оставаться анонимным внутри государства, которое закрыло весь внешний Интернет?» на текущий момент жива фактически лишь одна анонимная сеть — Hidden Lake. Сеть Herbivore имеет закрытый исходный код и нигде открыто не публиковалась. Сеть Dissent хоть и имеет исходный код, но сам репозиторий не поддерживается уже более 10 лет (заархивирован). Сеть M-A является лишь минимальным прототипом использования задачи на базе очередей.

Задача на базе очередей

Задача на базе очередей

Пара слов о задаче на базе очередей

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

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

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

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

Таким образом, если мы ставим в качестве основной задачи анонимный обмен файлами, то относительно готовым вариантом здесь может служить сеть Hidden Lake. На текущий момент написания данной статьи в сети Hidden Lake не существовало приложения способного обмениваться файлами. Единственным прикладным приложение был лишь мессенджер. И таким образом, сегодня мы попытаемся создать файлообменник внутри сети Hidden Lake.

Архитектура Hidden Lake

0aad90f3e2edcc16736190c5e4ae8d04.png

Анонимная сеть Hidden Lake (HL) представляет собой микросервисную архитектуру, где каждый отдельный сервис участвует в выполнении своей узкоспециализированной задачи. Забавным моментом здесь является и то, что сам анонимизирующий сервис HLS (service) точно также может быть удалён и заменён другой технологией, вследствие чего анонимность может быть удалена, например по ненадобности. Тем не менее, в таком случае сеть перестаёт быть анонимной, что говорит о явной зависимости сети Hidden Lake к сервису HLS, как к её ядру.

Помимо анонимизирующего сервиса HLS, в анонимной сети Hidden Lake также присутствуют следующие сервисы:

  • HLT (traffic) — вспомогательный сервис, выполняющий роль сохранения и распространения / ретранслирования анонимизирующего трафика. Может быть использован для формирования тайных каналов связи внутри централизованных сервисов,

  • HLM (messenger) — прикладной сервис, представляющий собой приложение мессенджер,

  • HLL (loader) — вспомогательный малый сервис, выполняющий лишь роль скачивания анонимизирующего трафика с одного HLT на другой,

  • HLE (encryptor) — вспомогательный малый сервис, выполняющий лишь роль шифрования и расшифрования сообщений.

Сеть Hidden Lake является по умолчанию F2F (friend-to-friend) сетью, где каждый пользователь самолично устанавливает доверенных участников, с которыми они будут впоследствии связываться и которые смогут отправлять ему сообщения. Если один абонент для другого не будет находиться в списке друзей, и при этом попытается отправить ему сообщение, то получатель данное сообщение просто проигнорирует. Вследствие этого механизма, HL убирает возможность спама сообщениями от недоверенных узлов, а также возможные уязвимости, которые либо могут быть скрыты в исходном коде, либо содержатся на уровне прикладных приложений.

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

Существует четыре слоя / уровня сообщений:

  1. Сетевой. На данном уровне сообщение упаковывается в вид сырых данных с тремя особенностями: 1) подтверждение работы, 2) разграничение сетей, 3) скрытие статичного размера сообщений.

    1. Подтверждение работы необходимо для предотвращения спама. На текущий момент каждое генерируемое сообщение в сети HL в интервале T=5s обладает сложность в 22 бита.

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

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

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

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

  4. Прикладной. На данном уровне обрабатывается непосредственно само оригинальное сообщение.

Стек протокола HL/go-peer на примере прикладного сервиса HLM

Стек протокола HL/go-peer на примере прикладного сервиса HLM

Каждый слой накапливает внутри себя следующий. Так например, Layer-1 уже содержит в себе Layer-2, Layer-2 содержит в себе Layer-3, а Layer-3 содержит Layer-4. Итого, получаем весь стек протокола HL/go-peer вида L1(L2(L3(L4(message)))).

Чисто технически, если бы нам не нужна была анонимность, то мы могли бы реализовать файлообменный сервис на первых двух уровнях, то есть на L1 и L2 за счёт использования двух сервисов: HLE (шифрующим и расшифровывающим сообщения) и HLT (сохраняющим шифрованные сообщения). Но так или иначе, это является менее интересной задачей, чем создание действительно анонимного файлообменника.

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

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

Изменение состояния связей между субъектами на примере использования сервисов HLT, HLS, HLM

Изменение состояния связей между субъектами на примере использования сервисов HLT, HLS, HLM

В таком примере вырисовывается три возможных коммуникации / связи:

  1. Сетевая. На данном этапе происходит распространение информации в привычном и открытом виде для наблюдателей, не дающая при этом понимания кто кому и что отправил (и отправил ли вообще). Выполняется на слоях L1, L2.

  2. Дружественная. На данном этапе происходит непосредственное получение аутентифицирующей информации между friend-to-friend узлами. Выполняется на слое L3.

  3. Логическая. На данном этапе происходит непосредственная связь с получателем / получателями открытой информации. Выполняется на слое L4.

Файлообменник HLF

Сервисы в сети Hidden Lake можно писать на любом языке программирования и с любой технологией, которая больше всего нравиться. Это я показывал тут и тут. Я же буду писать на языке Go. Назовём наш сервис как HLF (filesharer).

082a6dd7e266da2a712e52689a62de17.png

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

У нашего сервиса будет два возможных действия:

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

  2. Выгрузка чанков файла. В данном сценарии нам потребуется скачивать файл чанками (кусками) потому как Hidden Lake ограничивает общий объём сообщений 8KiB без учёта заголовочных байт.

Больше всего проблем именно со вторым пунктом, поэтому на данном этапе лучше будет для начала реализовать выгрузку списка файлов, а потом просто дойти до выгрузки чанков.

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


func HandleIncomigListHTTP(pCfg config.IConfig, pPathTo string) http.HandlerFunc {
	return func(pW http.ResponseWriter, pR *http.Request) {
		// Устанавливаем заголовок для HLS указывающий, что мы генерируем ответ
        pW.Header().Set(hls_settings.CHeaderResponseMode, hls_settings.CHeaderResponseModeON)

        // Метод должен быть = GET
		if pR.Method != http.MethodGet {
			api.Response(pW, http.StatusMethodNotAllowed, "failed: incorrect method")
			return
		}

        // Получаем номер страницы с файлами
		page, err := strconv.Atoi(pR.URL.Query().Get("page"))
		if err != nil {
			api.Response(pW, http.StatusBadRequest, "failed: incorrect page")
			return
		}

        // Получаем список файлов с дополнительной информацией (размер, хеш)
		result, err := getListFileInfo(pCfg, pPathTo, uint64(page))
		if err != nil {
			api.Response(pW, http.StatusInternalServerError, "failed: open storage")
			return
		}

        // Отправляем результат
		api.Response(pW, http.StatusOK, result)
	}
}

Основной неизвестной функцией здесь является лишь способ получения списка файлов.

func getListFileInfo(pCfg config.IConfig, pPathTo string, pPage uint64) ([]hlf_settings.SFileInfo, error) {
	// Количество файлов на одной странице
    pageOffset := pCfg.GetSettings().GetPageOffset()

    // Читаем содержимое директории-хранилища
	entries, err := os.ReadDir(hlf_settings.CPathSTG)
	if err != nil {
		return nil, err
	}
	lenEntries := uint64(len(entries))

	result := make([]hlf_settings.SFileInfo, 0, lenEntries)
	for i := (pPage * pageOffset); i < lenEntries; i++ {
		e := entries[i]
		if e.IsDir() {
			continue
		}
        // Если мы прочитали всю страницу, тогда прерываем чтение
		if i != (pPage*pageOffset) && i%pageOffset == 0 {
			break
		}
		fullPath := fmt.Sprintf("%s/%s/%s", pPathTo, hlf_settings.CPathSTG, e.Name())
		result = append(result, hlf_settings.SFileInfo{
			FName: e.Name(),
			FHash: getFileHash(fullPath),
			FSize: getFileSize(fullPath),
		})
	}
	return result, nil
}

func getFileSize(filename string) uint64 {
	stat, _ := os.Stat(filename)
	return uint64(stat.Size())
}

func getFileHash(filename string) string {
	f, err := os.Open(filename)
	if err != nil {
		return ""
	}
	defer f.Close()

	h := sha256.New()
	if _, err := io.Copy(h, f); err != nil {
		return ""
	}

    // Аналог функции hex.EncodeToString()
	return encoding.HexEncode(h.Sum(nil))
}

Всё, это есть весь Handler с выдачей списка файлов. Но можно задаться вполне логичным вопросом, а где анонимность, где анонимизация трафика? А суть в том, что HLS будет использовать наш написанный сервис лишь как чёрный ящик по типу:

HLS получает запрос из сети Hidden Lake, перенаправляет его в качестве запроса на наш сервис. Наш сервис создаёт ответ и отправляет его на HLS. HLS упаковывает ответ в привычный формат сети Hidden Lake и отправляет по адресу отправителя. Таким образом, заботиться о правильности или неправильности логики анонимизации нам сильно не приходится, главное чтобы сам сервис не выдал чего личного. Если интересно узнать о том, как собственно это всё работает из-под капота, то можно посмотреть данную информацию в статье «Пишем анонимный мессенджер с нуля».

Теперь нам необходимо написать обработчик с выгрузкой чанков. Здесь я также удалил строки с логгированием для упрощения кода.

func HandleIncomigLoadHTTP(pCfg config.IConfig, pPathTo string) http.HandlerFunc {
	return func(pW http.ResponseWriter, pR *http.Request) {
        // Устанавливаем заголовок для HLS указывающий, что мы генерируем ответ
		pW.Header().Set(hls_settings.CHeaderResponseMode, hls_settings.CHeaderResponseModeON)

        // Также метод должен быть только GET
		if pR.Method != http.MethodGet {
			api.Response(pW, http.StatusMethodNotAllowed, "failed: incorrect method")
			return
		}

		query := pR.URL.Query()

        // Узел должен нам указать имя файла, который он хочет скачивать...
		name := filepath.Base(query.Get("name"))
		if name != query.Get("name") {
			api.Response(pW, http.StatusConflict, "failed: got another name")
			return
		}

        // ... и также номер чанка
		chunk, err := strconv.Atoi(query.Get("chunk"))
		if err != nil || chunk < 0 {
			api.Response(pW, http.StatusBadRequest, "failed: incorrect chunk")
			return
		}

        // Пытаемся найти этот файл в нашей директории-хранилище
		fullPath := fmt.Sprintf("%s/%s/%s", pPathTo, hlf_settings.CPathSTG, name)
		stat, err := os.Stat(fullPath)
		if os.IsNotExist(err) || stat.IsDir() {
			api.Response(pW, http.StatusNotAcceptable, "failed: file not found")
			return
		}

        // Вычисляем размер чанка, который мы можем отправить 
        // по анонимной сети Hidden Lake
		chunkSize, err := getMessageLimit(getClient(pCfg))
		if err != nil {
			api.Response(pW, http.StatusNotAcceptable, "failed: get chunk size")
			return
		}

        // Вычисляем общее количество чанков для нашего файла
        // и если полученный номер чанка больше общего количество, то выдать ошибку
		chunks := utils.GetChunksCount(uint64(stat.Size()), chunkSize)
		if uint64(chunk) >= chunks {
			api.Response(pW, http.StatusNotAcceptable, "failed: chunk number")
			return
		}

        // Открываем полученный файл
		file, err := os.Open(fullPath)
		if err != nil {
			api.Response(pW, http.StatusNotAcceptable, "failed: open file")
			return
		}
		defer file.Close()

        // Создаём буфер размером с чанк и вычисляем позицию нужного чанка
		buf := make([]byte, chunkSize)
		chunkOffset := int64(chunk) * int64(chunkSize)

        // Сдвигаем чтение файла на позицию номера чанка
		nS, err := file.Seek(chunkOffset, io.SeekStart)
		if err != nil || nS != chunkOffset {
			api.Response(pW, http.StatusNotAcceptable, "failed: seek file")
			return
		}

        // Читаем только один чанк
		nR, err := file.Read(buf)
		if err != nil || (uint64(chunk) != chunks-1 && uint64(nR) != chunkSize) {
			api.Response(pW, http.StatusNotAcceptable, "failed: chunk number")
			return
		}

        // Отправляем прочтённый чанк
		api.Response(pW, http.StatusOK, buf[:nR])
	}
}

// ... package utils ...
func GetChunksCount(pBytesNum, pChunkSize uint64) uint64 {
	return uint64(math.Ceil(float64(pBytesNum) / float64(pChunkSize)))
}

В этом коде существует лишь одна неизвестная нам ранее функция getMessageSize.

var (
	// Вычисляем размер структуры сообщения без самих данных
    // {"code":200,"head":{"Content-Type":"application/octet-stream","Hl-Service-Response-Mode":"on"},"body":""}
	gRespSize = uint64(len(
		hls_response.NewResponse(200).
			WithHead(map[string]string{
				"Content-Type":                   api.CApplicationOctetStream,
				hls_settings.CHeaderResponseMode: hls_settings.CHeaderResponseModeON,
			}).
			WithBody([]byte{}).
			ToBytes(),
	))
)

func getMessageLimit(pHlsClient hls_client.IClient) (uint64, error) {
	// Получаем настройки от HLS
    sett, err := pHlsClient.GetSettings()
	if err != nil {
		return 0, fmt.Errorf("get settings from HLS (message size): %w", err)
	}

    // Получаем количество возможных байт, сколько мы можем записать
    // В этих байтах не учтён размер структуры сообщения
	msgLimitOrig := sett.GetLimitMessageSizeBytes()
	if gRespSize >= msgLimitOrig {
		return 0, errors.New("response size >= limit message size")
	}

    // Поэтому вычитаем из количества полученных байт структуру сообщения
	msgLimitBytes := msgLimitOrig - gRespSize

    // HLS кодирует данные в base64, которые находятся в поле body
    // Поэтому нам необходимо учесть сколько байт сожрёт ещё сама кодировка base64
	return base64.GetSizeInBase64(msgLimitBytes)
}

// ... package base64 ...
func GetSizeInBase64(pBytesNum uint64) (uint64, error) {
	if pBytesNum < 2 {
		return 0, errors.New("pBytesNum < 2")
	}
	// base64 encoding bytes with add 1/4 bytes of original
	// (-2) is a '=' characters in the suffix of encoding bytes
	return pBytesNum - uint64(math.Ceil(float64(pBytesNum)/4)) - 2, nil
}

Клиентская сторона вопроса

Как только мы написали сам сервис, нам необходимо будет также написать и клиентское приложение, которые будет связываться с нашим файлообменным сервисом. Для этого нам лучше будет написать API функции, которые будут взаимодействовать с HLF через HLS. И только после, на основе написанных функций, писать основную логику скачивания файлов.

API Функции

Интерфейсы

type IClient interface {
	GetListFiles(string, uint64) ([]hlf_settings.SFileInfo, error)
	LoadFileChunk(string, string, uint64) ([]byte, error)
}

type IRequester interface {
	GetListFiles(string, hls_request.IRequest) ([]hlf_settings.SFileInfo, error)
	LoadFileChunk(string, hls_request.IRequest) ([]byte, error)
}

type IBuilder interface {
	GetListFiles(uint64) hls_request.IRequest
	LoadFileChunk(string, uint64) hls_request.IRequest
}

Client

var (
	_ IClient = &sClient{}
)

type sClient struct {
	fBuilder   IBuilder
	fRequester IRequester
}

func NewClient(pBuilder IBuilder, pRequester IRequester) IClient {
	return &sClient{
		fBuilder:   pBuilder,
		fRequester: pRequester,
	}
}

func (p *sClient) GetListFiles(pAliasName string, pPage uint64) ([]hlf_settings.SFileInfo, error) {
	return p.fRequester.GetListFiles(pAliasName, p.fBuilder.GetListFiles(pPage))
}

func (p *sClient) LoadFileChunk(pAliasName, pName string, pChunk uint64) ([]byte, error) {
	return p.fRequester.LoadFileChunk(pAliasName, p.fBuilder.LoadFileChunk(pName, pChunk))
}

Builder

var (
	_ IBuilder = &sBuilder{}
)

type sBuilder struct {
}

func NewBuilder() IBuilder {
	return &sBuilder{}
}

func (p *sBuilder) GetListFiles(pPage uint64) hls_request.IRequest {
	return hls_request.NewRequest(
		http.MethodGet,
		hlf_settings.CTitlePattern,
		fmt.Sprintf("%s?page=%d", hlf_settings.CListPath, pPage),
	)
}

func (p *sBuilder) LoadFileChunk(pName string, pChunk uint64) hls_request.IRequest {
	return hls_request.NewRequest(
		http.MethodGet,
		hlf_settings.CTitlePattern,
		fmt.Sprintf("%s?name=%s&chunk=%d", hlf_settings.CLoadPath, pName, pChunk),
	)
}

Requester

var (
	_ IRequester = &sRequester{}
)

type sRequester struct {
	fHLSClient hls_client.IClient
}

func NewRequester(pHLSClient hls_client.IClient) IRequester {
	return &sRequester{
		fHLSClient: pHLSClient,
	}
}

func (p *sRequester) GetListFiles(pAliasName string, pRequest hls_request.IRequest) ([]hlf_settings.SFileInfo, error) {
	resp, err := p.fHLSClient.FetchRequest(pAliasName, pRequest)
	if err != nil {
		return nil, utils.MergeErrors(ErrRequest, err)
	}

	if resp.GetCode() != http.StatusOK {
		return nil, utils.MergeErrors(
			ErrDecodeResponse,
			fmt.Errorf("got %d code", resp.GetCode()),
		)
	}

	list := make([]hlf_settings.SFileInfo, 0, hlf_settings.CDefaultPageOffset)
	if err := encoding.DeserializeJSON(resp.GetBody(), &list); err != nil {
		return nil, utils.MergeErrors(ErrInvalidResponse, err)
	}

	for _, info := range list {
		if len(encoding.HexDecode(info.FHash)) != hashing.CSHA256Size {
			return nil, utils.MergeErrors(
				ErrInvalidResponse,
				errors.New("got invalid hash value"),
			)
		}
	}

	return list, nil
}

func (p *sRequester) LoadFileChunk(pAliasName string, pRequest hls_request.IRequest) ([]byte, error) {
	resp, err := p.fHLSClient.FetchRequest(pAliasName, pRequest)
	if err != nil {
		return nil, utils.MergeErrors(ErrRequest, err)
	}

	if resp.GetCode() != http.StatusOK {
		return nil, utils.MergeErrors(
			ErrDecodeResponse,
			fmt.Errorf("got %d code", resp.GetCode()),
		)
	}

	return resp.GetBody(), nil
}

Таким образом, чтобы получить список файлов, нам потребуется лишь выполнить следующий код:

filesList, err := hlfClient.GetListFiles(aliasName, page)
if err != nil {
	return err
}
...

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

chunksCount := utils.GetChunksCount(uint64(fileSize), chunkSize)
for i := uint64(startChunk); i < chunksCount; i++ {
	chunk, err := hlfClient.LoadFileChunk(aliasName, fileName, i)
	if err != nil {
		return err
	}
	...
}

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

Работа сервиса HLF (ускоренная x2)

Работа сервиса HLF (ускоренная x2)

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

services:
  hidden-lake-filesharer: 
    host: localhost:8081

Недостатки HLF

Результат работы, который был показан на GIF изображении ускорен в два раза и даже с этим нюансом, продолжительность записанного ролика исчисляется более чем минутой. При этом скаченное изображение, которое приводится в примере, занимает всего 17.8KiB! Это есть основной недостаток HLF — он скачивает файлы очень медленно. И с определённой точки зрения это выглядит даже забавно, когда изображение загружается как в старые добрые времена сверху-вниз по кусочкам.

С чем такое связано? А связано это с самой архитектурой Hidden Lake, где последняя ограничивает размер одного передаваемого сообщения периодом его генерации. В Hidden Lake каждое сообщение занимает 8KiB, а период равен 5 секундам. Тогда может возникнуть другой логичный вопрос -, а почему мы не скачали изображение за ~15 секунд, ведь этого было бы достаточно для данного изображения? А суть сводится к двум моментам: 1) из 8KiB реальная доля возможным передаваемых данных исчисляется ~3.5KiB; 2) на каждый отправленный чанк требуется полная связь типа »запрос-ответ», где запрос создаёт клиент, указывая номер чанка, а в качестве ответа от файлообменника передаётся сам чанк. Итого, для успешной передачи ~3.5KiB требуется минимум 10 секунд времени, что эквивалентно скорости ~358B/s. Такая же история с получением списка файлов. Для того чтобы получить одну страницу файлов, потребуется подождать примерно 10 секунд.

Заключение

В результат всего вышенаписанного и сделанного, мы смогли реализовать анонимный файлообменник с теоретически доказуемой анонимностью поверх сети Hidden Lake. Из плюсов можно выделить то, что он работает достаточно стабильно. Из минусов здесь конечно же скорость передачи данных. Таким образом, теоретически доказуемая анонимность даётся не бесплатно и за неё приходится платить временем.

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

© Habrahabr.ru