Введение

Анонимность — великая штука. Высшее наслаждение. Это что-то, чего ты не можешь оценить до тех пор, пока не потеряешь.

(Билл Мюррей)

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

Вследствие этого, можно сказать just-for-fun, у меня появился вопрос: можно ли реализовать анонимную сеть настолько малую, чтобы её программный код смог понять даже начинающий программист за короткое время?

Выбор ядра

Анонимные сети всегда базируются на каком-либо алгоритме запутывающей маршрутизации. Так например, Tor базируется на луковой маршрутизации, I2P на чесночной, Mixminion на перемешивании трафика, DC-сети (Herbivore, Dissent) на задаче обедающих криптографов, HIdden Lake на задаче очередей и т.д. Выбираемый алгоритм запутывающей маршрутизации не только может приводить к выстраиванию разных моделей угроз, но и к ограничению, либо расширению прикладного использования итоговой анонимной сети. Таким образом, можно говорить, что алгоритм запутывающей маршрутизации — это есть ядро анонимной сети.

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

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

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

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

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

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

Сеть на базе очередей при трёх участниках {A, B, C}
Сеть на базе очередей при трёх участниках {A, B, C}

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

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

Особенности

Задача на базе очередей обладает рядом особенностей, которые так или иначе стоит учитывать:

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

  2. Линейная нагрузка на систему. Чем больше пользователей будет находиться в системе, тем сильнее сама система будет нагружена. Связано это в первую очередь с тем, что каждому пользователю необходимо будет обрабатывать в период T: N-1 запросов, где N - количество пользователей в сети.

  3. Абоненты связаны между собой. В отличие от множества других анонимных сетей (например, Tor или I2P), разграничивающих отправителя и получателя между собой, сети же на базе очередей не делают такого различия. Это приводит к тому, что абонент априори связывает ваш сетевой адрес (IP) с вашим уникальным идентификатором (ID) в сети, и как следствие, при общении с абонентом вы не можете быть анонимным к нему (а он к вам).
    Данный случай никак не нарушает анонимность со стороны внешнего наблюдателя или внутреннего, он лишь свидетельствует о том, что вы знаете с кем общаетесь, ровно как и собеседник знает с кем он общается. Например, если один из абонентов становится активным атакующим, знающим лишь и только ID своего собеседника, то ему не составит труда получить также его IP за счёт сотрудничества с глобальным наблюдателем. Тем не менее, знание связи IP-ID даст ему только местоположение собеседника, но не информацию о его состоянии в сети (действие или бездействие). Поэтому и предполагается, что абонент должен быть заранее известен, чтобы исключить на корню полезность его сотрудничества с глобальным наблюдателем.

Проектирование

Для того, чтобы знать как реализовывать мы должны понять, а что мы собственно хотим сделать. Какой протокол брать в качестве основы для передачи данных? Как пользователи будут взаимодействовать друг с другом? Как будет происходить шифрование / расшифрование данных? Какого прикладного применения мы хотим достичь? Начнём пожалуй по порядку.

В качестве основы мы возьмём протокол HTTP. Т.к. в языке Go уже существует стандартная библиотека для работы с HTTP протоколом, то результирующего кода потребуется меньше, чем если бы мы писали все взаимодействия пользователей на уровне транспортного протокола (того же TCP).

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

Взаимодействие пользователей у нас будет формироваться по следующим правилам:

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

  2. Все получаемые данные будут приходить на HTTP сервер. В таком случае, HTTP-сервер будет выступать исключительно потребителем сообщений получаемых от очередей.

И последнее, шифрование / расшифрование данных у нас будет происходить асимметричным шифром RSA. Это сделано по нескольким причинам:

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

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

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

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

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

Реализация

Весь реализованный код у нас будет находиться в одном файле (для удобства копирования). Также при написании я использовал достаточно старую версию Go - v1.16, поэтому данный код не привередлив к новшествам по типу дженериков.

