Приветствую.

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


Предпосылки


There’s a man by the door
in a raincoat
smoking a cigarette

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

Казалось бы, решением проблемы с технической стороны могут послужить self-hosted решения с открытым кодом, типа RocketChat или Wire. И в некотором смысле это действительно так, однако, как говорится — есть нюанс.

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

Во вторых, пользуясь подобным решением приходится доверять хостеру — и если сервер будет скомпроментирован, данные попадут в чужие руки. Именно эту проблему и было интересно решить.

Защита от скомпроментированного хостера


My brother’s with them, did I tell you?
His wife is Russian and he
keeps asking me to fill out forms.

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

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

Генерация блокнота


500 mailers bought from
500 drug counters each one different
and 500 notebooks
with 500 pages in everyone.

Было принято решение хранить данные в формате JSON. Длина 1 страницы блокнота фиксирована и составляет 768 символов (цифра выбрана не случайно, при таком размере ключа и размере сообщения 1 сообщение будет занимать ровно 1 килобайт).

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

Исходный код
const crypto = require("crypto");
const fs = require('fs');


const MAX_MESSAGE_LENGTH = 768;

const readline = require('readline').createInterface({
    input: process.stdin,
    output: process.stdout
});

readline.question('Enter sheets amount: ', amount => {
    generateKeys(amount)
    readline.close();
});

const generateKeys = (amount) => {
    const keys = []
    for (let i = 0; i < amount; i++) {
        keys.push({
            number: i,
            key: toBase64(generateRealRandomString())
        })
    }
    saveFile(JSON.stringify(keys))
}
const generateRealRandomString = () => {
    let string = ''
    for (let i = 0; i < MAX_MESSAGE_LENGTH; i++) {
        string += String.fromCharCode(crypto.randomInt(0, 65530))
    }
    return string
}
const toBase64 = (srcString) => {
    return new Buffer(srcString).toString('base64');
}
const generateRandomString = (lenght) => {
    const alphabet = "QWERTYUIOPLKJHGFDSAZXCVBNMqwertyuioplkjhgfdsazxcvbnm";
    const shuffled = alphabet.split('').sort(() => 0.5 - Math.random()).join('');
    return shuffled.slice(0, lenght)
}

const saveFile = (content) => {
    const fileName = generateRandomString(20)
    fs.writeFile(`keys/${fileName}.json`, content, err => {
        if (err) {
            console.error(err);
        }
        console.log("Keys were generated")
    });
}


Что тут любопытного — по умолчанию, для кодирования символов в JS используется UTF-16. Изначальной задумкой было использовать для ключа последовательность символов до 65535, однако при этом некоторые символы «бились». Связано это с тем, что юникод хранится в нескольких символах подряд (в случае с UFT-16 — от 1 до 2х). И не все символы после расположения друг за другом расшифровываются корректно. Так как сообщение в общем смысле произвольное, гарантировать что такое не случится нельзя. Кроме того, экранировать символы тоже нельзя, путь вероятность случайного совпадения не высока — она отлична от нуля. В связи с этим, было принято решение после генерации случайных символов по основанию 65530 (последние 5 символов не используются осознанно, ниже уточню почему) кодировать их в base64. Это решение не лишено проблем, которые решаются уже на уровне приложения, но — при этом и возможна удобная отладка, и блокнот открывается корректно на абсолютно любом устройстве.

Пример блокнота
Для наглядности, длина ключа сокращена до 20 символов
[{"number":0,"key":"476W6LC477+94peZ47KB67OI7I6R5K6i6run65O866Wt6JaE4pWd6I606qia55Gs45S06ISZ45al5La2"},{"number":1,"key":"5rig7I6M74Cp5beT6KW85KyF76WU6a+A7Yqe47C06IG05r2c5KqN5aSW6a2S6Z6Mx7Lihbnuj4DsvLs="},{"number":2,"key":"57u57IGJ6aO47o2B65Sp57Sr57Wd6Iq06r247JK/55mC666/6Km56IOG4bCA74im5J+m5Imj74694b6w"}]


Сервер


I’ve put him in my diary
and the mailers are all lined up
on the bed, bloody in the glow
of the bar sign next door

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

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

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

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

Приложение


In the rain, at the bus stop.
black crows with black umbrellas
pretend to look at their watches, but
it’s not raining.

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

Подгрузка ключа


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

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

Передача данных в на сервер


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

Если все идет хорошо, то после этого кнопка шифрования заменится на кнопку копирования, а текст сообщения заменится на JSON строку с данными о хэше и номере страницы блокнота.

