Entrixy — сквозное шифрование

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

Решено 2026-04-19. Работа идёт этапами — см. раздел Фазы.

1. Цели и границы

2. Криптографический примитив

Algorithm: AES-256-GCM. Доступен в JVM из коробки (через javax.crypto), и поддержан Android Keystore. Даёт сразу и конфиденциальность, и аутентификацию.

Формат шифротекста

v1:<base64url(nonce || ciphertext || tag)>

Источник энтропии

SecureRandom() (системный). На Android это /dev/urandom + prng Linux.

3. Иерархия ключей

K_master — мастер-ключ владельца

K_obj — ключ объекта

K_guest — ключ гостевого бандла

Changing K_guest возможна только выдачей новой ссылки. На этом и построен отзыв: сервер удаляет запись ключа, и гость перестаёт получать актуальный bundle_cipher.

4. Что где хранится

На устройстве владельца (EncryptedSharedPreferences)

KeyЗначение
master_key32 байта в открытом виде (SharedPreferences всё равно их шифрует).
obj_key_{id}K_obj объекта, обёрнутый K_master.
obj_plain_{id}(необязательно) кэш расшифрованного JSON, чтобы интерфейс рисовался быстро.

На сервере

Таблица / полеСодержание
numbers.data_cipherJSON, зашифрованный K_obj: {phone, url, secret, geo_lat, geo_lon, share_lat, share_lon, snapshot, welcome, label}.
numbers.id / typeВ открытом виде — нужно для маршрутизации и показа типа иконки.
keys.bundle_cipherJSON, зашифрованный K_guest: {obj_ids: [1,2,3], obj_keys: {1: K_obj1, 2: K_obj2, ...}, welcome_cipher: ..., ...}.
keys.user_key / mode / force_when_busyВ открытом виде — для маршрутизации и быстрой проверки доступа.
Приветствие гостю шифруется отдельно внутри бандла, поэтому сервер не видит и его.
https://entrixy.com/key#<user_key(32)><K_guest_base64url(43)>

6. Основные сценарии

Создание объекта

  1. Владелец нажимает Добавить — создаётся K_obj.
  2. Поля упаковываются в JSON и шифруются K_objdata_cipher.
  3. POST /api/number_add с data_cipher плюс тип. Сервер возвращает id.
  4. Локально сохраняется: obj_key_{id} = encrypt_k_master(K_obj).

Обновление объекта

Собрать новый JSON локально, зашифровать существующим K_objPOST /api/number_update. K_obj не меняется.

Создание гостевого ключа

  1. Владелец выбрал [obj1, obj2, obj3]. Создаётся K_guest.
  2. Собирается JSON бандла: {obj_ids: [...], obj_keys: {1: K_obj1, ...}, welcome_cipher: ...}.
  3. Шифруется K_guestbundle_cipher.
  4. POST /api/key_create с bundle_cipher, obj_ids (в открытом виде — для матрицы доступа).
  5. Сервер возвращает user_key.
  6. Владельцу показывается QR-код со ссылкой entrixy.com/key#{user_key}{base64url(K_guest)}.

Приём ключа гостем

  1. Гость открывает QR-код или ссылку. Страница /key reads location.hash и передаёт ключ приложению или веб-версии.
  2. Клиент запрашивает POST /api/key_bundle.php с user_key → получает bundle_cipher и список obj_ids.
  3. Расшифровывает бандл ключом K_guest→ получая {obj_keys, welcome}.
  4. K_guest и расшифрованные obj_keys складываются в EncryptedSharedPreferences гостевого клиента.
  5. Дальше при каждом обращении: GET зашифрованного data_cipher объекта → расшифровка локальным obj_key_{id}.

Отзыв гостевого ключа

Владелец нажимает Удалить → POST /api/key_revoke.php → сервер удаляет запись ключа. При следующем запросе бандла гость получает отказ и теряет доступ.

Ротация ключа объекта

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

7. Резервная копия и восстановление

Этот раздел описывает план, который ещё не реализован: экспорта мастер-ключа в приложении сегодня нет.

Losing K_master означает потерю доступа ко всем объектам и ключам — потому экспорт и необходим.

Без парольной фразы резервный QR-код — это просто K_master, поэтому парольная фраза обязательна.

8. Фазы внедрения

Фаза 1 — криптохранилище на клиенте

Add Crypto.kt: обёртка AES-256-GCM, создание и хранение K_master, хелперы для K_obj, сериализация формата v1:<base64url>. Юнит-тесты: зашифровать → расшифровать → сравнить. На сервер пока ничего не уходит — просто готова обвязка.

Фаза 2 — миграция схемы сервера (сделано)

Все чувствительные поля переехали в numbers.data_cipher и user_keys.bundle_cipher. Старые колонки в открытом виде (phone, label, radius, time_*, geo_*, wifi_*, share_*, has_avatar, security_level, user_keys.label, hosts.label, pending_actions.phone, hosts.last_ip) удалены из базы (фаза 7). webhook_url и webhook_secret остаются в открытом виде ТОЛЬКО при webhook_mode='server' — без них сервер не сможет отправить HTTP-запрос. При webhook_mode='phone' они шифруются в data_cipher.

Фаза 3 — клиент читает и пишет блоб

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

Фаза 4 — гостевой бандл

Создание ключа порождает K_guest, собирает бандл и отправляет bundle_cipherна сервер. Ссылка несёт ключ во фрагменте, а страница /key читает хеш и передаёт ключ приложению.

Фаза 5 — резервная копия и восстановление

Экран настроек с Экспортом мастер-ключа: парольная фраза → Argon2id → QR-код. Восстановление через сканер плюс тесты переезда между устройствами. Не реализовано.

Фаза 6 — перенос существующих данных

Разовая процедура в клиенте при первом запуске новой версии:

  1. Прочитать все свои объекты с сервера (старые открытые поля).
  2. Создать K_obj для каждого из них.
  3. Зашифровать поля и отправить data_cipher.
  4. Сервер очищает старые колонки (phone=NULL, …) — но только после подтверждения.
После удачного переноса устаревшие поля удаляются через ALTER.

Фаза 7 — аудит

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

9. Риски и меры

RiskMeasure
Потеря K_masterОбязательный сценарий резервной копии (фаза 5). Пока копии нет, показывается предупреждающая плашка.
Компрометация устройства владельцаПолностью не предотвратимо: вход в приложение защищён PIN-кодом или биометрией, данные — EncryptedSharedPreferences и Android Keystore.
Ротация ключа объекта требует пересборки всех бандловКлиент владельца берёт все bundle_cipher — локально или с сервера, — пересобирает и отправляет одной пачкой. Операция редкая.
Сервер подменяет бандлТег AES-GCM не сойдётся, и клиент сообщит о повреждённом ключе.
Утечка K_guest через снимок экрана с QR-кодомТехнически не предотвратимо: ключ и есть сам код. Показывайте QR-код только тому гостю, кому доверяете.
Фрагмент адреса остаётся в истории браузераСтраница /key сразу после чтения хеша вызывает history.replaceState(..., '#'), что стирает фрагмент.

Последнее обновление: 19 апреля 2026