Политика хранения данных GasApp
Пункт M8 «Написать Data Retention Policy и Privacy Policy» (раздел 22), MASVS PRIVACY-2.
Редакция: 2026-10-07. Применимо к: серверу GasApp и клиентам Android/iOS.
Каждая строка ниже соответствует константе или запросу в коде, и рядом указано, где именно. Политика хранения, которую нельзя сверить с реализацией, — это обещание, а не политика.
Принцип
🔐 Сервер спроектирован так, чтобы нечего было выдавать. Это сильнее любого срока хранения: срок можно изменить приказом, а отсутствующие данные выдать нельзя.
Из этого следует главное свойство: сервер не знает, кто кому пишет при включённом sealed sender, не хранит IP-адресов и не ведёт журнала активности (раздел 15).
Что хранится на сервере
| Данные | Срок | Где в коде |
|---|---|---|
| Аккаунт: имя пользователя, identity-ключ | До удаления аккаунта — владельцем или после 12 месяцев без входа | migrations/0001_initial.sql, accounts |
| Открытая часть ключа восстановления аккаунта и момент последнего возвращения аккаунта | До удаления аккаунта; нужны, чтобы вернуть аккаунт с тем же именем на новом устройстве и не принять повтор запроса | accounts.recovery_key, recovery_pq_key, recovered_at, миграция 0013 |
| Журнал ключей: хеш имени, identity-ключ, хеш пост-квантовой половины и версия — по эпохам | 8 суток истории (последнее значение — пока аккаунт существует); нужен, чтобы собеседники проверяли ключ без личной сверки, а владелец — своё имя | kt_leaves, kt_epochs, миграция 0014. Нового знания нет: имя и ключ хранятся в accounts и так |
| Месяц последней активности аккаунта | Обновляется при входе, не чаще раза в месяц; нужен только правилу «год без входа» | accounts.active_month, миграция 0011 |
| Устройство: идентификатор, привязка к аккаунту | До удаления аккаунта | devices |
| Подписанные предключи | До замены новыми (ротация — 7 суток). Время публикации — с точностью до часа | signed_prekeys |
| Одноразовые предключи | До расходования | one_time_prekeys, DELETE при выдаче bundle |
| Недоставленные конверты | 30 суток | ENVELOPE_TTL, gasapp-server-db/src/lib.rs:37 |
| Вложения | 30 суток | ATTACHMENT_TTL, там же:50 |
| Токен push-уведомлений | До смены или удаления аккаунта | push_tokens |
| Хеш токена уведомлений: по нему устройство в фоне узнаёт, есть ли для него что-то в очереди. Времени запросов нет | До выключения уведомлений либо удаления устройства | notify_tokens, миграция 0028 |
| Ключ доступа sealed sender (SHA-256 от него) | До замены | sealed_sender_access |
| Жалобы: кто пожаловался — на кого, доказанные сертификатом отправителя | До удаления любого из аккаунтов или до решения оператора (удаление аккаунта, отклонение жалоб). Время — с точностью до часа | sealed_sender_reports |
| Журнал действий оператора: действие, причина, число жалобщиков — без имени и идентификатора аккаунта | Бессрочно: из него составляется отчёт о прозрачности. Время — с точностью до часа | admin_actions, миграция 0015 |
Хеш адреса почты (HMAC-SHA-256 на ключе узла GASAPP_EMAIL_KEY). Сам адрес не хранится | До удаления аккаунта | accounts.email_hash, миграция 0017 |
| Код подтверждения почты: хеш адреса, хеш кода, счётчик неверных вводов и счётчик писем за час | Код действует 15 минут; строка стирается при регистрации либо через час после истечения кода | email_codes, миграция 0017; email::purge_expired_codes |
| Премиум: пометка на аккаунте | До снятия оператором или удаления аккаунта | accounts.premium, миграция 0018 |
| Канал: владелец, открытый ключ подписи автора, зашифрованные название и описание, номер последней записи. Время создания — с точностью до часа | До удаления канала автором или удаления его аккаунта | channels, миграция 0018 |
| Записи канала: шифротекст (размер — с точностью до бакета дополнения), час публикации, номер изменения, счётчик просмотров | До удаления записи автором либо канала. Удалённая запись заменяется подписанной автором отметкой об удалении — она остаётся, чтобы читатели узнали об удалении | channel_posts, миграции 0018, 0019 |
| Файлы канала (фото, документы, аватар): шифроблобы и их размер, час загрузки | До удаления записи, к которой файл приложен, либо канала | channel_media, миграция 0019 |
| Публичное имя канала и ссылка-приглашение к нему. ⚠️ В ссылке ключ чтения: публичный канал сервер прочитать может | Пока канал публичный; стирается, когда автор делает канал закрытым, и вместе с каналом | channels.handle, channels.public_link, миграция 0019 |
| Подписчики канала: какие аккаунты подписаны на какие каналы, номер записи, до которой каждый дочитал, реакции (одна из восьми эмодзи на запись) и их счётчики | До отписки (приложение отписывает аккаунт, когда канал убирают с телефона), удаления канала либо аккаунта | channel_subscribers, channel_reactions, channel_reaction_counts, миграция 0019 |
| Администраторы канала: какие аккаунты, кроме владельца, пишут в канал, и публичный ключ подписи каждого. Время назначения — с точностью до часа | До снятия администратора владельцем, удаления канала либо аккаунта администратора | channel_admins, миграция 0020 |
| Закрытый канал: признак «закрытый»; хеши меток ссылок-приглашений (сами ссылки сервер не хранит) и час создания каждой | До отзыва ссылки (строка остаётся с отметкой «отозвана»), удаления канала | channels.closed, channel_invites, миграция 0021 |
| Заявки в закрытый канал: какой аккаунт просится в какой канал, одноразовый открытый ключ с подписью аккаунта, час подачи | До ответа на заявку (одобрения либо отказа), удаления канала либо аккаунта | channel_join_requests, миграция 0021 |
| Ответы на одобренные заявки: ключ чтения канала шифротекстом под ключ заявителя | До удаления подписчика из канала, удаления канала либо аккаунта | channel_grants, миграция 0021 |
| Удалённые из закрытого канала: какой аккаунт удалён из какого канала, час удаления | До снятия запрета тем, кто ведёт канал, удаления канала либо аккаунта | channel_bans, миграция 0021 |
| Смена ключа закрытого канала: номер действующей эпохи ключа; звенья цепочки (прежний ключ шифротекстом под новый) и час каждой смены | До удаления канала | channels.key_epoch, channel_epochs, миграция 0023 |
| Ключи доставки участников закрытого канала: открытый ключ за подписью аккаунта — под него участнику шифруют ключ новой эпохи | До удаления участника из канала, удаления канала либо аккаунта | channel_member_keys, миграция 0023 |
| Удостоверение участника закрытого канала: подпись того, кто ведёт канал, под ключом доставки участника и номером эпохи. Сервер его хранит, но ни проверить смысл, ни подделать не может | Вместе с ключом доставки | channel_member_keys.approval, миграция 0027 |
| Жалобы на каналы: какой аккаунт пожаловался на какой канал, причина, номер записи (если указан) и — только по выбору жалобщика — ссылка канала с ключом чтения | До решения оператора (удаление канала либо отклонение жалоб), удаления канала либо аккаунта жалобщика | channel_reports, миграция 0024 |
| Боты: какой аккаунт-бот принадлежит какому аккаунту-владельцу, час создания. Адреса почты у бота нет | До удаления бота владельцем либо удаления владельца (боты удаляются вместе с ним) | bots, миграция 0022 |
| Токены ботов: хеш одноразового токена, владелец, имя будущего бота | Час, либо до регистрации бота по токену | bot_tokens, миграция 0022 |
| Счёт токенов бота за текущие сутки: день и число — без времени каждого токена | До следующих суток, в которые аккаунт берёт токен | accounts.bot_tokens_day, миграция 0026 |
Конверт удаляется сразу после того, как получатель подтвердил приём (DELETE FROM envelopes …, repo.rs:507), а не по истечении срока. Тридцать суток — предел для тех, кто не вышел на связь.
⚠️ Вложения — исключение, и это записано в коде. Сервер не знает, кому адресовано вложение: ссылка на него лежит внутри зашифрованного конверта. Поэтому удалить вложения вместе с аккаунтом невозможно, и срок хранения — единственное, что их вообще стирает.
⚠️ Адрес почты — вне этой таблицы, и это надо понимать. Сервер получает адрес в запросе кода и в запросе регистрации, считает хеш и адрес забывает: в базу и в журнал сервера он не пишется. Но письмо с кодом отправляет почтовая служба (GASAPP_EMAIL_CMD), и у неё остаются адрес, время и текст письма — на её сроках, не на наших. Оператор обязан выбрать службу, срокам которой доверяет, и назвать её пользователям.
Чего на сервере нет
🔐 Перечислено не для красоты: отсутствие этих данных — и есть содержание политики.
- Адресов почты. Только хеш на ключе узла (см. таблицу выше). Ключ лежит отдельно от базы: дамп базы без него адресов не выдаёт, а с ним позволяет проверить конкретный известный адрес, но не перечислить все.
- Содержимого каналов без публичного имени — и открытых по ссылке, и закрытых (вход по заявке). Название, описание, записи и файлы — шифротекст; ключ чтения лежит в ссылке на канал и на сервер не попадает. ⚠️ У публичного канала (с
@именем) автор сам отдаёт серверу ссылку: такой канал сервер прочитать может, и ссылка лежит в базе, пока канал публичный. - IP-адресов. Не пишутся никуда. Приложение адресов не видит: ключ его ограничителей — хеш с солью процесса от идентификатора устройства (отправитель, получатель) или от искомого имени (
rate_limit::key_from_addr— имя историческое, адресов туда не передают). Балансировщик адрес видит — иначе перебор имён было бы нечем остановить — и держит его только в памятиlimit_req; журнал доступа выключен (server/deploy/РАЗВЁРТЫВАНИЕ.md). - Журнала «кто кому написал». При sealed sender сервер не знает отправителя по построению: право на отправку доказывается ключом доступа получателя, а не личностью отправителя. Отдельного журнала отправок нет и для остальных сообщений. Первое сообщение новому собеседнику тоже запечатано: вместо ключа доступа, которого ещё нет, — proof-of-work над этим конвертом. ⚠️ До 2026-09-24 оно шло за подписью отправителя, и пока конверт не был доставлен, база связывала его с отправителем.
- Кто загрузил вложение и кто его скачал. Загрузка анонимна с 2026-09-24 (миграция 0009): право на место — proof-of-work над самим блобом, растущий с размером, а квоту на устройство заменил общий потолок хранилища. Скачивание — по случайному адресу, без подписи. ⚠️ До этого вложение хранило, кто его загрузил (
uploader_device_id— для учёта квоты), и на малонагруженном сервере загрузившего можно было сопоставить с получателем по часу загрузки. Время загрузки и постановки конверта по-прежнему хранится с точностью до часа (миграция 0005), но сопоставлять теперь не с кем. - Времени доставки как отдельной записи. Конверт удаляется, запись не остаётся.
- Содержимого сообщений в читаемом виде. Конверт — шифроблоб, сервер его не разбирает.
- Списка контактов. Контакты живут только на устройстве.
- Метрик активности.
/healthне содержит счётчиков (раздел M9 плана), аналитических SDK нет (tools/check-no-telemetry.sh).
Резервные копии сервера
server/deploy/backup.sh снимает копию базы с исключением данных конвертов и вложений. 🔐 Иначе они переживали бы свой тридцатидневный срок в копиях — то есть срок хранения существовал бы только в основной базе. Копий четырнадцать, ежедневных: вложение, попавшее в последнюю копию перед истечением, жило бы до 30 + 14 суток. ⚠️ До 2026-09 вложения в копии попадали; проверка дампа (COPY public.<таблица>) теперь охватывает обе таблицы.
Ключ шифрования копий ротируется (rotate-backup-key.sh); старый ключ не удаляется сразу, иначе ротация превращалась бы в потерю всех прежних копий.
Что хранится на устройстве
| Данные | Срок |
|---|---|
| Переписка, контакты, ключи сессий | До удаления пользователем или до срабатывания таймера исчезающих сообщений |
| Исчезающие сообщения | По таймеру чата; чистка выполняется при каждом открытии хранилища (Core.open → purgeExpired). В резервную копию не попадают. ⚠️ До 2026-09 одно истёкшее сообщение с вложением останавливало чистку целиком, а приложение не сообщало об ошибке |
| Резервная копия (у того, где вы её храните) | До удаления вами. Исчезающих сообщений в ней нет. Удалённое сообщение остаётся в копиях, сделанных до удаления; при восстановлении всей цепочки оно убирается снова, из старой или неполной цепочки — возвращается |
| Файлы File Vault | До удаления пользователем: «Хранилище» → файл → «Удалить из хранилища». Стирается и запись в базе, и зашифрованный блоб на диске. ⚠️ До 2026-09 этого пункта не было в интерфейсе — строка обещала удаление, которого пользователь сделать не мог; файл жил до удаления аккаунта |
| Счётчик неверных попыток PIN | До верного ввода PIN |
| Имя и адрес почты незавершённой регистрации | С шага PIN до создания аккаунта; стирается при успешной регистрации либо вместе с базой, если регистрацию отменили |
| Каналы: ссылка-доступ (секрет чтения и ключ автора), название, расшифрованные записи; у своих каналов — приватный ключ подписи | До удаления канала с устройства («Убрать канал» / «Удалить канал») или удаления аккаунта. 🔐 В резервную копию входят ключи каналов (секрет чтения, свой ключ подписи, имена администраторов) — иначе потеря телефона означала бы потерю канала; записи не входят и забираются с сервера заново |
| Буфер обмена (скопированное из приложения) | 60 секунд либо до запирания приложения |
🔐 Всё это лежит в базе, зашифрованной SQLCipher; ключ выводится из PIN и секрета, не покидающего устройство (раздел 7.3).
Что значит «удалено» на устройстве. Удалённое и исчезнувшее сообщение перестаёт читаться из файла базы — в том числе тем, кто узнал ключ. Освобождённые страницы затираются (secure_delete), а журнал WAL после каждого удаления переносится в основной файл и обнуляется (checkpoint_truncate).
⚠️ До 2026-09 журнал не переносился: удалённый текст оставался в основном файле до автоматической контрольной точки — в тихой переписке это дни и недели. Проверяется тестами a_purged_message_does_not_linger_in_the_main_file и a_deleted_message_does_not_linger_in_the_main_file: образ основного файла, снятый при открытой базе, открывается тем же ключом.
⚠️ Физические ячейки флеш-памяти приложению недоступны: перезапись страницы файла их не затирает, и восстановление с самого носителя этой гарантией не покрывается.
Удаление аккаунта
DELETE /v1/accounts/{device_id} за подписью владельца. В приложении — «Приватность» → «Удалить аккаунт».
Удаляется сразу, а не помечается к удалению: отложенное стирание означало бы период, в который данные уже обещаны удалёнными, но ещё лежат на диске.
| Что | Судьба |
|---|---|
| Запись аккаунта, имя пользователя, identity-ключ | Удаляется |
| Все устройства аккаунта | Удаляются |
| Подписанные и одноразовые предключи | Удаляются |
| Недоставленные конверты этих устройств | Удаляются |
| Токены push | Удаляются |
| Ключи доступа sealed sender и жалобы | Удаляются — и поданные этим аккаунтом, и поданные на него. ⚠️ До 2026-09 жалобы на удалённый аккаунт оставались: ссылки на аккаунт у них не было (миграция 0006) |
| Вложения | Не связаны с аккаунтом: загрузка анонимна (миграция 0009). Истекают через 30 суток. ⚠️ До 2026-09-24 удалялись вместе с аккаунтом |
| Открытый сокет уведомлений | Закрывается |
| Хеш адреса почты и пометка о премиуме | Удаляются вместе с записью аккаунта; адрес снова свободен |
| Каналы аккаунта и их записи | Удаляются (каскад от channels.owner_account). У читателей остаётся то, что они уже загрузили |
| Журнал почтовой службы о письме с кодом | ⚠️ Не затрагивается: он не на сервере GasApp |
Каскады объявлены в схеме (0001_initial.sql и далее), а не собраны перечислением в коде: перечисление разошлось бы со схемой на следующей добавленной таблице, и остаток пережил бы удаление молча.
Ответ называет числа — сколько устройств, конвертов и вложений удалено. «Готово» без чисел неотличимо от «готово» при запросе, который ничего не сделал.
⚠️ Чего удаление не уносит.
- Вложения, отправленные с этого аккаунта. Сервер не знает, кто их загрузил, и найти их по аккаунту нечем.
- Вложения, присланные этому аккаунту. Их загрузил отправитель, и сервер не знает, кому они предназначались: связь лежит внутри шифротекста. Знать её значило бы хранить граф «кто кому шлёт файлы», не расшифровав ни байта. Их стирает тридцатидневный срок, общий для всех.
- Копии, уже доставленные собеседникам. Они на их устройствах.
- Имя пользователя не резервируется. Оно освобождается сразу: удерживать его значило бы хранить запись об удалённом аккаунте.
Локальные данные приложение стирает само, после подтверждения с сервера: база, соль, зашифрованный аппаратный секрет, ключ-обёртка в Android Keystore, история попыток PIN. Порядок обязателен — запрос подписывается ключом устройства, и стерев его первым, при обрыве связи человек остался бы с живым аккаунтом и без средства его удалить.
Запросы правоохранительных органов
Сервер может выдать только то, что у него есть. Применительно к конкретному имени пользователя это:
- факт существования аккаунта, его identity-ключ и месяц последней активности (дня создания сервер не хранит — миграция 0011);
- публичные предключи;
- факт наличия недоставленных конвертов и их количество — но не содержимое, не отправителя и не точное время отправки: в базе оно огрублено до часа. ⚠️ До 2026-09 здесь стояло «не время отправки», а база хранила его с точностью до микросекунды.
Сервер не может выдать: содержимое переписки, список собеседников, историю доставки, IP-адреса, местоположение.
⚠️ Это свойство архитектуры, а не политики: перечисленного нет в базе.
Изменения политики
Изменение сроков хранения — изменение констант в коде, а константы закреплены тестом retention_matches_the_plan (gasapp-server-db/src/lib.rs:193). Поменять срок, не тронув политику, можно; поменять его незаметно — нет: тест придётся править в том же коммите.