Анонимная сеть в 100 строк кода на Go

МЕНЮ


Главная страница
Поиск
Регистрация на сайте
Помощь проекту
Архив новостей

ТЕМЫ


Новости ИИРазработка ИИВнедрение ИИРабота разума и сознаниеМодель мозгаРобототехника, БПЛАТрансгуманизмОбработка текстаТеория эволюцииДополненная реальностьЖелезоКиберугрозыНаучный мирИТ индустрияРазработка ПОТеория информацииМатематикаЦифровая экономика

Авторизация



RSS


RSS новости


Введение

Прошло уже более года с тех пор как я написал статью - Анонимная сеть в 200 строк кода на Go. Пересмотрев её однажды осенним вечером я понял насколько всё в ней было ужасно - начиная с самого поведения логики кода и заканчивая его избыточностью. Сев за ноутбук и потратив от силы 20 минут у меня получилось написать сеть всего в 100 строк кода, используя лишь и только стандартную библиотеку языка.

Начало

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

Выбор задачи

Для того, чтобы написать минималистичную анонимную сеть - необходимо выбрать наиболее простую задачу анонимизации, чтобы она давала как можно больше гарантий анонимности и безопасности. Из наиболее простых задач можно выделить две: Proxy и QB (queue based). Первая задача предполагает либо использование готовых proxy-серверов, что уже априори становится немонолитным решением и каким-то хаком со стороны условия в 100 строк кода, либо написание собственных, но в таком случае код может увеличиться на достаточно сильную величину. При этом, даже если мы сможем уложить Proxy задачу в реализацию, то сам итог скорее всего получится мало-безопасным, т.к. сама же задача является наиболее слабой среди всего списка таковых задач. Вторая же задача анонимизации из нашего рассмотрения - напротив, наименее привередлива, т.к. ей не важны такие условия как: уровень централизации, количество узлов и связь между узлами. Плюс к этому, она является теоретически доказуемой, где любые пассивные наблюдения, включая наблюдения со стороны глобального наблюдателя, будут являться бессмысленными.

QB-задача

Задача на базе очередей может быть описана следующим списком действий:

  1. Каждое сообщение m шифруется ключом получателя k: c = Ek(m),

  2. Сообщение c отправляется в период = T всем участникам сети,

  3. Период T одного участника независим от периодов T1, T2, ..., Tn других участников,

  4. Если на период T сообщения не существует, то в сеть отправляется ложное сообщение v без получателя (со случайным ключом r): c = Er(v),

  5. Каждый участник пытается расшифровать принятое им сообщение из сети: m = Dk(c).

QB-сеть с тремя узлами A, B, C
QB-сеть с тремя узлами A, B, C

При такой модели глобальный наблюдатель будет видеть лишь факт генерации шифртекстов C = {c1, c2, ..., cn} в определённо заданные периоды времени = T без возможности дальнейшего различия истинности Ek(m) или ложности Er(v) выбираемых им шифртекстов.

Более подробный анализ безопасности задачи и её качества анонимности можно найти в первом разделе работы: Анонимная сеть «Hidden Lake».

Реализация

Программный код условно можно разделить на три части:

  1. Исполнение QB-задачи,

  2. Принятие сообщений из сети,

  3. Точка запуска.

Исполнение QB-задачи

