Однажды я наткнулся на вот эту потрясающую статью (здесь я о ней порассуждал), которая навела меня на одну мысль. Как я подошёл бы к задаче разработки тайника для передачи сообщений? И, если уж мы об этом заговорили — подумаем о том, что нам нужно от подобной системы.

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

  • Тайник должен быть полностью анонимным.

  • Для работы с ним не нужны ни аккаунты, ни регистрация.

  • Сервер не должен хранить никаких сведений о пользователях.

  • Система должна предотвращать отслеживание метаданных.

  • Скрытым должно быть не только содержание сообщений.

  • Не должно быть возможности выяснить, общался ли пользователь A с пользователем B или C.

  • Нужно, чтобы обратиться к тайнику можно было бы через Tor. Это позволит защититься от анализа трафика.

  • Нужно, чтобы никак нельзя было узнать о том, что кто‑то общался с создателем тайника.

  • Необходимо, чтобы учитывалось то, что сервер может быть захвачен злоумышленником.

  • Нужно, чтобы не было бы необходимости давать серверу какие‑то особые полномочия.

Обзор идеи

(Этот раздел можно пропустить и перейти сразу к рассказу о реализации тайника).

Рассмотрим реальный сценарий использования тайника. Имеется информатор Уилл (Whistleblower Will, WW; в дальнейшем — ИУ), который хочет отправить особо важную информацию журналистке Джейн (Journalist Jane, JJ; в дальнейшем — ЖД). Предположим, что их враг в данном случае — здоровенный Левиафан. Полагаю, что сегодня в роли такого вот монстра может выступать Boeing: огромная компания, в которой недавно произошла пара странных инцидентов, связанных с информаторами.

Если ИУ хочет что‑то передать ЖД — почему бы ему просто не отправить письмо на адрес jj@nice.journalist со своего личного почтового ящика ww1994@aol.com? С практической точки зрения, электронные письма в наши дни, в любом случае, отправляют с использованием TLS. Поэтому можно считать, что никто это письмо не перехватит и не прочитает. Более того, даже весьма продвинутые системы анализа трафика столкнутся со сложностями при поиске источника этого письма, так как ИУ обращается к AOL (или к GMail, или ещё к какому‑нибудь почтовому провайдеру), а потом уже AOL взаимодействует с сервером nice.journalist (работающим, вероятно, под управлением Exchange, GMail или ещё чего‑то такого). Иначе говоря — любое подобное сообщение, весьма вероятно, затеряется в тумане обычного трафика.

Но, хотя это и «весьма вероятно», нельзя сказать, что это «гарантированно». Учитывая то, что информатор в нашем сценарии подвержен риску для жизни и здоровья, нам нужно именно гарантировать то, что никто этого письма не заметит и не перехватит. Я пишу этот материал из‑за того, что рассматриваемый здесь сценарий кажется мне интересным, а не из‑за того, что у меня есть настоящий вариант его реализации. И, как обычно бывает в моих материалах о криптографии, отмечу, что это — всего лишь мои размышления. Не считайте меня авторитетным специалистом в этом вопросе. С учётом сказанного — мне весьма интересно ознакомиться с реальными моделями угроз в подобных проектах. Если вам есть что об этом сказать — с интересом вас выслушаю.

Тут стоит обратить внимание на следующий момент: сейчас моя модель угроз представлена нехорошей компанией. А что если эта модель будет выглядеть как государство? Хочу обратить ваше внимание на отличную статью Джеймса Микенса This World of Ours. Это, одновременно, и забавное, и информативное чтение. Те ресурсы, которые государство может противопоставить своему оппоненту, воистину огромны.

Если подумать о государствах и об информаторах… Первым в голову приходит Эдвард Сноуден. В данном случае проблема не в том, что по сети отправили обычный текст, который кто‑то может перехватить. Представьте себе, что вы шлёте письмо с сервера AOL на сервер Google. Обе компании, если им предоставить соответствующий судебный приказ, выдадут любые данные, которые у них есть, включая полное содержимое сообщений.

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

  • Работает в почтовом провайдере.

  • Обладает доступом к содержимому электронных писем.

  • Может найти применение некоему финансовому вливанию, попавшему в его карман.

