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

Это не обзор «какой сервис выбрать» и не пересказ карточки инструмента. Вопрос узкий: если сайт обещает шифрование в браузере, что видно прямо сейчас во вкладке «Сеть» и в адресной строке? В MyPassGen шифрование файлов и одноразовая ссылка считают AES-256-GCM через Web Crypto API; все инструменты открываются без аккаунта. Ниже та же проверка применяется к этой границе. Верить фразе «мы ничего не храним» не требуется.

Слоган не проверить. Трафик — да

«Клиентское шифрование» затёрли до дыр. Один сайт шлёт файл POST на свой сервер, там оборачивает его в AES и отдаёт ссылку на скачивание — и всё равно пишет «шифруем онлайн». Другой вызывает crypto.subtle.encrypt во вкладке, сохраняет .lock или .enc и не делает рабочей загрузки. На лендинге это одна фраза. Во вкладке «Сеть» это два разных мира.

Браузер не рецензирует копирайт. Он перечисляет документ, скрипты, картинки и вызовы XHR / Fetch, которые породил этот клик. Видно, есть ли открытый текст в Request URL, есть ли пароль в теле, попал ли ключ после # в параметр аналитики url. Если этих строк нет, тезис «по умолчанию не покидает устройство» ещё держится. Если есть — слоган уже мёртв.

Исходник читать не обязательно. Нужен один повторяемый проход: открыть панель, сделать одно конкретное действие (сгенерировать пароль, проверить одноразовую фразу, зашифровать маленький файл или создать одноразовую ссылку) и открыть каждый новый запрос. Сначала разведём, что называют «локальным». Затем отделим решётку от query. Потом — шаги.

Что именно называют «локальным»

Web Crypto API — криптографический слой браузера. Через crypto.subtle считают хеш, выводят ключ, шифруют и расшифровывают. В сценариях W3C есть законная схема: пользователь выбирает ключ в браузере, шифрует документ и затем отправляет уже зашифрованные байты провайдеру. API гарантирует, что математика может идти на этом устройстве. Он не запрещает потом выпустить шифротекст и не мешает автору сначала загрузить файл, а шифровать уже на сервере.

Web Crypto требует безопасного контекста: в продакшене это HTTPS (исключение обычно localhost). Это условие работы интерфейса, а не маркетинговый пункт. Если страница на обычном HTTP пишет «используем Web Crypto», сначала смотрите замок в адресной строке, потом спорьте об алгоритме.

Что значит AES-256-GCM на этом устройстве

В первой версии MyPassGen используется только AES-256-GCM. По MDN AesGcmParams и цитируемому NIST SP 800-38D, GCM — аутентифицированное шифрование: если шифротекст изменили, расшифровка падает. Вы не получите «кашу», которую можно принять за документ. Рекомендуемый IV — 96 бит, то есть 12 байт, и при одном ключе каждый проход должен брать новый IV. Сам IV секретом не является, его можно класть рядом с шифротекстом. Тег подлинности по умолчанию — 128 бит. Эти числа проверяются в реализации. Их не нужно заучивать как слоган.

Для файла ключ ещё выводят из пароля. Здесь это PBKDF2, 100 000 итераций, SHA-256, соль 16 байт, затем 256-битный ключ AES-GCM. Один файл — до 5 ГБ. На выходе .lock или .enc. Если пароль уехал тем же POST, что и файл, число итераций уже не спасает. Первый объект проверки — трафик, не KDF.

Три разных «шифрования на сайте»

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

Сначала назовите объект, потом жмите «Старт»

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

Решётка и вопросительный знак — разные объекты

