Korat
Написано для внимательного читателя

Аккаунты, доступ и то, чего мы пока не умеем

Эта страница написана так, как это объяснили бы другому инженеру: что защищает ваш аккаунт, что защищает ваши строки в базе данных и — внизу, тем же кеглем — список того, чего Korat не делает и чего не стоит ставить ему в заслугу.

Основной вес несут две мысли. Ваш аккаунт — настоящая сессия аутентификации, а не строка, с паролем из которой мы сверяем ввод. И что вам позволено читать, решает база данных, а не приложение — так что изменённый клиент или тот, кто обращается к API напрямую с публичным ключом, получает ровно те же ответы, что и приложение.

Аккаунты: bcrypt, настоящая сессия и мост к профилю

Личность живёт в Supabase Auth. Пароли хешируются bcrypt на сервере; приложение никогда не видит хеша пароля, и самодельного сравнения нигде на пути входа нет. Вход выдаёт JWT, ограниченный вами, а колонка в строке вашего профиля (auth_uid) — единственная связь между пользователем аутентификации и профилем, и именно она делает выразимыми все политики ниже.

Войти можно по адресу почты и паролю или через Google с помощью Credential Manager в Android, который передаёт Supabase настоящий Google ID token для обмена. Выход на всех устройствах убивает сессию глобально.

Осталась унаследованная колонка PIN — со времён до Supabase Auth. Её никто не читает, новые аккаунты создаются с пустым значением, и она стоит в очереди на удаление. Упомянута здесь только потому, что существует и вы бы её нашли.

Passkey: проверка на сервере и защита от повтора

Korat поддерживает passkey — пару ключей, созданную в защищённом аппаратном хранилище телефона, разблокируемую отпечатком или лицом и никогда не покидающую устройство. Доверяющая сторона — koratland.com, связанная с приложением Android через assetlinks.json, а проверка выполняется в Cloudflare Worker, а не библиотекой:

  • challenge одноразовый и живёт пять минут, а строка удаляется при использовании
  • rpIdHash в данных аутентификатора обязан совпадать с SHA-256 от koratland.com
  • флаг присутствия пользователя обязан быть выставлен
  • подпись проверяется через WebCrypto — ECDSA P-256 или RS256
  • счётчик подписей обязан расти; повторившийся или уменьшившийся счётчик отвергается как повтор

Регистрация требует существующей сессии и берёт личность вызывающего из его bearer-токена, а не из идентификатора в теле запроса, — иначе кто угодно мог бы зарегистрировать passkey на чужой аккаунт. У хранимой строки учётных данных нет политики UPDATE вообще, поэтому даже владелец не может переписать свой открытый ключ или счётчик подписей; переименование идёт через функцию, которая может изменить только название.

Одно осознанное упущение: аттестация не проверяется. Korat не выясняет, какой производитель сделал аутентификатор, поэтому ничто здесь не следует читать как утверждение, подтверждённое аппаратно.

Честный статус. *Регистрация* passkey проверена и работает. *Вход* по passkey ещё ни разу не проходили от начала до конца, а в веб-версии поддержки passkey нет вовсе — это функция Android в процессе выкатки. Ежедневно используются почта с паролем и вход через Google.

Защита на уровне строк: отвечает база, а не приложение

Защита на уровне строк включена по всей базе данных. Политики написаны через четыре функции, и почти каждое правило в Korat — одна из них:

app_uid()
Идентификатор профиля вызывающего или null, когда никто не вошёл. Основа любой политики «только своя строка».
is_member_of(business_id)
Истина для владельца или для любого, у кого есть действующая строка членства в этом бизнесе.
has_capability(business_id, capability)
Раскрывает ролевые пресеты — владелец, управляющий, официант, кухня — на сервере, так что база никогда не верит клиенту в вопросе, что означает роль.
can_review(business_id)
Читает представление права на отзыв, благодаря чему «писать отзыв может только настоящий клиент» — свойство базы данных, а не обещание приложения.