В картинках
До шифрования:

После шифрования (обрезано из соображений компактности):


При нажатии на кнопку «Зашифровать» происходят следующие дествия:
  • В объекте с ключами находится ключ с выбранным номером. При его отсутсвии кидается ошибка
  • Исходное сообщение кодируется в base64.
  • Происходит шифрование выбранным ключом.
  • Сообщение отправляется на сервер, в ответ приходит хэш.
  • Из хэша и номер страницы формируется JSON который подставляется в текстовое поле.

Более подробно стоит остановиться на функции шифрования. Изначально было желание брать код символа i-ого символа ключа и сообщения, XOR`ить их и записывать в результирующее сообщение символ с получившимся кодом символа. Однако подход «в лоб» был неудачным — ввиду того, что нельзя гарантировать, что результат операции тоже будет входить в 64-х символьное подмножество.

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

Полный код функции шифрования/дешифрования приведен под спойлером:

Функция шифрования/дешифрования
const charSet = "QWERTYUIOPASDFGHJKLZXCVBNMqwertyuiopasdfghjklzxcvbnm0123456789+/=";
const getIndexByChar = (char) => {
    return charSet.indexOf(char)
}
const getCharByIndex = (index) => {
    return charSet[index]
}
export const encode = (text, key) => {
    let encodedString = ""
    for (let i = 0; i < text.length; i++) {
        if (text[i] === "=" || text[i] === undefined) break;

        const orderedTextCharCode = getIndexByChar(text[i]);
        const orderedKeyCharCode = getIndexByChar(key[i]);

        const orderedEncryptedChar = getCharByIndex(orderedKeyCharCode ^ orderedTextCharCode)
        encodedString = encodedString + orderedEncryptedChar
    }

    return encodedString
}


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

Получение данных с сервера


При получении данных с сервера цепочка раскручивается в обратном порядке. Сначала Боб выбирает тот же самый блокнот, который использовала Алиса. Затем — вводит JSON, полученный от нее в поле ввода и нажимает «Расшифровать», после чего код заменяется на исходное сообщение.

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

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

Экран-обманка


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

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

Пример секундомера


Его назначение в другом. Если остановить таймер на 4х секундах, с погрешностью не более 0,5 секунды, то после долгого зажатия заголовка приложение будет перенаправлено на экран отправки сообщений. И тут может быть на самом деле любое приложение и любая логика, главное, чтобы она нормально воспроизводилась только тем, кто знает как это воспроизвести — например, змейка, в которой надо 4 раза подряд проиграть на 5 очках или калькулятор, в котором надо посчитать ln(-pi^e).

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

Навигация в 1 касание
export const useGestureNavigation = (touchX, touchY, setTouchX, setTouchY, xDest)=>{

    const navigate = useNavigate();
    const gestureEnd = (e) => {
        console.log(e.nativeEvent)
        const xDiff = e.nativeEvent.locationX - touchX
        const yDiff = e.nativeEvent.locationY - touchY

        if (Math.abs(yDiff) > constants.Y_SENSITIVITY) {
            navigate(ROUTES.MOCK)
            return
        }

        if (Math.abs(xDiff) > constants.Y_SENSITIVITY) {
            navigate(xDest)
        }

    }
    const gestureStart = (e) => {
        setTouchX(e.nativeEvent.locationX);
        setTouchY(e.nativeEvent.locationY)
    }
    return [gestureStart, gestureEnd]
}


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

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

Заключение


Did I tell you I can’t go out no more?
There’s a man by the door
in a raincoat.

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

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

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

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

Исходники

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


  1. NSA-bot
    00.00.0000 00:00

    А что тут про разработку под iOS?


    1. p07a1330 Автор
      00.00.0000 00:00

      ReactNative же? Полученное приложение под IOS собирается.

      Или этот хаб иначе работает?


      1. NSA-bot
        00.00.0000 00:00
        +3

        Как-то привык, что при этом рассказывается о программировании на нативных языках (Swift ObjectiveC) ну или что-то связанное с особенностями OSей от Apple, влияющими на программирование под неё. Может я конечно ошибаюсь. Но вот так. А у вас тут, как мне кажется, больше про криптографию и безопасность.


  1. Maxim_Q
    00.00.0000 00:00
    +1

    Что сейчас требуется от мессенджера с моей точки зрения:

     - Удобный и быстрый UI

    - Качественная аудио связь

    - Сносная видео связь

    - Удобный обмен файлами

    - Поддержка групп

    - Возможность расшарить экран

    - Синхронизация контактов и истории между разными устройствами

    - Защищенность (end-to-end encryption) по умолчанию

    - Открытость протокола и клиента (иначе нельзя обеспечить защищенность)

    - Кросс-платформенность (iOS, android, Win, MacOS, Linux)

    - Федеративность (аналогично email, decentralized and distributed), что бы любой мог запустить свой сервер.

    - Возможность полной анонимности

    - Без привязки к номеру телефона;

     

    Компании будут сопротивляться, т.к. переписка пользователей хороший источник статистики, а так же возможность для показа рекламы, и даже манипулирования общественным мнением. За примером далеко ходить не надо — ICQ, альтернативные клиенты выжимались до тех пор, пока сеть не умерла.

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


    1. NSA-bot
      00.00.0000 00:00
      +1

      Как-то сомнительно, что с применением шифра Вернама получится реализовать вот это всё:

      - Качественная аудио связь

      - Сносная видео связь

      - Удобный обмен файлами

      - Поддержка групп

      - Возможность расшарить экран

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

      Или (это вопрос к автору) извиняюсь и я опять что-то не так понял :)


      1. Maxim_Q
        00.00.0000 00:00

        Я просто высказал мнение как я вижу мессенджер, и там реально аудиосвязь должна присутствовать, без нее щас никуда. В идеале возможно автор что-то переделает и добавит аудиосвязь, без переделки щас наверное ничего не получится.


      1. p07a1330 Автор
        00.00.0000 00:00

        Часто отвечать не могу, проблемы с кармой, поэтому постараюсь ответить одновременно и @Maxim_Q

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

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

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

        По поводу аудио и дальнейшего развития концепта - до коммерческого использования доводить явно не планировал, это именно что прототип. Если будет появляться время - постепенно буду дорабатывать, но проект в любом случае - "в стол" или в коммьюнити. Однако МР готов принять, если есть желающие.

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


        1. NSA-bot
          00.00.0000 00:00

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

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


  1. dreams_killer
    00.00.0000 00:00

    jabber подходит по всем параметрам, криптография может быть добавлена плагином к клиенту. не решение?


    1. Maxim_Q
      00.00.0000 00:00

      Да все верно, сегодня Jabber отлично подходит для параноиков. Вот самое удобное для телефона приложение https://codeberg.org/kriztan/blabber.im

      И вот для компьютера: https://www.miranda-ng.org/ru/

      Общение текстом реализованно в этих программах довольно удобно и все хорошо защищенно. Если и стремиться в разработке то примерно вот к этим программам.


      1. dreams_killer
        00.00.0000 00:00

        jabber по моему уже много лет остаётся "для параноиков" , в многих моментах может казаться ну удобным (но ведь удобство и простота всегда враг безопасности).

        за ссылку на мобильный клиент спасибо, опробую. а вот с мирандой ... отсутствие версии под linux и mac останавливают от использования.


        1. Maxim_Q
          00.00.0000 00:00

          Для Linux и Mac можно использовать https://www.psi-plus.com/ она отностительно приемлемая но там не столько много настроек как в Миранде.

          А поповоду Jabber'а разве там есть сильные проблемы в безопасности? Там все довольно хорошо, я не смогу найти больших проблем.


          1. dreams_killer
            00.00.0000 00:00

            с безопасностью - всё в руках пользователя) с удобством - не всегда. всё очень зависит от приложения клиента( в psi plus у меня периодами были проблемы с плагинами, в классическом psi (не +) чего то может не хватать, pidjin не отображает капчу для регистрации.. и т.д. ). Но при желании именно джаббер покрывает все хотелки прарноика.


            1. p07a1330 Автор
              00.00.0000 00:00

              Насколько знаю, для шифрования в джаббере есть 2 протокола - OTR и PGP. Отр требует, чтобы оба пользователя одновременно были в сети, а PGP - не обладает свойством отрицаемости и forward secrecy.


              1. dreams_killer
                00.00.0000 00:00

                ещё есть omemo. pgp - универсален и применяется не только в jabber. otr - по личному мнению удобен(относительно безопасен) , но к большому сожалению поддерживается не многими клиентами.

                p.s. приемущество jabber могут являтся и его недостатками - как самый очевидный пример: отсутствие оффлайн сообщений. (вопрос решаемый , но со стороны клиента)


                1. Maxim_Q
                  00.00.0000 00:00

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


                  1. dreams_killer
                    00.00.0000 00:00

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


              1. Maxim_Q
                00.00.0000 00:00

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


                1. dreams_killer
                  00.00.0000 00:00

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