Zero-knowledge архитектура: сервер не имеет ключей расшифровки ни для чего, кроме технических идентификаторов. Все содержательные поля (телефоны, URL, координаты, webhook-секреты, Wi-Fi BSSID, приветствия) шифруются на устройстве владельца и лежат на сервере как непрозрачный blob. Ключ для расшифровки выдаётся гостю внутри ссылки-приглашения (в URL-fragment, который браузер не отправляет серверу).
Алгоритм: AES-256-GCM. Доступен из коробки в JVM (через javax.crypto),
поддержан Android Keystore. Даёт одновременно конфиденциальность и аутентификацию.
v1:<base64url(nonce || ciphertext || tag)>
v1: — префикс версии, на будущее для ротации формата.nonce — 12 байт, рандом на каждую шифровку.ciphertext+tag — всё что отдаёт Cipher.doFinal. Tag 128-bit встроен.SecureRandom() (системный). На Android это /dev/urandom + Linux prng.
EncryptedSharedPreferences — `"master_key"`. Никогда не покидает устройство в открытом виде.data_cipher на сервере).K_obj_new → пере-зашифровать data_cipher + все бандлы, где он был.{obj_id: K_obj} для объектов, разрешённых гостю.bundle_cipher. Сам K_guest сервер не видит.gate.simfolder.com/go/<user_key>#k=<base64url(K_guest)>.
Fragment не отправляется серверу при HTTP-запросе — это базовая браузерная гарантия.K_guest невозможна без выдачи новой ссылки. Это и есть «отзыв доступа»:
сервер удаляет строку ключа, гость теряет возможность получить актуальный bundle_cipher.| Ключ | Значение |
|---|---|
master_key | 32 байта 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 / type | Plain — нужно для роутинга и отображения типа иконки. |
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_busy | Plain — для роутинга и быстрого access-чека. |
https://entrixy.com/go/<user_key>#k=<K_guest_base64url>&v=1
user_key — существующий server-side идентификатор. Без него сервер не отдаст bundle.#...) не попадает в server-log. Это гарантия браузера.gate.simfolder.com/go/* — чистый JS, читает location.hash, пихает в intent Entrixy
приложения (intent://...) чтобы K_guest попал прямо в клиент без round-trip через сервер.K_obj.K_obj → data_cipher.POST /api/number_add с data_cipher + type. Сервер возвращает 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 (plain — для access-матрицы).user_key.gate.simfolder.com/go/{user_key}#k={base64url(K_guest)}.location.hash, передаёт в APK через intent.GET /api/key_info?user_key=X → получает bundle_cipher, список obj_ids.K_guest-ом → получает {obj_keys, welcome}.K_guest и расшифрованные obj_keys сохраняются в EncryptedSharedPreferences клиента-гостя.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-ы пачкой.
Потеря K_master = потеря доступа ко всем объектам и ключам. Обязателен экспорт.
K_master → получается master_backup_blob.obj_keys с сервера (они же лежат там в bundle для ключей владельца-себя) и ре-шифрует локально.K_master. Фраза обязательна.Добавить Crypto.kt: AES-256-GCM wrapper, генерация + хранение K_master,
helper-ы для K_obj, сериализация формата v1:<base64url>.
Unit-тесты «зашифровать → расшифровать → сверить».
Ничего на сервер пока не уходит — инфра готова.
Все 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.
При create/update объекта клиент шифрует JSON и отправляет data_cipher. При sync — читает blob и расшифровывает. Новые объекты — полностью в blob, старые (без K_obj) продолжают использовать открытые поля (backward compat).
На keyCreate генерируется K_guest, собирается bundle, заливается bundle_cipher.
Ссылка с fragment. Лендинг на gate.simfolder.com ловит hash и передаёт в APK.
UI на экране настроек: «Экспортировать мастер-ключ». Парольная фраза → Argon2id → QR. Восстановление через сканнер. Тесты миграции между устройствами.
Одноразовый скрипт в клиенте при первом запуске новой версии:
K_obj на каждый.data_cipher.phone=NULL, ...) — но только после подтверждения.Внешний или self-hosted security review. Проверка, что сервер действительно не видит чувствительного. DB-dump должен показывать только blob-ы.
| Риск | Митигация |
|---|---|
| Потеря K_master | Обязательный backup-flow (Фаза 5). Warning-баннер пока backup не сделан. |
| Компрометация устройства владельца | Неизбежна — PIN/биометрия для запуска приложения. EncryptedSharedPreferences + Android Keystore. |
| Ротация ключа объекта — rebuild всех bundle | Клиент владельца хранит свою копию всех bundle_cipher локально (или тянет с сервера), пересобирает, шлёт пачкой. Операция редкая. |
| Сервер подменит bundle | AES-GCM tag не сойдётся → «повреждённый ключ». Клиент показывает ошибку. |
| Утечка K_guest через скрин QR | Никак — QR содержит K_guest. Владельцу рекомендуется показывать QR только доверенному гостю (как и раньше с паролем). |
| URL-fragment в истории браузера | Лендинг на gate сразу после считывания hash делает history.replaceState(..., '#') — fragment стирается. |
Последнее обновление: 19.04.2026