А ещё я предположил бы, что объём этого «вливания» окажется на удивление скромным.

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

Короче говоря — тому, кто взялся за разработку подобного тайника, везде будут мерещиться враги. Учитывая такой вот параноидальный настрой, поговорим о том, как создать такую систему.

Реализация

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

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

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

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

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

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

Для начала взглянем на код, а потом поговорим о том, что в этом коде происходит:

BASE_URL = "https://deaddrop0j22dp4vl2id.onion" # example only
JOURNALIST_PUB_KEY = base64.b64decode('GVT0GzjFRvMxcDh9c6jpmXkHoGB5KoIp9vyU3RozT2A=')


data = open('secrets.zip', 'rb').read() # file to send
searler = SealedBox(PublicKey(JOURNALIST_PUB_KEY))
enc_file = searler.encrypt(data)


with torpy.http.requests() as s:
    res = s.get(BASE_URL + "/upload-url").json()
    s.put(res.get('url'),files={'file':enc_file}).raise_for_status()
    file_id = res.get('id').encode('ascii') + b'='
    enc_id = searler.encrypt(base64.urlsafe_b64decode(base64_id))
    s.put(BASE_URL +"/register-id", data=enc_id).raise_for_status()

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

Следующий шаг заключается в использовании Tor для вызова GET /upload-url. Это даст нам JSON-объект со свойствами url и id. Выходные данные подобного запроса могут выглядеть так:

{
  "id": "ArMvWgDBEXyVap-O7VbD-ELzDJ0ZB_2ir9E51RVv9-4",
  "url": "https://cloud-dead-drop.s3.amazonaws.com/uploads/ArMvWgDBEXyVap-O7VbD-ELzDJ0ZB
_2ir9E51RVv9-4?X-Amz-Algorithm=AWS4-HMAC-SHA256&X-Amz-Credential=ASIARAM4NNC5DUOXEDI7%2F20240522%2Fil-central-1%2Fs3%2Faws4_request&X-Amz-Date=20240522T18412
7Z&X-Amz-Expires=3600&X-Amz-SignedHeaders=host&X-Amz-Security-Token=IQoJb3JpZ2
luX2VjEE-redacted-Z1BIyzgmj%2F9NhhNqdIPwnSV%2F6nvRhWrthEz0H8jRNU6U%2BoPh7zZTtQ
IrU5ahNmpWjLNGUnqNMYfNCNU%2FRX%2BUyERFwlMT7yrYIbxUyWUDwde1IXOHjkTns07kXmBlLG1u
Bvt6RDrE0xjFs%3D&X-Amz-Signature=c9498fb-redacted-9150ce4cb85"

}

В принципе — выходные данные этого запроса представляют собой случайным образом созданное имя файла и заранее заданный URL S3. Обратите внимание на то, что идентификатор файла представляет собой base64-значение с удалёнными дополняющими символами. Именно поэтому мне нужно вернуть знак = при декодировании значения.

Этот URL можно применить для загрузки файла на S3 (или на совместимый сервис), а затем воспользоваться запросом PUT /register-id с id зашифрованного файла, опять же, применив открытый ключ журналиста.

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

  • Шифрование файла.

  • Запрос URL для загрузки файла и получение случайного id файла.

  • Загрузка зашифрованного файла.

  • Шифрование id файла и его регистрация.

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

Интересно отметить то, что одного только использования Tor не достаточно для обеспечения нужного уровня безопасности. Гарвардский студент, который «заминировал» своё учебное заведение, был арестован из-за того, что пользовался Tor через Wi-Fi с территории университета. Было достаточно легко сузить поиск до «всех пользователей Tor, работавших в определённое время из определённого места», и всех их, по одному, проверить.