Начнём мы пожалуй с основной функции main. Здесь всё достаточно просто. Мы должны при запуске указать наш никнейм, хост и порт на котором мы будем поднимать HTTP сервер. Далее, параллельно запускаем HTTP-сервер (на получение сообщений) и две очереди (на отправление сообщений), где одна очередь с реальными сообщениями (runQueue), другая с ложными (runQueueVoid). После этого уходим в бесконечный цикл чтения команд от клиента.

Таковых команд всего две: attach и send. Первая команда привязывает публичный ключ при помощи которого мы будем шифровать все дальнейшие сообщения. Вторая команда шифрует сообщение привязанным публичным ключом и отправляет результат шифрования в очередь.

func main() {
	if len(os.Args) != 3 {
		panic("example run: ./main [nickname] [host:port]")
	}

	go runService()
	go runQueueVoid()
	go runQueue()

	for {
		cmd := readCmd(startInput)
		if len(cmd) != 2 {
			fmt.Println("len cmd != 2")
			continue
		}
		switch cmd[0] {
		case "attach":
			if err := getPubKey(cmd[1], attach); err != nil {
				fmt.Println("error:", err)
				continue
			}
			fmt.Println("ok")
		case "send":
			msg := fmt.Sprintf("%s%s: %s", authBytes, os.Args[1], strings.TrimSpace(cmd[1]))
			encBytes, err := rsa.EncryptOAEP(sha256.New(), rand.Reader, attach, []byte(msg), nil)
			if err != nil {
				panic(err)
			}
			queue <- encBytes
		}
	}
}

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

func runQueue() {
	for {
		time.Sleep(5 * time.Second)
		encBytes := <-getQueue()
		for _, conn := range connects {
			go func(conn string) {
				req, err := http.NewRequest(http.MethodPost, conn, bytes.NewBuffer(encBytes))
				if err != nil {
					panic(err)
				}
				client := &http.Client{Timeout: 5 * time.Second}
				_, _ = client.Do(req)
			}(conn)
		}
	}
}

func getQueue() chan []byte {
	if len(queue) == 0 {
		return queueVoid
	}
	return queue
}

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

func runQueueVoid() {
	for {
		if len(queueVoid) == queueSize {
			time.Sleep(time.Second)
			continue
		}
		encBytes, err := rsa.EncryptOAEP(sha256.New(), rand.Reader, &privKey.PublicKey, []byte(""), nil)
		if err != nil {
			panic(err)
		}
		queueVoid <- encBytes
	}
}

Ну и сама серверная часть выглядит таким образом. Её основная цель - это получить запрос, далее попытаться его расшифровать, при успешном расшифровании проверить существование ключа сети в самом сообщении и если таковой есть, то напечатать полученное сообщение. В любом случае, кроме ошибки чтения данных или неправильного метода мы должны всегда возвращать http.StatusOK = 200, для того, чтобы исключить атаки ориентируемые на подбор authBytes. Успешность кода не будет давать злоумышленнику дополнительной информации о состоянии обработки полученных данных.

func runService() {
	http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
		if r.Method != http.MethodPost {
			w.WriteHeader(http.StatusMethodNotAllowed)
			return
		}
		encBytes, err := io.ReadAll(r.Body)
		if err != nil {
			w.WriteHeader(http.StatusBadRequest)
			return
		}
		defer w.WriteHeader(http.StatusOK)
		msg, err := rsa.DecryptOAEP(sha256.New(), rand.Reader, privKey, encBytes, nil)
		if err != nil {
			return
		}
		if !bytes.HasPrefix(msg, []byte(authBytes)) {
			return
		}
		msg = bytes.TrimPrefix(msg, []byte(authBytes))
		fmt.Printf("\n%s\n%s", string(msg), startInput)
	})
	http.ListenAndServe(os.Args[2], nil)
}

Всё оставшееся является уже скорее удобством в использовании, чтобы получать authBytes, privKey, attach, connects и прочее. Поэтому внутренний механизм нашей анонимной сети вышел лишь в 100 строк кода, оставшаяся сотня представляет собой уже дополнительные функции.

Весь исходный код
package main

