Entrixy — End-to-End Encryption

Zero-knowledge архитектура: сервер не имеет ключей расшифровки ни для чего, кроме технических идентификаторов. Все содержательные поля (телефоны, URL, координаты, webhook-секреты, Wi-Fi BSSID, приветствия) шифруются на устройстве владельца и лежат на сервере как непрозрачный blob. Ключ для расшифровки выдаётся гостю внутри ссылки-приглашения (в URL-fragment, который браузер не отправляет серверу).

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

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

2. Крипто-примитив

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

Формат ciphertext

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

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

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

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

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

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

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

Ротация K_guest невозможна без выдачи новой ссылки. Это и есть «отзыв доступа»: сервер удаляет строку ключа, гость теряет возможность получить актуальный bundle_cipher.

4. Что где лежит

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

КлючЗначение
master_key32 байта plain (SharedPreferences уже сам шифрует).
obj_key_{id}K_obj объекта, обёрнутый K_master.
obj_plain_{id}(опционально) кэш расшифрованного JSON, чтобы быстро рисовать UI.

На сервере

Таблица / полеСодержимое
numbers.data_cipherЗашифрованный K_obj-ом JSON: {phone, url, secret, geo_lat, geo_lon, share_lat, share_lon, snapshot, welcome, label}.
numbers.id / typePlain — нужно для роутинга и отображения типа иконки.
keys.bundle_cipherЗашифрованный K_guest-ом JSON: {obj_ids: [1,2,3], obj_keys: {1: K_obj1, 2: K_obj2, ...}, welcome_cipher: ..., ...}.
keys.user_key / mode / force_when_busyPlain — для роутинга и быстрого access-чека.
Приветствие гостю шифруется отдельно внутри бандла — чтобы сервер не видел и его тоже.
https://entrixy.com/go/<user_key>#k=<K_guest_base64url>&v=1

6. Основные flow

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

  1. Владелец жмёт «Добавить». Генерируется K_obj.
  2. Поля упаковываются в JSON, шифруются K_objdata_cipher.
  3. POST /api/number_add с data_cipher + type. Сервер возвращает 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. Собирается bundle JSON: {obj_ids: [...], obj_keys: {1: K_obj1, ...}, welcome_cipher: ...}.
  3. Шифруется K_guestbundle_cipher.
  4. POST /api/key_create с bundle_cipher, obj_ids (plain — для access-матрицы).
  5. Сервер возвращает user_key.
  6. Владельцу показывается QR gate.simfolder.com/go/{user_key}#k={base64url(K_guest)}.

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

  1. Гость тапает QR или ссылку. Лендинг читает location.hash, передаёт в APK через intent.
  2. Клиент запрашивает GET /api/key_info?user_key=X → получает bundle_cipher, список obj_ids.
  3. Расшифровывает bundle K_guest-ом → получает {obj_keys, welcome}.
  4. K_guest и расшифрованные obj_keys сохраняются в EncryptedSharedPreferences клиента-гостя.
  5. Далее при каждом вызове: GET зашифрованный data_cipher объекта → расшифровка локальным obj_key_{id}.

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

Владелец нажимает «Удалить» → POST /api/key_delete → сервер удаляет строку keys. Гость при следующем key_info получает 404 — доступ потерян.

Ротация объекта

Владелец жмёт «Перегенерировать ключ» в настройках объекта. Генерируется K_obj_new, все bundle_cipher с этим объектом пере-шифровываются (клиент их сам не знает — нужен отдельный бандл-rebuild на клиенте-владельце, который достаёт bundle_cipher каждого своего ключа, расшифровывает, вставляет K_obj_new, перешифровывает). На сервер идут новые blob-ы пачкой.

7. Backup / Restore

Потеря K_master = потеря доступа ко всем объектам и ключам. Обязателен экспорт.

Без парольной фразы QR-бэкап — это просто голый K_master. Фраза обязательна.

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

Фаза 1 — клиентский криптосклад

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

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

Все sensitive-поля вынесены в numbers.data_cipher и user_keys.bundle_cipher. Старые plain-колонки (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 остаются plain ТОЛЬКО для webhook_mode='server' — без них сервер не сможет стрелять HTTP. Для webhook_mode='phone' они шифруются в data_cipher.

Фаза 3 — клиент пишет/читает blob

При create/update объекта клиент шифрует JSON и отправляет data_cipher. При sync — читает blob и расшифровывает. Новые объекты — полностью в blob, старые (без K_obj) продолжают использовать открытые поля (backward compat).

Фаза 4 — гостевой bundle

На keyCreate генерируется K_guest, собирается bundle, заливается bundle_cipher. Ссылка с fragment. Лендинг на gate.simfolder.com ловит hash и передаёт в APK.

Фаза 5 — backup/restore

UI на экране настроек: «Экспортировать мастер-ключ». Парольная фраза → Argon2id → QR. Восстановление через сканнер. Тесты миграции между устройствами.

Фаза 6 — миграция существующих данных

Одноразовый скрипт в клиенте при первом запуске новой версии:

  1. Читает все свои объекты с сервера (старые открытые поля).
  2. Генерит K_obj на каждый.
  3. Шифрует поля → шлёт data_cipher.
  4. Сервер чистит старые колонки (phone=NULL, ...) — но только после подтверждения.
После успешной миграции deprecated-поля удаляются ALTER-ом.

Фаза 7 — аудит

Внешний или self-hosted security review. Проверка, что сервер действительно не видит чувствительного. DB-dump должен показывать только blob-ы.

9. Риски и митигации

РискМитигация
Потеря K_masterОбязательный backup-flow (Фаза 5). Warning-баннер пока backup не сделан.
Компрометация устройства владельцаНеизбежна — PIN/биометрия для запуска приложения. EncryptedSharedPreferences + Android Keystore.
Ротация ключа объекта — rebuild всех bundleКлиент владельца хранит свою копию всех bundle_cipher локально (или тянет с сервера), пересобирает, шлёт пачкой. Операция редкая.
Сервер подменит bundleAES-GCM tag не сойдётся → «повреждённый ключ». Клиент показывает ошибку.
Утечка K_guest через скрин QRНикак — QR содержит K_guest. Владельцу рекомендуется показывать QR только доверенному гостю (как и раньше с паролем).
URL-fragment в истории браузераЛендинг на gate сразу после считывания hash делает history.replaceState(..., '#') — fragment стирается.

Последнее обновление: 19.04.2026