В документации к SecureDrop имеется целый список шагов, которые нужно предпринять для того, чтобы повысить безопасность системы на этой стадии работы. И Tor, кстати, это только начало. Первостепенное значение тут отводится физической безопасности.

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

Генерирование URL для загрузки материалов
Генерирование URL для загрузки материалов

Следующий код реализует логику для конечной точки upload-url. Как видите, тут, на самом деле, нет практически ничего. Мы случайным образом генерируем 32-байтный токен, который будет играть роль id файла, затем выдаём заранее заданный URL и передаём всё это вызывающей стороне.

upload_bucket = os.environ.get('UPLOAD_BUCKET')
s3 = boto3.client('s3')
def generate_upload_url(event, context):
    id = secrets.token_urlsafe(32)
    resp = s3.generate_presigned_url('put_object',
      Params={'Bucket': upload_bucket, 'Key': 'uploads/' + id },
      ExpiresIn=3600)  
    body = json.dumps({'id': id, 'url': resp})
    return {'statusCode': 200,'body': body}

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

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

При правильной настройке нашей системы корзина S3 не будет ничего логировать. Если даже предположить, что она это делает — единственными данными, которые могут попасть в лог, будет IP-адрес выходного узла Tor. У сервера, захваченного злоумышленником, ещё будет доступ к загруженному файлу, но от этого, без закрытого ключа журналиста, не будет никакой пользы.

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

Оповещение журналиста о новых файлах
Оповещение журналиста о новых файлах

Тут имеется множество «движущихся частей». Архитектура специально спроектирована такой, чтобы её сложно было бы взломать, так как мы хотим обеспечить такое поведение системы, что, даже если ей управляет злоумышленник, она будет сохранять большую часть своих возможностей по обеспечению безопасности. (Опять же — это, для меня, умственное упражнение, я занимаюсь этим ради удовольствия. Если ваша жизнь или свобода зависят от подобной системы — вам, вероятно, стоит проконсультироваться с кем-то ещё по поводу этой архитектуры).

Как сообщить журналисту о том, что имеется новый файл для него (и, попутно, как сделать так, чтобы никто больше об этом не узнал?). У информатора есть id файла, полученный от сервера. Этот идентификатор используется в роли имени файла при его загрузке.

Я тут пользуюсь проектом Libsodium SealedBox. SealedBox — это механизм для шифрования данных с использованием открытого ключа. Причём, делается это так, что получить доступ к данным может только владелец закрытого ключа, связанного с этим открытым ключом.

Размер «конверта» SealedBox составляет 48 байтов. Другими словами шифрование 32-байтного id и 48-байтного конверта даст ровно 80 байтов.

Информатор воспользуется SealedBox для шифрования имени файла, а затем вызовет конечную точку для регистрации id. Вот — соответствующая лямбда-функция бэкенда для конечной точки register-id.

queue_url = os.environ.get('NOTIFICATIONS_QUEUE')
sqs = boto3.client('sqs') 


def register_id(event, context):
    return register_id_internal(event['body'])
def register_id_internal(msg):
    # 32 байта полезной нагрузки  + SealedBox = 80 байтов -> base 64 == 108 байтов
    if len(msg) != 108: 
        return {'statusCode': 400, 'body': 'Invalid ID'}
    sqs.send_message(QueueUrl=queue_url, MessageBody=msg)
    return {'statusCode': 204}

Эта функция… Не слишком нагружена работой (вы можете заметить тут закономерность). Мы просто проверяем длину значения, после чего отправляем значение в очередь SQS.

Кстати: если мы отправляем 80 байтов — почему мы получаем 108 байтов? А дело в том, что, так как мы пользуемся лямбда-функцией, двоичные данные кодируются в base64 инфраструктурой поддержки таких функций.

