Архитектура нулевого знания: у сервера нет ключей расшифровки ни к чему, кроме технических идентификаторов. Каждое осмысленное поле — номера телефонов, адреса, координаты, секреты webhook, BSSID сетей Wi-Fi, приветствия — шифруется на устройстве владельца и лежит на сервере непрозрачным блобом. Ключ расшифровки приходит гостю внутри пригласительной ссылки, во фрагменте адреса, который браузер серверу не отправляет.
Algorithm: AES-256-GCM. Доступен в JVM из коробки (через javax.crypto),
и поддержан Android Keystore. Даёт сразу и конфиденциальность, и аутентификацию.
v1:<base64url(nonce || ciphertext || tag)>
v1: — префикс версии, чтобы потом менять формат.nonce — 12 случайных байтов, новые на каждое шифрование.ciphertext+tag — всё, что вернул Cipher.doFinal; 128-битный тег включён.SecureRandom() (системный). На Android это /dev/urandom + prng Linux.
EncryptedSharedPreferences под именем master_key и никогда не покидает устройство в открытом виде.K_obj для локального хранения.encrypt_k_master(K_obj) под ключом obj_key_{id}.data_cipher на сервере).K_obj_new → перешифровать data_cipher и все бандлы, где он был.{obj_id: K_obj} по объектам, разрешённым гостю.bundle_cipher. Чип K_guest сервер не видит никогда.entrixy.com/key#<user_key><base64url(K_guest)> — единый блок из 75 символов.
Фрагмент не уходит на сервер вместе с HTTP-запросом — это базовая гарантия браузера.K_guest возможна только выдачей новой ссылки. На этом и построен отзыв:
сервер удаляет запись ключа, и гость перестаёт получать актуальный bundle_cipher.| Key | Значение |
|---|---|
master_key | 32 байта в открытом виде (SharedPreferences всё равно их шифрует). |
obj_key_{id} | K_obj объекта, обёрнутый K_master. |
obj_plain_{id} | (необязательно) кэш расшифрованного JSON, чтобы интерфейс рисовался быстро. |
| Таблица / поле | Содержание |
|---|---|
numbers.data_cipher | JSON, зашифрованный K_obj: {phone, url, secret, geo_lat, geo_lon, share_lat, share_lon, snapshot, welcome, label}. |
numbers.id / type | В открытом виде — нужно для маршрутизации и показа типа иконки. |
keys.bundle_cipher | JSON, зашифрованный 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)>
user_key и 43 символа гостевого ключа.user_key — идентификатор на стороне сервера. Без него сервер бандл не отдаст.#...) никогда не попадает в лог сервера — это гарантия браузера./key — обычный JS: читает location.hash, разбирает блок и передаёт ключ
либо приложению, либо веб-версии /app/. Сервер ключа не видит никогда./download/android,
откуда приложение подхватывает ключ само.K_obj.K_obj → data_cipher.POST /api/number_add с data_cipher плюс тип. Сервер возвращает id.obj_key_{id} = encrypt_k_master(K_obj).Собрать новый JSON локально, зашифровать существующим K_obj → POST /api/number_update.
K_obj не меняется.
[obj1, obj2, obj3]. Создаётся K_guest.{obj_ids: [...], obj_keys: {1: K_obj1, ...}, welcome_cipher: ...}.K_guest → bundle_cipher.POST /api/key_create с bundle_cipher, obj_ids (в открытом виде — для матрицы доступа).user_key.entrixy.com/key#{user_key}{base64url(K_guest)}./key reads location.hash и передаёт ключ приложению или веб-версии.POST /api/key_bundle.php с user_key → получает bundle_cipher и список obj_ids.K_guest→ получая {obj_keys, welcome}.K_guest и расшифрованные obj_keys складываются в EncryptedSharedPreferences гостевого клиента.data_cipher объекта → расшифровка локальным obj_key_{id}.Владелец нажимает Удалить → POST /api/key_revoke.php → сервер удаляет запись ключа.
При следующем запросе бандла гость получает отказ и теряет доступ.
Владелец нажимает Перевыпустить ключ в настройках объекта. Создаётся K_obj_new,
и перешифровываются все bundle_cipher, где есть этот объект. Клиент их наизусть не знает,
поэтому клиент владельца пересобирает бандлы: забирает bundle_cipher каждого своего ключа,
расшифровывает, подставляет K_obj_newи шифрует заново.
Новые блобы уходят на сервер одной пачкой.
Losing K_master означает потерю доступа ко всем объектам и ключам — потому экспорт и необходим.
K_master → получается master_backup_blob.obj_keys с сервера — они лежат в бандле собственных ключей владельца — и перешифровывает их локально.K_master, поэтому парольная фраза обязательна.Add Crypto.kt: обёртка AES-256-GCM, создание и хранение K_master,
хелперы для K_obj, сериализация формата v1:<base64url>.
Юнит-тесты: зашифровать → расшифровать → сравнить.
На сервер пока ничего не уходит — просто готова обвязка.
Все чувствительные поля переехали в 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.
При создании или изменении клиент шифрует JSON и отправляет data_cipher. При синхронизации читает блоб и расшифровывает. Новые объекты живут целиком в блобе, старые без K_obj продолжают пользоваться открытыми полями ради обратной совместимости.
Создание ключа порождает K_guest, собирает бандл и отправляет bundle_cipherна сервер.
Ссылка несёт ключ во фрагменте, а страница /key читает хеш и передаёт ключ приложению.
Экран настроек с Экспортом мастер-ключа: парольная фраза → Argon2id → QR-код. Восстановление через сканер плюс тесты переезда между устройствами. Не реализовано.
Разовая процедура в клиенте при первом запуске новой версии:
K_obj для каждого из них.data_cipher.phone=NULL, …) — но только после подтверждения.Аудит безопасности, внешний или свой. Проверяем, что сервер и правда не видит чувствительных данных: в дампе базы не должно быть ничего, кроме блобов.
| Risk | Measure |
|---|---|
| Потеря K_master | Обязательный сценарий резервной копии (фаза 5). Пока копии нет, показывается предупреждающая плашка. |
| Компрометация устройства владельца | Полностью не предотвратимо: вход в приложение защищён PIN-кодом или биометрией, данные — EncryptedSharedPreferences и Android Keystore. |
| Ротация ключа объекта требует пересборки всех бандлов | Клиент владельца берёт все bundle_cipher — локально или с сервера, — пересобирает и отправляет одной пачкой. Операция редкая. |
| Сервер подменяет бандл | Тег AES-GCM не сойдётся, и клиент сообщит о повреждённом ключе. |
| Утечка K_guest через снимок экрана с QR-кодом | Технически не предотвратимо: ключ и есть сам код. Показывайте QR-код только тому гостю, кому доверяете. |
| Фрагмент адреса остаётся в истории браузера | Страница /key сразу после чтения хеша вызывает history.replaceState(..., '#'), что стирает фрагмент. |
Последнее обновление: 19 апреля 2026