import (
	"bufio"
	"bytes"
	"crypto/rand"
	"crypto/rsa"
	"crypto/sha256"
	"crypto/x509"
	"encoding/pem"
	"fmt"
	"io"
	"net/http"
	"os"
	"strings"
	"time"
)

const (
	initDir    = "_init/"
	keysDir    = "_keys/"
	startInput = "> "
	queueSize  = 32
)

var (
	queueVoid = make(chan []byte, queueSize)
	queue     = make(chan []byte, queueSize)
)

var (
	authBytes string
	connects  []string
	privKey   *rsa.PrivateKey
	attach    *rsa.PublicKey
)

func initApp() {
	if len(os.Args) != 3 {
		panic("example run: ./main [nickname] [host:port]")
	}

	authBytes = getAuthKey()
	connects = getConnects()
	privKey = getPrivKey()
	attach = &privKey.PublicKey
}

func main() {
	initApp()

	go runService()
	go runQueueVoid()
	go runQueue()

	for {
		cmd := readCmd(startInput)
		if len(cmd) != 2 {
			fmt.Println("len cmd != 2")
			continue
		}
		switch cmd[0] {
		case "attach":
			if err := getPubKey(cmd[1], attach); err != nil {
				fmt.Println("error:", err)
				continue
			}
			fmt.Println("ok")
		case "send":
			msg := fmt.Sprintf("%s%s: %s", authBytes, os.Args[1], strings.TrimSpace(cmd[1]))
			encBytes, err := rsa.EncryptOAEP(sha256.New(), rand.Reader, attach, []byte(msg), nil)
			if err != nil {
				panic(err)
			}
			queue <- encBytes
		}
	}
}

func runService() {
	http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
		if r.Method != http.MethodPost {
			w.WriteHeader(http.StatusMethodNotAllowed)
			return
		}
		encBytes, err := io.ReadAll(r.Body)
		if err != nil {
			w.WriteHeader(http.StatusBadRequest)
			return
		}
		defer w.WriteHeader(http.StatusOK)
		msg, err := rsa.DecryptOAEP(sha256.New(), rand.Reader, privKey, encBytes, nil)
		if err != nil {
			return
		}
		if !bytes.HasPrefix(msg, []byte(authBytes)) {
			return
		}
		msg = bytes.TrimPrefix(msg, []byte(authBytes))
		fmt.Printf("\n%s\n%s", string(msg), startInput)
	})
	http.ListenAndServe(os.Args[2], nil)
}

func runQueue() {
	for {
		time.Sleep(5 * time.Second)
		encBytes := <-getQueue()
		for _, conn := range connects {
			go func(conn string) {
				req, err := http.NewRequest(http.MethodPost, conn, bytes.NewBuffer(encBytes))
				if err != nil {
					panic(err)
				}
				client := &http.Client{Timeout: 5 * time.Second}
				_, _ = client.Do(req)
			}(conn)
		}
	}
}

func runQueueVoid() {
	for {
		if len(queueVoid) == queueSize {
			time.Sleep(time.Second)
			continue
		}
		msg := ""
		encBytes, err := rsa.EncryptOAEP(sha256.New(), rand.Reader, &privKey.PublicKey, []byte(msg), nil)
		if err != nil {
			panic(err)
		}
		queueVoid <- encBytes
	}
}

func getQueue() chan []byte {
	if len(queue) == 0 {
		return queueVoid
	}
	return queue
}

func getPubKey(filename string, pubKey *rsa.PublicKey) error {
	pubKeyBytes, err := os.ReadFile(keysDir + filename)
	if err != nil {
		return err
	}
	pubKeyBlock, _ := pem.Decode(pubKeyBytes)
	if pubKeyBlock == nil || pubKeyBlock.Type != "PUBLIC KEY" {
		panic("pem block is invalid")
	}
	pub, err := x509.ParsePKCS1PublicKey(pubKeyBlock.Bytes)
	if err != nil {
		panic(err)
	}
	*pubKey = *pub
	return nil
}

func getAuthKey() string {
	authKeyBytes, err := os.ReadFile(initDir + "auth.key")
	if err != nil || len(authKeyBytes) == 0 {
		panic(err)
	}
	return string(authKeyBytes)
}