Но если просто зарегистрировать (зашифрованный) id файла в очереди — особой пользы от этого не будет. Эти данные просто… стоят в очереди. Что должно их прочитать и обработать? Именно тут в игру вступают два таймера. Обратите внимание на то, что на архитектурной диаграмме имеются упоминания событий, происходящих каждую минуту и каждые 5 минут. Каждую минуту запускается лямбда-функция, которая, возможно, опубликует значение-обманку. Вот её код:

queue_url = os.environ.get('NOTIFICATIONS_QUEUE')
sqs = boto3.client('sqs') 


def maybe_publish_decoy(event, context):
    if secrets.randbelow(4) != 0:
        return # в 75% случаев ничего не делается
    # в 25% случаев генерируются сообщения-обманки
    return register_id_internal(
        base64.urlsafe_b64encode(secrets.token_bytes(80)).decode('ascii')
    )

Лямбда-функция maybe_publish_decoy() обычно не делает абсолютно ничего. Но в 25% случаев она регистрирует в очереди SQS значение-обманку. Обратите внимание на то, что генерируемое нами сообщение представляет собой случайный набор байтов, в котором нет ничего осмысленного.

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

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

Последний компонент нашей системы работает с сообщениями в очереди. Его работу поддерживает функция publish_ids(), которая вызывается каждые 5 минут. Посмотрим на её код:

MAX_MSGS = 8
upload_bucket = os.environ.get('UPLOAD_BUCKET')
def publish_ids(event, context):
    while True:
        result = sqs.receive_message(QueueUrl=queue_url,
                                     MaxNumberOfMessages=MAX_MSGS)
        msgs = result.get('Messages', [])
        ids = [msg['Body'] for msg in msgs]
        while len(ids) < MAX_MSGS:
             rnd = secrets.token_bytes(80)
             fake_id = base64.urlsafe_b64encode(rnd).decode('ascii')
            ids.append(fake_id)


        ids = sorted(ids, key=lambda _: secrets.randbelow(1024))
        output = byte('\n'.join(ids), 'ascii')
        now = datetime.datetime.now(datetime.timezone.utc).isoformat()
        s3.put_object(Bucket=upload_bucket, Key='ids/' + now, Body=output)


        if len(msgs) == 0:
            break
        sqs.delete_message_batch(QueueUrl=queue_url, Entries=[
           {'Id': msg['MessageId'], 'ReceiptHandle': msg['ReceiptHandle']}
           for msg in msgs
        ])

Функция publish_ids() запускается каждые 5 минут. Она начинает работу, читая сообщения из очереди. За раз считывается пакет из максимум 8 сообщений. Если в очереди недостаточно сообщений — пакет дополняется до нужного размера специально сгенерированными идентификаторами. Вот как выглядит файл с идентификаторами:

K7VruHnGhlpzWssB92OpMUjAw0-FoDyED_6p4w2LMgcV7JrsVB4SQdH7VNQzAT-jywYZsVhHM8lNF-JiWWUgXONK_Qb2DJw29aLVqw9rvIs=
HG3vHyYVCC42gzeZqgugwIciPqzeQEdNcQrFdqcpUcY5dMRInZKA_ZSFBuyvPdAfJnZm8wkS-jE0cdXZUZmp1wx2CZYWcPGu1uXocdWn2D4=
OpJWZRfQkMGuXRci8x8YrHx0REE4PdBZctj27gXjH0JvRtaSFMweL47q9nB9r6XomGnOfu5632JbEuMKPOEkkdYiVvst-1Qpw1TNzTPcQmY=
drjhGt6d-aV3h8_BjC81cE5kayXiWikgD8qxWEPYL0T4l8BrW-MadhanXcr465vIs7eBzK-DdwrmtqO8rQsrHN60-f2KirpN-qHpdlpxSbk=
rDKdp0CHSm4-Dvf8BOToLQSv79GpfqLnV3fXLECwUK9HdVEDeRK-T3SycyDmwvjUgjkH0vNMB9Yx_AeaHIS87hD2mCpyEGYKNpGMsnWZlHg=
CQ-5sgobc29-1x6adr09tOgk2yb4WNirzZ2dflQOHkXKDY0uk5B9pq_KKDjNoyWZVsRazgvqRPz3mqan2yKb3P0xAQDmF2CjyN6hMR3bjsQ=
44DYBoGFiPeN8dP7FGn579W7vFgUp8-lblI7nfFP3a0TUqo5sjCnV_Ozr4aPXbdVam6kpyhkpqkQeSeroQNP_x7iq2dpNskjx2x4WO8ezJ0=
TMC5ralR9BjHwTf0xk36kuUcbseD6HVkZgK3e1bpckyk62O_trNINa7FMNLLEwUZeQRvUBuj1CRhNWiz0wRjBvv_hxpbi9ToFymJXkz1ocA=

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

