SHA256
Довести DM v1 до соответствия коду и документации
This commit is contained in:
@@ -71,7 +71,8 @@
|
||||
Важно:
|
||||
|
||||
- это server-side existence-check;
|
||||
- если пользователя нет в локальной БД, он может быть импортирован при авторизации из Solana PDA.
|
||||
- если пользователя нет в локальной БД, сервер сразу пытается lazy-import из Solana PDA;
|
||||
- поэтому `GetUser` можно использовать как актуальный способ получить `clientKey` и базовые поля пользователя перед E2EE DM.
|
||||
|
||||
### Запрос
|
||||
|
||||
|
||||
@@ -57,7 +57,6 @@
|
||||
| `AddCloseFriend` | `11_Connections_API.md` | добавить близкого друга |
|
||||
| `UpsertPushToken` | `12_Direct_Messages_Push_Calls_API.md` | регистрация WebPush-токена |
|
||||
| `SendTestWebPush` | `12_Direct_Messages_Push_Calls_API.md` | тестовая push-доставка |
|
||||
| `SendDirectMessage` | `12_Direct_Messages_Push_Calls_API.md` | отправка подписанного DM-пакета |
|
||||
| `SendMessagePair` | `12_Direct_Messages_Push_Calls_API.md` | отправка пары входящий/исходящий DM |
|
||||
| `ReceiveOutcomingMessage` | `12_Direct_Messages_Push_Calls_API.md` | алиас `SendMessagePair` |
|
||||
| `ReceiveIncomingMessage` | `12_Direct_Messages_Push_Calls_API.md` | прием входящего DM-блока |
|
||||
@@ -70,6 +69,7 @@
|
||||
## Важные замечания
|
||||
|
||||
- `ReceiveOutcomingMessage` сейчас зарегистрирован как алиас того же handler/request-класса, что и `SendMessagePair`.
|
||||
- Legacy-операция `SendDirectMessage` больше не зарегистрирована и не должна использоваться для DM v1.
|
||||
- Отдельных HTTP endpoints для DM-файлов сейчас нет.
|
||||
- Классы `Net_MarkChannelMessagesSeen_*` существуют в коде, но операция `MarkChannelMessagesSeen` не зарегистрирована в `JsonHandlerRegistry`, поэтому в публичный список API не входит.
|
||||
- HTTP debug endpoints из `src/main/java/server/debug/` не входят в этот индекс WebSocket `op`; они описаны отдельно в `13_HTTP_Debug_API.md`.
|
||||
|
||||
@@ -7,6 +7,11 @@
|
||||
- `Dev_Docs/Personal_Messages/Протокол_DM_v1.md`
|
||||
- `Dev_Docs/Personal_Messages/Формат_DM_v1.md`
|
||||
|
||||
Важно:
|
||||
|
||||
- legacy-операция `SendDirectMessage` отключена и не зарегистрирована в публичном API;
|
||||
- для DM v1 нужно использовать только `SendMessagePair`, `ReceiveOutcomingMessage`, `ReceiveIncomingMessage`, `DeleteMessage`, `DeleteConversation`.
|
||||
|
||||
## 1. `UpsertPushToken`
|
||||
|
||||
Требует авторизации.
|
||||
@@ -111,7 +116,7 @@
|
||||
- `400 / BAD_FIELDS` — пустой `incomingBlobB64` или `outgoingBlobB64`
|
||||
- `400 / BAD_BLOCK_FORMAT` — base64 или бинарный контейнер повреждён
|
||||
- `400 / BAD_CRYPTO_METHOD` — неподдерживаемый метод шифрования контейнера `EncryptedBody_v1_0`
|
||||
- `404 / USER_NOT_FOUND` — один из логинов не найден
|
||||
- `404 / USER_NOT_FOUND` — один из логинов не найден даже после попытки lazy-import из Solana PDA
|
||||
- `460 / BAD_SIGNATURE` — подпись блока не прошла проверку
|
||||
|
||||
## 4. `ReceiveIncomingMessage`
|
||||
@@ -223,6 +228,8 @@
|
||||
## 11. Замечания
|
||||
|
||||
- все DM-типы `1..8` используют `SHiNE_DM`
|
||||
- `GetUser` может lazy-import пользователя из Solana PDA, поэтому именно через него клиент обычно получает `clientKey` адресата для E2EE
|
||||
- сервер не расшифровывает DM и не использует ciphertext как preview текста
|
||||
- сервер хранит последнюю применённую ревизию контентного сообщения и отдельные tombstone-события удаления
|
||||
- сервер хранит последнюю применённую версию контентного сообщения по правилу `revisionTimeMs`, а при равенстве — по `reencryptedAtMs`
|
||||
- если сервер уже знает tombstone удаления переписки и получает старое сообщение до этой границы, он перерассылает известный `DeleteConversation` на `access_servers` обеих сторон
|
||||
- HTTP endpoints для DM-файлов сейчас отсутствуют
|
||||
|
||||
@@ -11,9 +11,13 @@
|
||||
5. Удаление исходящего сообщения отправителем (`type=5`) и удаление входящего сообщения получателем (`type=6`).
|
||||
6. Удаление истории переписки (`type=7/8`) и игнорирование более старых сообщений после tombstone.
|
||||
7. Сценарий с несколькими `access_servers`, включая случай общего сервера у обеих сторон.
|
||||
8. Первый DM пользователю, которого ещё нет в локальной `solana_users`, с lazy-import из Solana PDA.
|
||||
9. Две версии одного сообщения с одинаковым `revisionTimeMs`, но разным `reencryptedAtMs`: должна победить версия с большим `reencryptedAtMs`.
|
||||
10. После уже известного `DeleteConversation` отправка старого сообщения с другого сервера должна приводить к повторной рассылке tombstone удаления переписки.
|
||||
11. Попытка вызвать legacy `SendDirectMessage` должна не работать, а новый UI должен использовать только `SendMessagePair`.
|
||||
|
||||
- ожидаемый результат:
|
||||
Сообщения шифруются и расшифровываются только на клиенте, сервер не показывает plaintext из ciphertext, старые ревизии не оживают после tombstone, общие серверы не получают лишних дублей пары, а удаление и read-receipt корректно доходят обеим сторонам.
|
||||
Сообщения шифруются и расшифровываются только на клиенте, сервер не показывает plaintext из ciphertext, lazy-import из PDA позволяет отправить первый DM без ручного прогрева БД, версия с большим `reencryptedAtMs` корректно замещает старую при равном `revisionTimeMs`, старые ревизии не оживают после tombstone, общие серверы не получают лишних дублей пары, а удаление и read-receipt корректно доходят обеим сторонам.
|
||||
|
||||
- статус:
|
||||
pending
|
||||
|
||||
@@ -15,3 +15,4 @@
|
||||
|
||||
- код DM и оба документа `Протокол_DM_v1.md` + `Формат_DM_v1.md` всегда должны обновляться синхронно;
|
||||
- если меняется поведение DM в коде, в том же наборе изменений обновляется и эта документация.
|
||||
- локальная таблица пользователей для DM считается кэшем, а источником истины остаётся Solana PDA; если серверная логика DM меняет правила lazy-import пользователей из PDA, это тоже обязательно фиксируется в документации.
|
||||
|
||||
@@ -71,6 +71,19 @@
|
||||
- сервер не должен расшифровывать DM;
|
||||
- ciphertext у `type=1` и `type=2` обычно разный и не обязан совпадать побайтно.
|
||||
|
||||
### 2.3. Источник истины по пользователю
|
||||
|
||||
Для DM-проверки сервер использует:
|
||||
|
||||
- локальную `solana_users` как кэш;
|
||||
- Solana PDA как источник истины по `clientKey` и `access_servers`.
|
||||
|
||||
Если нужного пользователя нет локально, сервер обязан попытаться lazy-import из Solana PDA:
|
||||
|
||||
- в `GetUser`;
|
||||
- при серверной верификации DM-подписи;
|
||||
- перед межсерверной маршрутизацией через `access_servers`.
|
||||
|
||||
## 3. Типы сообщений
|
||||
|
||||
В версии v1 используются следующие `messageType`:
|
||||
@@ -135,7 +148,8 @@
|
||||
- plaintext может остаться тем же;
|
||||
- ciphertext меняется;
|
||||
- `reencryptedAtMs` получает ненулевое значение;
|
||||
- `revisionTimeMs` увеличивается.
|
||||
- `revisionTimeMs` может остаться прежним, в том числе нулевым, если менялось только шифрование без редактирования текста;
|
||||
- сервер при выборе актуальной версии обязан учитывать обе метки времени.
|
||||
|
||||
Если сообщение уже удалено одним из типов `5/6`, его перешифровывать больше нельзя.
|
||||
|
||||
@@ -223,7 +237,7 @@
|
||||
то сервер:
|
||||
|
||||
- не принимает это сообщение;
|
||||
- в ответ возвращает то же сообщение удаления переписки;
|
||||
- распространяет уже известный tombstone удаления переписки на серверы доступа обеих сторон;
|
||||
- ожидает, что вторая сторона обработает его как обычное уже известное удаление переписки.
|
||||
|
||||
### 6.5. Будущие новые сообщения
|
||||
@@ -281,6 +295,8 @@
|
||||
- `SendMessagePair`
|
||||
- `ReceiveOutcomingMessage`
|
||||
- `ReceiveIncomingMessage`
|
||||
- `DeleteMessage`
|
||||
- `DeleteConversation`
|
||||
|
||||
Их роли в v1:
|
||||
|
||||
@@ -290,34 +306,27 @@
|
||||
|
||||
### 8.2. Новые методы, которые нужны
|
||||
|
||||
Нужны как минимум отдельные операции:
|
||||
|
||||
- `DeleteMessage`
|
||||
- `DeleteConversation`
|
||||
|
||||
Названия могут быть другими, но логически это две разные операции:
|
||||
|
||||
- удалить одно сообщение у обеих сторон;
|
||||
- удалить всю переписку до заданной временной границы.
|
||||
Отдельный legacy-метод `SendDirectMessage` в DM v1 не используется и должен оставаться отключённым, чтобы не было параллельного старого стека доставки.
|
||||
|
||||
## 9. Правила валидации и применения
|
||||
|
||||
### 9.1. Общее правило по ревизиям
|
||||
|
||||
Для одного и того же `messageKey` сервер сравнивает только:
|
||||
Для одного и того же контентного сообщения сервер сравнивает пару:
|
||||
|
||||
- уже сохранённый `revisionTimeMs`;
|
||||
- `revisionTimeMs` входящего сообщения.
|
||||
- `revisionTimeMs`;
|
||||
- `reencryptedAtMs`.
|
||||
|
||||
Правило:
|
||||
|
||||
- если новый `revisionTimeMs` больше сохранённого, сообщение применяется;
|
||||
- если новый `revisionTimeMs` равен сохранённому, сообщение не применяется;
|
||||
- если новый `revisionTimeMs` меньше сохранённого, сообщение не применяется.
|
||||
- если `revisionTimeMs` равен, но новый `reencryptedAtMs` больше сохранённого, сообщение применяется;
|
||||
- если обе величины равны, сообщение не применяется;
|
||||
- если новая пара (`revisionTimeMs`, `reencryptedAtMs`) меньше или равна сохранённой, сообщение не применяется.
|
||||
|
||||
Содержимое `body` при этом сравнении не участвует.
|
||||
|
||||
То есть если `revisionTimeMs` совпадает, сервер считает, что такая ревизия у него уже есть.
|
||||
То есть если сервер уже видел ту же пару (`revisionTimeMs`, `reencryptedAtMs`), он считает, что такая версия у него уже есть.
|
||||
|
||||
### 9.2. Повторный tombstone
|
||||
|
||||
@@ -353,6 +362,8 @@
|
||||
- не восстанавливает старую историю;
|
||||
- не применяет это событие повторно.
|
||||
|
||||
Если это старое контентное сообщение или входящая копия, сервер дополнительно перерассылает уже известный tombstone удаления переписки на `access_servers` обеих сторон, чтобы отстающие серверы сами подчистили историю.
|
||||
|
||||
Такие сообщения считаются частью уже удалённой истории.
|
||||
|
||||
### 9.4. Приоритет удаления переписки
|
||||
@@ -475,6 +486,8 @@ Request:
|
||||
|
||||
Если часть серверов совпадает, это допустимо.
|
||||
|
||||
Если один и тот же сервер присутствует у обеих сторон, он не должен слать сообщение сам себе повторно, но обязан локально сохранить событие и доставить его в нужные пользовательские сессии.
|
||||
|
||||
Идемпотентность обязательна.
|
||||
|
||||
### 11.3. Ошибки доставки
|
||||
@@ -513,6 +526,9 @@ Request:
|
||||
- удаление одного сообщения должно стать терминальным tombstone;
|
||||
- удаление всей переписки должно стать отдельным служебным событием;
|
||||
- межсерверная маршрутизация DM должна идти через `access_servers`;
|
||||
- сервер должен добирать отсутствующих пользователей из Solana PDA до проверки подписи DM;
|
||||
- при выборе актуальной версии должен учитываться `reencryptedAtMs`, если `revisionTimeMs` совпадает;
|
||||
- legacy `SendDirectMessage` должен быть отключён;
|
||||
- логика должна быть безопасна для нескольких серверов у каждой стороны.
|
||||
|
||||
## 14. Что в v1 пока не входит
|
||||
|
||||
@@ -81,11 +81,12 @@ SHiNE_DM
|
||||
|
||||
### `revisionTimeMs`
|
||||
|
||||
- `0` для исходной версии сообщения;
|
||||
- ненулевое значение для новой ревизии сообщения;
|
||||
- при редактировании или удалении должно быть больше предыдущего значения для этого же `messageKey`.
|
||||
- `0` для исходной версии контентного сообщения;
|
||||
- ненулевое значение для новой текстовой ревизии сообщения;
|
||||
- для типов `5/6` это время tombstone удаления одного сообщения;
|
||||
- для типов `7/8` всегда `0`.
|
||||
|
||||
То есть это `0` или время, когда сообщение было отредактировано либо удалено.
|
||||
То есть `revisionTimeMs` отвечает именно за изменение смыслового содержимого сообщения, а не за сам факт перешифровки.
|
||||
|
||||
### `reencryptedAtMs`
|
||||
|
||||
@@ -94,6 +95,13 @@ SHiNE_DM
|
||||
|
||||
Для типов `5/6/7/8` должно быть `0`.
|
||||
|
||||
`reencryptedAtMs` не требует, чтобы `revisionTimeMs` был ненулевым. Допустим сценарий:
|
||||
|
||||
- `revisionTimeMs = 0`
|
||||
- `reencryptedAtMs > 0`
|
||||
|
||||
если исходное сообщение не редактировалось, но было перешифровано позже.
|
||||
|
||||
### `bodyLen`
|
||||
|
||||
- `> 0` для типов `1/2/3/4`;
|
||||
@@ -213,7 +221,7 @@ ReadReceiptBody_v1_0
|
||||
|
||||
### Перешифровка
|
||||
|
||||
- `revisionTimeMs > 0`
|
||||
- `revisionTimeMs = 0` или `> 0`
|
||||
- `reencryptedAtMs > 0`
|
||||
|
||||
## 8. Контент типов `3/4`
|
||||
|
||||
@@ -11,6 +11,17 @@ import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
public final class SignedMessagesV2DAO {
|
||||
public enum ApplyStatus {
|
||||
APPLIED,
|
||||
DUPLICATE_OR_OLDER,
|
||||
BLOCKED_BY_MESSAGE_TOMBSTONE,
|
||||
BLOCKED_BY_CONVERSATION_TOMBSTONE;
|
||||
|
||||
public boolean applied() {
|
||||
return this == APPLIED;
|
||||
}
|
||||
}
|
||||
|
||||
private static volatile SignedMessagesV2DAO instance;
|
||||
private final SqliteDbController db = SqliteDbController.getInstance();
|
||||
|
||||
@@ -25,8 +36,11 @@ public final class SignedMessagesV2DAO {
|
||||
return instance;
|
||||
}
|
||||
|
||||
public boolean insertIfAbsent(SignedMessageV2Entry e) throws Exception {
|
||||
public ApplyStatus insertIfAbsent(SignedMessageV2Entry e) throws Exception {
|
||||
try (Connection c = db.getConnection()) {
|
||||
if (isBlockedByConversationDelete(c, e.getFromLogin(), e.getToLogin(), e.getTimeMs())) {
|
||||
return ApplyStatus.BLOCKED_BY_CONVERSATION_TOMBSTONE;
|
||||
}
|
||||
String sql = """
|
||||
INSERT OR IGNORE INTO signed_messages_v2 (
|
||||
message_key, base_key, target_login, from_login, to_login,
|
||||
@@ -37,7 +51,7 @@ public final class SignedMessagesV2DAO {
|
||||
""";
|
||||
try (PreparedStatement ps = c.prepareStatement(sql)) {
|
||||
bindSignedMessage(ps, e);
|
||||
return ps.executeUpdate() > 0;
|
||||
return ps.executeUpdate() > 0 ? ApplyStatus.APPLIED : ApplyStatus.DUPLICATE_OR_OLDER;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -67,25 +81,25 @@ public final class SignedMessagesV2DAO {
|
||||
}
|
||||
}
|
||||
|
||||
public boolean upsertContentPair(SignedMessageV2Entry incoming, SignedMessageV2Entry outgoing) throws Exception {
|
||||
public ApplyStatus upsertContentPair(SignedMessageV2Entry incoming, SignedMessageV2Entry outgoing) throws Exception {
|
||||
try (Connection c = db.getConnection()) {
|
||||
boolean prevAutoCommit = c.getAutoCommit();
|
||||
c.setAutoCommit(false);
|
||||
try {
|
||||
if (isBlockedByConversationDelete(c, incoming.getFromLogin(), incoming.getToLogin(), incoming.getTimeMs())) {
|
||||
c.rollback();
|
||||
return false;
|
||||
return ApplyStatus.BLOCKED_BY_CONVERSATION_TOMBSTONE;
|
||||
}
|
||||
if (hasMessageDeleteTombstone(c, incoming.getBaseKey())) {
|
||||
c.rollback();
|
||||
return false;
|
||||
return ApplyStatus.BLOCKED_BY_MESSAGE_TOMBSTONE;
|
||||
}
|
||||
|
||||
Long currentRevision = getCurrentContentRevision(c, incoming.getBaseKey());
|
||||
long nextRevision = incoming.getRevisionTimeMs();
|
||||
if (currentRevision != null && nextRevision <= currentRevision) {
|
||||
RevisionMarker currentMarker = getCurrentContentMarker(c, incoming.getBaseKey());
|
||||
RevisionMarker nextMarker = RevisionMarker.of(incoming);
|
||||
if (currentMarker != null && compareMarkers(nextMarker, currentMarker) <= 0) {
|
||||
c.rollback();
|
||||
return false;
|
||||
return ApplyStatus.DUPLICATE_OR_OLDER;
|
||||
}
|
||||
|
||||
upsertMessage(c, incoming);
|
||||
@@ -94,7 +108,7 @@ public final class SignedMessagesV2DAO {
|
||||
resetDeliveryRows(c, outgoing.getMessageKey());
|
||||
|
||||
c.commit();
|
||||
return true;
|
||||
return ApplyStatus.APPLIED;
|
||||
} catch (Exception ex) {
|
||||
try { c.rollback(); } catch (Exception ignored) {}
|
||||
throw ex;
|
||||
@@ -104,31 +118,31 @@ public final class SignedMessagesV2DAO {
|
||||
}
|
||||
}
|
||||
|
||||
public boolean upsertIncomingCopy(SignedMessageV2Entry incoming) throws Exception {
|
||||
public ApplyStatus upsertIncomingCopy(SignedMessageV2Entry incoming) throws Exception {
|
||||
try (Connection c = db.getConnection()) {
|
||||
boolean prevAutoCommit = c.getAutoCommit();
|
||||
c.setAutoCommit(false);
|
||||
try {
|
||||
if (isBlockedByConversationDelete(c, incoming.getFromLogin(), incoming.getToLogin(), incoming.getTimeMs())) {
|
||||
c.rollback();
|
||||
return false;
|
||||
return ApplyStatus.BLOCKED_BY_CONVERSATION_TOMBSTONE;
|
||||
}
|
||||
if (hasMessageDeleteTombstone(c, incoming.getBaseKey())) {
|
||||
c.rollback();
|
||||
return false;
|
||||
return ApplyStatus.BLOCKED_BY_MESSAGE_TOMBSTONE;
|
||||
}
|
||||
|
||||
Long currentRevision = getRevisionTimeMs(c, incoming.getMessageKey());
|
||||
long nextRevision = incoming.getRevisionTimeMs();
|
||||
if (currentRevision != null && nextRevision <= currentRevision) {
|
||||
RevisionMarker currentMarker = getRevisionMarkerByMessageKey(c, incoming.getMessageKey());
|
||||
RevisionMarker nextMarker = RevisionMarker.of(incoming);
|
||||
if (currentMarker != null && compareMarkers(nextMarker, currentMarker) <= 0) {
|
||||
c.rollback();
|
||||
return false;
|
||||
return ApplyStatus.DUPLICATE_OR_OLDER;
|
||||
}
|
||||
|
||||
upsertMessage(c, incoming);
|
||||
resetDeliveryRows(c, incoming.getMessageKey());
|
||||
c.commit();
|
||||
return true;
|
||||
return ApplyStatus.APPLIED;
|
||||
} catch (Exception ex) {
|
||||
try { c.rollback(); } catch (Exception ignored) {}
|
||||
throw ex;
|
||||
@@ -138,18 +152,18 @@ public final class SignedMessagesV2DAO {
|
||||
}
|
||||
}
|
||||
|
||||
public boolean applyDeleteMessage(SignedMessageV2Entry tombstone) throws Exception {
|
||||
public ApplyStatus applyDeleteMessage(SignedMessageV2Entry tombstone) throws Exception {
|
||||
try (Connection c = db.getConnection()) {
|
||||
boolean prevAutoCommit = c.getAutoCommit();
|
||||
c.setAutoCommit(false);
|
||||
try {
|
||||
if (isBlockedByConversationDelete(c, tombstone.getFromLogin(), tombstone.getToLogin(), tombstone.getTimeMs())) {
|
||||
c.rollback();
|
||||
return false;
|
||||
return ApplyStatus.BLOCKED_BY_CONVERSATION_TOMBSTONE;
|
||||
}
|
||||
if (hasMessageDeleteTombstone(c, tombstone.getBaseKey())) {
|
||||
c.rollback();
|
||||
return false;
|
||||
return ApplyStatus.DUPLICATE_OR_OLDER;
|
||||
}
|
||||
|
||||
deleteMessageContentAndReceipts(c, tombstone.getBaseKey());
|
||||
@@ -157,7 +171,7 @@ public final class SignedMessagesV2DAO {
|
||||
resetDeliveryRows(c, tombstone.getMessageKey());
|
||||
|
||||
c.commit();
|
||||
return true;
|
||||
return ApplyStatus.APPLIED;
|
||||
} catch (Exception ex) {
|
||||
try { c.rollback(); } catch (Exception ignored) {}
|
||||
throw ex;
|
||||
@@ -167,7 +181,7 @@ public final class SignedMessagesV2DAO {
|
||||
}
|
||||
}
|
||||
|
||||
public boolean applyDeleteConversation(SignedMessageV2Entry tombstone) throws Exception {
|
||||
public ApplyStatus applyDeleteConversation(SignedMessageV2Entry tombstone) throws Exception {
|
||||
try (Connection c = db.getConnection()) {
|
||||
boolean prevAutoCommit = c.getAutoCommit();
|
||||
c.setAutoCommit(false);
|
||||
@@ -175,7 +189,7 @@ public final class SignedMessagesV2DAO {
|
||||
Long currentBoundary = getLatestConversationDeleteBoundary(c, tombstone.getFromLogin(), tombstone.getToLogin());
|
||||
if (currentBoundary != null && tombstone.getTimeMs() <= currentBoundary) {
|
||||
c.rollback();
|
||||
return false;
|
||||
return ApplyStatus.DUPLICATE_OR_OLDER;
|
||||
}
|
||||
|
||||
deleteConversationHistoryBefore(c, tombstone.getFromLogin(), tombstone.getToLogin(), tombstone.getTimeMs());
|
||||
@@ -183,7 +197,7 @@ public final class SignedMessagesV2DAO {
|
||||
resetDeliveryRows(c, tombstone.getMessageKey());
|
||||
|
||||
c.commit();
|
||||
return true;
|
||||
return ApplyStatus.APPLIED;
|
||||
} catch (Exception ex) {
|
||||
try { c.rollback(); } catch (Exception ignored) {}
|
||||
throw ex;
|
||||
@@ -214,6 +228,12 @@ public final class SignedMessagesV2DAO {
|
||||
}
|
||||
}
|
||||
|
||||
public SignedMessageV2Entry getLatestConversationDelete(String fromLogin, String toLogin) throws Exception {
|
||||
try (Connection c = db.getConnection()) {
|
||||
return getLatestConversationDelete(c, fromLogin, toLogin);
|
||||
}
|
||||
}
|
||||
|
||||
public void ensureDeliveryRow(String messageKey, String sessionId, long nowMs) throws Exception {
|
||||
try (Connection c = db.getConnection()) {
|
||||
String sql = """
|
||||
@@ -338,30 +358,36 @@ public final class SignedMessagesV2DAO {
|
||||
}
|
||||
}
|
||||
|
||||
private Long getRevisionTimeMs(Connection c, String messageKey) throws SQLException {
|
||||
String sql = "SELECT revision_time_ms FROM signed_messages_v2 WHERE message_key = ? LIMIT 1";
|
||||
private RevisionMarker getRevisionMarkerByMessageKey(Connection c, String messageKey) throws SQLException {
|
||||
String sql = """
|
||||
SELECT revision_time_ms, reencrypted_at_ms
|
||||
FROM signed_messages_v2
|
||||
WHERE message_key = ?
|
||||
LIMIT 1
|
||||
""";
|
||||
try (PreparedStatement ps = c.prepareStatement(sql)) {
|
||||
ps.setString(1, messageKey);
|
||||
try (ResultSet rs = ps.executeQuery()) {
|
||||
if (!rs.next()) return null;
|
||||
return rs.getLong(1);
|
||||
return new RevisionMarker(rs.getLong(1), rs.getLong(2));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private Long getCurrentContentRevision(Connection c, String baseKey) throws SQLException {
|
||||
private RevisionMarker getCurrentContentMarker(Connection c, String baseKey) throws SQLException {
|
||||
String sql = """
|
||||
SELECT MAX(revision_time_ms)
|
||||
SELECT revision_time_ms, reencrypted_at_ms
|
||||
FROM signed_messages_v2
|
||||
WHERE base_key = ?
|
||||
AND message_type IN (1, 2)
|
||||
ORDER BY revision_time_ms DESC, reencrypted_at_ms DESC
|
||||
LIMIT 1
|
||||
""";
|
||||
try (PreparedStatement ps = c.prepareStatement(sql)) {
|
||||
ps.setString(1, baseKey);
|
||||
try (ResultSet rs = ps.executeQuery()) {
|
||||
if (!rs.next()) return null;
|
||||
long value = rs.getLong(1);
|
||||
return rs.wasNull() ? null : value;
|
||||
return new RevisionMarker(rs.getLong(1), rs.getLong(2));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -410,6 +436,34 @@ public final class SignedMessagesV2DAO {
|
||||
}
|
||||
}
|
||||
|
||||
private SignedMessageV2Entry getLatestConversationDelete(Connection c, String fromLogin, String toLogin) throws Exception {
|
||||
String sql = """
|
||||
SELECT
|
||||
message_key, base_key, target_login, from_login, to_login,
|
||||
time_ms, nonce, message_type, revision_time_ms, reencrypted_at_ms,
|
||||
raw_block, created_at_ms, source_api, origin_session_id,
|
||||
receipt_ref_base_key, receipt_ref_type
|
||||
FROM signed_messages_v2
|
||||
WHERE message_type IN (7, 8)
|
||||
AND (
|
||||
(from_login = ? COLLATE NOCASE AND to_login = ? COLLATE NOCASE)
|
||||
OR (from_login = ? COLLATE NOCASE AND to_login = ? COLLATE NOCASE)
|
||||
)
|
||||
ORDER BY time_ms DESC
|
||||
LIMIT 1
|
||||
""";
|
||||
try (PreparedStatement ps = c.prepareStatement(sql)) {
|
||||
ps.setString(1, fromLogin);
|
||||
ps.setString(2, toLogin);
|
||||
ps.setString(3, toLogin);
|
||||
ps.setString(4, fromLogin);
|
||||
try (ResultSet rs = ps.executeQuery()) {
|
||||
if (!rs.next()) return null;
|
||||
return mapRow(rs);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void deleteMessageContentAndReceipts(Connection c, String baseKey) throws SQLException {
|
||||
deleteDeliveryRowsByMessageSelection(c, """
|
||||
SELECT message_key
|
||||
@@ -532,6 +586,12 @@ public final class SignedMessagesV2DAO {
|
||||
return msg.contains("constraint") || msg.contains("unique") || msg.contains("primary key");
|
||||
}
|
||||
|
||||
private int compareMarkers(RevisionMarker left, RevisionMarker right) {
|
||||
int revisionCompare = Long.compare(left.revisionTimeMs, right.revisionTimeMs);
|
||||
if (revisionCompare != 0) return revisionCompare;
|
||||
return Long.compare(left.reencryptedAtMs, right.reencryptedAtMs);
|
||||
}
|
||||
|
||||
private SignedMessageV2Entry mapRow(ResultSet rs) throws Exception {
|
||||
SignedMessageV2Entry e = new SignedMessageV2Entry();
|
||||
e.setMessageKey(rs.getString("message_key"));
|
||||
@@ -553,4 +613,10 @@ public final class SignedMessagesV2DAO {
|
||||
e.setReceiptRefType(rs.wasNull() ? null : maybeRefType);
|
||||
return e;
|
||||
}
|
||||
|
||||
private record RevisionMarker(long revisionTimeMs, long reencryptedAtMs) {
|
||||
private static RevisionMarker of(SignedMessageV2Entry entry) {
|
||||
return new RevisionMarker(entry.getRevisionTimeMs(), entry.getReencryptedAtMs());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
-2
@@ -195,7 +195,6 @@ public final class JsonHandlerRegistry {
|
||||
// --- direct messages / push ---
|
||||
Map.entry("UpsertPushToken", new Net_UpsertPushToken_Handler()),
|
||||
Map.entry("SendTestWebPush", new Net_SendTestWebPush_Handler()),
|
||||
Map.entry("SendDirectMessage", new Net_SendDirectMessage_Handler()),
|
||||
Map.entry("SendMessagePair", new Net_SendMessagePair_Handler()),
|
||||
Map.entry("ReceiveOutcomingMessage", new Net_SendMessagePair_Handler()),
|
||||
Map.entry("ReceiveIncomingMessage", new Net_ReceiveIncomingMessage_Handler()),
|
||||
@@ -276,7 +275,6 @@ public final class JsonHandlerRegistry {
|
||||
// --- direct messages / push ---
|
||||
Map.entry("UpsertPushToken", Net_UpsertPushToken_Request.class),
|
||||
Map.entry("SendTestWebPush", Net_SendTestWebPush_Request.class),
|
||||
Map.entry("SendDirectMessage", Net_SendDirectMessage_Request.class),
|
||||
Map.entry("SendMessagePair", Net_SendMessagePair_Request.class),
|
||||
Map.entry("ReceiveOutcomingMessage", Net_SendMessagePair_Request.class),
|
||||
Map.entry("ReceiveIncomingMessage", Net_ReceiveIncomingMessage_Request.class),
|
||||
|
||||
+4
@@ -5,6 +5,7 @@ import org.slf4j.LoggerFactory;
|
||||
import server.logic.ws_protocol.JSON.ConnectionContext;
|
||||
import server.logic.ws_protocol.JSON.entyties.Net_Request;
|
||||
import server.logic.ws_protocol.JSON.entyties.Net_Response;
|
||||
import server.logic.ws_protocol.JSON.handlers.auth.SolanaUserPdaImportService;
|
||||
import server.logic.ws_protocol.JSON.handlers.JsonMessageHandler;
|
||||
import server.logic.ws_protocol.JSON.handlers.tempToTest.entyties.Net_GetUser_Request;
|
||||
import server.logic.ws_protocol.JSON.handlers.tempToTest.entyties.Net_GetUser_Response;
|
||||
@@ -42,6 +43,9 @@ public class Net_GetUser_Handler implements JsonMessageHandler {
|
||||
|
||||
try {
|
||||
SolanaUserEntry u = usersDAO.getByLogin(req.getLogin());
|
||||
if (u == null) {
|
||||
u = SolanaUserPdaImportService.findOrImportByLogin(req.getLogin());
|
||||
}
|
||||
|
||||
Net_GetUser_Response resp = new Net_GetUser_Response();
|
||||
resp.setOp(req.getOp());
|
||||
|
||||
+3
-3
@@ -39,13 +39,13 @@ public class Net_DeleteConversation_Handler implements JsonMessageHandler {
|
||||
}
|
||||
|
||||
SignedMessageV2Entry entry = SignedMessagesCore.toEntry(block, "DeleteConversation", null);
|
||||
boolean applied = SignedMessagesV2DAO.getInstance().applyDeleteConversation(entry);
|
||||
SignedMessagesV2DAO.ApplyStatus status = SignedMessagesV2DAO.getInstance().applyDeleteConversation(entry);
|
||||
|
||||
SignedMessagesRealtime.DeliveryCounters counters = new SignedMessagesRealtime.DeliveryCounters();
|
||||
if (applied) {
|
||||
if (status.applied()) {
|
||||
counters = SignedMessagesRealtime.deliverToRelevantSessions(entry, block);
|
||||
}
|
||||
if (applied && ctx != null && ctx.isAuthenticatedUser()) {
|
||||
if (status.applied() && ctx != null && ctx.isAuthenticatedUser()) {
|
||||
DmFederationService.fanOutDeleteConversation(block.fromLogin, block.toLogin, block.messageType, req.getBlobB64().trim());
|
||||
}
|
||||
|
||||
|
||||
+3
-3
@@ -39,13 +39,13 @@ public class Net_DeleteMessage_Handler implements JsonMessageHandler {
|
||||
}
|
||||
|
||||
SignedMessageV2Entry entry = SignedMessagesCore.toEntry(block, "DeleteMessage", null);
|
||||
boolean applied = SignedMessagesV2DAO.getInstance().applyDeleteMessage(entry);
|
||||
SignedMessagesV2DAO.ApplyStatus status = SignedMessagesV2DAO.getInstance().applyDeleteMessage(entry);
|
||||
|
||||
SignedMessagesRealtime.DeliveryCounters counters = new SignedMessagesRealtime.DeliveryCounters();
|
||||
if (applied) {
|
||||
if (status.applied()) {
|
||||
counters = SignedMessagesRealtime.deliverToRelevantSessions(entry, block);
|
||||
}
|
||||
if (applied && ctx != null && ctx.isAuthenticatedUser()) {
|
||||
if (status.applied() && ctx != null && ctx.isAuthenticatedUser()) {
|
||||
DmFederationService.fanOutDeleteMessage(block.fromLogin, block.toLogin, block.messageType, req.getBlobB64().trim());
|
||||
}
|
||||
|
||||
|
||||
+18
-2
@@ -11,6 +11,8 @@ import server.logic.ws_protocol.WireCodes;
|
||||
import shine.db.dao.SignedMessagesV2DAO;
|
||||
import shine.db.entities.SignedMessageV2Entry;
|
||||
|
||||
import java.util.Base64;
|
||||
|
||||
public class Net_ReceiveIncomingMessage_Handler implements JsonMessageHandler {
|
||||
@Override
|
||||
public Net_Response handle(Net_Request baseRequest, ConnectionContext ctx) throws Exception {
|
||||
@@ -45,13 +47,16 @@ public class Net_ReceiveIncomingMessage_Handler implements JsonMessageHandler {
|
||||
return NetExceptionResponseFactory.error(req, WireCodes.Status.BAD_REQUEST, ex.getMessage(), "Некорректный payload подтверждения");
|
||||
}
|
||||
|
||||
boolean inserted = incoming.isContentType()
|
||||
SignedMessagesV2DAO.ApplyStatus status = incoming.isContentType()
|
||||
? SignedMessagesV2DAO.getInstance().upsertIncomingCopy(entry)
|
||||
: SignedMessagesV2DAO.getInstance().insertIfAbsent(entry);
|
||||
SignedMessagesRealtime.DeliveryCounters counters = new SignedMessagesRealtime.DeliveryCounters();
|
||||
if (inserted) {
|
||||
if (status.applied()) {
|
||||
counters = SignedMessagesRealtime.deliverToRelevantSessions(entry, incoming);
|
||||
}
|
||||
if (status == SignedMessagesV2DAO.ApplyStatus.BLOCKED_BY_CONVERSATION_TOMBSTONE) {
|
||||
bounceConversationDeleteIfKnown(incoming.fromLogin, incoming.toLogin);
|
||||
}
|
||||
|
||||
Net_ReceiveIncomingMessage_Response resp = new Net_ReceiveIncomingMessage_Response();
|
||||
resp.setOp(req.getOp());
|
||||
@@ -67,4 +72,15 @@ public class Net_ReceiveIncomingMessage_Handler implements JsonMessageHandler {
|
||||
private boolean isBlank(String s) {
|
||||
return s == null || s.isBlank();
|
||||
}
|
||||
|
||||
private void bounceConversationDeleteIfKnown(String fromLogin, String toLogin) throws Exception {
|
||||
SignedMessageV2Entry tombstone = SignedMessagesV2DAO.getInstance().getLatestConversationDelete(fromLogin, toLogin);
|
||||
if (tombstone == null || tombstone.getRawBlock() == null || tombstone.getRawBlock().length == 0) return;
|
||||
server.sync.DmFederationService.fanOutDeleteConversation(
|
||||
tombstone.getFromLogin(),
|
||||
tombstone.getToLogin(),
|
||||
tombstone.getMessageType(),
|
||||
Base64.getEncoder().encodeToString(tombstone.getRawBlock())
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
+25
-6
@@ -12,6 +12,8 @@ import server.sync.DmFederationService;
|
||||
import shine.db.dao.SignedMessagesV2DAO;
|
||||
import shine.db.entities.SignedMessageV2Entry;
|
||||
|
||||
import java.util.Base64;
|
||||
|
||||
public class Net_SendMessagePair_Handler implements JsonMessageHandler {
|
||||
@Override
|
||||
public Net_Response handle(Net_Request baseRequest, ConnectionContext ctx) throws Exception {
|
||||
@@ -50,17 +52,19 @@ public class Net_SendMessagePair_Handler implements JsonMessageHandler {
|
||||
return NetExceptionResponseFactory.error(req, WireCodes.Status.BAD_REQUEST, ex.getMessage(), "Некорректный payload подтверждения");
|
||||
}
|
||||
|
||||
boolean pairInserted;
|
||||
SignedMessagesV2DAO.ApplyStatus pairStatus;
|
||||
if (incoming.isContentType()) {
|
||||
pairInserted = SignedMessagesV2DAO.getInstance().upsertContentPair(
|
||||
pairStatus = SignedMessagesV2DAO.getInstance().upsertContentPair(
|
||||
incomingEntry, outgoingEntry
|
||||
);
|
||||
} else {
|
||||
pairInserted = SignedMessagesV2DAO.getInstance().insertPairBothOrNothing(incomingEntry, outgoingEntry);
|
||||
pairStatus = SignedMessagesV2DAO.getInstance().insertPairBothOrNothing(incomingEntry, outgoingEntry)
|
||||
? SignedMessagesV2DAO.ApplyStatus.APPLIED
|
||||
: SignedMessagesV2DAO.ApplyStatus.DUPLICATE_OR_OLDER;
|
||||
}
|
||||
|
||||
SignedMessagesRealtime.DeliveryCounters inCounters = new SignedMessagesRealtime.DeliveryCounters();
|
||||
if (pairInserted) {
|
||||
if (pairStatus.applied()) {
|
||||
inCounters = SignedMessagesRealtime.deliverToRelevantSessions(incomingEntry, incoming);
|
||||
}
|
||||
|
||||
@@ -69,11 +73,15 @@ public class Net_SendMessagePair_Handler implements JsonMessageHandler {
|
||||
excludeSessionId = ctx.getSessionId();
|
||||
}
|
||||
SignedMessagesRealtime.DeliveryCounters outCounters = new SignedMessagesRealtime.DeliveryCounters();
|
||||
if (pairInserted) {
|
||||
if (pairStatus.applied()) {
|
||||
outCounters = SignedMessagesRealtime.deliverToRelevantSessions(outgoingEntry, outgoing, excludeSessionId);
|
||||
}
|
||||
|
||||
if (pairInserted && ctx != null && ctx.isAuthenticatedUser()) {
|
||||
if (pairStatus == SignedMessagesV2DAO.ApplyStatus.BLOCKED_BY_CONVERSATION_TOMBSTONE) {
|
||||
bounceConversationDeleteIfKnown(incoming.fromLogin, incoming.toLogin);
|
||||
}
|
||||
|
||||
if (pairStatus.applied() && ctx != null && ctx.isAuthenticatedUser()) {
|
||||
DmFederationService.fanOutPair(
|
||||
incoming.fromLogin,
|
||||
incoming.toLogin,
|
||||
@@ -97,4 +105,15 @@ public class Net_SendMessagePair_Handler implements JsonMessageHandler {
|
||||
private boolean isBlank(String s) {
|
||||
return s == null || s.isBlank();
|
||||
}
|
||||
|
||||
private void bounceConversationDeleteIfKnown(String fromLogin, String toLogin) throws Exception {
|
||||
SignedMessageV2Entry tombstone = SignedMessagesV2DAO.getInstance().getLatestConversationDelete(fromLogin, toLogin);
|
||||
if (tombstone == null || tombstone.getRawBlock() == null || tombstone.getRawBlock().length == 0) return;
|
||||
DmFederationService.fanOutDeleteConversation(
|
||||
tombstone.getFromLogin(),
|
||||
tombstone.getToLogin(),
|
||||
tombstone.getMessageType(),
|
||||
Base64.getEncoder().encodeToString(tombstone.getRawBlock())
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
-6
@@ -116,12 +116,6 @@ final class SignedMessageBlock {
|
||||
if (messageType == TYPE_INCOMING_TEXT || messageType == TYPE_OUTGOING_COPY) {
|
||||
if (bodyBytes.length == 0) throw new IllegalArgumentException("BAD_MESSAGE_LEN");
|
||||
encryptedBody = EncryptedBodyPayload.parse(bodyBytes, maxBodyBytes);
|
||||
if (revisionTimeMs == 0 && reencryptedAtMs != 0) {
|
||||
throw new IllegalArgumentException("BAD_REENCRYPTED_TIME");
|
||||
}
|
||||
if (reencryptedAtMs > 0 && revisionTimeMs == 0) {
|
||||
throw new IllegalArgumentException("BAD_REENCRYPTED_TIME");
|
||||
}
|
||||
} else if (messageType == TYPE_READ_INCOMING || messageType == TYPE_READ_OUTGOING_COPY) {
|
||||
if (bodyBytes.length == 0) throw new IllegalArgumentException("BAD_RECEIPT_PAYLOAD_LEN");
|
||||
if (revisionTimeMs != 0 || reencryptedAtMs != 0) {
|
||||
|
||||
+10
-2
@@ -1,5 +1,6 @@
|
||||
package server.logic.ws_protocol.JSON.messages;
|
||||
|
||||
import server.logic.ws_protocol.JSON.handlers.auth.SolanaUserPdaImportService;
|
||||
import shine.db.dao.SolanaUsersDAO;
|
||||
import shine.db.entities.SignedMessageV2Entry;
|
||||
import shine.db.entities.SolanaUserEntry;
|
||||
@@ -46,8 +47,8 @@ final class SignedMessagesCore {
|
||||
}
|
||||
|
||||
static void verifyUsersAndSignature(SignedMessageBlock block) throws Exception {
|
||||
SolanaUserEntry from = SolanaUsersDAO.getInstance().getByLogin(block.fromLogin);
|
||||
SolanaUserEntry to = SolanaUsersDAO.getInstance().getByLogin(block.toLogin);
|
||||
SolanaUserEntry from = resolveUser(block.fromLogin);
|
||||
SolanaUserEntry to = resolveUser(block.toLogin);
|
||||
if (from == null || to == null) {
|
||||
throw new IllegalArgumentException("USER_NOT_FOUND");
|
||||
}
|
||||
@@ -60,6 +61,13 @@ final class SignedMessagesCore {
|
||||
}
|
||||
}
|
||||
|
||||
private static SolanaUserEntry resolveUser(String login) throws Exception {
|
||||
SolanaUsersDAO usersDao = SolanaUsersDAO.getInstance();
|
||||
SolanaUserEntry user = usersDao.getByLogin(login);
|
||||
if (user != null) return user;
|
||||
return SolanaUserPdaImportService.findOrImportByLogin(login);
|
||||
}
|
||||
|
||||
static void validatePair(SignedMessageBlock incoming, SignedMessageBlock outgoing) {
|
||||
if (incoming.messageType % 2 == 0) throw new IllegalArgumentException("BAD_PAIR_TYPES");
|
||||
if (outgoing.messageType != incoming.messageType + 1) throw new IllegalArgumentException("BAD_PAIR_TYPES");
|
||||
|
||||
+2
-2
@@ -1,2 +1,2 @@
|
||||
client.version=1.2.299
|
||||
server.version=1.2.279
|
||||
client.version=1.2.300
|
||||
server.version=1.2.280
|
||||
|
||||
+1
-1
@@ -1088,7 +1088,7 @@ async function init() {
|
||||
let refBaseKey = String(payload.receiptRefBaseKey || '').trim();
|
||||
if (!refBaseKey) {
|
||||
try {
|
||||
const ref = authService.parseReadReceiptPayload(parsed.payloadBytes);
|
||||
const ref = parsed.readReceiptPayload || authService.parseReadReceiptPayload(parsed.payloadBytes);
|
||||
refBaseKey = `${ref.refFromLogin}|${ref.refToLogin}|${ref.refTimeMs}|${ref.refNonce}`;
|
||||
} catch {}
|
||||
}
|
||||
|
||||
@@ -330,6 +330,42 @@ function parseEncryptedDmBodyBytes(payloadBytes) {
|
||||
};
|
||||
}
|
||||
|
||||
function parseReadReceiptBodyBytes(payloadBytes) {
|
||||
if (!(payloadBytes instanceof Uint8Array)) throw new Error('BAD_RECEIPT_PAYLOAD_LEN');
|
||||
let o = 0;
|
||||
const read = (n) => {
|
||||
if (o + n > payloadBytes.length) throw new Error('BAD_RECEIPT_PAYLOAD_LEN');
|
||||
const out = payloadBytes.slice(o, o + n);
|
||||
o += n;
|
||||
return out;
|
||||
};
|
||||
const readU8 = () => read(1)[0];
|
||||
const readU32 = () => {
|
||||
const part = read(4);
|
||||
return new DataView(part.buffer, part.byteOffset, 4).getUint32(0, false);
|
||||
};
|
||||
const readU64 = () => {
|
||||
const part = read(8);
|
||||
return Number(new DataView(part.buffer, part.byteOffset, 8).getBigUint64(0, false));
|
||||
};
|
||||
const readAscii = (code) => {
|
||||
const len = readU8();
|
||||
const part = read(len);
|
||||
for (let i = 0; i < part.length; i += 1) {
|
||||
const c = part[i];
|
||||
if (c < 0x20 || c > 0x7e) throw new Error(code);
|
||||
}
|
||||
return new TextDecoder().decode(part);
|
||||
};
|
||||
|
||||
const refToLogin = readAscii('BAD_RECEIPT_TO_LOGIN');
|
||||
const refFromLogin = readAscii('BAD_RECEIPT_FROM_LOGIN');
|
||||
const refTimeMs = readU64();
|
||||
const refNonce = readU32();
|
||||
if (o !== payloadBytes.length) throw new Error('BAD_RECEIPT_PAYLOAD_LEN');
|
||||
return { refToLogin, refFromLogin, refTimeMs, refNonce };
|
||||
}
|
||||
|
||||
function parseSignedMessageBlockBytes(bytes) {
|
||||
if (!(bytes instanceof Uint8Array)) throw new Error('Expected Uint8Array');
|
||||
let o = 0;
|
||||
@@ -374,11 +410,17 @@ function parseSignedMessageBlockBytes(bytes) {
|
||||
read(DM_PREFIX_V1.length);
|
||||
const formatVersionMajor = readU8();
|
||||
const formatVersionMinor = readU8();
|
||||
if (formatVersionMajor !== DM_FORMAT_VERSION_MAJOR || formatVersionMinor !== DM_FORMAT_VERSION_MINOR) {
|
||||
throw new Error('BAD_FORMAT_VERSION');
|
||||
}
|
||||
const toLogin = readAscii();
|
||||
const fromLogin = readAscii();
|
||||
const timeMs = readU64();
|
||||
const nonce = readU32();
|
||||
const messageType = readU8();
|
||||
if (messageType < DM_TYPE_INCOMING || messageType > DM_TYPE_CONVERSATION_DELETED_BY_RECIPIENT) {
|
||||
throw new Error('BAD_MESSAGE_TYPE');
|
||||
}
|
||||
const revisionTimeMs = readU64();
|
||||
const reencryptedAtMs = readU64();
|
||||
const bodyLen = readU32();
|
||||
@@ -392,9 +434,20 @@ function parseSignedMessageBlockBytes(bytes) {
|
||||
let encryptedBodyPayload = null;
|
||||
let readReceiptPayload = null;
|
||||
if (messageType === DM_TYPE_INCOMING || messageType === DM_TYPE_OUTGOING_COPY) {
|
||||
if (bodyBytes.length === 0) throw new Error('BAD_MESSAGE_LEN');
|
||||
encryptedBodyPayload = parseEncryptedDmBodyBytes(bodyBytes);
|
||||
} else if (messageType === DM_TYPE_READ_INCOMING || messageType === DM_TYPE_READ_OUTGOING_COPY) {
|
||||
readReceiptPayload = null;
|
||||
if (bodyBytes.length === 0) throw new Error('BAD_RECEIPT_PAYLOAD_LEN');
|
||||
if (revisionTimeMs !== 0 || reencryptedAtMs !== 0) throw new Error('BAD_RECEIPT_REVISION');
|
||||
readReceiptPayload = parseReadReceiptBodyBytes(bodyBytes);
|
||||
} else if (messageType === DM_TYPE_MESSAGE_DELETED_BY_SENDER || messageType === DM_TYPE_MESSAGE_DELETED_BY_RECIPIENT) {
|
||||
if (bodyBytes.length !== 0) throw new Error('BAD_DELETE_BODY');
|
||||
if (revisionTimeMs <= 0) throw new Error('BAD_REVISION_TIME');
|
||||
if (reencryptedAtMs !== 0) throw new Error('BAD_REENCRYPTED_TIME');
|
||||
} else if (messageType === DM_TYPE_CONVERSATION_DELETED_BY_SENDER || messageType === DM_TYPE_CONVERSATION_DELETED_BY_RECIPIENT) {
|
||||
if (bodyBytes.length !== 0) throw new Error('BAD_DELETE_BODY');
|
||||
if (revisionTimeMs !== 0) throw new Error('BAD_REVISION_TIME');
|
||||
if (reencryptedAtMs !== 0) throw new Error('BAD_REENCRYPTED_TIME');
|
||||
}
|
||||
|
||||
return {
|
||||
@@ -2278,34 +2331,7 @@ export class AuthService {
|
||||
}
|
||||
|
||||
parseReadReceiptPayload(payloadBytes) {
|
||||
if (!(payloadBytes instanceof Uint8Array)) throw new Error('Expected Uint8Array');
|
||||
let o = 0;
|
||||
const read = (n) => {
|
||||
if (o + n > payloadBytes.length) throw new Error('BAD_RECEIPT_LEN');
|
||||
const out = payloadBytes.slice(o, o + n);
|
||||
o += n;
|
||||
return out;
|
||||
};
|
||||
const readU8 = () => read(1)[0];
|
||||
const readU32 = () => {
|
||||
const part = read(4);
|
||||
return new DataView(part.buffer, part.byteOffset, 4).getUint32(0, false);
|
||||
};
|
||||
const readU64 = () => {
|
||||
const part = read(8);
|
||||
return Number(new DataView(part.buffer, part.byteOffset, 8).getBigUint64(0, false));
|
||||
};
|
||||
const readAscii = () => {
|
||||
const len = readU8();
|
||||
const part = read(len);
|
||||
return new TextDecoder().decode(part);
|
||||
};
|
||||
const refToLogin = readAscii();
|
||||
const refFromLogin = readAscii();
|
||||
const refTimeMs = readU64();
|
||||
const refNonce = readU32();
|
||||
if (o !== payloadBytes.length) throw new Error('BAD_RECEIPT_LEN');
|
||||
return { refToLogin, refFromLogin, refTimeMs, refNonce };
|
||||
return parseReadReceiptBodyBytes(payloadBytes);
|
||||
}
|
||||
|
||||
async decryptSignedMessageContent({ parsed, blobB64 = '', login, storagePwd }) {
|
||||
|
||||
Reference in New Issue
Block a user