Repository navigation
Sync Protocol ru
English · Русский
Синхронизация необязательна, размещается самостоятельно и работает по принципу zero-knowledge:
сервер держит шифротексты, SRP-6a-верификатор и метаданные. Пользовательская сторона —
Синхронизация и устройства, эксплуатация —
server/README.md.
| Часть | Где |
|---|---|
SyncEngine |
shared/sync — что пушить, что тянуть, как сливать |
SyncClient |
shared/sync — контракт транспорта (регистрация, вход, push, pull, устройства, привязка) |
KtorSyncClient |
shared/jvmSharedMain/sync — реализация на HTTP/WebSocket |
SyncCoordinator |
composeApp/ui/sync — жизненный цикл, состояние, отчёт об ошибках |
:sync-wire |
DTO, которые компилируют обе стороны — руками не дублируются |
server/ |
Ktor + Exposed + HikariCP; по умолчанию SQLite, PostgreSQL — сменой URL |
sequenceDiagram
participant C as Клиент
participant S as Сервер
C->>C: Argon2id(мастер-пароль) → masterKey<br/>masterKey → authKey, обёрнутый dataKey
C->>S: POST /auth/register {соль и верификатор SRP, wrappedDataKey, устройство}
S-->>C: access + refresh JWT
Note over C,S: позже, на другом устройстве
C->>S: POST /auth/srp/challenge {accountId}
S-->>C: {challengeId, salt, B}
C->>S: POST /auth/srp/verify {A, M1}
S-->>C: {M2, токены} %% M2 аутентифицирует сервер клиенту
C->>S: GET /vault/keys → wrappedDataKey
C->>C: развернуть masterKey'ом → dataKey
Сам пароль не передаётся никогда; сервер хранит верификатор и не может его восстановить.
changePassword доказывает текущий пароль полным SRP-рукопожатием, затем атомарно меняет
верификатор и обёрнутый dataKey и отзывает все прочие устройства. Сам dataKey не меняется —
меняется только его обёртка.
sequenceDiagram
participant A as Устройство A
participant S as Сервер
participant B as Устройство B
A->>S: PUT /vault/records [зашифрованные записи]
S-->>A: победители LWW + новый курсор
S-->>B: WS /sync — «есть изменения», только курсор
B->>S: GET /vault/records?since=курсор
S-->>B: дельта
B->>B: mergeRemote → applied / rejected
-
Конфликты: last-writer-wins по
version(счётчик Лампорта), при равенстве — поdeviceId. -
Удаления: надгробия, физически чистятся через
SKERRY_TOMBSTONE_DAYS. - Живой push несёт новый курсор и никогда — содержимое.
- Отклонённые слияния: запись, чей блоб не прошёл аутентификацию против заявленных метаданных, не сохраняется и показывается — это и есть защита от подделки на сервере.
Курсор — позиция в истории одного сервера, поэтому важно, под каким ключом он хранится:
SyncStateStore ключуется целым ServerLink (один id аккаунта называет разные аккаунты на домашнем
и рабочем инстансах), а область команды — связкой ссылки и области через
shared/sync/KeyedStateStore. Общий курсор на два сервера означает, что все записи ниже вершины
другого не будут вытянуты никогда — реальный баг (#242), ради которого это ключевание и сделано.
Типы записей, появившиеся после того, как серверы уже развернули, пушатся отдельной
best-effort пачкой, поэтому старый сервер, отвергнув один тип, не роняет весь push. За «сервер
просто старый» считаются только PROTOCOL (400) и NOT_FOUND (404); любой другой сбой
принадлежит вызывающему. Тип покидает это множество, когда его принимает каждый поддерживаемый
релиз сервера.
Записи команд для сервера — шифротекст; членство выдаётся запечатанными конвертами под
опубликованные публичные ключи участников, а у области доступа свой ключ. Сервер обеспечивает роли
и ACL областей, но расшифровать не может ничего. Маршруты — /teams/*, плюс /account/key и
/account/keys/{accountId} для публикации и поиска ключей.
Поскольку конверты запечатаны под ключ команды, приглашения и ротация ключей — только клиентские операции: браузерная сессия их выполнить не может.
Общий терминал кадрируется share/SessionShareFrame / SessionShareCodec и передаётся через
ShareChannel; SessionShareHost ведёт сторону хозяина, SharedSessionViewer — зрителя. Релей
двигает зашифрованные кадры и не узнаёт о сессии ничего.
| Область | Маршруты |
|---|---|
| Здоровье |
GET /healthz, GET /readyz, GET /metrics (по умолчанию выключен) |
| Аутентификация |
/auth/register, /auth/srp/challenge, /auth/srp/verify, /auth/refresh, /auth/change-password, /auth/web-password, /auth/web-login
|
| Хранилище |
GET /vault/keys, GET/PUT /vault/records, GET /vault/envelopes, WS /sync
|
| Аккаунт |
GET /account/summary, GET /account/activity
|
| Устройства |
GET/DELETE /devices, /pairing/start, /pairing/claim
|
| Команды |
/teams, /teams/{id}/members, /teams/{id}/records, /teams/{id}/activity, роли и области |
| Админ |
/admin/* за X-Admin-Token, то же самое умеет CLI skerry-admin
|
Правила серверной стороны, которые стоит помнить: страж админ-токена — один плагин на область
маршрутов, а не проверка в каждом; идентификаторы и лимиты идут через requiredPathId /
limitParam; ввод валидируется до побочных эффектов (400, а не 500 плюс сожжённый одноразовый
код); и ни одна метка метрики не несёт accountId, deviceId, recordId, teamId, scopeId или IP.
DELETE /admin/accounts/{id} в одной транзакции убирает записи, устройства, сессии привязки,
опубликованные ключи Teams, членства и выдачи областей. Команда, которой аккаунт владел,
переходит к самому старшему активному участнику; если активных не осталось, команда удаляется
вместе с записями и выдачами. Оставшимся стоит ротировать ключ команды — удалённый аккаунт его
знал, а ротировать может только клиент.
User guide
- Installation
- Getting started
- Hosts and connections
- Vault and credentials
- Terminal
- Files (SFTP)
- Port forwarding
- Remote desktops
- Snippets and runbooks
- Monitoring and guard
- AI assistant
- Sync and devices
- Teams and sharing
- Settings and shortcuts
- Android
- Troubleshooting
Development
- Architecture
- Building and running
- Connections and transports
- Terminal engine
- Vault and crypto
- Sync protocol
- Remote desktop stacks
- UI layer
- Testing and the gate
- Contributing and releases
Русский — оглавление