Корзина S3
Корзина S3

Как видите — примерно каждые 5 минут сюда пишутся некие значения. Размеры файлов всегда одинаковы. На основе размера файла нельзя понять — были ли в определённое время отправлены какие-то сообщения.

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

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

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

  • uploads/ — позволяет выполнять анонимные GET-запросы для загрузки файлов. Срок хранения файлов автоматически истекает через 14 дней, для выгрузки файлов в эту папку нужен заранее заданный URL.

  • ids/ — позволяет выполнять анонимные запросы GET и LIST, срок хранения данных автоматически истекает через 14 дней, запись в папку осуществляется только бэкендом (данные берутся из очереди).

Журналист будет проверять наличие новых сообщений каждые 5-10 минут, используя Tor и обращаясь к содержимому корзины ids/. Выглядит это так:

def read_messages(session, base_url, reveal, last_file = ""):
    while True:
        res = session.get(base_url, params={
            'prefix': 'ids/', 
            'start-after': last_file, 
            'list-type': 2})
        res.raise_for_status()
        dict_data = xmltodict.parse(res.content)
        contents = dict_data.get('ListBucketResult').get('Contents')
        if len(contents) == 0:
            time.sleep(5 * 60)
            continue
        for file in contents:
            last_file = file.get('Key')
            data = session.get(base_url + last_file)
            for line in io.BytesIO(data.content).readlines():
                id = base64.urlsafe_b64decode(line)
                try:
                    file_name = reveal.decrypt(id)
                    yield file_name
                except:
                    pass

Здесь мы сканируем папку ids/, получая все id-файлы, которых ещё не видели. Далее — мы получаем каждый из этих файлов и пытаемся расшифровать каждую из его строк. После завершения обработки всех файлов в папке мы ждём 5 минут, после чего читаем следующий файл.

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

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

Каждому журналисту нужно будет пройтись примерно по 250 Кб идентификаторов в день, чтобы проверить их на предмет отправленных для него сообщений. Тут мы предполагаем, что речь идёт о слабо нагруженной системе, в которую каждые 5 минут поступает менее 8 сообщений. Учтите, что даже так это — более 2300 сообщений в день.

А если говорить о такой же системе, но работающей под очень высокой нагрузкой, которая обрабатывает 100000 сообщений в день, окажется, что объём данных, которые нужно просканировать каждому журналисту, не превысит 10 Мб. Это — приятные значения, особенно учитывая то, что мы не ожидаем, что подобная система будет работать под высокой нагрузкой.

Когда у журналиста имеется id файла — журналист может загрузить и расшифровать данные следующим образом:

BASE_URL = "https://deaddrop0j22dp4vl2id.onion" # пример
PRIVATE_KEY = 'Iei28jYsIl5E/Kks9BzGeg/36CKsrojEh65IUE2eNvA='
key = PrivateKey(base64.b64decode(PRIVATE_KEY))
reveal = SealedBox(key)
with torpy.http.requests() as s:
    for msg in read_messages(s, BASE_URL, reveal):
        res = s.get(BASE_URL + "uploads/" + msg)
        try:
            print(reveal.decrypt(res.content))
        except:
            pass

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