func getPrivKey() *rsa.PrivateKey {
	privKeyBytes, err := os.ReadFile(initDir + "priv.key")
	if err != nil {
		panic(err)
	}
	privateKeyBlock, _ := pem.Decode(privKeyBytes)
	if privateKeyBlock == nil || privateKeyBlock.Type != "PRIVATE KEY" {
		panic("pem block is invalid")
	}
	priv, err := x509.ParsePKCS1PrivateKey(privateKeyBlock.Bytes)
	if err != nil {
		panic(err)
	}
	return priv
}

func getConnects() []string {
	cFile, err := os.Open(initDir + "connects.txt")
	if err != nil {
		panic(err)
	}
	defer cFile.Close()

	connects := make([]string, 0, 100)
	scanner := bufio.NewScanner(cFile)
	for scanner.Scan() {
		conn := strings.TrimSpace(scanner.Text())
		if conn == "" {
			continue
		}
		connects = append(connects, conn)
	}
	return connects
}

func readCmd(s string) []string {
	fmt.Print(s)
	input, _, err := bufio.NewReader(os.Stdin).ReadLine()
	if err != nil {
		panic(err)
	}
	cmd := strings.Split(string(input), "$")
	for i := range cmd {
		cmd[i] = strings.TrimSpace(cmd[i])
	}
	return cmd
}

Использование

В первую очередь, чтобы воспользоваться анонимной сетью нам необходимо структурировать директорию в которой запускается наш гошный файл. В директории _init находятся наши настройки - ключ сети auth.key, приватный ключ priv.key и список соединений connects.txt. Публичный ключ pub.key здесь необязателен.

Структура проекта
Структура проекта

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

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

Код для генерации ключей
package main

import (
	"crypto/rand"
	"crypto/rsa"
	"crypto/x509"
	"encoding/pem"
	"io/ioutil"
)

func main() {
	privKey, err := rsa.GenerateKey(rand.Reader, 4096)
	if err != nil {
		panic(err)
	}
	privateKeyBlock := &pem.Block{
		Type:  "PRIVATE KEY",
		Bytes: x509.MarshalPKCS1PrivateKey(privKey),
	}
	ioutil.WriteFile("priv.key", pem.EncodeToMemory(privateKeyBlock), 0644)

	publicKeyBlock := &pem.Block{
		Type:  "PUBLIC KEY",
		Bytes: x509.MarshalPKCS1PublicKey(&privKey.PublicKey),
	}
	ioutil.WriteFile("pub.key", pem.EncodeToMemory(publicKeyBlock), 0644)
}

Пример работоспособности итоговой анонимной сети можно посмотреть на следующей гифке.

Пример чата двух пользователей
Пример чата двух пользователей

Безопасность

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

Предположим, что существует кооперация внешнего активного наблюдателя (глобального наблюдателя), способного менять маршрут пакетов, блокировать узлы и прочее, и внутреннего активного наблюдателя, способного генерировать запросы в сети, DDoS'ить узлы и подобное. Возьмём в качестве их цели деанонимизацию узла посредством определения его текущего состояния (общается ли он с кем-то или бездействует). Определить состояние было бы возможным при условии, что 1) активный внутренний наблюдатель находится уже внутри сети (знает ключ сети), 2) располагает публичным ключом жертвы, 3) любой узел в сети на каждый запрос создаёт свой ответ (что успешно получил сообщение). В таком случае, сам ответ бы ложился в основную очередь, тем самым её "перегружая". Если кто-то в определённый момент бы уже отправил запрос, то очередь будет не пуста, т.к. в ней будет находиться уже ответ. В таком случае, внутренний активный наблюдатель может раз в N-ое время отправлять запрос жертве и если ответ будет сгенерирован лишь спустя несколько периодом T (итераций), то это будет означать, что жертва с кем-то общается. Если количество узлов в сети ограничено, то злоумышленник может исходить из априорных вероятностей связи и на основе этого частично деанонимизировать факт отправления/получения информации.

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

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

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

  1. Необходимо, чтобы все пользователи обменялись своими публичными ключами. Данное обстоятельство также может быть проблемой, потому как публичный ключ может быть подменён извне. Из-за децентрализованного характера сети, не допускается использование центров сертификации (ЦС). Тем не менее, можно воспользоваться N-ым количеством централизованных сервисов явно не связанных между собой для безопасной передачи публичных ключей.

  2. Сгенерировать ключ сети K и зашифровать его всеми полученными публичными ключами Pub1, Pub2, Pub3, ... => EPub1(K), EPub2(K), EPub3(K), ... После этого отправить шифрованные версии ключа всем абонентам. В таком случае, будет сгенерирован и распространён ключ сети.