URL делится на схему, хост, путь, query (после ?) и фрагмент (после #). В RFC 3986 §3.5 часть после # названа fragment: это вторичная позиция, которую клиент разбирает уже после получения основного ресурса. В MDN про фрагмент URI сказано прямее: при запросе этого URI фрагмент не отправляется на сервер. Клиент обрабатывает его, когда ресурс уже пришёл.

Строка запроса ведёт себя наоборот. Открываете https — пары имя=значение после ? входят в строку запроса и в журналы на той стороне. В предыдущей заметке, какие параметры URL снимать при пересылке ссылки, utm_source и id= уже разобраны как имена query. Ключ в виде ?key= видят сервер, обратный прокси и журналы CDN. Ключ после # в обычной строке запроса не появляется.

Referer тоже отрезает фрагмент. В MDN Referer указано: заголовок может нести источник, путь и query. Фрагмент URL и пару логин/пароль он нести не должен. В W3C Referrer Policy на шаге «очистить перед отправкой как referrer» фрагмент тоже обнуляется. Значит, «ключ после # не уходит по HTTP на машину, которая хранит шифротекст» — поведение протокола, а не обещание конкретного сайта.

Одноразовая ссылка здесь выглядит как s.html?id={id}#{key}: в query только номер шифротекста, ключ живёт только после решётки. При создании вкладка берёт случайный ключ на 32 байта, считает AES-256-GCM с IV 12 байт и отправляет POST с шифротекстом. При чтении скрипт берёт ключ из location.hash и расшифровывает на этом устройстве. Открытый текст — не больше 32 КБ. Что смотреть: в JSON создания должно быть поле шифротекста и не должно быть исходной фразы; в запросе документа страницы чтения должен быть id= и не должно быть хвоста после #.

Проверка во вкладке «Сеть»

В Chrome, Edge, Firefox и Safari есть панель сети. Названия чуть отличаются, шаги одни. Плагины не нужны, «сайт-сканер» тоже не нужен: отдать ему полный URL или файл — значит расширить поверхность, а не сузить её.

  1. Откройте инструменты разработчика, вкладку «Сеть» (Network). Включите «Сохранять журнал» / Preserve log, чтобы список не обнулился после перехода.
  2. Фильтр поставьте на Fetch / XHR. Статику можно досмотреть позже; рабочая загрузка почти всегда идёт этими запросами.
  3. Сделайте то действие, которое проверяете: сгенерируйте пароль, введите одноразовую тестовую фразу, вставьте ссылку с UTM, выберите маленький файл для шифрования или напишите тестовый секрет и создайте одноразовую ссылку.
  4. Откройте каждый новый запрос. Смотрите Request URL: в адресе документа и API не должно быть только что введённого открытого текста и не должно быть # вместе с ключом.
  5. Откройте Payload / Request. В JSON или форме не должно быть исходной фразы, пароля, проверяемого пароля или байтов файла. У одноразовой ссылки поле шифротекста допустимо: оно должно выглядеть как случайный Base64, а не как только что набранное предложение.
  6. Отдельно пройдите запросы аналитики (часто в имени есть matomo, collect, g/collect). В адресе страницы или в своих параметрах не должно уехать location.href вместе с решёткой.

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

Страница проверки пароля может скачать статический список слабых фраз (здесь это leaked-top10k.txt). Это словарь, не ваш ввод. В запросах словарь видеть можно; проверяемый пароль из поля — в query или в теле — нельзя. Это не проверка по всей базе HIBP: там на внешний интерфейс уходит хеш или кусок пароля.

Что вы делаете Во вкладке «Сеть» можно увидеть Не должно появиться
Сгенерировать случайный пароль Скрипты и стили самой страницы Результат и набор символов в POST
Проверить фразу на устройстве Статический список слабых паролей Пароль из поля ввода
Почистить ссылку или скрыть текст Нет рабочих запросов с исходником Полный URL, номер документа, почта как есть
Одноразовая ссылка Шифротекст, id, срок и число чтений Открытый текст; ключ после # в строке запроса
Зашифровать файл Нет рабочей загрузки тела файла Байты файла, пароль

MyPassGen работает по этой таблице: длина пароля в случайном режиме 6–128 символов (по умолчанию 16; короче 8 — предупреждение, что слабо); проверка идёт по локальному списку, не по всей сети; чистка и шифрование файла по умолчанию не отправляют исходник как рабочие данные; одноразовая ссылка хранит только шифротекст. Всё открывается сразу, регистрации нет. Строку «не должно появиться» вы сами отмечаете в панели. Бренд для этого принимать не нужно.

Выход шифротекста — не выход открытого текста

Обмен с нулевым знанием ключа и «полностью офлайн» часто склеивают в одну фразу. Шифрование файлов — второе: результат падает в папку загрузок, у сервера нет рабочей копии этого файла. Одноразовая ссылка — первое: на той стороне должна лежать порция шифротекста, чтобы получатель расшифровал её у себя. Сервер видит шифротекст и не видит ключ. Так задумано, это не сбой.

Критерий утечки поэтому разводят. При создании ссылки длинный Base64 в теле POST — норма; если в том же поле читается только что введённый «тестовый API-ключ», это уже выход открытого текста. Запрос документа страницы чтения должен быть s.html?id=…; Request URL в панели по спецификации не несёт #. Откройте Headers этого запроса и убедитесь, что ни строка запроса, ни Referer не содержат ключ.

Тег GCM мешает схеме «подкрутить пару байт и всё равно расшифровать»: тег не сошёлся — decrypt сразу падает. Это целостность и подлинность, а не обещание «ссылку никто не перешлёт». Кто получил полный URL (включая ключ после решётки), тот успеет расшифровать, пока не кончились чтения. Вкладка «Сеть» отвечает на вопрос, есть ли ключ у сервера. Переписка, почта и скриншоты — другой риск, о нём в следующем разделе.

Не вставляйте полную одноразовую ссылку на «сайт-проверку», который забирает исходник

Ключ после решётки невидим HTTP-серверу. Следующему сайту, куда вы его вставили, он виден целиком. Проверяйте запросы в своих инструментах разработчика, а не отдавайте строку s.html?id=…#… незнакомой странице-сканеру.

Утечки, которые протокол не закрывает

HTTP не шлёт фрагмент. Это не значит, что ключ никогда не покинет компьютер. Скрипт страницы может прочитать window.location.hash и сам сделать fetch — это ошибка реализации, не дыра в протоколе. Поэтому шестой шаг смотрит аналитику: если счётчик отправляет location.href как адрес страницы как есть, хвост после решётки попадает в журнал. Правильно слать путь или отбрасывать hash, а не надеяться, что «HTTP и так не возьмёт».

История браузера, синхронизированные вкладки и часть отчётов о сбоях сохраняют полный URL. Оставить одноразовую ссылку в адресной строке — оставить ключ в локальной истории. Для передачи берите одноразовый канал; после чтения не кладите адрес с # в шаблон тикета. Referer режет фрагмент, но query всё ещё может уехать на третью сторону; ключ нельзя перекладывать в ?key=.

Экран, буфер обмена и лог чата живут вне протокола. Коллега вставил полную ссылку в общий чат — у сервера ключа по-прежнему нет, у посторонних уже есть. Локальное шифрование отвечает на вопрос «где считают и кто по умолчанию не видит открытый текст». Оно не отвечает «перешлёт ли человек, у которого уже полный URL». С паролем файла то же: .lock можно положить на диск, пароль должен ехать другим сообщением. Короткую фразу лучше отдать одноразовой ссылкой, а не писать пароль в то же письмо, что и файл.

Типичные ошибки

«Без сети не работает — значит, уходит открытый текст.» Одноразовой ссылке нужно временно положить шифротекст; падение без сети говорит только о том, что есть одна передача шифротекста. Смотреть нужно содержимое этой передачи, а не факт запроса.

«HTTPS уже шифрует, локальное шифрование ни к чему.» HTTPS закрывает подслушивание на пути. Сервер как конец TLS по-прежнему видит тело и query. Локальное шифрование закрывает слой «сервер или сторонний счётчик по умолчанию читает открытый текст». Это другая обязанность, она складывается с TLS, а не дублирует его.

«На странице написано Web Crypto — значит, открытый текст не уходит.» Web Crypto даёт примитивы. Сначала encrypt, потом загрузка шифротекста и сначала FormData, потом AES на сервере могут одинаково украсить себя именем API. Тело в панели важнее слогана в подвале.

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

С чего начать

Возьмите одно действие на сегодня, которое не жалко потерять. Файл сверяется проще всего: выберите скриншот без секретов, введите временный пароль, откройте «Сеть», нажмите шифрование и убедитесь, что тела файла в POST нет, затем скачайте .lock или .enc. Если ту же картинку нужно отдать другому человеку, шифротекст — на диск, пароль — отдельным сообщением. Один файл не больше 5 ГБ.

Если передаёте фразу или код восстановления, берите одноразовую ссылку: напишите тестовое предложение, создайте ссылку, проверьте, что в запросе создания только шифротекст; откройте страницу чтения в приватном окне и посмотрите, есть ли в строке запроса документа только id=. Страницу чтения не стоит раздавать как обычный контент для индекса — это разовый сценарий с шифротекстом. Чистку ссылки продолжайте по таблице из предыдущей заметки: снимайте UTM и click ID, тоже на этом устройстве.

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

Частые вопросы

Почему файл шифруется без сети, а одноразовая ссылка — нет?

Результат шифрования файла — скачивание на это устройство. Одноразовая ссылка должна дать получателю позже забрать шифротекст, поэтому при создании уходит POST с шифротекстом. AES-256-GCM в обоих случаях может закончиться в браузере. Право выходить наружу разное: у файла не должно быть тела, у ссылки шифротекст можно, открытый текст и ключ — нельзя.

Сервер правда не получает ключ после решётки?

В обычной реализации URI и HTTP запрос документа и Referer фрагмент не несут. Это видно в той же строке документа во вкладке «Сеть». Если скрипт сам читает location.hash и отправляет его, это уже поведение страницы — его ищите в списке Fetch.

Не проще ли положить ключ в query?

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

Нужен ли аккаунт, чтобы это проверить? Заберёт ли аналитика исходник?

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