Обратите внимание на то, что вся эта система не обязательно должна работать в онлайн-режиме. Данные, без проблем, можно сканировать раз в день, или раз в неделю. Это добавляет системе привлекательности, так как журналист вполне может запланировать еженедельное путешествие в какое-нибудь отдалённое место, где его никто не знает, и где он может анонимно проверить, есть ли в его «почтовом ящике» что-то новое.

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

Осторожный план действий

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

В SecureDrop советуют обращаться к системе только через Tor, делая это из таких мест, которые удалены от тех мест, которые обычно посещает человек. До удалённого места надо добираться без телефона, платить надо наличными, пользоваться надо LiveCD-версией ОС Tails.

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

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

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

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

Или давайте рассмотрим схему, в которой используется несколько ботов, которые будут выполнять следующие действия (производя каждую операцию через разные выходные узлы Tor):

  • Постоянно запрашивать URL-адреса для загрузки файлов.

  • Время от времени загружать по этим URL файлы со всяким мусором.

  • Читать файлы из папки ids/.

  • Загружать файлы по некоторым из URL.

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

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

Последствия захвата сервера злоумышленником

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

Архитектура системы призывает нас к выполнению следующих важных настроек:

  • Отключение логирования сведений о доступе к системе (S3, конечные точки и прочее).

  • Все файлы (идентификаторы, загрузки и прочее) удаляются по прошествии 14 дней.

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

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

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

Поразмышляем о том, что именно увидит злоумышленник, взломавший систему. Он сможет видеть связь между вызовами upload-url и загруженными файлами. Ещё можно связать зарегистрированные id с выгруженными файлами при условии сравнительно низкого уровня трафика (что, вероятно, так и будет).

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

Сервер теперь может выполнять анализ трафика. Например — мониторить сеансы чтения папки ids/ и загрузки файлов из папки uploads/. Но от этих сведений будет не особенно много толку. А именно:

  • Реальные сеансы связи замаскированы трафиком, который генерируют боты.

  • Злоумышленник выяснит лишь сведения о выходном узле Tor пользователя.

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

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

Побочные каналы передачи данных

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

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

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

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

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

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

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

Утечка ключа

Что произойдёт, если у журналиста похитят пару ключей? Это будет катастрофа и для него, и для его информатора, так как это позволит расшифровать любые отправляемые сообщения. Борьба с этим заключается в хранении сообщений лишь в течение 14 дней. Но тут надо учитывать то, что у врага могут быть копии всех сообщений, которые мы когда-либо отправили.

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

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

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

Итоги

У меня получился очень длинный пост. Я начал его писать, чтобы поэкспериментировать с идеей бессерверной архитектуры системы, но отвлёкся на всё остальное. Он достаточно длинный, поэтому я не будут углубляться в бессерверные аспекты моей системы. Может — я ещё об этом напишу.

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

О, а приходите к нам работать? ? ?

Мы в wunderfund.io занимаемся высокочастотной алготорговлей с 2014 года. Высокочастотная торговля — это непрерывное соревнование лучших программистов и математиков всего мира. Присоединившись к нам, вы станете частью этой увлекательной схватки.

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

Сейчас мы ищем плюсовиков, питонистов, дата-инженеров и мл-рисерчеров.

Присоединяйтесь к нашей команде

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


  1. nickasm
    22.07.2024 11:00

    Идея с обманками кажется мне неправильной в корне. Думаю, это плохая практика. Если удастся взломать систему, то они не помогут. А если система надежна, то они не нужны.


  1. CaptainFlint
    22.07.2024 11:00

    Я не уловил, откуда в бессерверной технологии вдруг появляется сервер? В моём понятии бессерверные технологии — это пиринговые сети и аналогичные системы. Здесь, судя по всему, имеется в виду что-то кардинально другое. Но что? И почему оно тогда называется "бессерверным"?