func runQBProblem(ctx context.Context, receiverKey *rsa.PublicKey, hosts []string) error { 	queue := make(chan []byte, 256)      // Генерируем ложные шифртексты, если очередь пуста     go func() {         // Разово генерируем ключ псевдо-получателя 		pr, err := rsa.GenerateKey(rand.Reader, receiverKey.N.BitLen()) 		doif(err != nil, func() { panic(err) }) 		for { 			select { 			case <-ctx.Done(): 				return 			default: 				if len(queue) == 0 { 					encBytes, err := rsa.EncryptOAEP(sha256.New(), rand.Reader, &pr.PublicKey, []byte("_"), nil) 					doif(err == nil, func() { queue <- encBytes }) 				} 			} 		} 	}()      // Генерируем истинные шифртексты, если можем вычитать из stdin 	go func() { 		for { 			select { 			case <-ctx.Done(): 				return 			default: 				input, _, _ := bufio.NewReader(os.Stdin).ReadLine() 				encBytes, err := rsa.EncryptOAEP(sha256.New(), rand.Reader, receiverKey, input, nil) 				doif(err == nil, func() { queue <- encBytes }) 			} 		} 	}()      // Отсылаем сгенерированные шифртексты каждые 5 секунд всем узлам в сети 	for { 		select { 		case <-ctx.Done(): 			return ctx.Err() 		case <-time.After(5 * time.Second):             encBytes := <-queue 			for _, host := range hosts { 				client := &http.Client{Timeout: time.Second} 				_, _ = client.Post(fmt.Sprintf("http://%s/push", host), "text/plain", bytes.NewBuffer(encBytes)) 			} 		} 	} } 

Принятие сообщений из сети

func runMessageHandler(ctx context.Context, privateKey *rsa.PrivateKey, addr string) error { 	mux := http.NewServeMux() 	mux.HandleFunc("/push", func(w http.ResponseWriter, r *http.Request) { 		encBytes, _ := io.ReadAll(r.Body) 		decBytes, err := rsa.DecryptOAEP(sha256.New(), rand.Reader, privateKey, encBytes, nil) 		doif(err == nil, func() { fmt.Println(string(decBytes)) }) 	}) 	server := &http.Server{Addr: addr, Handler: mux} 	go func() { 		<-ctx.Done() 		server.Close() 	}() 	return server.ListenAndServe() }

Точка запуска

// Пример: // go run . :8080 ./example/node1/priv.key ./example/node2/pub.key localhost:7070 func main() { 	ctx := context.TODO() 	go func() { _ = runQBProblem(ctx, getReceiverKey(os.Args[3]), os.Args[4:]) }() 	_ = runMessageHandler(ctx, getPrivateKey(os.Args[2]), os.Args[1]) }

Запускаем

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

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

Как только оба узла запущены, один из них может что-либо написать и это сообщение будет успешно передано, примерно через 5 секунд, другому абоненту.

# Terminal-1 $ go run . :7070 ./example/node2/priv.key ./example/node1/pub.key localhost:8080  # Terminal-2 $ go run . :8080 ./example/node1/priv.key ./example/node2/pub.key localhost:7070  # Terminal-1 (ввод) > hello  # Terminal-2 (вывод) > hello

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

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

Наиболее простая атака активного наблюдателя будет сводиться к DoS/DDoS'у сети, т.к. здесь отсутствует F2F (friend-to-friend) коммуникация, из-за чего любой пользователь может начать спамить сообщениями (если знает публичный ключ) и засорять очередь, отсутствует доказательство работы, из-за чего любой пользователь может аккумулировать у себя большое количество шифртекстов, чтобы все участники тратили свои процессорные мощности лишь на расшифровку, помимо прочего наличие io.ReadAll в функции принятия сообщений из сети также не очень хорошо сказывается на отказоустойчивости и может засорить всю оперативную память одним большим отправленным сообщением.

С DoS/DDoS всё понятно, а что насчёт деанонимизирующих активных наблюдений? Вот здесь всё куда интереснее. Если наблюдатель не будет знать нашего публичного ключа, то осуществить какую бы то ни было активную атаку ему будет проблематично. С другой стороны, если он всё же получит публичный ключ, то он получит доступ к изменению состояния нашей очереди queue. Тем не менее этого наблюдателю будет мало, но не из-за того, что QB-сети защищают от такой атаки, а от того, что в нашем прикладном приложении (чате) отсутствует автоматическая связь вида: «запрос-ответ». Если бы чат был не чатом, а например файлообменником, то ситуация стала бы более плачевной, т.к. позволяла злоумышленнику измерять время ответа относительно периодов генерации шифртекстов. Из-за этого рушилась бы анонимность факта отправления и получения сообщений, а с появлением сговора активных наблюдателей на нескольких узлах, рушилась бы анонимность и связи между отправителем и получателем. Влияние такой атаки на QB-сеть возможно уменьшить либо внедрением F2F, либо созданием нескольких очередей, привязанных к конкретным узлам, либо отсутствием прикладных приложений требующих «запрос-ответ». Наша сеть, по счастливому стечению обстоятельств, придерживается последнего способа. Но стоит также сказать, что этот способ неидеален. Если абонент будет активно общаться сразу с несколькими собеседниками, среди которых будет также наблюдатель, то очередь сообщений будет постоянно накапливаться, а время ответа увеличиваться. Вследствие этого, наблюдатель (являющийся одним из собеседников) сможет предположить, что его абонент, будучи очень общительным и разговорчивым человеком, вряд-ли сможет так долго не отвечать на его сообщение «о выборе тортика на день рождения».

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

Заключение

В результате анонимная сеть была успешно переписана с нуля, с сокращением и без того малого количества кода в два раза, с 200 до 100 строк кода. Исходный код анонимной сети можно найти в репозитории Github'a или просто в спойлере ниже.

Анонимная сеть M-A
package main  import ( 	"bufio" 	"bytes" 	"context" 	"crypto/rand" 	"crypto/rsa" 	"crypto/sha256" 	"crypto/x509" 	"fmt" 	"io" 	"net/http" 	"os" 	"time" )  func main() { 	ctx := context.TODO() 	go func() { _ = runQBProblem(ctx, getReceiverKey(os.Args[3]), os.Args[4:]) }() 	_ = runMessageHandler(ctx, getPrivateKey(os.Args[2]), os.Args[1]) }  func runMessageHandler(ctx context.Context, privateKey *rsa.PrivateKey, addr string) error { 	mux := http.NewServeMux() 	mux.HandleFunc("/push", func(w http.ResponseWriter, r *http.Request) { 		encBytes, _ := io.ReadAll(r.Body) 		decBytes, err := rsa.DecryptOAEP(sha256.New(), rand.Reader, privateKey, encBytes, nil) 		doif(err == nil, func() { fmt.Println(string(decBytes)) }) 	}) 	server := &http.Server{Addr: addr, Handler: mux} 	go func() { 		<-ctx.Done() 		server.Close() 	}() 	return server.ListenAndServe() }  func runQBProblem(ctx context.Context, receiverKey *rsa.PublicKey, hosts []string) error { 	queue := make(chan []byte, 256) 	go func() { 		pr, err := rsa.GenerateKey(rand.Reader, receiverKey.N.BitLen()) 		doif(err != nil, func() { panic(err) }) 		for { 			select { 			case <-ctx.Done(): 				return 			default: 				if len(queue) == 0 { 					encBytes, err := rsa.EncryptOAEP(sha256.New(), rand.Reader, &pr.PublicKey, []byte("_"), nil) 					doif(err == nil, func() { queue <- encBytes }) 				} 			} 		} 	}() 	go func() { 		for { 			select { 			case <-ctx.Done(): 				return 			default: 				input, _, _ := bufio.NewReader(os.Stdin).ReadLine() 				encBytes, err := rsa.EncryptOAEP(sha256.New(), rand.Reader, receiverKey, input, nil) 				doif(err == nil, func() { queue <- encBytes }) 			} 		} 	}() 	for { 		select { 		case <-ctx.Done(): 			return ctx.Err() 		case <-time.After(5 * time.Second):             encBytes := <-queue 			for _, host := range hosts { 				client := &http.Client{Timeout: time.Second} 				_, _ = client.Post(fmt.Sprintf("http://%s/push", host), "text/plain", bytes.NewBuffer(encBytes)) 			} 		} 	} }  func getPrivateKey(privateKeyFile string) *rsa.PrivateKey { 	privKeyBytes, _ := os.ReadFile(privateKeyFile) 	priv, err := x509.ParsePKCS1PrivateKey(privKeyBytes) 	doif(err != nil, func() { panic(err) }) 	return priv }  func getReceiverKey(receiverKeyFile string) *rsa.PublicKey { 	pubKeyBytes, _ := os.ReadFile(receiverKeyFile) 	pub, err := x509.ParsePKCS1PublicKey(pubKeyBytes) 	doif(err != nil, func() { panic(err) }) 	return pub }  func doif(isTrue bool, do func()) { 	if isTrue { 		do() 	} } 

Источник: habr.com

Комментарии: