Как DocuChan работает на самом деле — и как это проверить
«Поверьте нам» — это не модель безопасности. На этой странице — модель угроз DocuChan, изложенная так, как мы сами хотели бы получить её от другого инженера: что шифруется и где, всё, что хранят наши серверы, от чего именно мы не можем вас защитить и как проверить каждое утверждение самостоятельно с помощью инструментов разработчика в браузере.
Что не так с «безопасным» обменом файлами
Почти каждый сервис обмена файлами называет себя безопасным, и почти каждый имеет в виду одни и те же две вещи: TLS при передаче и шифрование данных на своих дисках. И то и другое хорошо. Но ни то ни другое не мешает самому сервису прочитать ваш файл — ведь ключи у него. Ваш договор, скан паспорта или ещё не выпущенная сборка лежат на чужом сервере в таком виде, что этот сервер может их открыть, а страница скачивания нередко окупается за счёт рекламы рядом с файлом.
Сквозное шифрование — гарантия куда сильнее: файл шифруется ещё до того, как покинет ваше устройство, ключом, который сервис никогда не получает. Тогда уже не так важно, что происходит на стороне сервера — любопытный администратор, судебный запрос, утечка из хранилища: там лежит только шифртекст. В DocuChan это обеспечивает сама архитектура, а не обещание в политике конфиденциальности.
В этой истории перемещаются две вещи, и они никогда не идут одной дорогой. Смотрите на цвета: ключ синий, шифртекст зелёный.
Ключ до нас не доходит — благодаря старому правилу браузеров
Когда вы загружаете файл, браузер с помощью Web Crypto API генерирует случайный 256-битный ключ AES-GCM. Этот ключ попадает в ссылку — но в строго определённое место: во фрагмент URL, то есть в часть после #.
Фрагменты старше JavaScript: их придумали для перехода к заголовку внутри страницы, поэтому по спецификации браузеры никогда не включают их в HTTP-запросы. Это правило соблюдают все браузеры. Когда получатель открывает ссылку, наш сервер видит запрос к /d/x7Kq0aH2 — и ничего больше; JavaScript на устройстве получателя читает фрагмент локально и там же расшифровывает файл. Ту же схему использовал Firefox Send, и благодаря ей обещание «никаких бэкдоров» опирается на то, каким путём движется ключ, а не на наши добрые намерения.
Как шифруется файл
Файлы шифруются в потоковом режиме, блоками по 8 МиБ: прочитать блок, зашифровать, загрузить, перейти к следующему. Браузер никогда не держит весь файл в памяти, поэтому загрузка в несколько гигабайт работает даже на телефоне или ноутбуке с небольшим объёмом оперативной памяти. Каждый блок получает собственный случайный 12-байтовый IV и запечатывается алгоритмом AES-256-GCM; формат передачи предельно прост — это повторяющиеся записи [IV | ciphertext + auth tag].
GCM — режим с аутентификацией: каждый блок несёт тег, который проверяется при расшифровании. Если в сохранённом шифртексте изменится хотя бы один бит — по вине недобросовестного поставщика хранилища, злоумышленника посередине или сбоя диска, — расшифрование завершится явной ошибкой и файл при скачивании просто не будет создан. Незаметно получить подменённый документ невозможно.
Имя файла защищено так же. Это метаданные, которые нашему серверу читать незачем, поэтому имя шифруется на стороне клиента тем же ключом, что и сам файл, ещё до сохранения (в виде строки с префиксом e2e:). Для нас ваша загрузка — это случайный идентификатор, непрозрачная метка и число байтов. В качестве MIME-типа мы храним application/octet-stream — мы не знаем, PDF это или фотография.
Куда на самом деле идут байты
Одну деталь стоит проговорить отдельно: зашифрованные блоки загружаются напрямую в объектное хранилище по предварительно подписанным URL. Наш сервер приложения разрешает загрузку и выдаёт эти URL, но байты файла через него не проходят — нет промежуточной точки, где открытый текст мог бы оказаться хотя бы случайно. Поставщик хранилища, в свою очередь, хранит шифртекст, ключа к которому у него нет. Скачивание идёт тем же путём в обратную сторону: блоки расшифровываются потоком по мере поступления (в браузерах без File System Access API — через сервис-воркер, чтобы большие файлы и там не раздували потребление памяти).
Всё, что хранит наш сервер, — полный список
Честный рассказ о метаданных важнее лозунгов о шифровании, поэтому вот полная запись, которую мы храним о каждой передаче, — в точности по схеме базы данных:
- случайный, неугадываемый идентификатор ссылки (он же имя объекта в хранилище)
- зашифрованная метка с именем файла (прочитать её мы не можем)
- размер шифртекста в байтах
- срок действия, признак удаления после скачивания, счётчик скачиваний
- если вы задали пароль: проверочный хэш PBKDF2 — но никогда не сам пароль (см. следующий раздел)
- время создания и идентификатор вашего аккаунта, если вы вошли в систему
Кроме того, для работы сервиса мы недолго храним хэши IP-адресов с солью (а не сами адреса) — чтобы ограничивать частоту запросов и защищаться от злоупотреблений, — а также стандартные журналы веб-сервера. Аналитика работает на нашем собственном сервере и не использует файлы cookie. На страницах скачивания, запросов файлов, подарков, аккаунта и Rooms она не загружается вовсе. Страница PDF-инструмента отправляет один просмотр (только путь) нашим собственным кодом: рядом с вашим документом не работает никакой скрипт-трекер, а её Content-Security-Policy не разрешает других адресов. На всех остальных страницах аналитика отбрасывает строку запроса и фрагмент после #, поэтому записать ключ она не может в принципе. Её события содержат лишь обобщённые счётчики и диапазоны размеров — никогда не имена файлов. Если завтра нам придёт судебный запрос, всё, что мы сможем выдать о вашем файле, — это список выше плюс шифртекст. Мы можем подтвердить факт передачи, но не можем выдать её содержимое.
Пароль — это пропуск, а не ключ
На ссылку можно поставить пароль. Важно точно понимать, что он делает: ключ шифрования берётся не из пароля — ключ в любом случае находится во фрагменте. Пароль — это контроль доступа: без него наш сервер не выдаст шифртекст. Это защищает вас, если ссылка утекла туда, куда пароль не попал (в пересланное письмо, историю переписки, на снимок экрана).
Сам пароль мы тоже никогда не узнаём. Браузер вычисляет из него проверочное значение с помощью PBKDF2 (SHA-256, 210 000 итераций, случайная соль — она передаётся во фрагменте рядом с ключом), и только это значение хранится на сервере и сравнивается при скачивании — за постоянное время. Утечка базы данных дала бы злоумышленнику хэш, который долго перебирать, а не ваш пароль.
Как именно работает самоуничтожение
Режим «Удаление после скачивания» стирает файл после первого завершённого скачивания: браузер получателя сначала подтверждает, что файл сохранён полностью, и только потом срабатывает удаление, поэтому обрыв связи на 90% не уничтожит файл, так и не дошедший до получателя. Отменённое скачивание тоже не приводит к удалению. Ссылки с ограниченным сроком действия по его истечении удаляет фоновый процесс очистки. Удаление означает, что объект с шифртекстом стирается из хранилища, а ссылка перестаёт открываться; поскольку у нас никогда не было ничего, кроме шифртекста, копии в открытом виде, которую пришлось бы разыскивать, просто не существует.
Beam: когда файлы вообще не должны касаться сервера
При передаче с устройства на устройство (Beam) файл идёт напрямую между двумя браузерами по каналу данных WebRTC. Наш сервер лишь пересылает служебные сообщения для установки соединения (SDP/ICE — сетевые координаты, несколько килобайт). Но у встроенного в WebRTC шифрования DTLS есть известное слабое место: тот, кто пересылает эти сообщения, теоретически может встать посередине. Поэтому поверх него Beam использует собственный уровень сквозного шифрования, спроектированный так, что в модель угроз включены мы сами:
- Каждое устройство генерирует эфемерную пару ключей ECDH P-256 и первым делом отправляет обязательство — хэш SHA-256, который связывает его эфемерный открытый ключ с постоянным ключом-идентификатором устройства.
- Только получив обязательство другой стороны, каждое устройство раскрывает эти ключи, и их сверяют с обязательством. Затем оба ключа участвуют в выработке сеансового ключа AES-256-GCM и короткого кода.
- На обоих экранах появляется код из 6 символов (короткая строка аутентификации, SAS); люди один раз подтверждают, что коды совпадают, и могут запомнить устройство как доверенное — тогда в следующий раз всё произойдёт автоматически.
Короткий код надёжен именно потому, что обязательство фиксируется до раскрытия ключей: злонамеренный посредник должен выбрать подменные ключи до того, как увидит ваши, и поэтому не может перебором подобрать пару ключей, при которой на обоих экранах окажется одинаковый код. У него есть лишь одна попытка вслепую — с шансом примерно один на миллиард. Ту же схему использовал ZRTP для защиты голосовых звонков. Когда прямое соединение невозможно и трафик идёт через ретранслятор (TURN), тот видит только уже зашифрованный поток.
Rooms: сервер знакомит участников, но не видит документов
DocuChan Rooms — редактор для совместной работы над документами, но не облачный сервис, где эти документы хранятся. Комнату описывают два независимых значения: случайный публичный идентификатор, по которому участники находят друг друга, и 256-битный секрет, который передаётся в ссылке-приглашении после #key=. Страница считывает этот фрагмент ещё до запуска приложения, убирает его из адресной строки и никогда не отправляет в API Rooms. Во всём разделе Rooms аналитика отключена.
Наш сервер хранит недолговечные записи о присутствии участников и ограниченный объём сигнальных данных WebRTC — чтобы браузеры могли найти друг друга. Содержимое документа, Markdown, комментарии, выделения, встроенные изображения и исходный код диаграмм Mermaid передаются между браузерами участников по зашифрованным каналам данных WebRTC. Протокол приложения проверяет, что каждый участник соединения знает секрет комнаты, и шифрует собственные сообщения ключами AES-256-GCM, выработанными через HKDF; сигнальный сервис не может ни прочитать изменения документа, ни незаметно их подменить.
Создатель комнаты получает второй случайный токен — он нужен только для блокировки комнаты и решений о допуске участников. Этот токен остаётся в хранилище сайта в браузере создателя и никогда не попадает ни в ссылку-приглашение, ни в ссылку для продолжения работы; сервер хранит только его хэш SHA-256. Незаблокированная комната сразу впускает участника с корректным запросом на вход. В заблокированной комнате запрос ждёт, пока организатор, находящийся в сети, не одобрит или не отклонит его. Состояние блокировки, хэши токенов участников, статус допуска, имена, которые участники указали сами, и временные метки — это недолговечные метаданные на сервере; ни ключа документа, ни его содержимого в них всё равно никогда нет.
На устройстве самого пользователя ради возможности восстановления данные хранятся иначе. Rooms держит документ Yjs в открытом виде в IndexedDB этого браузера и может сохранить секрет комнаты в локальном хранилище, чтобы после случайного закрытия вкладки работу можно было продолжить. На стартовой странице Rooms можно удалить отдельные локальные копии или разом очистить все известные базы данных Rooms и ключи восстановления. Резервных копий на сервере нет, и восстановить ключ, утерянный пользователем, DocuChan не может.
Clipboard: напрямую или никак
Clipboard передаёт текст или изображение PNG между вашими собственными устройствами. Здесь правила строже, чем в Beam: публичные STUN-серверы браузеры используют только для того, чтобы найти прямой путь WebRTC, а запасного маршрута через ретранслятор TURN у Clipboard нет вовсе — если прямое соединение не устанавливается, передача просто не происходит, а не идёт через кого-то ещё. Наш сервер видит лишь недолговечные записи о присутствии и сигнальные данные, которые истекают сами. Каждый элемент дополнительно шифруется на уровне приложения ключами, о которых договариваются два браузера, а вы, как и в Beam, сверяете короткий код на обоих экранах. Полученные элементы хранятся только в памяти этой вкладки — не больше 20 последних — и исчезают, как только вы её закроете.
От чего мы не можем вас защитить
Страница о безопасности, на которой не перечислены ограничения, — это рекламная страница. Вот наши:
- Вы доверяете JavaScript, который мы отдаём. Шифрование выполняет код, который приходит с нашего сервера при каждой загрузке страницы. Если бы мы стали действовать злонамеренно, нас бы к этому принудили или нас бы взломали, этот код можно было бы изменить. Это справедливо для любого браузерного инструмента со сквозным шифрованием — включая веб-версии защищённых мессенджеров, — и мы предпочитаем сказать об этом прямо, а не ждать, пока вы прочитаете это в чьём-нибудь комментарии. Мы всерьёз рассматриваем возможность открыть исходный код клиента для проверки — это честный способ снизить такой риск.
- Независимого аудита пока не было. Архитектура опирается на хорошо изученные схемы (ключ во фрагменте ссылки, как в Firefox Send; SAS по образцу ZRTP), но независимые специалисты эту реализацию ещё не проверяли. Когда это изменится, ссылка на отчёт появится здесь.
- Ссылка — это и есть секрет. Любой, у кого есть полная ссылка (и пароль, если он задан), может расшифровать файл. Передавайте её по каналу, которому доверяете, и включайте удаление после скачивания, чтобы утёкшая ссылка перестала работать после первого использования.
- Ваше устройство — вне нашей зоны ответственности. Вредоносная программа или камера за вашим плечом обойдут любое шифрование, включая наше.
- Потерянный ключ — потерянный файл. Мы не можем восстановить, сбросить или заново сгенерировать ключ ссылки — у нас его никогда не было. Так и задумано, но это удивляет тех, кто привык к кнопке «Забыли пароль?».
Нашли брешь? Расскажите нам.
Если вы нашли уязвимость, мы правда хотим о ней узнать — и это не дежурная фраза: эта страница существует именно для того, чтобы нашу работу проверяли со стороны. Напишите на abuse@docuchan.com со словом «Security» в теме письма (адрес также указан в security.txt). Мы читаем каждое сообщение, отвечаем на него и с благодарностью указываем авторов находок, если они этого хотят. Пожалуйста, не проводите проверки на чужих файлах.
— Beki, создатель DocuChan · объяснение попроще · попробовать на настоящем файле