Сделал общие сады без аккаунтов: сервер не знает ни кто с кем связан, ни что в конверте
Привет!
Я тут уже дважды был с «Садиком Добрых Дел» — это приложение, где ребёнок вечером записывает, что хорошего сегодня сделал, и от этого растёт цветок. Первый раз пришёл за критикой и получил её, второй — отчитался, что починил.
Сейчас третий заход. Я сделал самую большую фичу за всё время — общие сады — и переделал обмен целиком уже после того, как он «работал». Аккаунтов в приложении так и не появилось: сервер не знает ни кто с кем связан, ни что лежит в конверте.
Что за фича. Раньше сад был один и жил на телефоне: ни аккаунтов, ни регистрации, ни сервера вообще. Теперь сады можно связать. Папа видит сад ребёнка, ребёнок — сад папы. И главное: в чужом саду можно записать то, что ты за этим человеком заметил. Родитель пишет «сам заметил, что бабушке тяжело» — и у ребёнка растёт цветок. Прямо при нём, пока они сидят рядом. Ребёнок ничего не нажимал.
Ссылка, чтобы не искать: RuStore.
Сервер, который про людей ничего не знает
Условие, которое я себе поставил: аккаунтов не будет. Ни телефона, ни почты, ни входа. Ребёнок семи лет не должен ничего регистрировать, а я не должен хранить базу детей.
Получилось так. Ключ связи рождается на телефоне того, кто зовёт, и уходит в приглашении — во фрагменте ссылки, после решётки. Фрагмент по устройству сети на сервер не уходит вообще: даже если ссылку открыть в браузере, ключ никуда не отправится.
Из ключа обе стороны считают две разные производные:
идентификатор ящика = sha256(ключ + "sadik-box-id")
токен доступа = sha256(ключ + "sadik-box-token")
Сервер хранит идентификатор открыто, а от токена — только его хеш. То есть он может проверить право на ящик, не зная ни ключа, ни токена, и не может ни притвориться участником, ни прочитать содержимое: внутри лежит шифротекст AES-GCM, ключ от которого есть только у двоих.
Весь сервер — это PUT и GET в почтовый ящик. Он не знает, кто с кем связан, сколько у человека связей и что в конверте. Из этого потом вылезет неожиданное следствие, но об этом ниже.
Что передаётся между телефонами
Первая версия обмена передавала участок снимком: счётчики, стадия цветка, архив. Пока сад был один, передавать было нечего вовсе, и снимок казался естественной формой — состояние как состояние. Но у него появился второй писатель, и снимок начал спорить сам с собой: кто приложил последним, тот и прав, чужая запись исчезает.
Разрешать конфликты я не стал. Сначала пересчитал, сколько в системе мест, куда пишут двое, и оказалось, что такое место ровно одно: запись, сделанная про другого человека. Цветок, настроение, пересадки пишет только хозяин сада, и спорить там не с кем.
Поэтому передаются теперь не счётчики, а сами записи, и сливаются объединением: порядок доставки не важен, повтор безвреден, «оба написали одновременно» перестаёт быть особым случаем. Счётчики и стадия цветка пересчитываются из записей на месте — они не данные, а следствие.
Свои слова при этом наружу не передаются вовсе. Другой видит, что момент был, и цветок от него вырос, но текста не читает: в списке стоит строка «Момент, о котором можно спросить». Об этом и разговор — приложение задаёт вопрос и не отвечает на него.
Почему не пуши
Хотелось, чтобы сад обновлялся быстро: вся сцена ради того, чтобы цветок вырос при вас. Первая мысль — пуши. Посчитал и передумал.
Чтобы отправить пуш, сервер обязан знать: «в ящик X положили → разбудить устройство D». Это таблица «кто с кем связан» плюс пуш-токен — устойчивый идентификатор телефона. Прямо сейчас сервер не знает ничего подобного и не может сопоставить двух людей. Пуши это ломают, и шифрование содержимого тут не помогает: утечёт граф.
Сделал проще: пока приложение на экране, оно ходит за почтой раз в десять секунд, после минуты тишины — раз в тридцать. Арифметика: три связи, опрос раз в десять секунд — 18 запросов в минуту на человека; сто человек одновременно — 30 запросов в секунду. Для Postgres на маленькой машине это ничто.
Вопрос к вам
Как бы вы доставляли обновление на второй телефон, не заводя аккаунтов и не отдавая серверу граф связей?
Есть ли третий путь, который я не вижу?