Три правила дались достаточно дорого, чтобы их назвать. Политика, написанная только с точки зрения заведения, исключает клиента — у сессии за столом, вызова официанта и транзакции по два законных читателя, поэтому они читаются как user_id = app_uid() or is_member_of(business_id). Дочерняя таблица должна читаться ровно настолько же, насколько родительская, поэтому комментарии и реакции делегируют посту, а не заводят собственное представление о том, кому можно. И функция-definer выполняется от владельца, а значит, обязана проверять права сама.

Чего RLS не умеет и что её заменяет

Защита на уровне строк умеет отвечать на вопрос *кто обращается*. Она никогда не ответит на вопрос *нёс ли этот запрос действительный токен* — QR стола, ссылку, которой поделились, приглашение. Поэтому каждый путь в Korat, закрытый токеном, — это функция SECURITY DEFINER, проверяющая токен сама, а таблица остаётся закрытой.

Вход за стол. Тот, кто собирается отсканировать QR, не хозяин сессии и не сотрудник, поэтому таблицу сессий за столами он не может прочитать вообще — даже чтобы посмотреть токен. join_session_by_token сверяет токен на сервере, делает первого сканировавшего хозяином, добавляет участника и не возвращает ровным счётом ничего, когда токен неверен, вместо сообщения, по которому можно было бы сузить перебор.

Ссылки, которыми делятся. resolve_share возвращает тип и объект ссылки и больше ничего — ни автора, ни счётчик переходов. Подсчёт перехода идёт через record_share_click, поэтому любого можно посчитать без всякого права на вставку в аналитическую таблицу; политики INSERT там намеренно нет ни у кого.

Деньги. Оформление заказа целиком выполняется на сервере. Он сам считает каждую строку и читает стоимость доставки из собственной записи заведения, игнорируя присланное клиентом, — потому что стоимость, которую задаёт клиент, — это скидка, которую он выписывает себе сам.

Контакты, устройства и вход, которого вы не совершали

Код подтверждения для запасного адреса почты создаёт функция, вызвать которую может только сервисная роль. Хранится только SHA-256 кода, он истекает через десять минут, допускает пять попыток и ограничен одним запросом в минуту. Worker, отправляющий письмо, никогда не возвращает код обратно в приложение.

Отметить свой контакт подтверждённым нельзя. Триггер базы принудительно ставит флаг подтверждения в ложь при любой записи со стороны клиента, а поставить истину может только функция-definer, которая действительно сверила код. Это выглядит паранойей, пока не додумаешь: без этого добавление своего адреса к чужому аккаунту и нажатие «забыли пароль» — полный захват. По той же причине контакт должен быть подтверждён, прежде чем станет основным: именно на основной адрес система пишет.

Каждое устройство, где выполнен вход, перечислено в настройках, и каждое можно отозвать отдельно. У таблицы устройств нет ни политики вставки, ни обновления, ни удаления, потому что только что отозванное устройство ещё держит действительный токен и иначе смогло бы снять собственный отзыв. Отзыв устройства удаляет и лежащую под ним сессию, так что refresh-токен умирает вместе с ним.

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

Предел отзыва, названный прямо. Уже выданный access-токен живёт около часа, и отозвать его на лету нельзя. Отозванное устройство, находящееся офлайн, продолжает работать, пока его токен не истечёт. Такова природа JWT, а не ошибка, — и именно поэтому существует список устройств, а не утверждение, что отзыв мгновенный.

Что вы можете сделать со своим аккаунтом

Удаление аккаунта делается самостоятельно, в настройках — никому писать не нужно. Это записанный и отменяемый *запрос*, а не кнопка, которую вы нажимаете в два часа ночи и уже не можете отменить. Статус запроса виден в приложении. privacy@koratland.com остаётся для остальных прав по PDPA: доступ, исправление, переносимость и отзыв согласия.

Жалобы охватывают и материалы, и людей — профили, посты, комментарии, истории, сообщения и страницы заведений. Функция, принимающая жалобу, — SECURITY INVOKER, единственная такая во всей административной части. Работая от имени владельца, она *видела бы* скрытые и приватные посты и отвечала бы по ним иначе, а это превращает кнопку жалобы в оракул о материалах, которые вам видеть не положено. Работая от имени вызывающего, «такого поста нет» и «этот пост вам не виден» сливаются в один ответ — и это честно, потому что с места жалующегося это одно и то же. Ограничение по частоте живёт в триггере, а не в приложении.