Единственной оставшейся проблемой здесь является лишь возможность существования злоумышленника в кругу абонентов, которым передаётся ключ сети. Чтобы частично побороть данную проблему, можно внести в код элемент случайности, который будет с определённой долей вероятности выносить несколько зашифрованных сообщений из queueVoidв queue, тем самым нарушая однозначность связи хранения истинных сообщений в queue. При данном сценарии лишь будет страдать производительность системы, где для успешного отправления потребуется теперь не 5 секунд времени, а 5n секунд, где n - количество перемещённых сообщений из queueVoidв queue.

Заключение

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

UPD. Добавил уточнение в разделе Задача на базе очередей на счёт периода = T (его выбор/установка не приводит к необходимости в синхронизации с другими узлами). В разделе Безопасность изменил причину по которой пункт 3 (создание ответа) может присутствовать/отсутствовать.

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


  1. anzay911
    01.07.2023 05:02
    +1

    Линейная нагрузка только обозначена или может быть обойдена?


    1. Number571 Автор
      01.07.2023 05:02
      +2

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


  1. ReadOnlySadUser
    01.07.2023 05:02

    RSA

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

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

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

    Кароч, ключевая система - это основа всего, без неё всё эти телодвижения по анонимности бессмысленны.

    Линейная сложность

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

    Период Т

    И вот тут мы плавно переходим к третьей проблеме: стабильности работы сети. При наличии одного глобального периода Т возникает проблема доверенного источника времени. Как мы будем синхронизировать часы? Атакующий может создать по серверу времени для каждой пары участников и рассылать им слегка сдвинутые по времени точки начала периода. Тем самым деанонимизируя факты передачи.


    1. Number571 Автор
      01.07.2023 05:02

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

      В статье об этом говорится и о решении тоже:

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

      ---

      Есть всего два вменяемыхсценария распределения ключей - это квантовое распределение ключей (менее надёжно)

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

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

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

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

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

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

      Совершенно верно. Полная связность необходима лишь для упрощения самого механизма работы, т.к. это исключает маршрутизацию и, как следствие, дополнительный код. Целью статьи являлось, в первую очередь, создание минимальной анонимной сети. Все дальнейшие совершенствования лишь увеличили бы её количество кода. Так например, в основной моей анонимной сети, которую я пишу в свободное время, как раз неважна топология. Но в любом случае, какую бы мы топологию не выбрали, нагрузка на сеть будет всегда линейна. Если бы мы выбрали топологию "Звезда", то от одной точки к любой другой также бы сообщение доходило и также бы обрабатывалось.

      И вот тут мы плавно переходим к третьей проблеме: стабильности работы сети. При наличии одного глобального периода Т возникает проблема доверенного источника времени.

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

      Примерно как-то так, надеюсь ответил на все вопросы и замечания.


      1. ReadOnlySadUser
        01.07.2023 05:02

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

        Откуда им взяться в сети с глобальным наблюдателем? Помним, что мы строим сеть в которой есть дядя, который может принудить делать все УЦ всё что угодно. Опять же, это не решает проблемы атаки злоумышленника на сам УЦ и проведение MITM атак. Причём забавно, что чем больше мы этих сервисов добавляем, тем сложнее становится алгоритм консенсуса "а кому из них мы можем доверять". Любой скомпроментированный УЦ автоматически ложит нам вообще всю сеть. Ну такое.

        Квантовые технологии лишь укрепят безопасность связи клиент-сервер, но не клиент-клиент.

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

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

        Почему нельзя сказазать этого о симметричной? Откуда такой вывод 0_o?

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

        Плюсом помимо пассивных атак бывают еще и активные и тут ассиметрия сливает максимально, т.к. векторов для атаки вагон и маленькая тележка.

        Факт, только в какой пропорции происходит увеличение вероятности раскрытия информации, сколько потребуется памяти для её осуществления?

        Сложно оценить, алгоритмы разные бывают :)

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

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

        нагрузка на сеть будет всегда линейна.

        Мы можем ограничить пользователя, скажем, N связями с другими оконечными узлами. С точки зрения глобального наблюдателя вопрос "пользователь общается с кем-то из N получателей" ничем не отличается от "пользователь общается с кем-то из N+1 получателей" при N > 1. Единственным условием является отправка сообщений на все N узлов сразу. Условно, если в сети 1 млн узлов, а мне нужна связь только с одним из всех этих узлов, при N = 100 я выбираю еще 99 фиксированных узлов на которые буду слать сообщения (эти 99 узлов я выбираю один раз и навсегда).

        Да - это сузит область прослушивания для глобального наблюдателя, но это не решит его проблему - он всё также не сможет определить кто из этих 100 получателей мой целевой.

        Я не знаток подсчёта сложности, но по интуитивным ощущениям при такой постановке задачи предельная сложность всё равно остаётся линейной, но амортизационная должна вроде как стремиться к константе N.


        1. Number571 Автор
          01.07.2023 05:02
          +1

          Откуда им взяться в сети с глобальным наблюдателем?

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

          Причём забавно, что чем больше мы этих сервисов добавляем, тем сложнее становится алгоритм консенсуса "а кому из них мы можем доверять". Любой скомпроментированный УЦ автоматически ложит нам вообще всю сеть. Ну такое.

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

          > Причем интересно то, что пользователям предлагается обменяться публичными ключами самостоятельно, что на самом эквивалентно обмену симметричными ключами и сводит весь смысл использования ассиметрии к нулю.

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

          Почему нельзя сказазать этого о симметричной? Откуда такой вывод 0_o?

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

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

          Как минимум простота обмена ключами: N(N-1)/2, вместо 2N, и существование идентификатора со связью 1 (публичный ключ) ко многим абонентам, а не 1 (сеансовый ключ) к одному абоненту.

          Да - это сузит область прослушивания для глобального наблюдателя, но это не решит его проблему - он всё также не сможет определить кто из этих 100 получателей мой целевой.

          Совершенно верно. Подобного же принципа придерживается сеть Herbivore, разделяющая участников сети по группам. Тем не менее у такого принципа есть нюанс в плане того, что децентрализованные сети сами по себе являются динамичными структурами. Если будут существовать в сети статичные узлы, то вероятность того, что пользователь общается именно с ними будет стремиться к нулю. Если общение двух абонентов частое, то из всего множества N=100 можно со временем по крупицам убирать узлы, которых ранее не существовало и оставлять только тех, которые до сих пор активно работают. В таком случае, будет существовать вероятность, при которой N будет стремиться к единице за счёт продолжительной связи между двумя абонентами. Когда же мы в качестве N берём все узлы в системе, такого уже не будет возникать.


        1. Number571 Автор
          01.07.2023 05:02

          Помним, что мы строим сеть в которой есть дядя, который может принудить делать все УЦ всё что угодно.

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


  1. vp7
    01.07.2023 05:02
    +1

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

    Мне сложно придумать практическую применимость такого подхода. Обмен информацией между сенсорами, у которых целевой трафик не превышает сотни байт в сутки? Если только.

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

    p.s. Но как исследовательская задачка - класс, мне понравилось.


  1. loginoffvich
    01.07.2023 05:02

    Mobile ID changes - Provides random mobile ID (IMSI) changes, using oblivious authentication during changes. ID changes are on demand. This decouples you as a user from each ID your phone is given, and neither INVISV nor the mobile provider know which ID you received. - $3/change + decoupling principle mpr(multi-party relays)