Чего Korat не делает

Ради этого списка остальную страницу и стоит читать. Всё здесь правдиво на сегодня.

  • Ничто не шифруется сквозным шифрованием. Сообщения, файлы и звонки защищены в канале через TLS, а в хранении — защитой на уровне строк. Сервер может их прочитать. У Korat нет ключей на устройствах и нет управления ключами, и его не стоит выбирать для того, что этого требует.
  • Шифрования при хранении на уровне приложения нет сверх того, что даёт управляемая база данных.
  • Ни один документ, удостоверяющий личность, никогда не проверялся. eKYC нет. Нигде в Korat не написано «личность подтверждена», а значки доверия вместо этого сообщают, какие есть доказательства.
  • Телефонные номера подтвердить нельзя — SMS-провайдер не подключён, и эндпоинт говорит об этом прямо, а не притворяется.
  • Загруженные файлы читаются по прямой ссылке кем угодно. Аватарки, изображения постов и изображения из чатов лежат в публичных хранилищах; для записи нужна сессия, но утёкшая ссылка — это файл, который можно скачать. Подписанных URL пока нет.
  • Проверки прав в консоли заведения сегодня выполняются на устройстве, при том что та же матрица доступна на сервере как has_capability(). Считайте права консоли организационной мерой, а не границей безопасности, пока эта выкатка не закончена.
  • У звонков нет TURN-сервера, и сквозного шифрования у них тоже нет; часть звонков между разными сетями не соединится.
  • На текущем тарифе нет резервных копий базы данных, и автоматических тестов тоже нет.
  • В веб-версии нет push-уведомлений. Push — возможность приложения для Android; в браузере уведомление видно при открытии вкладки, а не в момент, когда оно приходит.

Если что-то из этого изменится, изменится и страница. Написать абзац про шифрование банковского уровня и пойти дальше было бы проще; так — полезнее. Рассуждения, стоящие за этими решениями, лежат на странице о проекте.

Частые вопросы

Шифруются ли сообщения Korat сквозным шифрованием?

Нет. Сообщения идут по TLS и защищены в базе политиками уровня строк, которые дают их прочитать только двум участникам переписки, — но сервер их читать может. Если вам нужно сквозное шифрование, для этого разговора Korat не подходящий инструмент.

Что будет, если у меня украдут пароль?

Первый же вход с устройства, которым вы не пользовались, вызовет уведомление о новом устройстве — в приложении и письмом на ваши подтверждённые контакты. Это устройство можно отозвать в настройках, что удалит его сессию, и сменить пароль. Добавленный passkey убирает пароль из атаки на этом устройстве полностью.

Видит ли заведение мой профиль из-за того, что я сделал заказ?

Заведение видит то, что нужно для выполнения заказа: ваше имя за столом, позиции и, для доставки, введённый адрес. У каждого поля профиля своя настройка «публично / для друзей / приватно», применяемая, когда профиль открывает кто-то другой. Лояльность и список клиентов строятся из журнала продаж, а не из вашего профиля.

Соответствует ли Korat PDPA?

Korat спрашивает согласие по PDPA до создания аккаунта, за экраном, который надо пролистать до конца, с настоящей возможностью отказаться. Он сообщает, что собирается и зачем, не продаёт персональные данные и поддерживает доступ, исправление, удаление, возражение, переносимость и отзыв согласия через privacy@koratland.com. Соответствие — постоянная обязанность, а не значок, и перечисленные выше пробелы — часть честного ответа.

Как сообщить о проблеме безопасности?

Напишите на hello@koratland.com с подробностями. Программы вознаграждений нет, но на настоящий отчёт придёт настоящий ответ.

Остались вопросы, на которые страница не ответила?

Напишите на hello@koratland.com. На вопросы о безопасности приходит прямой ответ, включая «мы этого ещё не сделали».