From 185d90e8fc847e3484b11aefc468df14dbf94669 Mon Sep 17 00:00:00 2001 From: otsmr Date: Thu, 24 Sep 2026 14:30:38 +0200 Subject: [PATCH] fix broken signal sessions --- .gitignore | 3 +- .../xcshareddata/swiftpm/Package.resolved | 12 +- ios/build/.last_build_id | 1 - lib/core/bridge/wrapper/signal.dart | 8 + lib/core/frb_generated.dart | 77 ++- lib/src/database/daos/receipts.dao.dart | 37 ++ .../generated/app_localizations.dart | 122 ++++- .../generated/app_localizations_de.dart | 87 ++- .../generated/app_localizations_en.dart | 86 ++- lib/src/localization/translations/de.arb | 25 + lib/src/localization/translations/en.arb | 25 + .../api/websocket/client_to_server.pb.dart | 516 +++++++++++++++++- .../websocket/client_to_server.pbjson.dart | 411 +++++++++++--- .../api/websocket/server_to_client.pb.dart | 19 +- .../websocket/server_to_client.pbjson.dart | 11 +- lib/src/services/api/api.service.dart | 6 +- lib/src/services/api/messages.api.dart | 85 ++- lib/src/services/api/server_messages.api.dart | 33 ++ lib/src/services/signal/identity.signal.dart | 2 + lib/src/services/signal/session.signal.dart | 211 ++++--- .../main_camera_controller.dart | 6 +- .../visual/views/contact/contact.view.dart | 46 ++ pubspec.yaml | 2 +- rust/src/backup/backup_archive.rs | 155 +++++- rust/src/bridge/wrapper/signal.rs | 11 + rust/src/frb_generated.rs | 79 ++- rust/src/signal/engine.rs | 92 +++- test/model/mailbox_drained_proto_test.dart | 13 + test/model/pqc_upload_proto_test.dart | 27 + .../receipts_out_of_sync_detection_test.dart | 86 +++ 30 files changed, 2009 insertions(+), 285 deletions(-) delete mode 100644 ios/build/.last_build_id create mode 100644 test/model/mailbox_drained_proto_test.dart create mode 100644 test/model/pqc_upload_proto_test.dart create mode 100644 test/services/receipts_out_of_sync_detection_test.dart diff --git a/.gitignore b/.gitignore index 10e2410d..cac9d13e 100644 --- a/.gitignore +++ b/.gitignore @@ -56,4 +56,5 @@ devtools_options.yaml rust/target rust_dependencies/target fastlane/repo/status/running.json -.cache/ \ No newline at end of file +.cache/ +APKS/*.apk diff --git a/ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved b/ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved index 3a5a88ff..881ef11f 100644 --- a/ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved +++ b/ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved @@ -14,8 +14,8 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/google/app-check.git", "state" : { - "revision" : "61b85103a1aeed8218f17c794687781505fbbef5", - "version" : "11.2.0" + "revision" : "97f7d74dd0e8f3d0fe5fc75cc8957aa73bdc948a", + "version" : "11.3.2" } }, { @@ -23,8 +23,8 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/firebase/firebase-ios-sdk", "state" : { - "revision" : "8d5b4189f1f482df8d5c58c9985ea70491ef5382", - "version" : "12.14.0" + "revision" : "346daa9f46316aa372b35b317e18224acc2e9063", + "version" : "12.18.0" } }, { @@ -50,8 +50,8 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/google/GoogleAppMeasurement.git", "state" : { - "revision" : "219e564a8510e983e675c94f77f7f7c50049f22d", - "version" : "12.14.0" + "revision" : "f04760d460296cc0fa430935a7be212e5bd67fc5", + "version" : "12.18.0" } }, { diff --git a/ios/build/.last_build_id b/ios/build/.last_build_id deleted file mode 100644 index 7769d20e..00000000 --- a/ios/build/.last_build_id +++ /dev/null @@ -1 +0,0 @@ -ce49e7d90cd902197f9a9cbc84219d23 \ No newline at end of file diff --git a/lib/core/bridge/wrapper/signal.dart b/lib/core/bridge/wrapper/signal.dart index 6aaf439a..c659e702 100644 --- a/lib/core/bridge/wrapper/signal.dart +++ b/lib/core/bridge/wrapper/signal.dart @@ -47,6 +47,14 @@ class RustSignal { bundle: bundle, ); + /// Removes the current v2 session for a contact. The Dart hotfix layer + /// immediately establishes its replacement from a freshly fetched bundle. + static Future resetContactSession({required PlatformInt64 contactId}) => + RustLib.instance.api + .crateBridgeWrapperSignalRustSignalResetContactSession( + contactId: contactId, + ); + @override int get hashCode => 0; diff --git a/lib/core/frb_generated.dart b/lib/core/frb_generated.dart index 67e16043..35bdf234 100644 --- a/lib/core/frb_generated.dart +++ b/lib/core/frb_generated.dart @@ -78,7 +78,7 @@ class RustLib extends BaseEntrypoint { String get codegenVersion => '2.12.0'; @override - int get rustContentHash => 1109927244; + int get rustContentHash => -2042422055; static const kDefaultExternalLibraryLoaderConfig = ExternalLibraryLoaderConfig( @@ -284,6 +284,10 @@ abstract class RustLibApi extends BaseApi { required FrbPreKeyBundle bundle, }); + Future crateBridgeWrapperSignalRustSignalResetContactSession({ + required PlatformInt64 contactId, + }); + Future> crateBridgeWrapperRustUtilsGenerateShares({ required List secret, required int total, @@ -1853,6 +1857,41 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { argNames: ["name", "deviceId", "bundle"], ); + @override + Future crateBridgeWrapperSignalRustSignalResetContactSession({ + required PlatformInt64 contactId, + }) { + return handler.executeNormal( + NormalTask( + callFfi: (port_) { + final serializer = SseSerializer(generalizedFrbRustBinding); + sse_encode_i_64(contactId, serializer); + pdeCallFfi( + generalizedFrbRustBinding, + serializer, + funcId: 37, + port: port_, + ); + }, + codec: SseCodec( + decodeSuccessData: sse_decode_unit, + decodeErrorData: sse_decode_AnyhowException, + ), + constMeta: + kCrateBridgeWrapperSignalRustSignalResetContactSessionConstMeta, + argValues: [contactId], + apiImpl: this, + ), + ); + } + + TaskConstMeta + get kCrateBridgeWrapperSignalRustSignalResetContactSessionConstMeta => + const TaskConstMeta( + debugName: "rust_signal_reset_contact_session", + argNames: ["contactId"], + ); + @override Future> crateBridgeWrapperRustUtilsGenerateShares({ required List secret, @@ -1869,7 +1908,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { pdeCallFfi( generalizedFrbRustBinding, serializer, - funcId: 37, + funcId: 38, port: port_, ); }, @@ -1904,7 +1943,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { pdeCallFfi( generalizedFrbRustBinding, serializer, - funcId: 38, + funcId: 39, port: port_, ); }, @@ -1940,7 +1979,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { pdeCallFfi( generalizedFrbRustBinding, serializer, - funcId: 39, + funcId: 40, port: port_, ); }, @@ -1977,7 +2016,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { pdeCallFfi( generalizedFrbRustBinding, serializer, - funcId: 40, + funcId: 41, port: port_, ); }, @@ -2015,7 +2054,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { pdeCallFfi( generalizedFrbRustBinding, serializer, - funcId: 41, + funcId: 42, port: port_, ); }, @@ -2053,7 +2092,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { pdeCallFfi( generalizedFrbRustBinding, serializer, - funcId: 42, + funcId: 43, port: port_, ); }, @@ -2091,7 +2130,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { pdeCallFfi( generalizedFrbRustBinding, serializer, - funcId: 43, + funcId: 44, port: port_, ); }, @@ -2130,7 +2169,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { pdeCallFfi( generalizedFrbRustBinding, serializer, - funcId: 44, + funcId: 45, port: port_, ); }, @@ -2169,7 +2208,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { pdeCallFfi( generalizedFrbRustBinding, serializer, - funcId: 45, + funcId: 46, port: port_, ); }, @@ -2214,7 +2253,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { pdeCallFfi( generalizedFrbRustBinding, serializer, - funcId: 46, + funcId: 47, port: port_, ); }, @@ -2266,7 +2305,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { pdeCallFfi( generalizedFrbRustBinding, serializer, - funcId: 47, + funcId: 48, port: port_, ); }, @@ -2307,7 +2346,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { pdeCallFfi( generalizedFrbRustBinding, serializer, - funcId: 48, + funcId: 49, port: port_, ); }, @@ -2345,7 +2384,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { pdeCallFfi( generalizedFrbRustBinding, serializer, - funcId: 49, + funcId: 50, port: port_, ); }, @@ -2383,7 +2422,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { pdeCallFfi( generalizedFrbRustBinding, serializer, - funcId: 50, + funcId: 51, port: port_, ); }, @@ -2421,7 +2460,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { pdeCallFfi( generalizedFrbRustBinding, serializer, - funcId: 51, + funcId: 52, port: port_, ); }, @@ -2459,7 +2498,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { pdeCallFfi( generalizedFrbRustBinding, serializer, - funcId: 52, + funcId: 53, port: port_, ); }, @@ -2501,7 +2540,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { pdeCallFfi( generalizedFrbRustBinding, serializer, - funcId: 53, + funcId: 54, port: port_, ); }, @@ -2541,7 +2580,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { pdeCallFfi( generalizedFrbRustBinding, serializer, - funcId: 54, + funcId: 55, port: port_, ); }, diff --git a/lib/src/database/daos/receipts.dao.dart b/lib/src/database/daos/receipts.dao.dart index daa68a97..189a89e4 100644 --- a/lib/src/database/daos/receipts.dao.dart +++ b/lib/src/database/daos/receipts.dao.dart @@ -150,6 +150,43 @@ class ReceiptsDao extends DatabaseAccessor with _$ReceiptsDaoMixin { .get(); } + /// Finds contacts for which repeated, unacknowledged message delivery is a + /// strong indication that only the outgoing Signal ratchet is broken. + Future> getContactsWithStuckSignalMessages({ + required int retryThreshold, + int minimumMessages = 2, + }) async { + final messageCount = countAll(); + final query = selectOnly(receipts) + ..addColumns([receipts.contactId, messageCount]) + ..where( + receipts.retryCount.isBiggerThanValue(retryThreshold) & + receipts.willBeRetriedByMediaUpload.equals(false), + ) + ..groupBy([receipts.contactId]); + + final rows = await query.get(); + return rows + .where((row) => (row.read(messageCount) ?? 0) >= minimumMessages) + .map((row) => row.read(receipts.contactId)!) + .toList(); + } + + /// Starts retry accounting again after establishing a fresh Signal session. + Future resetMessageRetryCountsForContact(int contactId) async { + await (update(receipts)..where( + (receipt) => + receipt.contactId.equals(contactId) & + receipt.willBeRetriedByMediaUpload.equals(false), + )) + .write( + const ReceiptsCompanion( + retryCount: Value(0), + lastRetry: Value(null), + ), + ); + } + Future> getReceiptsForMediaRetransmissions() async { final markedRetriesTime = clock.now().subtract( const Duration( diff --git a/lib/src/localization/generated/app_localizations.dart b/lib/src/localization/generated/app_localizations.dart index 1f0b67fc..0ac28adc 100644 --- a/lib/src/localization/generated/app_localizations.dart +++ b/lib/src/localization/generated/app_localizations.dart @@ -635,13 +635,13 @@ abstract class AppLocalizations { /// No description provided for @settingsNotifyResetTitle. /// /// In en, this message translates to: - /// **'Didn\'t receive a test notification?'** + /// **'Reset notification tokens'** String get settingsNotifyResetTitle; /// No description provided for @settingsNotifyResetTitleSubtitle. /// /// In en, this message translates to: - /// **'If you haven\'t received any test notifications, click here to reset your notification tokens.'** + /// **'Reset your notification tokens if you have problems receiving push notifications.'** String get settingsNotifyResetTitleSubtitle; /// No description provided for @settingsNotifyResetTitleReset. @@ -956,6 +956,42 @@ abstract class AppLocalizations { /// **'Block'** String get contactBlock; + /// No description provided for @contactTroubleshoot. + /// + /// In en, this message translates to: + /// **'Troubleshoot connection'** + String get contactTroubleshoot; + + /// No description provided for @contactResetSessionTitle. + /// + /// In en, this message translates to: + /// **'Reset connection with {username}?'** + String contactResetSessionTitle(String username); + + /// No description provided for @contactResetSessionBody. + /// + /// In en, this message translates to: + /// **'twonly will remove the current encrypted connection and establish a new one. Messages currently in transit may be lost.'** + String get contactResetSessionBody; + + /// No description provided for @contactResetSessionConfirm. + /// + /// In en, this message translates to: + /// **'Reset connection'** + String get contactResetSessionConfirm; + + /// No description provided for @contactResetSessionSuccess. + /// + /// In en, this message translates to: + /// **'The encrypted connection with {username} was re-established.'** + String contactResetSessionSuccess(String username); + + /// No description provided for @contactResetSessionFailed. + /// + /// In en, this message translates to: + /// **'The connection could not be reset. Check your internet connection and try again.'** + String get contactResetSessionFailed; + /// No description provided for @contactBlockTitle. /// /// In en, this message translates to: @@ -1226,6 +1262,18 @@ abstract class AppLocalizations { /// **'You are part of the paid plan of {username}!'** String partOfPaidPlanOf(Object username); + /// No description provided for @partOfPaidPlanFrom. + /// + /// In en, this message translates to: + /// **'Part of the paid plan of'** + String get partOfPaidPlanFrom; + + /// No description provided for @subscriptionCurrentPlanBadge. + /// + /// In en, this message translates to: + /// **'Current'** + String get subscriptionCurrentPlanBadge; + /// No description provided for @year. /// /// In en, this message translates to: @@ -1253,67 +1301,67 @@ abstract class AppLocalizations { /// No description provided for @proFeature1. /// /// In en, this message translates to: - /// **'✓ Unlimited media file uploads'** + /// **'Unlimited media file uploads'** String get proFeature1; /// No description provided for @proFeature2. /// /// In en, this message translates to: - /// **'✓ 1 additional Plus user'** + /// **'1 additional Plus user'** String get proFeature2; /// No description provided for @proFeature3. /// /// In en, this message translates to: - /// **'✓ 25GB Memories storage'** + /// **'25GB Memories storage'** String get proFeature3; /// No description provided for @proFeature4. /// /// In en, this message translates to: - /// **'✓ Restore flames'** + /// **'Restore flames'** String get proFeature4; /// No description provided for @familyFeature1. /// /// In en, this message translates to: - /// **'✓ Unlimited media file uploads'** + /// **'Unlimited media file uploads'** String get familyFeature1; /// No description provided for @familyFeature2. /// /// In en, this message translates to: - /// **'✓ 4 additional Plus user'** + /// **'4 additional Plus user'** String get familyFeature2; /// No description provided for @familyFeature3. /// /// In en, this message translates to: - /// **'✓ 50GB Memories storage'** + /// **'50GB Memories storage'** String get familyFeature3; /// No description provided for @familyFeature4. /// /// In en, this message translates to: - /// **'✓ Support twonly'** + /// **'Restore flames'** String get familyFeature4; /// No description provided for @freeFeature1. /// /// In en, this message translates to: - /// **'✓ 10 Media file uploads per day'** + /// **'10 Media file uploads per day'** String get freeFeature1; /// No description provided for @plusFeature1. /// /// In en, this message translates to: - /// **'✓ Unlimited media file uploads'** + /// **'Unlimited media file uploads'** String get plusFeature1; /// No description provided for @plusFeature2. /// /// In en, this message translates to: - /// **'✓ Additional features (coming-soon)'** + /// **'Additional features (coming-soon)'** String get plusFeature2; /// No description provided for @manageAdditionalUsers. @@ -1838,6 +1886,12 @@ abstract class AppLocalizations { /// **'Edit history'** String get editHistory; + /// No description provided for @fileSize. + /// + /// In en, this message translates to: + /// **'File size'** + String get fileSize; + /// No description provided for @archivedChats. /// /// In en, this message translates to: @@ -2228,6 +2282,12 @@ abstract class AppLocalizations { /// **'Messages from other users.'** String get notificationCategoryMessageDesc; + /// No description provided for @notificationConnectionFallback. + /// + /// In en, this message translates to: + /// **'You may have new messages.'** + String get notificationConnectionFallback; + /// No description provided for @groupContextMenuDeleteGroup. /// /// In en, this message translates to: @@ -4537,6 +4597,42 @@ abstract class AppLocalizations { /// In en, this message translates to: /// **'Exporting memories...'** String get memoriesExportingProgress; + + /// No description provided for @fileLimitReachedTitle. + /// + /// In en, this message translates to: + /// **'File too large to send'** + String get fileLimitReachedTitle; + + /// No description provided for @fileLimitReachedDetail. + /// + /// In en, this message translates to: + /// **'This file is {size}, but your plan allows at most {limit} per send.'** + String fileLimitReachedDetail(String size, String limit); + + /// No description provided for @fileLimitReachedDetailNoSize. + /// + /// In en, this message translates to: + /// **'Your plan allows at most {limit} per send.'** + String fileLimitReachedDetailNoSize(String limit); + + /// No description provided for @fileLimitReachedHint. + /// + /// In en, this message translates to: + /// **'Record a shorter video and send it again.'** + String get fileLimitReachedHint; + + /// No description provided for @fileLimitReachedHintFree. + /// + /// In en, this message translates to: + /// **'Record a shorter video, or upgrade your plan to send larger files.'** + String get fileLimitReachedHintFree; + + /// No description provided for @fileLimitReachedUpgrade. + /// + /// In en, this message translates to: + /// **'Upgrade plan'** + String get fileLimitReachedUpgrade; } class _AppLocalizationsDelegate diff --git a/lib/src/localization/generated/app_localizations_de.dart b/lib/src/localization/generated/app_localizations_de.dart index 3a8fb0db..eefb8b3f 100644 --- a/lib/src/localization/generated/app_localizations_de.dart +++ b/lib/src/localization/generated/app_localizations_de.dart @@ -294,11 +294,11 @@ class AppLocalizationsDe extends AppLocalizations { 'Um eine Testbenachrichtigung zu erhalten, klicke auf OK. Falls du die Testbenachrichtigung nicht erhältst, klicke bitte auf den neuen Menüpunkt, der nach dem Klicken auf „OK“ angezeigt wird.'; @override - String get settingsNotifyResetTitle => 'Keine Testbenachrichtigung erhalten?'; + String get settingsNotifyResetTitle => 'Benachrichtigungstoken zurücksetzen'; @override String get settingsNotifyResetTitleSubtitle => - 'Falls du keine Testbenachrichtigungen erhalten hast, klicke hier, um deine Benachrichtigungstoken zurückzusetzen.'; + 'Setze deine Benachrichtigungstoken zurück, wenn du Probleme beim Empfang von Push-Benachrichtigungen hast.'; @override String get settingsNotifyResetTitleReset => @@ -475,6 +475,30 @@ class AppLocalizationsDe extends AppLocalizations { @override String get contactBlock => 'Blockieren'; + @override + String get contactTroubleshoot => 'Verbindungsprobleme beheben'; + + @override + String contactResetSessionTitle(String username) { + return 'Verbindung zu $username zurücksetzen?'; + } + + @override + String get contactResetSessionBody => + 'twonly löscht die aktuelle verschlüsselte Verbindung und baut sie anschließend neu auf. Nachrichten, die gerade übertragen werden, können dabei verloren gehen.'; + + @override + String get contactResetSessionConfirm => 'Verbindung zurücksetzen'; + + @override + String contactResetSessionSuccess(String username) { + return 'Die verschlüsselte Verbindung zu $username wurde neu aufgebaut.'; + } + + @override + String get contactResetSessionFailed => + 'Die Verbindung konnte nicht zurückgesetzt werden. Prüfe deine Internetverbindung und versuche es erneut.'; + @override String contactBlockTitle(Object username) { return 'Blockiere $username'; @@ -633,6 +657,12 @@ class AppLocalizationsDe extends AppLocalizations { return 'Du bist Teil des bezahlten Plans von $username!'; } + @override + String get partOfPaidPlanFrom => 'Teil des bezahlten Plans von'; + + @override + String get subscriptionCurrentPlanBadge => 'Aktuell'; + @override String get year => 'Jahr'; @@ -646,37 +676,37 @@ class AppLocalizationsDe extends AppLocalizations { String get monthly => 'Monatlich'; @override - String get proFeature1 => '✓ Unbegrenzte Medien-Datei-Uploads'; + String get proFeature1 => 'Unbegrenzte Medien-Datei-Uploads'; @override - String get proFeature2 => '✓ 1 zusätzlicher Plus Benutzer'; + String get proFeature2 => '1 zusätzlicher Plus Benutzer'; @override - String get proFeature3 => '✓ 25GB Memories Speicher'; + String get proFeature3 => '25GB Memories Speicher'; @override - String get proFeature4 => '✓ Flammen wiederherstellen'; + String get proFeature4 => 'Flammen wiederherstellen'; @override - String get familyFeature1 => '✓ Unbegrenzte Medien-Datei-Uploads'; + String get familyFeature1 => 'Unbegrenzte Medien-Datei-Uploads'; @override - String get familyFeature2 => '✓ 4 zusätzliche Plus Benutzer'; + String get familyFeature2 => '4 zusätzliche Plus Benutzer'; @override - String get familyFeature3 => '✓ 50GB Memories Speicher'; + String get familyFeature3 => '50GB Memories Speicher'; @override - String get familyFeature4 => '✓ Flammen wiederherstellen'; + String get familyFeature4 => 'Flammen wiederherstellen'; @override - String get freeFeature1 => '✓ 10 Medien-Datei-Uploads pro Tag'; + String get freeFeature1 => '10 Medien-Datei-Uploads pro Tag'; @override - String get plusFeature1 => '✓ Unbegrenzte Medien-Datei-Uploads'; + String get plusFeature1 => 'Unbegrenzte Medien-Datei-Uploads'; @override - String get plusFeature2 => '✓ Zusatzfunktionen (coming-soon)'; + String get plusFeature2 => 'Zusatzfunktionen (coming-soon)'; @override String get manageAdditionalUsers => 'Zusätzliche Benutzer verwalten'; @@ -983,6 +1013,9 @@ class AppLocalizationsDe extends AppLocalizations { @override String get editHistory => 'Bearbeitungshistorie'; + @override + String get fileSize => 'Dateigröße'; + @override String get archivedChats => 'Archivierte Chats'; @@ -1241,6 +1274,10 @@ class AppLocalizationsDe extends AppLocalizations { String get notificationCategoryMessageDesc => 'Nachrichten von anderen Benutzern.'; + @override + String get notificationConnectionFallback => + 'Du könntest neue Nachrichten haben.'; + @override String get groupContextMenuDeleteGroup => 'Dadurch werden alle Nachrichten in diesem Chat dauerhaft gelöscht.'; @@ -2622,4 +2659,28 @@ class AppLocalizationsDe extends AppLocalizations { @override String get memoriesExportingProgress => 'Erinnerungen werden exportiert...'; + + @override + String get fileLimitReachedTitle => 'Datei zu groß zum Senden'; + + @override + String fileLimitReachedDetail(String size, String limit) { + return 'Diese Datei ist $size groß, dein Tarif erlaubt aber höchstens $limit pro Sendung.'; + } + + @override + String fileLimitReachedDetailNoSize(String limit) { + return 'Dein Tarif erlaubt höchstens $limit pro Sendung.'; + } + + @override + String get fileLimitReachedHint => + 'Nimm ein kürzeres Video auf und sende es erneut.'; + + @override + String get fileLimitReachedHintFree => + 'Nimm ein kürzeres Video auf oder wechsle den Tarif, um größere Dateien zu senden.'; + + @override + String get fileLimitReachedUpgrade => 'Tarif wechseln'; } diff --git a/lib/src/localization/generated/app_localizations_en.dart b/lib/src/localization/generated/app_localizations_en.dart index 90201697..b4737f77 100644 --- a/lib/src/localization/generated/app_localizations_en.dart +++ b/lib/src/localization/generated/app_localizations_en.dart @@ -290,11 +290,11 @@ class AppLocalizationsEn extends AppLocalizations { 'Press OK to receive a test notification. If you do not receive the test notification, please click on the new menu item that appears after you click “OK”.'; @override - String get settingsNotifyResetTitle => 'Didn\'t receive a test notification?'; + String get settingsNotifyResetTitle => 'Reset notification tokens'; @override String get settingsNotifyResetTitleSubtitle => - 'If you haven\'t received any test notifications, click here to reset your notification tokens.'; + 'Reset your notification tokens if you have problems receiving push notifications.'; @override String get settingsNotifyResetTitleReset => @@ -471,6 +471,30 @@ class AppLocalizationsEn extends AppLocalizations { @override String get contactBlock => 'Block'; + @override + String get contactTroubleshoot => 'Troubleshoot connection'; + + @override + String contactResetSessionTitle(String username) { + return 'Reset connection with $username?'; + } + + @override + String get contactResetSessionBody => + 'twonly will remove the current encrypted connection and establish a new one. Messages currently in transit may be lost.'; + + @override + String get contactResetSessionConfirm => 'Reset connection'; + + @override + String contactResetSessionSuccess(String username) { + return 'The encrypted connection with $username was re-established.'; + } + + @override + String get contactResetSessionFailed => + 'The connection could not be reset. Check your internet connection and try again.'; + @override String contactBlockTitle(Object username) { return 'Block $username'; @@ -628,6 +652,12 @@ class AppLocalizationsEn extends AppLocalizations { return 'You are part of the paid plan of $username!'; } + @override + String get partOfPaidPlanFrom => 'Part of the paid plan of'; + + @override + String get subscriptionCurrentPlanBadge => 'Current'; + @override String get year => 'year'; @@ -641,37 +671,37 @@ class AppLocalizationsEn extends AppLocalizations { String get monthly => 'Monthly'; @override - String get proFeature1 => '✓ Unlimited media file uploads'; + String get proFeature1 => 'Unlimited media file uploads'; @override - String get proFeature2 => '✓ 1 additional Plus user'; + String get proFeature2 => '1 additional Plus user'; @override - String get proFeature3 => '✓ 25GB Memories storage'; + String get proFeature3 => '25GB Memories storage'; @override - String get proFeature4 => '✓ Restore flames'; + String get proFeature4 => 'Restore flames'; @override - String get familyFeature1 => '✓ Unlimited media file uploads'; + String get familyFeature1 => 'Unlimited media file uploads'; @override - String get familyFeature2 => '✓ 4 additional Plus user'; + String get familyFeature2 => '4 additional Plus user'; @override - String get familyFeature3 => '✓ 50GB Memories storage'; + String get familyFeature3 => '50GB Memories storage'; @override - String get familyFeature4 => '✓ Support twonly'; + String get familyFeature4 => 'Restore flames'; @override - String get freeFeature1 => '✓ 10 Media file uploads per day'; + String get freeFeature1 => '10 Media file uploads per day'; @override - String get plusFeature1 => '✓ Unlimited media file uploads'; + String get plusFeature1 => 'Unlimited media file uploads'; @override - String get plusFeature2 => '✓ Additional features (coming-soon)'; + String get plusFeature2 => 'Additional features (coming-soon)'; @override String get manageAdditionalUsers => 'Manage additional users'; @@ -976,6 +1006,9 @@ class AppLocalizationsEn extends AppLocalizations { @override String get editHistory => 'Edit history'; + @override + String get fileSize => 'File size'; + @override String get archivedChats => 'Archived chats'; @@ -1233,6 +1266,9 @@ class AppLocalizationsEn extends AppLocalizations { @override String get notificationCategoryMessageDesc => 'Messages from other users.'; + @override + String get notificationConnectionFallback => 'You may have new messages.'; + @override String get groupContextMenuDeleteGroup => 'This will permanently delete all messages in this chat.'; @@ -2597,4 +2633,28 @@ class AppLocalizationsEn extends AppLocalizations { @override String get memoriesExportingProgress => 'Exporting memories...'; + + @override + String get fileLimitReachedTitle => 'File too large to send'; + + @override + String fileLimitReachedDetail(String size, String limit) { + return 'This file is $size, but your plan allows at most $limit per send.'; + } + + @override + String fileLimitReachedDetailNoSize(String limit) { + return 'Your plan allows at most $limit per send.'; + } + + @override + String get fileLimitReachedHint => + 'Record a shorter video and send it again.'; + + @override + String get fileLimitReachedHintFree => + 'Record a shorter video, or upgrade your plan to send larger files.'; + + @override + String get fileLimitReachedUpgrade => 'Upgrade plan'; } diff --git a/lib/src/localization/translations/de.arb b/lib/src/localization/translations/de.arb index 832c7abc..7c9a6c45 100644 --- a/lib/src/localization/translations/de.arb +++ b/lib/src/localization/translations/de.arb @@ -94,6 +94,10 @@ "settingsNotification": "Benachrichtigung", "settingsNotifyPermission": "Benachrichtigungsberechtigung", "settingsNotifyPermissionDesc": "Systemeinstellungen öffnen, um Push-Benachrichtigungen zu erlauben.", + "settingsNotifyTroubleshooting": "Fehlersuche", + "settingsNotifyTroubleshootingDesc": "Hier klicken, wenn Probleme beim Empfang von Push-Benachrichtigungen auftreten.", + "settingsNotifyTroubleshootingNoProblem": "Kein Problem festgestellt", + "settingsNotifyTroubleshootingNoProblemDesc": "Um eine Testbenachrichtigung zu erhalten, klicke auf OK. Falls du die Testbenachrichtigung nicht erhältst, klicke bitte auf den neuen Menüpunkt, der nach dem Klicken auf „OK“ angezeigt wird.", "settingsNotifyResetTitle": "Benachrichtigungstoken zurücksetzen", "settingsNotifyResetTitleSubtitle": "Setze deine Benachrichtigungstoken zurück, wenn du Probleme beim Empfang von Push-Benachrichtigungen hast.", "settingsNotifyResetTitleReset": "Deine Benachrichtigungstoken wurden zurückgesetzt.", @@ -168,6 +172,26 @@ "contactNickname": "Spitzname", "contactNicknameNew": "Neuer Spitzname", "contactBlock": "Blockieren", + "contactTroubleshoot": "Verbindungsprobleme beheben", + "contactResetSessionTitle": "Verbindung zu {username} zurücksetzen?", + "@contactResetSessionTitle": { + "placeholders": { + "username": { + "type": "String" + } + } + }, + "contactResetSessionBody": "twonly löscht die aktuelle verschlüsselte Verbindung und baut sie anschließend neu auf. Nachrichten, die gerade übertragen werden, können dabei verloren gehen.", + "contactResetSessionConfirm": "Verbindung zurücksetzen", + "contactResetSessionSuccess": "Die verschlüsselte Verbindung zu {username} wurde neu aufgebaut.", + "@contactResetSessionSuccess": { + "placeholders": { + "username": { + "type": "String" + } + } + }, + "contactResetSessionFailed": "Die Verbindung konnte nicht zurückgesetzt werden. Prüfe deine Internetverbindung und versuche es erneut.", "contactRemove": "Kontakt löschen", "contactRemoveTitle": "{username} löschen?", "contactRemoveBody": "Den Kontakt dauerhaft entfernen. Wenn der Kontakt versucht, dir eine neue Nachricht zu senden, musst du den Kontakt erst wieder akzeptieren.", @@ -205,6 +229,7 @@ "errorUsernameAlreadyTaken": "Der Benutzername ist bereits vergeben.", "errorUsernameNotValid": "Der von dir angegebene Benutzername entspricht nicht den erforderlichen Kriterien. Bitte wähle einen gültigen Benutzernamen.", "upgradeToPaidPlanButton": "Auf {planId} upgraden{sufix}", + "partOfPaidPlanOf": "Du bist Teil des bezahlten Plans von {username}!", "partOfPaidPlanFrom": "Teil des bezahlten Plans von", "subscriptionCurrentPlanBadge": "Aktuell", "errorNotEnoughCredit": "Du hast nicht genügend twonly-Guthaben.", diff --git a/lib/src/localization/translations/en.arb b/lib/src/localization/translations/en.arb index 0658b6fe..efc930d8 100644 --- a/lib/src/localization/translations/en.arb +++ b/lib/src/localization/translations/en.arb @@ -90,6 +90,10 @@ "settingsNotification": "Notification", "settingsNotifyPermission": "Notification permissions", "settingsNotifyPermissionDesc": "Open system settings to allow push notifications.", + "settingsNotifyTroubleshooting": "Troubleshooting", + "settingsNotifyTroubleshootingDesc": "Click here if you have problems receiving push notifications.", + "settingsNotifyTroubleshootingNoProblem": "No problem detected", + "settingsNotifyTroubleshootingNoProblemDesc": "Press OK to receive a test notification. If you do not receive the test notification, please click on the new menu item that appears after you click “OK”.", "settingsNotifyResetTitle": "Reset notification tokens", "settingsNotifyResetTitleSubtitle": "Reset your notification tokens if you have problems receiving push notifications.", "settingsNotifyResetTitleReset": "Your notification tokens have been reset.", @@ -164,6 +168,26 @@ "contactNickname": "Nickname", "contactNicknameNew": "New nickname", "contactBlock": "Block", + "contactTroubleshoot": "Troubleshoot connection", + "contactResetSessionTitle": "Reset connection with {username}?", + "@contactResetSessionTitle": { + "placeholders": { + "username": { + "type": "String" + } + } + }, + "contactResetSessionBody": "twonly will remove the current encrypted connection and establish a new one. Messages currently in transit may be lost.", + "contactResetSessionConfirm": "Reset connection", + "contactResetSessionSuccess": "The encrypted connection with {username} was re-established.", + "@contactResetSessionSuccess": { + "placeholders": { + "username": { + "type": "String" + } + } + }, + "contactResetSessionFailed": "The connection could not be reset. Check your internet connection and try again.", "contactBlockTitle": "Block {username}", "@contactBlockTitle": { "placeholders": { @@ -220,6 +244,7 @@ "errorPlanNotAllowed": "This feature is not available in your current plan.", "errorPlanUpgradeNotYearly": "The plan upgrade must be paid for annually, as the current plan is also billed annually.", "upgradeToPaidPlanButton": "Upgrade to {planId}{sufix}", + "partOfPaidPlanOf": "You are part of the paid plan of {username}!", "partOfPaidPlanFrom": "Part of the paid plan of", "subscriptionCurrentPlanBadge": "Current", "year": "year", diff --git a/lib/src/model/protobuf/api/websocket/client_to_server.pb.dart b/lib/src/model/protobuf/api/websocket/client_to_server.pb.dart index 51fb7fc9..f188949b 100644 --- a/lib/src/model/protobuf/api/websocket/client_to_server.pb.dart +++ b/lib/src/model/protobuf/api/websocket/client_to_server.pb.dart @@ -259,6 +259,7 @@ class Handshake_Register extends $pb.GeneratedMessage { $core.String? langCode, $fixnum.Int64? proofOfWork, $core.List<$core.int>? loginToken, + Handshake_InitialPqcKeys? initialPqcKeys, }) { final result = create(); if (username != null) result.username = username; @@ -273,6 +274,7 @@ class Handshake_Register extends $pb.GeneratedMessage { if (langCode != null) result.langCode = langCode; if (proofOfWork != null) result.proofOfWork = proofOfWork; if (loginToken != null) result.loginToken = loginToken; + if (initialPqcKeys != null) result.initialPqcKeys = initialPqcKeys; return result; } @@ -305,6 +307,8 @@ class Handshake_Register extends $pb.GeneratedMessage { ..aInt64(10, _omitFieldNames ? '' : 'proofOfWork') ..a<$core.List<$core.int>>( 11, _omitFieldNames ? '' : 'loginToken', $pb.PbFieldType.OY) + ..aOM(12, _omitFieldNames ? '' : 'initialPqcKeys', + subBuilder: Handshake_InitialPqcKeys.create) ..hasRequiredFields = false; @$core.Deprecated('See https://github.com/google/protobuf.dart/issues/998.') @@ -344,6 +348,8 @@ class Handshake_Register extends $pb.GeneratedMessage { @$pb.TagNumber(2) void clearInviteCode() => $_clearField(2); + /// Deprecated legacy Signal fields. The server ignores these fields; they + /// remain allocated only to avoid reusing their wire tags. @$pb.TagNumber(3) $core.List<$core.int> get publicIdentityKey => $_getN(2); @$pb.TagNumber(3) @@ -425,6 +431,287 @@ class Handshake_Register extends $pb.GeneratedMessage { $core.bool hasLoginToken() => $_has(10); @$pb.TagNumber(11) void clearLoginToken() => $_clearField(11); + + @$pb.TagNumber(12) + Handshake_InitialPqcKeys get initialPqcKeys => $_getN(11); + @$pb.TagNumber(12) + set initialPqcKeys(Handshake_InitialPqcKeys value) => $_setField(12, value); + @$pb.TagNumber(12) + $core.bool hasInitialPqcKeys() => $_has(11); + @$pb.TagNumber(12) + void clearInitialPqcKeys() => $_clearField(12); + @$pb.TagNumber(12) + Handshake_InitialPqcKeys ensureInitialPqcKeys() => $_ensure(11); +} + +class Handshake_InitialPqcKeys_PqcPreKey extends $pb.GeneratedMessage { + factory Handshake_InitialPqcKeys_PqcPreKey({ + $fixnum.Int64? eccPreKeyId, + $core.List<$core.int>? eccPreKey, + $fixnum.Int64? kyberPreKeyId, + $core.List<$core.int>? kyberPreKey, + $core.List<$core.int>? kyberPreKeySignature, + }) { + final result = create(); + if (eccPreKeyId != null) result.eccPreKeyId = eccPreKeyId; + if (eccPreKey != null) result.eccPreKey = eccPreKey; + if (kyberPreKeyId != null) result.kyberPreKeyId = kyberPreKeyId; + if (kyberPreKey != null) result.kyberPreKey = kyberPreKey; + if (kyberPreKeySignature != null) + result.kyberPreKeySignature = kyberPreKeySignature; + return result; + } + + Handshake_InitialPqcKeys_PqcPreKey._(); + + factory Handshake_InitialPqcKeys_PqcPreKey.fromBuffer( + $core.List<$core.int> data, + [$pb.ExtensionRegistry registry = $pb.ExtensionRegistry.EMPTY]) => + create()..mergeFromBuffer(data, registry); + factory Handshake_InitialPqcKeys_PqcPreKey.fromJson($core.String json, + [$pb.ExtensionRegistry registry = $pb.ExtensionRegistry.EMPTY]) => + create()..mergeFromJson(json, registry); + + static final $pb.BuilderInfo _i = $pb.BuilderInfo( + _omitMessageNames ? '' : 'Handshake.InitialPqcKeys.PqcPreKey', + package: + const $pb.PackageName(_omitMessageNames ? '' : 'client_to_server'), + createEmptyInstance: create) + ..aInt64(1, _omitFieldNames ? '' : 'eccPreKeyId') + ..a<$core.List<$core.int>>( + 2, _omitFieldNames ? '' : 'eccPreKey', $pb.PbFieldType.OY) + ..aInt64(3, _omitFieldNames ? '' : 'kyberPreKeyId') + ..a<$core.List<$core.int>>( + 4, _omitFieldNames ? '' : 'kyberPreKey', $pb.PbFieldType.OY) + ..a<$core.List<$core.int>>( + 5, _omitFieldNames ? '' : 'kyberPreKeySignature', $pb.PbFieldType.OY) + ..hasRequiredFields = false; + + @$core.Deprecated('See https://github.com/google/protobuf.dart/issues/998.') + Handshake_InitialPqcKeys_PqcPreKey clone() => deepCopy(); + @$core.Deprecated('See https://github.com/google/protobuf.dart/issues/998.') + Handshake_InitialPqcKeys_PqcPreKey copyWith( + void Function(Handshake_InitialPqcKeys_PqcPreKey) updates) => + super.copyWith((message) => + updates(message as Handshake_InitialPqcKeys_PqcPreKey)) + as Handshake_InitialPqcKeys_PqcPreKey; + + @$core.override + $pb.BuilderInfo get info_ => _i; + + @$core.pragma('dart2js:noInline') + static Handshake_InitialPqcKeys_PqcPreKey create() => + Handshake_InitialPqcKeys_PqcPreKey._(); + @$core.override + Handshake_InitialPqcKeys_PqcPreKey createEmptyInstance() => create(); + @$core.pragma('dart2js:noInline') + static Handshake_InitialPqcKeys_PqcPreKey getDefault() => _defaultInstance ??= + $pb.GeneratedMessage.$_defaultFor( + create); + static Handshake_InitialPqcKeys_PqcPreKey? _defaultInstance; + + @$pb.TagNumber(1) + $fixnum.Int64 get eccPreKeyId => $_getI64(0); + @$pb.TagNumber(1) + set eccPreKeyId($fixnum.Int64 value) => $_setInt64(0, value); + @$pb.TagNumber(1) + $core.bool hasEccPreKeyId() => $_has(0); + @$pb.TagNumber(1) + void clearEccPreKeyId() => $_clearField(1); + + @$pb.TagNumber(2) + $core.List<$core.int> get eccPreKey => $_getN(1); + @$pb.TagNumber(2) + set eccPreKey($core.List<$core.int> value) => $_setBytes(1, value); + @$pb.TagNumber(2) + $core.bool hasEccPreKey() => $_has(1); + @$pb.TagNumber(2) + void clearEccPreKey() => $_clearField(2); + + @$pb.TagNumber(3) + $fixnum.Int64 get kyberPreKeyId => $_getI64(2); + @$pb.TagNumber(3) + set kyberPreKeyId($fixnum.Int64 value) => $_setInt64(2, value); + @$pb.TagNumber(3) + $core.bool hasKyberPreKeyId() => $_has(2); + @$pb.TagNumber(3) + void clearKyberPreKeyId() => $_clearField(3); + + @$pb.TagNumber(4) + $core.List<$core.int> get kyberPreKey => $_getN(3); + @$pb.TagNumber(4) + set kyberPreKey($core.List<$core.int> value) => $_setBytes(3, value); + @$pb.TagNumber(4) + $core.bool hasKyberPreKey() => $_has(3); + @$pb.TagNumber(4) + void clearKyberPreKey() => $_clearField(4); + + @$pb.TagNumber(5) + $core.List<$core.int> get kyberPreKeySignature => $_getN(4); + @$pb.TagNumber(5) + set kyberPreKeySignature($core.List<$core.int> value) => $_setBytes(4, value); + @$pb.TagNumber(5) + $core.bool hasKyberPreKeySignature() => $_has(4); + @$pb.TagNumber(5) + void clearKyberPreKeySignature() => $_clearField(5); +} + +class Handshake_InitialPqcKeys extends $pb.GeneratedMessage { + factory Handshake_InitialPqcKeys({ + $core.List<$core.int>? publicIdentityKey, + $fixnum.Int64? registrationId, + $fixnum.Int64? eccSignedPrekeyId, + $core.List<$core.int>? eccSignedPrekey, + $core.List<$core.int>? eccSignedPrekeySignature, + $fixnum.Int64? kyberSignedPrekeyId, + $core.List<$core.int>? kyberSignedPrekey, + $core.List<$core.int>? kyberSignedPrekeySignature, + $core.Iterable? prekeys, + }) { + final result = create(); + if (publicIdentityKey != null) result.publicIdentityKey = publicIdentityKey; + if (registrationId != null) result.registrationId = registrationId; + if (eccSignedPrekeyId != null) result.eccSignedPrekeyId = eccSignedPrekeyId; + if (eccSignedPrekey != null) result.eccSignedPrekey = eccSignedPrekey; + if (eccSignedPrekeySignature != null) + result.eccSignedPrekeySignature = eccSignedPrekeySignature; + if (kyberSignedPrekeyId != null) + result.kyberSignedPrekeyId = kyberSignedPrekeyId; + if (kyberSignedPrekey != null) result.kyberSignedPrekey = kyberSignedPrekey; + if (kyberSignedPrekeySignature != null) + result.kyberSignedPrekeySignature = kyberSignedPrekeySignature; + if (prekeys != null) result.prekeys.addAll(prekeys); + return result; + } + + Handshake_InitialPqcKeys._(); + + factory Handshake_InitialPqcKeys.fromBuffer($core.List<$core.int> data, + [$pb.ExtensionRegistry registry = $pb.ExtensionRegistry.EMPTY]) => + create()..mergeFromBuffer(data, registry); + factory Handshake_InitialPqcKeys.fromJson($core.String json, + [$pb.ExtensionRegistry registry = $pb.ExtensionRegistry.EMPTY]) => + create()..mergeFromJson(json, registry); + + static final $pb.BuilderInfo _i = $pb.BuilderInfo( + _omitMessageNames ? '' : 'Handshake.InitialPqcKeys', + package: + const $pb.PackageName(_omitMessageNames ? '' : 'client_to_server'), + createEmptyInstance: create) + ..a<$core.List<$core.int>>( + 1, _omitFieldNames ? '' : 'publicIdentityKey', $pb.PbFieldType.OY) + ..aInt64(2, _omitFieldNames ? '' : 'registrationId') + ..aInt64(3, _omitFieldNames ? '' : 'eccSignedPrekeyId') + ..a<$core.List<$core.int>>( + 4, _omitFieldNames ? '' : 'eccSignedPrekey', $pb.PbFieldType.OY) + ..a<$core.List<$core.int>>(5, + _omitFieldNames ? '' : 'eccSignedPrekeySignature', $pb.PbFieldType.OY) + ..aInt64(6, _omitFieldNames ? '' : 'kyberSignedPrekeyId') + ..a<$core.List<$core.int>>( + 7, _omitFieldNames ? '' : 'kyberSignedPrekey', $pb.PbFieldType.OY) + ..a<$core.List<$core.int>>(8, + _omitFieldNames ? '' : 'kyberSignedPrekeySignature', $pb.PbFieldType.OY) + ..pPM( + 9, _omitFieldNames ? '' : 'prekeys', + subBuilder: Handshake_InitialPqcKeys_PqcPreKey.create) + ..hasRequiredFields = false; + + @$core.Deprecated('See https://github.com/google/protobuf.dart/issues/998.') + Handshake_InitialPqcKeys clone() => deepCopy(); + @$core.Deprecated('See https://github.com/google/protobuf.dart/issues/998.') + Handshake_InitialPqcKeys copyWith( + void Function(Handshake_InitialPqcKeys) updates) => + super.copyWith((message) => updates(message as Handshake_InitialPqcKeys)) + as Handshake_InitialPqcKeys; + + @$core.override + $pb.BuilderInfo get info_ => _i; + + @$core.pragma('dart2js:noInline') + static Handshake_InitialPqcKeys create() => Handshake_InitialPqcKeys._(); + @$core.override + Handshake_InitialPqcKeys createEmptyInstance() => create(); + @$core.pragma('dart2js:noInline') + static Handshake_InitialPqcKeys getDefault() => _defaultInstance ??= + $pb.GeneratedMessage.$_defaultFor(create); + static Handshake_InitialPqcKeys? _defaultInstance; + + @$pb.TagNumber(1) + $core.List<$core.int> get publicIdentityKey => $_getN(0); + @$pb.TagNumber(1) + set publicIdentityKey($core.List<$core.int> value) => $_setBytes(0, value); + @$pb.TagNumber(1) + $core.bool hasPublicIdentityKey() => $_has(0); + @$pb.TagNumber(1) + void clearPublicIdentityKey() => $_clearField(1); + + @$pb.TagNumber(2) + $fixnum.Int64 get registrationId => $_getI64(1); + @$pb.TagNumber(2) + set registrationId($fixnum.Int64 value) => $_setInt64(1, value); + @$pb.TagNumber(2) + $core.bool hasRegistrationId() => $_has(1); + @$pb.TagNumber(2) + void clearRegistrationId() => $_clearField(2); + + @$pb.TagNumber(3) + $fixnum.Int64 get eccSignedPrekeyId => $_getI64(2); + @$pb.TagNumber(3) + set eccSignedPrekeyId($fixnum.Int64 value) => $_setInt64(2, value); + @$pb.TagNumber(3) + $core.bool hasEccSignedPrekeyId() => $_has(2); + @$pb.TagNumber(3) + void clearEccSignedPrekeyId() => $_clearField(3); + + @$pb.TagNumber(4) + $core.List<$core.int> get eccSignedPrekey => $_getN(3); + @$pb.TagNumber(4) + set eccSignedPrekey($core.List<$core.int> value) => $_setBytes(3, value); + @$pb.TagNumber(4) + $core.bool hasEccSignedPrekey() => $_has(3); + @$pb.TagNumber(4) + void clearEccSignedPrekey() => $_clearField(4); + + @$pb.TagNumber(5) + $core.List<$core.int> get eccSignedPrekeySignature => $_getN(4); + @$pb.TagNumber(5) + set eccSignedPrekeySignature($core.List<$core.int> value) => + $_setBytes(4, value); + @$pb.TagNumber(5) + $core.bool hasEccSignedPrekeySignature() => $_has(4); + @$pb.TagNumber(5) + void clearEccSignedPrekeySignature() => $_clearField(5); + + @$pb.TagNumber(6) + $fixnum.Int64 get kyberSignedPrekeyId => $_getI64(5); + @$pb.TagNumber(6) + set kyberSignedPrekeyId($fixnum.Int64 value) => $_setInt64(5, value); + @$pb.TagNumber(6) + $core.bool hasKyberSignedPrekeyId() => $_has(5); + @$pb.TagNumber(6) + void clearKyberSignedPrekeyId() => $_clearField(6); + + @$pb.TagNumber(7) + $core.List<$core.int> get kyberSignedPrekey => $_getN(6); + @$pb.TagNumber(7) + set kyberSignedPrekey($core.List<$core.int> value) => $_setBytes(6, value); + @$pb.TagNumber(7) + $core.bool hasKyberSignedPrekey() => $_has(6); + @$pb.TagNumber(7) + void clearKyberSignedPrekey() => $_clearField(7); + + @$pb.TagNumber(8) + $core.List<$core.int> get kyberSignedPrekeySignature => $_getN(7); + @$pb.TagNumber(8) + set kyberSignedPrekeySignature($core.List<$core.int> value) => + $_setBytes(7, value); + @$pb.TagNumber(8) + $core.bool hasKyberSignedPrekeySignature() => $_has(7); + @$pb.TagNumber(8) + void clearKyberSignedPrekeySignature() => $_clearField(8); + + @$pb.TagNumber(9) + $pb.PbList get prekeys => $_getList(8); } class Handshake_GetAuthChallenge extends $pb.GeneratedMessage { @@ -602,6 +889,7 @@ class Handshake_Authenticate extends $pb.GeneratedMessage { $core.String? appVersion, $fixnum.Int64? deviceId, $core.bool? inBackground, + $core.bool? supportsMailboxV2, }) { final result = create(); if (userId != null) result.userId = userId; @@ -609,6 +897,7 @@ class Handshake_Authenticate extends $pb.GeneratedMessage { if (appVersion != null) result.appVersion = appVersion; if (deviceId != null) result.deviceId = deviceId; if (inBackground != null) result.inBackground = inBackground; + if (supportsMailboxV2 != null) result.supportsMailboxV2 = supportsMailboxV2; return result; } @@ -632,6 +921,7 @@ class Handshake_Authenticate extends $pb.GeneratedMessage { ..aOS(3, _omitFieldNames ? '' : 'appVersion') ..aInt64(4, _omitFieldNames ? '' : 'deviceId') ..aOB(5, _omitFieldNames ? '' : 'inBackground') + ..aOB(6, _omitFieldNames ? '' : 'supportsMailboxV2') ..hasRequiredFields = false; @$core.Deprecated('See https://github.com/google/protobuf.dart/issues/998.') @@ -698,6 +988,17 @@ class Handshake_Authenticate extends $pb.GeneratedMessage { $core.bool hasInBackground() => $_has(4); @$pb.TagNumber(5) void clearInBackground() => $_clearField(5); + + /// Set by clients that understand PendingMessagesV2 / RequestPendingMessages. + /// Absent or false means the legacy NewMessage(s) mailbox protocol is used. + @$pb.TagNumber(6) + $core.bool get supportsMailboxV2 => $_getBF(5); + @$pb.TagNumber(6) + set supportsMailboxV2($core.bool value) => $_setBool(5, value); + @$pb.TagNumber(6) + $core.bool hasSupportsMailboxV2() => $_has(5); + @$pb.TagNumber(6) + void clearSupportsMailboxV2() => $_clearField(6); } class Handshake_AuthenticateWithLoginToken extends $pb.GeneratedMessage { @@ -707,6 +1008,7 @@ class Handshake_AuthenticateWithLoginToken extends $pb.GeneratedMessage { $core.String? appVersion, $fixnum.Int64? deviceId, $core.bool? inBackground, + $core.bool? supportsMailboxV2, }) { final result = create(); if (userId != null) result.userId = userId; @@ -714,6 +1016,7 @@ class Handshake_AuthenticateWithLoginToken extends $pb.GeneratedMessage { if (appVersion != null) result.appVersion = appVersion; if (deviceId != null) result.deviceId = deviceId; if (inBackground != null) result.inBackground = inBackground; + if (supportsMailboxV2 != null) result.supportsMailboxV2 = supportsMailboxV2; return result; } @@ -738,6 +1041,7 @@ class Handshake_AuthenticateWithLoginToken extends $pb.GeneratedMessage { ..aOS(3, _omitFieldNames ? '' : 'appVersion') ..aInt64(4, _omitFieldNames ? '' : 'deviceId') ..aOB(5, _omitFieldNames ? '' : 'inBackground') + ..aOB(6, _omitFieldNames ? '' : 'supportsMailboxV2') ..hasRequiredFields = false; @$core.Deprecated('See https://github.com/google/protobuf.dart/issues/998.') @@ -807,6 +1111,16 @@ class Handshake_AuthenticateWithLoginToken extends $pb.GeneratedMessage { $core.bool hasInBackground() => $_has(4); @$pb.TagNumber(5) void clearInBackground() => $_clearField(5); + + /// See Authenticate.supports_mailbox_v2. + @$pb.TagNumber(6) + $core.bool get supportsMailboxV2 => $_getBF(5); + @$pb.TagNumber(6) + set supportsMailboxV2($core.bool value) => $_setBool(5, value); + @$pb.TagNumber(6) + $core.bool hasSupportsMailboxV2() => $_has(5); + @$pb.TagNumber(6) + void clearSupportsMailboxV2() => $_clearField(6); } class Handshake_GetServerKeyForPasswordLessRecovery @@ -1388,11 +1702,13 @@ class ApplicationData_TextMessage extends $pb.GeneratedMessage { $fixnum.Int64? userId, $core.List<$core.int>? body, $core.List<$core.int>? pushData, + $core.bool? wakeReceiver, }) { final result = create(); if (userId != null) result.userId = userId; if (body != null) result.body = body; if (pushData != null) result.pushData = pushData; + if (wakeReceiver != null) result.wakeReceiver = wakeReceiver; return result; } @@ -1415,6 +1731,7 @@ class ApplicationData_TextMessage extends $pb.GeneratedMessage { 3, _omitFieldNames ? '' : 'body', $pb.PbFieldType.OY) ..a<$core.List<$core.int>>( 4, _omitFieldNames ? '' : 'pushData', $pb.PbFieldType.OY) + ..aOB(5, _omitFieldNames ? '' : 'wakeReceiver') ..hasRequiredFields = false; @$core.Deprecated('See https://github.com/google/protobuf.dart/issues/998.') @@ -1465,6 +1782,15 @@ class ApplicationData_TextMessage extends $pb.GeneratedMessage { $core.bool hasPushData() => $_has(2); @$pb.TagNumber(4) void clearPushData() => $_clearField(4); + + @$pb.TagNumber(5) + $core.bool get wakeReceiver => $_getBF(3); + @$pb.TagNumber(5) + set wakeReceiver($core.bool value) => $_setBool(3, value); + @$pb.TagNumber(5) + $core.bool hasWakeReceiver() => $_has(3); + @$pb.TagNumber(5) + void clearWakeReceiver() => $_clearField(5); } class ApplicationData_GetUserByUsername extends $pb.GeneratedMessage { @@ -2176,6 +2502,8 @@ class ApplicationData_UploadPqcPreKeys extends $pb.GeneratedMessage { $core.List<$core.int>? kyberSignedPrekey, $core.List<$core.int>? kyberSignedPrekeySignature, $core.Iterable? prekeys, + $core.List<$core.int>? publicIdentityKey, + $fixnum.Int64? registrationId, }) { final result = create(); if (eccSignedPrekeyId != null) result.eccSignedPrekeyId = eccSignedPrekeyId; @@ -2188,6 +2516,8 @@ class ApplicationData_UploadPqcPreKeys extends $pb.GeneratedMessage { if (kyberSignedPrekeySignature != null) result.kyberSignedPrekeySignature = kyberSignedPrekeySignature; if (prekeys != null) result.prekeys.addAll(prekeys); + if (publicIdentityKey != null) result.publicIdentityKey = publicIdentityKey; + if (registrationId != null) result.registrationId = registrationId; return result; } @@ -2218,6 +2548,9 @@ class ApplicationData_UploadPqcPreKeys extends $pb.GeneratedMessage { _omitFieldNames ? '' : 'kyberSignedPrekeySignature', $pb.PbFieldType.OY) ..pPM(7, _omitFieldNames ? '' : 'prekeys', subBuilder: ApplicationData_PqcPreKey.create) + ..a<$core.List<$core.int>>( + 8, _omitFieldNames ? '' : 'publicIdentityKey', $pb.PbFieldType.OY) + ..aInt64(9, _omitFieldNames ? '' : 'registrationId') ..hasRequiredFields = false; @$core.Deprecated('See https://github.com/google/protobuf.dart/issues/998.') @@ -2301,6 +2634,26 @@ class ApplicationData_UploadPqcPreKeys extends $pb.GeneratedMessage { @$pb.TagNumber(7) $pb.PbList get prekeys => $_getList(6); + + /// Required when uploading keys for the first time after a keyless + /// registration. Optional for later key refreshes. + @$pb.TagNumber(8) + $core.List<$core.int> get publicIdentityKey => $_getN(7); + @$pb.TagNumber(8) + set publicIdentityKey($core.List<$core.int> value) => $_setBytes(7, value); + @$pb.TagNumber(8) + $core.bool hasPublicIdentityKey() => $_has(7); + @$pb.TagNumber(8) + void clearPublicIdentityKey() => $_clearField(8); + + @$pb.TagNumber(9) + $fixnum.Int64 get registrationId => $_getI64(8); + @$pb.TagNumber(9) + set registrationId($fixnum.Int64 value) => $_setInt64(8, value); + @$pb.TagNumber(9) + $core.bool hasRegistrationId() => $_has(8); + @$pb.TagNumber(9) + void clearRegistrationId() => $_clearField(9); } class ApplicationData_DownloadDone extends $pb.GeneratedMessage { @@ -3301,6 +3654,52 @@ class ApplicationData_DisableMemoriesBackup extends $pb.GeneratedMessage { static ApplicationData_DisableMemoriesBackup? _defaultInstance; } +/// Asks the server to (re)start a mailbox drain for the authenticated user. +/// The server responds immediately; messages arrive as PendingMessagesV2. +class ApplicationData_RequestPendingMessages extends $pb.GeneratedMessage { + factory ApplicationData_RequestPendingMessages() => create(); + + ApplicationData_RequestPendingMessages._(); + + factory ApplicationData_RequestPendingMessages.fromBuffer( + $core.List<$core.int> data, + [$pb.ExtensionRegistry registry = $pb.ExtensionRegistry.EMPTY]) => + create()..mergeFromBuffer(data, registry); + factory ApplicationData_RequestPendingMessages.fromJson($core.String json, + [$pb.ExtensionRegistry registry = $pb.ExtensionRegistry.EMPTY]) => + create()..mergeFromJson(json, registry); + + static final $pb.BuilderInfo _i = $pb.BuilderInfo( + _omitMessageNames ? '' : 'ApplicationData.RequestPendingMessages', + package: + const $pb.PackageName(_omitMessageNames ? '' : 'client_to_server'), + createEmptyInstance: create) + ..hasRequiredFields = false; + + @$core.Deprecated('See https://github.com/google/protobuf.dart/issues/998.') + ApplicationData_RequestPendingMessages clone() => deepCopy(); + @$core.Deprecated('See https://github.com/google/protobuf.dart/issues/998.') + ApplicationData_RequestPendingMessages copyWith( + void Function(ApplicationData_RequestPendingMessages) updates) => + super.copyWith((message) => + updates(message as ApplicationData_RequestPendingMessages)) + as ApplicationData_RequestPendingMessages; + + @$core.override + $pb.BuilderInfo get info_ => _i; + + @$core.pragma('dart2js:noInline') + static ApplicationData_RequestPendingMessages create() => + ApplicationData_RequestPendingMessages._(); + @$core.override + ApplicationData_RequestPendingMessages createEmptyInstance() => create(); + @$core.pragma('dart2js:noInline') + static ApplicationData_RequestPendingMessages getDefault() => + _defaultInstance ??= $pb.GeneratedMessage.$_defaultFor< + ApplicationData_RequestPendingMessages>(create); + static ApplicationData_RequestPendingMessages? _defaultInstance; +} + enum ApplicationData_ApplicationData { textMessage, getUserByUsername, @@ -3330,6 +3729,7 @@ enum ApplicationData_ApplicationData { deleteMemory, disableMemoriesBackup, uploadPqcPrekeys, + requestPendingMessages, notSet } @@ -3363,6 +3763,7 @@ class ApplicationData extends $pb.GeneratedMessage { ApplicationData_DeleteMemory? deleteMemory, ApplicationData_DisableMemoriesBackup? disableMemoriesBackup, ApplicationData_UploadPqcPreKeys? uploadPqcPrekeys, + ApplicationData_RequestPendingMessages? requestPendingMessages, }) { final result = create(); if (textMessage != null) result.textMessage = textMessage; @@ -3404,6 +3805,8 @@ class ApplicationData extends $pb.GeneratedMessage { if (disableMemoriesBackup != null) result.disableMemoriesBackup = disableMemoriesBackup; if (uploadPqcPrekeys != null) result.uploadPqcPrekeys = uploadPqcPrekeys; + if (requestPendingMessages != null) + result.requestPendingMessages = requestPendingMessages; return result; } @@ -3446,6 +3849,7 @@ class ApplicationData extends $pb.GeneratedMessage { 38: ApplicationData_ApplicationData.deleteMemory, 39: ApplicationData_ApplicationData.disableMemoriesBackup, 40: ApplicationData_ApplicationData.uploadPqcPrekeys, + 43: ApplicationData_ApplicationData.requestPendingMessages, 0: ApplicationData_ApplicationData.notSet }; static final $pb.BuilderInfo _i = $pb.BuilderInfo( @@ -3481,7 +3885,8 @@ class ApplicationData extends $pb.GeneratedMessage { 37, 38, 39, - 40 + 40, + 43 ]) ..aOM(1, _omitFieldNames ? '' : 'textMessage', protoName: 'textMessage', @@ -3579,6 +3984,9 @@ class ApplicationData extends $pb.GeneratedMessage { ..aOM( 40, _omitFieldNames ? '' : 'uploadPqcPrekeys', subBuilder: ApplicationData_UploadPqcPreKeys.create) + ..aOM( + 43, _omitFieldNames ? '' : 'requestPendingMessages', + subBuilder: ApplicationData_RequestPendingMessages.create) ..hasRequiredFields = false; @$core.Deprecated('See https://github.com/google/protobuf.dart/issues/998.') @@ -3628,6 +4036,7 @@ class ApplicationData extends $pb.GeneratedMessage { @$pb.TagNumber(38) @$pb.TagNumber(39) @$pb.TagNumber(40) + @$pb.TagNumber(43) ApplicationData_ApplicationData whichApplicationData() => _ApplicationData_ApplicationDataByTag[$_whichOneof(0)]!; @$pb.TagNumber(1) @@ -3658,6 +4067,7 @@ class ApplicationData extends $pb.GeneratedMessage { @$pb.TagNumber(38) @$pb.TagNumber(39) @$pb.TagNumber(40) + @$pb.TagNumber(43) void clearApplicationData() => $_clearField($_whichOneof(0)); @$pb.TagNumber(1) @@ -4003,6 +4413,20 @@ class ApplicationData extends $pb.GeneratedMessage { void clearUploadPqcPrekeys() => $_clearField(40); @$pb.TagNumber(40) ApplicationData_UploadPqcPreKeys ensureUploadPqcPrekeys() => $_ensure(27); + + @$pb.TagNumber(43) + ApplicationData_RequestPendingMessages get requestPendingMessages => + $_getN(28); + @$pb.TagNumber(43) + set requestPendingMessages(ApplicationData_RequestPendingMessages value) => + $_setField(43, value); + @$pb.TagNumber(43) + $core.bool hasRequestPendingMessages() => $_has(28); + @$pb.TagNumber(43) + void clearRequestPendingMessages() => $_clearField(43); + @$pb.TagNumber(43) + ApplicationData_RequestPendingMessages ensureRequestPendingMessages() => + $_ensure(28); } class Response_PreKey extends $pb.GeneratedMessage { @@ -4173,18 +4597,84 @@ class Response_PqcPrekeys extends $pb.GeneratedMessage { $pb.PbList get prekeys => $_getList(0); } -enum Response_Ok_Ok { none, prekeys, prekeysPqc, notSet } +/// Response to a PendingMessagesV2 batch. Contains only the delivery IDs the +/// client has durably persisted; the server deletes exactly those rows. +class Response_AcknowledgedPendingMessages extends $pb.GeneratedMessage { + factory Response_AcknowledgedPendingMessages({ + $core.Iterable<$fixnum.Int64>? deliveryIds, + }) { + final result = create(); + if (deliveryIds != null) result.deliveryIds.addAll(deliveryIds); + return result; + } + + Response_AcknowledgedPendingMessages._(); + + factory Response_AcknowledgedPendingMessages.fromBuffer( + $core.List<$core.int> data, + [$pb.ExtensionRegistry registry = $pb.ExtensionRegistry.EMPTY]) => + create()..mergeFromBuffer(data, registry); + factory Response_AcknowledgedPendingMessages.fromJson($core.String json, + [$pb.ExtensionRegistry registry = $pb.ExtensionRegistry.EMPTY]) => + create()..mergeFromJson(json, registry); + + static final $pb.BuilderInfo _i = $pb.BuilderInfo( + _omitMessageNames ? '' : 'Response.AcknowledgedPendingMessages', + package: + const $pb.PackageName(_omitMessageNames ? '' : 'client_to_server'), + createEmptyInstance: create) + ..p<$fixnum.Int64>( + 1, _omitFieldNames ? '' : 'deliveryIds', $pb.PbFieldType.K6) + ..hasRequiredFields = false; + + @$core.Deprecated('See https://github.com/google/protobuf.dart/issues/998.') + Response_AcknowledgedPendingMessages clone() => deepCopy(); + @$core.Deprecated('See https://github.com/google/protobuf.dart/issues/998.') + Response_AcknowledgedPendingMessages copyWith( + void Function(Response_AcknowledgedPendingMessages) updates) => + super.copyWith((message) => + updates(message as Response_AcknowledgedPendingMessages)) + as Response_AcknowledgedPendingMessages; + + @$core.override + $pb.BuilderInfo get info_ => _i; + + @$core.pragma('dart2js:noInline') + static Response_AcknowledgedPendingMessages create() => + Response_AcknowledgedPendingMessages._(); + @$core.override + Response_AcknowledgedPendingMessages createEmptyInstance() => create(); + @$core.pragma('dart2js:noInline') + static Response_AcknowledgedPendingMessages getDefault() => + _defaultInstance ??= $pb.GeneratedMessage.$_defaultFor< + Response_AcknowledgedPendingMessages>(create); + static Response_AcknowledgedPendingMessages? _defaultInstance; + + @$pb.TagNumber(1) + $pb.PbList<$fixnum.Int64> get deliveryIds => $_getList(0); +} + +enum Response_Ok_Ok { + none, + prekeys, + prekeysPqc, + acknowledgedPendingMessages, + notSet +} class Response_Ok extends $pb.GeneratedMessage { factory Response_Ok({ $core.bool? none, Response_Prekeys? prekeys, Response_PqcPrekeys? prekeysPqc, + Response_AcknowledgedPendingMessages? acknowledgedPendingMessages, }) { final result = create(); if (none != null) result.none = none; if (prekeys != null) result.prekeys = prekeys; if (prekeysPqc != null) result.prekeysPqc = prekeysPqc; + if (acknowledgedPendingMessages != null) + result.acknowledgedPendingMessages = acknowledgedPendingMessages; return result; } @@ -4201,6 +4691,7 @@ class Response_Ok extends $pb.GeneratedMessage { 1: Response_Ok_Ok.none, 2: Response_Ok_Ok.prekeys, 3: Response_Ok_Ok.prekeysPqc, + 4: Response_Ok_Ok.acknowledgedPendingMessages, 0: Response_Ok_Ok.notSet }; static final $pb.BuilderInfo _i = $pb.BuilderInfo( @@ -4208,12 +4699,15 @@ class Response_Ok extends $pb.GeneratedMessage { package: const $pb.PackageName(_omitMessageNames ? '' : 'client_to_server'), createEmptyInstance: create) - ..oo(0, [1, 2, 3]) + ..oo(0, [1, 2, 3, 4]) ..aOB(1, _omitFieldNames ? '' : 'None', protoName: 'None') ..aOM(2, _omitFieldNames ? '' : 'prekeys', subBuilder: Response_Prekeys.create) ..aOM(3, _omitFieldNames ? '' : 'prekeysPqc', subBuilder: Response_PqcPrekeys.create) + ..aOM( + 4, _omitFieldNames ? '' : 'acknowledgedPendingMessages', + subBuilder: Response_AcknowledgedPendingMessages.create) ..hasRequiredFields = false; @$core.Deprecated('See https://github.com/google/protobuf.dart/issues/998.') @@ -4238,10 +4732,12 @@ class Response_Ok extends $pb.GeneratedMessage { @$pb.TagNumber(1) @$pb.TagNumber(2) @$pb.TagNumber(3) + @$pb.TagNumber(4) Response_Ok_Ok whichOk() => _Response_Ok_OkByTag[$_whichOneof(0)]!; @$pb.TagNumber(1) @$pb.TagNumber(2) @$pb.TagNumber(3) + @$pb.TagNumber(4) void clearOk() => $_clearField($_whichOneof(0)); @$pb.TagNumber(1) @@ -4274,6 +4770,20 @@ class Response_Ok extends $pb.GeneratedMessage { void clearPrekeysPqc() => $_clearField(3); @$pb.TagNumber(3) Response_PqcPrekeys ensurePrekeysPqc() => $_ensure(2); + + @$pb.TagNumber(4) + Response_AcknowledgedPendingMessages get acknowledgedPendingMessages => + $_getN(3); + @$pb.TagNumber(4) + set acknowledgedPendingMessages(Response_AcknowledgedPendingMessages value) => + $_setField(4, value); + @$pb.TagNumber(4) + $core.bool hasAcknowledgedPendingMessages() => $_has(3); + @$pb.TagNumber(4) + void clearAcknowledgedPendingMessages() => $_clearField(4); + @$pb.TagNumber(4) + Response_AcknowledgedPendingMessages ensureAcknowledgedPendingMessages() => + $_ensure(3); } enum Response_Response { ok, error, notSet } diff --git a/lib/src/model/protobuf/api/websocket/client_to_server.pbjson.dart b/lib/src/model/protobuf/api/websocket/client_to_server.pbjson.dart index a7aa3f2c..086055e1 100644 --- a/lib/src/model/protobuf/api/websocket/client_to_server.pbjson.dart +++ b/lib/src/model/protobuf/api/websocket/client_to_server.pbjson.dart @@ -183,6 +183,7 @@ const Handshake$json = { '3': [ Handshake_RequestPOW$json, Handshake_Register$json, + Handshake_InitialPqcKeys$json, Handshake_GetAuthChallenge$json, Handshake_GetUserIdByUsername$json, Handshake_GetAuthToken$json, @@ -221,18 +222,46 @@ const Handshake_Register$json = { '3': 3, '4': 1, '5': 12, - '10': 'publicIdentityKey' + '9': 1, + '10': 'publicIdentityKey', + '17': true + }, + { + '1': 'signed_prekey', + '3': 4, + '4': 1, + '5': 12, + '9': 2, + '10': 'signedPrekey', + '17': true }, - {'1': 'signed_prekey', '3': 4, '4': 1, '5': 12, '10': 'signedPrekey'}, { '1': 'signed_prekey_signature', '3': 5, '4': 1, '5': 12, - '10': 'signedPrekeySignature' + '9': 3, + '10': 'signedPrekeySignature', + '17': true + }, + { + '1': 'signed_prekey_id', + '3': 6, + '4': 1, + '5': 3, + '9': 4, + '10': 'signedPrekeyId', + '17': true + }, + { + '1': 'registration_id', + '3': 7, + '4': 1, + '5': 3, + '9': 5, + '10': 'registrationId', + '17': true }, - {'1': 'signed_prekey_id', '3': 6, '4': 1, '5': 3, '10': 'signedPrekeyId'}, - {'1': 'registration_id', '3': 7, '4': 1, '5': 3, '10': 'registrationId'}, {'1': 'is_ios', '3': 8, '4': 1, '5': 8, '10': 'isIos'}, {'1': 'lang_code', '3': 9, '4': 1, '5': 9, '10': 'langCode'}, {'1': 'proof_of_work', '3': 10, '4': 1, '5': 3, '10': 'proofOfWork'}, @@ -241,14 +270,114 @@ const Handshake_Register$json = { '3': 11, '4': 1, '5': 12, - '9': 1, + '9': 6, '10': 'loginToken', '17': true }, + { + '1': 'initial_pqc_keys', + '3': 12, + '4': 1, + '5': 11, + '6': '.client_to_server.Handshake.InitialPqcKeys', + '9': 7, + '10': 'initialPqcKeys', + '17': true + }, ], '8': [ {'1': '_invite_code'}, + {'1': '_public_identity_key'}, + {'1': '_signed_prekey'}, + {'1': '_signed_prekey_signature'}, + {'1': '_signed_prekey_id'}, + {'1': '_registration_id'}, {'1': '_login_token'}, + {'1': '_initial_pqc_keys'}, + ], +}; + +@$core.Deprecated('Use handshakeDescriptor instead') +const Handshake_InitialPqcKeys$json = { + '1': 'InitialPqcKeys', + '2': [ + { + '1': 'public_identity_key', + '3': 1, + '4': 1, + '5': 12, + '10': 'publicIdentityKey' + }, + {'1': 'registration_id', '3': 2, '4': 1, '5': 3, '10': 'registrationId'}, + { + '1': 'ecc_signed_prekey_id', + '3': 3, + '4': 1, + '5': 3, + '10': 'eccSignedPrekeyId' + }, + { + '1': 'ecc_signed_prekey', + '3': 4, + '4': 1, + '5': 12, + '10': 'eccSignedPrekey' + }, + { + '1': 'ecc_signed_prekey_signature', + '3': 5, + '4': 1, + '5': 12, + '10': 'eccSignedPrekeySignature' + }, + { + '1': 'kyber_signed_prekey_id', + '3': 6, + '4': 1, + '5': 3, + '10': 'kyberSignedPrekeyId' + }, + { + '1': 'kyber_signed_prekey', + '3': 7, + '4': 1, + '5': 12, + '10': 'kyberSignedPrekey' + }, + { + '1': 'kyber_signed_prekey_signature', + '3': 8, + '4': 1, + '5': 12, + '10': 'kyberSignedPrekeySignature' + }, + { + '1': 'prekeys', + '3': 9, + '4': 3, + '5': 11, + '6': '.client_to_server.Handshake.InitialPqcKeys.PqcPreKey', + '10': 'prekeys' + }, + ], + '3': [Handshake_InitialPqcKeys_PqcPreKey$json], +}; + +@$core.Deprecated('Use handshakeDescriptor instead') +const Handshake_InitialPqcKeys_PqcPreKey$json = { + '1': 'PqcPreKey', + '2': [ + {'1': 'ecc_pre_key_id', '3': 1, '4': 1, '5': 3, '10': 'eccPreKeyId'}, + {'1': 'ecc_pre_key', '3': 2, '4': 1, '5': 12, '10': 'eccPreKey'}, + {'1': 'kyber_pre_key_id', '3': 3, '4': 1, '5': 3, '10': 'kyberPreKeyId'}, + {'1': 'kyber_pre_key', '3': 4, '4': 1, '5': 12, '10': 'kyberPreKey'}, + { + '1': 'kyber_pre_key_signature', + '3': 5, + '4': 1, + '5': 12, + '10': 'kyberPreKeySignature' + }, ], }; @@ -307,11 +436,21 @@ const Handshake_Authenticate$json = { '10': 'inBackground', '17': true }, + { + '1': 'supports_mailbox_v2', + '3': 6, + '4': 1, + '5': 8, + '9': 3, + '10': 'supportsMailboxV2', + '17': true + }, ], '8': [ {'1': '_app_version'}, {'1': '_device_id'}, {'1': '_in_background'}, + {'1': '_supports_mailbox_v2'}, ], }; @@ -330,6 +469,18 @@ const Handshake_AuthenticateWithLoginToken$json = { {'1': 'app_version', '3': 3, '4': 1, '5': 9, '10': 'appVersion'}, {'1': 'device_id', '3': 4, '4': 1, '5': 3, '10': 'deviceId'}, {'1': 'in_background', '3': 5, '4': 1, '5': 8, '10': 'inBackground'}, + { + '1': 'supports_mailbox_v2', + '3': 6, + '4': 1, + '5': 8, + '9': 0, + '10': 'supportsMailboxV2', + '17': true + }, + ], + '8': [ + {'1': '_supports_mailbox_v2'}, ], }; @@ -443,39 +594,59 @@ final $typed_data.Uint8List handshakeDescriptor = $convert.base64Decode( 'ZmljYXRpb25IAFIgcmVnaXN0ZXJQYXNzd29yZGxlc3NOb3RpZmljYXRpb24SjQEKI2NoZWNrX2' 'Zvcl9wYXNzd29yZGxlc3Nfbm90aWZpY2F0aW9uGAogASgLMjwuY2xpZW50X3RvX3NlcnZlci5I' 'YW5kc2hha2UuQ2hlY2tGb3JQYXNzd29yZGxlc3NOb3RpZmljYXRpb25IAFIgY2hlY2tGb3JQYX' - 'Nzd29yZGxlc3NOb3RpZmljYXRpb24aDAoKUmVxdWVzdFBPVxrKAwoIUmVnaXN0ZXISGgoIdXNl' + 'Nzd29yZGxlc3NOb3RpZmljYXRpb24aDAoKUmVxdWVzdFBPVxrCBQoIUmVnaXN0ZXISGgoIdXNl' 'cm5hbWUYASABKAlSCHVzZXJuYW1lEiQKC2ludml0ZV9jb2RlGAIgASgJSABSCmludml0ZUNvZG' - 'WIAQESLgoTcHVibGljX2lkZW50aXR5X2tleRgDIAEoDFIRcHVibGljSWRlbnRpdHlLZXkSIwoN' - 'c2lnbmVkX3ByZWtleRgEIAEoDFIMc2lnbmVkUHJla2V5EjYKF3NpZ25lZF9wcmVrZXlfc2lnbm' - 'F0dXJlGAUgASgMUhVzaWduZWRQcmVrZXlTaWduYXR1cmUSKAoQc2lnbmVkX3ByZWtleV9pZBgG' - 'IAEoA1IOc2lnbmVkUHJla2V5SWQSJwoPcmVnaXN0cmF0aW9uX2lkGAcgASgDUg5yZWdpc3RyYX' - 'Rpb25JZBIVCgZpc19pb3MYCCABKAhSBWlzSW9zEhsKCWxhbmdfY29kZRgJIAEoCVIIbGFuZ0Nv' - 'ZGUSIgoNcHJvb2Zfb2Zfd29yaxgKIAEoA1ILcHJvb2ZPZldvcmsSJAoLbG9naW5fdG9rZW4YCy' - 'ABKAxIAVIKbG9naW5Ub2tlbogBAUIOCgxfaW52aXRlX2NvZGVCDgoMX2xvZ2luX3Rva2VuGhIK' - 'EEdldEF1dGhDaGFsbGVuZ2UaMQoTR2V0VXNlcklkQnlVc2VybmFtZRIaCgh1c2VybmFtZRgBIA' - 'EoCVIIdXNlcm5hbWUaQwoMR2V0QXV0aFRva2VuEhcKB3VzZXJfaWQYASABKANSBnVzZXJJZBIa' - 'CghyZXNwb25zZRgCIAEoDFIIcmVzcG9uc2Ua6AEKDEF1dGhlbnRpY2F0ZRIXCgd1c2VyX2lkGA' - 'EgASgDUgZ1c2VySWQSHQoKYXV0aF90b2tlbhgCIAEoDFIJYXV0aFRva2VuEiQKC2FwcF92ZXJz' - 'aW9uGAMgASgJSABSCmFwcFZlcnNpb26IAQESIAoJZGV2aWNlX2lkGAQgASgDSAFSCGRldmljZU' - 'lkiAEBEigKDWluX2JhY2tncm91bmQYBSABKAhIAlIMaW5CYWNrZ3JvdW5kiAEBQg4KDF9hcHBf' - 'dmVyc2lvbkIMCgpfZGV2aWNlX2lkQhAKDl9pbl9iYWNrZ3JvdW5kGsYBChpBdXRoZW50aWNhdG' - 'VXaXRoTG9naW5Ub2tlbhIXCgd1c2VyX2lkGAEgASgDUgZ1c2VySWQSLAoSc2VjcmV0X2xvZ2lu' - 'X3Rva2VuGAIgASgMUhBzZWNyZXRMb2dpblRva2VuEh8KC2FwcF92ZXJzaW9uGAMgASgJUgphcH' - 'BWZXJzaW9uEhsKCWRldmljZV9pZBgEIAEoA1IIZGV2aWNlSWQSIwoNaW5fYmFja2dyb3VuZBgF' - 'IAEoCFIMaW5CYWNrZ3JvdW5kGqUCCiNHZXRTZXJ2ZXJLZXlGb3JQYXNzd29yZExlc3NSZWNvdm' - 'VyeRIXCgd1c2VyX2lkGAEgASgDUgZ1c2VySWQSMgoVc2VydmVyX2tleV9wcm90ZWN0aW9uGAIg' - 'ASgMUhNzZXJ2ZXJLZXlQcm90ZWN0aW9uEi0KEHBpbl91bmxvY2tfdG9rZW4YAyABKAxIAFIOcG' - 'luVW5sb2NrVG9rZW6IAQESMQoScGluX3Byb3RlY3Rpb25fa2V5GAQgASgMSAFSEHBpblByb3Rl' - 'Y3Rpb25LZXmIAQESGQoFZW1haWwYBSABKAlIAlIFZW1haWyIAQFCEwoRX3Bpbl91bmxvY2tfdG' - '9rZW5CFQoTX3Bpbl9wcm90ZWN0aW9uX2tleUIICgZfZW1haWwaywEKIFJlZ2lzdGVyUGFzc3dv' - 'cmRsZXNzTm90aWZpY2F0aW9uEicKD25vdGlmaWNhdGlvbl9pZBgBIAEoCVIObm90aWZpY2F0aW' - '9uSWQSLgoTZG93bmxvYWRfYXV0aF90b2tlbhgCIAEoDFIRZG93bmxvYWRBdXRoVG9rZW4SGwoJ' - 'bGFuZ19jb2RlGAMgASgJUghsYW5nQ29kZRIiCgpnb29nbGVfZmNtGAQgASgJSABSCWdvb2dsZU' - 'ZjbYgBAUINCgtfZ29vZ2xlX2ZjbRq8AQogQ2hlY2tGb3JQYXNzd29yZGxlc3NOb3RpZmljYXRp' - 'b24SJwoPbm90aWZpY2F0aW9uX2lkGAEgASgJUg5ub3RpZmljYXRpb25JZBIuChNkb3dubG9hZF' - '9hdXRoX3Rva2VuGAIgASgMUhFkb3dubG9hZEF1dGhUb2tlbhI/ChxhbHJlYWR5X3JlY2VpdmVk' - 'X21lc3NhZ2VfaWRzGAMgAygDUhlhbHJlYWR5UmVjZWl2ZWRNZXNzYWdlSWRzQgsKCUhhbmRzaG' - 'FrZQ=='); + 'WIAQESMwoTcHVibGljX2lkZW50aXR5X2tleRgDIAEoDEgBUhFwdWJsaWNJZGVudGl0eUtleYgB' + 'ARIoCg1zaWduZWRfcHJla2V5GAQgASgMSAJSDHNpZ25lZFByZWtleYgBARI7ChdzaWduZWRfcH' + 'Jla2V5X3NpZ25hdHVyZRgFIAEoDEgDUhVzaWduZWRQcmVrZXlTaWduYXR1cmWIAQESLQoQc2ln' + 'bmVkX3ByZWtleV9pZBgGIAEoA0gEUg5zaWduZWRQcmVrZXlJZIgBARIsCg9yZWdpc3RyYXRpb2' + '5faWQYByABKANIBVIOcmVnaXN0cmF0aW9uSWSIAQESFQoGaXNfaW9zGAggASgIUgVpc0lvcxIb' + 'CglsYW5nX2NvZGUYCSABKAlSCGxhbmdDb2RlEiIKDXByb29mX29mX3dvcmsYCiABKANSC3Byb2' + '9mT2ZXb3JrEiQKC2xvZ2luX3Rva2VuGAsgASgMSAZSCmxvZ2luVG9rZW6IAQESWQoQaW5pdGlh' + 'bF9wcWNfa2V5cxgMIAEoCzIqLmNsaWVudF90b19zZXJ2ZXIuSGFuZHNoYWtlLkluaXRpYWxQcW' + 'NLZXlzSAdSDmluaXRpYWxQcWNLZXlziAEBQg4KDF9pbnZpdGVfY29kZUIWChRfcHVibGljX2lk' + 'ZW50aXR5X2tleUIQCg5fc2lnbmVkX3ByZWtleUIaChhfc2lnbmVkX3ByZWtleV9zaWduYXR1cm' + 'VCEwoRX3NpZ25lZF9wcmVrZXlfaWRCEgoQX3JlZ2lzdHJhdGlvbl9pZEIOCgxfbG9naW5fdG9r' + 'ZW5CEwoRX2luaXRpYWxfcHFjX2tleXMa1AUKDkluaXRpYWxQcWNLZXlzEi4KE3B1YmxpY19pZG' + 'VudGl0eV9rZXkYASABKAxSEXB1YmxpY0lkZW50aXR5S2V5EicKD3JlZ2lzdHJhdGlvbl9pZBgC' + 'IAEoA1IOcmVnaXN0cmF0aW9uSWQSLwoUZWNjX3NpZ25lZF9wcmVrZXlfaWQYAyABKANSEWVjY1' + 'NpZ25lZFByZWtleUlkEioKEWVjY19zaWduZWRfcHJla2V5GAQgASgMUg9lY2NTaWduZWRQcmVr' + 'ZXkSPQobZWNjX3NpZ25lZF9wcmVrZXlfc2lnbmF0dXJlGAUgASgMUhhlY2NTaWduZWRQcmVrZX' + 'lTaWduYXR1cmUSMwoWa3liZXJfc2lnbmVkX3ByZWtleV9pZBgGIAEoA1ITa3liZXJTaWduZWRQ' + 'cmVrZXlJZBIuChNreWJlcl9zaWduZWRfcHJla2V5GAcgASgMUhFreWJlclNpZ25lZFByZWtleR' + 'JBCh1reWJlcl9zaWduZWRfcHJla2V5X3NpZ25hdHVyZRgIIAEoDFIaa3liZXJTaWduZWRQcmVr' + 'ZXlTaWduYXR1cmUSTgoHcHJla2V5cxgJIAMoCzI0LmNsaWVudF90b19zZXJ2ZXIuSGFuZHNoYW' + 'tlLkluaXRpYWxQcWNLZXlzLlBxY1ByZUtleVIHcHJla2V5cxrUAQoJUHFjUHJlS2V5EiMKDmVj' + 'Y19wcmVfa2V5X2lkGAEgASgDUgtlY2NQcmVLZXlJZBIeCgtlY2NfcHJlX2tleRgCIAEoDFIJZW' + 'NjUHJlS2V5EicKEGt5YmVyX3ByZV9rZXlfaWQYAyABKANSDWt5YmVyUHJlS2V5SWQSIgoNa3li' + 'ZXJfcHJlX2tleRgEIAEoDFILa3liZXJQcmVLZXkSNQoXa3liZXJfcHJlX2tleV9zaWduYXR1cm' + 'UYBSABKAxSFGt5YmVyUHJlS2V5U2lnbmF0dXJlGhIKEEdldEF1dGhDaGFsbGVuZ2UaMQoTR2V0' + 'VXNlcklkQnlVc2VybmFtZRIaCgh1c2VybmFtZRgBIAEoCVIIdXNlcm5hbWUaQwoMR2V0QXV0aF' + 'Rva2VuEhcKB3VzZXJfaWQYASABKANSBnVzZXJJZBIaCghyZXNwb25zZRgCIAEoDFIIcmVzcG9u' + 'c2UatQIKDEF1dGhlbnRpY2F0ZRIXCgd1c2VyX2lkGAEgASgDUgZ1c2VySWQSHQoKYXV0aF90b2' + 'tlbhgCIAEoDFIJYXV0aFRva2VuEiQKC2FwcF92ZXJzaW9uGAMgASgJSABSCmFwcFZlcnNpb26I' + 'AQESIAoJZGV2aWNlX2lkGAQgASgDSAFSCGRldmljZUlkiAEBEigKDWluX2JhY2tncm91bmQYBS' + 'ABKAhIAlIMaW5CYWNrZ3JvdW5kiAEBEjMKE3N1cHBvcnRzX21haWxib3hfdjIYBiABKAhIA1IR' + 'c3VwcG9ydHNNYWlsYm94VjKIAQFCDgoMX2FwcF92ZXJzaW9uQgwKCl9kZXZpY2VfaWRCEAoOX2' + 'luX2JhY2tncm91bmRCFgoUX3N1cHBvcnRzX21haWxib3hfdjIakwIKGkF1dGhlbnRpY2F0ZVdp' + 'dGhMb2dpblRva2VuEhcKB3VzZXJfaWQYASABKANSBnVzZXJJZBIsChJzZWNyZXRfbG9naW5fdG' + '9rZW4YAiABKAxSEHNlY3JldExvZ2luVG9rZW4SHwoLYXBwX3ZlcnNpb24YAyABKAlSCmFwcFZl' + 'cnNpb24SGwoJZGV2aWNlX2lkGAQgASgDUghkZXZpY2VJZBIjCg1pbl9iYWNrZ3JvdW5kGAUgAS' + 'gIUgxpbkJhY2tncm91bmQSMwoTc3VwcG9ydHNfbWFpbGJveF92MhgGIAEoCEgAUhFzdXBwb3J0' + 'c01haWxib3hWMogBAUIWChRfc3VwcG9ydHNfbWFpbGJveF92MhqlAgojR2V0U2VydmVyS2V5Rm' + '9yUGFzc3dvcmRMZXNzUmVjb3ZlcnkSFwoHdXNlcl9pZBgBIAEoA1IGdXNlcklkEjIKFXNlcnZl' + 'cl9rZXlfcHJvdGVjdGlvbhgCIAEoDFITc2VydmVyS2V5UHJvdGVjdGlvbhItChBwaW5fdW5sb2' + 'NrX3Rva2VuGAMgASgMSABSDnBpblVubG9ja1Rva2VuiAEBEjEKEnBpbl9wcm90ZWN0aW9uX2tl' + 'eRgEIAEoDEgBUhBwaW5Qcm90ZWN0aW9uS2V5iAEBEhkKBWVtYWlsGAUgASgJSAJSBWVtYWlsiA' + 'EBQhMKEV9waW5fdW5sb2NrX3Rva2VuQhUKE19waW5fcHJvdGVjdGlvbl9rZXlCCAoGX2VtYWls' + 'GssBCiBSZWdpc3RlclBhc3N3b3JkbGVzc05vdGlmaWNhdGlvbhInCg9ub3RpZmljYXRpb25faW' + 'QYASABKAlSDm5vdGlmaWNhdGlvbklkEi4KE2Rvd25sb2FkX2F1dGhfdG9rZW4YAiABKAxSEWRv' + 'd25sb2FkQXV0aFRva2VuEhsKCWxhbmdfY29kZRgDIAEoCVIIbGFuZ0NvZGUSIgoKZ29vZ2xlX2' + 'ZjbRgEIAEoCUgAUglnb29nbGVGY22IAQFCDQoLX2dvb2dsZV9mY20avAEKIENoZWNrRm9yUGFz' + 'c3dvcmRsZXNzTm90aWZpY2F0aW9uEicKD25vdGlmaWNhdGlvbl9pZBgBIAEoCVIObm90aWZpY2' + 'F0aW9uSWQSLgoTZG93bmxvYWRfYXV0aF90b2tlbhgCIAEoDFIRZG93bmxvYWRBdXRoVG9rZW4S' + 'PwocYWxyZWFkeV9yZWNlaXZlZF9tZXNzYWdlX2lkcxgDIAMoA1IZYWxyZWFkeVJlY2VpdmVkTW' + 'Vzc2FnZUlkc0ILCglIYW5kc2hha2U='); @$core.Deprecated('Use applicationDataDescriptor instead') const ApplicationData$json = { @@ -733,6 +904,15 @@ const ApplicationData$json = { '9': 0, '10': 'uploadPqcPrekeys' }, + { + '1': 'request_pending_messages', + '3': 43, + '4': 1, + '5': 11, + '6': '.client_to_server.ApplicationData.RequestPendingMessages', + '9': 0, + '10': 'requestPendingMessages' + }, ], '3': [ ApplicationData_TextMessage$json, @@ -763,7 +943,8 @@ const ApplicationData$json = { ApplicationData_GetMemoriesUrl$json, ApplicationData_GetMemoriesUsage$json, ApplicationData_DeleteMemory$json, - ApplicationData_DisableMemoriesBackup$json + ApplicationData_DisableMemoriesBackup$json, + ApplicationData_RequestPendingMessages$json ], '8': [ {'1': 'ApplicationData'}, @@ -773,6 +954,8 @@ const ApplicationData$json = { {'1': 11, '2': 12}, {'1': 13, '2': 18}, {'1': 19, '2': 20}, + {'1': 41, '2': 42}, + {'1': 42, '2': 43}, ], }; @@ -791,6 +974,7 @@ const ApplicationData_TextMessage$json = { '10': 'pushData', '17': true }, + {'1': 'wake_receiver', '3': 5, '4': 1, '5': 8, '10': 'wakeReceiver'}, ], '8': [ {'1': '_push_data'}, @@ -951,6 +1135,28 @@ const ApplicationData_UploadPqcPreKeys$json = { '6': '.client_to_server.ApplicationData.PqcPreKey', '10': 'prekeys' }, + { + '1': 'public_identity_key', + '3': 8, + '4': 1, + '5': 12, + '9': 0, + '10': 'publicIdentityKey', + '17': true + }, + { + '1': 'registration_id', + '3': 9, + '4': 1, + '5': 3, + '9': 1, + '10': 'registrationId', + '17': true + }, + ], + '8': [ + {'1': '_public_identity_key'}, + {'1': '_registration_id'}, ], }; @@ -1108,6 +1314,11 @@ const ApplicationData_DisableMemoriesBackup$json = { '1': 'DisableMemoriesBackup', }; +@$core.Deprecated('Use applicationDataDescriptor instead') +const ApplicationData_RequestPendingMessages$json = { + '1': 'RequestPendingMessages', +}; + /// Descriptor for `ApplicationData`. Decode as a `google.protobuf.DescriptorProto`. final $typed_data.Uint8List applicationDataDescriptor = $convert.base64Decode( 'Cg9BcHBsaWNhdGlvbkRhdGESUQoLdGV4dE1lc3NhZ2UYASABKAsyLS5jbGllbnRfdG9fc2Vydm' @@ -1161,49 +1372,55 @@ final $typed_data.Uint8List applicationDataDescriptor = $convert.base64Decode( 'c2VydmVyLkFwcGxpY2F0aW9uRGF0YS5EaXNhYmxlTWVtb3JpZXNCYWNrdXBIAFIVZGlzYWJsZU' '1lbW9yaWVzQmFja3VwEmIKEnVwbG9hZF9wcWNfcHJla2V5cxgoIAEoCzIyLmNsaWVudF90b19z' 'ZXJ2ZXIuQXBwbGljYXRpb25EYXRhLlVwbG9hZFBxY1ByZUtleXNIAFIQdXBsb2FkUHFjUHJla2' - 'V5cxpqCgtUZXh0TWVzc2FnZRIXCgd1c2VyX2lkGAEgASgDUgZ1c2VySWQSEgoEYm9keRgDIAEo' - 'DFIEYm9keRIgCglwdXNoX2RhdGEYBCABKAxIAFIIcHVzaERhdGGIAQFCDAoKX3B1c2hfZGF0YR' - 'ovChFHZXRVc2VyQnlVc2VybmFtZRIaCgh1c2VybmFtZRgBIAEoCVIIdXNlcm5hbWUaLAoOQ2hh' - 'bmdlVXNlcm5hbWUSGgoIdXNlcm5hbWUYASABKAlSCHVzZXJuYW1lGjUKFFVwZGF0ZUdvb2dsZU' - 'ZjbVRva2VuEh0KCmdvb2dsZV9mY20YASABKAlSCWdvb2dsZUZjbRomCgtHZXRVc2VyQnlJZBIX' - 'Cgd1c2VyX2lkGAEgASgDUgZ1c2VySWQaEwoRR2V0QXZhaWxhYmxlUGxhbnMaFQoTR2V0Q3Vycm' - 'VudFBsYW5JbmZvcxovChRSZW1vdmVBZGRpdGlvbmFsVXNlchIXCgd1c2VyX2lkGAEgASgDUgZ1' - 'c2VySWQaLQoSR2V0UHJla2V5c0J5VXNlcklkEhcKB3VzZXJfaWQYASABKANSBnVzZXJJZBoyCh' - 'dHZXRTaWduZWRQcmVLZXlCeVVzZXJJZBIXCgd1c2VyX2lkGAEgASgDUgZ1c2VySWQamwEKElVw' - 'ZGF0ZVNpZ25lZFByZUtleRIoChBzaWduZWRfcHJla2V5X2lkGAEgASgDUg5zaWduZWRQcmVrZX' - 'lJZBIjCg1zaWduZWRfcHJla2V5GAIgASgMUgxzaWduZWRQcmVrZXkSNgoXc2lnbmVkX3ByZWtl' - 'eV9zaWduYXR1cmUYAyABKAxSFXNpZ25lZFByZWtleVNpZ25hdHVyZRrUAQoJUHFjUHJlS2V5Ei' - 'MKDmVjY19wcmVfa2V5X2lkGAEgASgDUgtlY2NQcmVLZXlJZBIeCgtlY2NfcHJlX2tleRgCIAEo' - 'DFIJZWNjUHJlS2V5EicKEGt5YmVyX3ByZV9rZXlfaWQYAyABKANSDWt5YmVyUHJlS2V5SWQSIg' - 'oNa3liZXJfcHJlX2tleRgEIAEoDFILa3liZXJQcmVLZXkSNQoXa3liZXJfcHJlX2tleV9zaWdu' - 'YXR1cmUYBSABKAxSFGt5YmVyUHJlS2V5U2lnbmF0dXJlGp0DChBVcGxvYWRQcWNQcmVLZXlzEi' - '8KFGVjY19zaWduZWRfcHJla2V5X2lkGAEgASgDUhFlY2NTaWduZWRQcmVrZXlJZBIqChFlY2Nf' - 'c2lnbmVkX3ByZWtleRgCIAEoDFIPZWNjU2lnbmVkUHJla2V5Ej0KG2VjY19zaWduZWRfcHJla2' - 'V5X3NpZ25hdHVyZRgDIAEoDFIYZWNjU2lnbmVkUHJla2V5U2lnbmF0dXJlEjMKFmt5YmVyX3Np' - 'Z25lZF9wcmVrZXlfaWQYBCABKANSE2t5YmVyU2lnbmVkUHJla2V5SWQSLgoTa3liZXJfc2lnbm' - 'VkX3ByZWtleRgFIAEoDFIRa3liZXJTaWduZWRQcmVrZXkSQQoda3liZXJfc2lnbmVkX3ByZWtl' - 'eV9zaWduYXR1cmUYBiABKAxSGmt5YmVyU2lnbmVkUHJla2V5U2lnbmF0dXJlEkUKB3ByZWtleX' - 'MYByADKAsyKy5jbGllbnRfdG9fc2VydmVyLkFwcGxpY2F0aW9uRGF0YS5QcWNQcmVLZXlSB3By' - 'ZWtleXMaNQoMRG93bmxvYWREb25lEiUKDmRvd25sb2FkX3Rva2VuGAEgASgMUg1kb3dubG9hZF' - 'Rva2VuGk4KClJlcG9ydFVzZXISKAoQcmVwb3J0ZWRfdXNlcl9pZBgBIAEoA1IOcmVwb3J0ZWRV' - 'c2VySWQSFgoGcmVhc29uGAIgASgJUgZyZWFzb24acQoLSVBBUHVyY2hhc2USHQoKcHJvZHVjdF' - '9pZBgBIAEoCVIJcHJvZHVjdElkEhYKBnNvdXJjZRgCIAEoCVIGc291cmNlEisKEXZlcmlmaWNh' - 'dGlvbl9kYXRhGAMgASgJUhB2ZXJpZmljYXRpb25EYXRhGg8KDUlQQUZvcmNlQ2hlY2saDwoNRG' - 'VsZXRlQWNjb3VudBosChFBZGRBZGRpdGlvbmFsVXNlchIXCgd1c2VyX2lkGAEgASgDUgZ1c2Vy' - 'SWQaMAoNU2V0TG9naW5Ub2tlbhIfCgtsb2dpbl90b2tlbhgBIAEoDFIKbG9naW5Ub2tlbhqOAQ' - 'ocUmVnaXN0ZXJQYXNzd29yZExlc3NSZWNvdmVyeRIuChJlbmNyeXB0ZWRTZXJ2ZXJLZXkYASAB' - 'KAxSEmVuY3J5cHRlZFNlcnZlcktleRIrCg5waW5VbmxvY2tUb2tlbhgCIAEoDEgAUg5waW5Vbm' - 'xvY2tUb2tlbogBAUIRCg9fcGluVW5sb2NrVG9rZW4acAoYUGFzc3dvcmRsZXNzTm90aWZpY2F0' - 'aW9uEicKD25vdGlmaWNhdGlvbl9pZBgBIAEoCVIObm90aWZpY2F0aW9uSWQSKwoRZW5jcnlwdG' - 'VkX21lc3NhZ2UYAiABKAxSEGVuY3J5cHRlZE1lc3NhZ2UaawoVUmVxdWVzdE1lbW9yaWVzVXBs' - 'b2FkEhIKBHNpemUYASABKANSBHNpemUSIwoNb3JpZ2luYWxfZGF0ZRgCIAEoA1IMb3JpZ2luYW' - 'xEYXRlEhkKCG1lZGlhX2lkGAMgASgJUgdtZWRpYUlkGjIKFUNvbmZpcm1NZW1vcmllc1VwbG9h' - 'ZBIZCghtZWRpYV9pZBgBIAEoCVIHbWVkaWFJZBpICg9HZXRNZW1vcmllc0xpc3QSHwoLb2Zmc2' - 'V0X2RhdGUYASABKANSCm9mZnNldERhdGUSFAoFbGltaXQYAiABKANSBWxpbWl0GkkKDkdldE1l' - 'bW9yaWVzVXJsEhkKCG1lZGlhX2lkGAEgASgJUgdtZWRpYUlkEhwKCXRodW1ibmFpbBgCIAEoCF' - 'IJdGh1bWJuYWlsGhIKEEdldE1lbW9yaWVzVXNhZ2UaKQoMRGVsZXRlTWVtb3J5EhkKCG1lZGlh' - 'X2lkGAEgASgJUgdtZWRpYUlkGhcKFURpc2FibGVNZW1vcmllc0JhY2t1cEIRCg9BcHBsaWNhdG' - 'lvbkRhdGFKBAgJEApKBAgLEAxKBAgNEBJKBAgTEBQ='); + 'V5cxJ0ChhyZXF1ZXN0X3BlbmRpbmdfbWVzc2FnZXMYKyABKAsyOC5jbGllbnRfdG9fc2VydmVy' + 'LkFwcGxpY2F0aW9uRGF0YS5SZXF1ZXN0UGVuZGluZ01lc3NhZ2VzSABSFnJlcXVlc3RQZW5kaW' + '5nTWVzc2FnZXMajwEKC1RleHRNZXNzYWdlEhcKB3VzZXJfaWQYASABKANSBnVzZXJJZBISCgRi' + 'b2R5GAMgASgMUgRib2R5EiAKCXB1c2hfZGF0YRgEIAEoDEgAUghwdXNoRGF0YYgBARIjCg13YW' + 'tlX3JlY2VpdmVyGAUgASgIUgx3YWtlUmVjZWl2ZXJCDAoKX3B1c2hfZGF0YRovChFHZXRVc2Vy' + 'QnlVc2VybmFtZRIaCgh1c2VybmFtZRgBIAEoCVIIdXNlcm5hbWUaLAoOQ2hhbmdlVXNlcm5hbW' + 'USGgoIdXNlcm5hbWUYASABKAlSCHVzZXJuYW1lGjUKFFVwZGF0ZUdvb2dsZUZjbVRva2VuEh0K' + 'Cmdvb2dsZV9mY20YASABKAlSCWdvb2dsZUZjbRomCgtHZXRVc2VyQnlJZBIXCgd1c2VyX2lkGA' + 'EgASgDUgZ1c2VySWQaEwoRR2V0QXZhaWxhYmxlUGxhbnMaFQoTR2V0Q3VycmVudFBsYW5JbmZv' + 'cxovChRSZW1vdmVBZGRpdGlvbmFsVXNlchIXCgd1c2VyX2lkGAEgASgDUgZ1c2VySWQaLQoSR2' + 'V0UHJla2V5c0J5VXNlcklkEhcKB3VzZXJfaWQYASABKANSBnVzZXJJZBoyChdHZXRTaWduZWRQ' + 'cmVLZXlCeVVzZXJJZBIXCgd1c2VyX2lkGAEgASgDUgZ1c2VySWQamwEKElVwZGF0ZVNpZ25lZF' + 'ByZUtleRIoChBzaWduZWRfcHJla2V5X2lkGAEgASgDUg5zaWduZWRQcmVrZXlJZBIjCg1zaWdu' + 'ZWRfcHJla2V5GAIgASgMUgxzaWduZWRQcmVrZXkSNgoXc2lnbmVkX3ByZWtleV9zaWduYXR1cm' + 'UYAyABKAxSFXNpZ25lZFByZWtleVNpZ25hdHVyZRrUAQoJUHFjUHJlS2V5EiMKDmVjY19wcmVf' + 'a2V5X2lkGAEgASgDUgtlY2NQcmVLZXlJZBIeCgtlY2NfcHJlX2tleRgCIAEoDFIJZWNjUHJlS2' + 'V5EicKEGt5YmVyX3ByZV9rZXlfaWQYAyABKANSDWt5YmVyUHJlS2V5SWQSIgoNa3liZXJfcHJl' + 'X2tleRgEIAEoDFILa3liZXJQcmVLZXkSNQoXa3liZXJfcHJlX2tleV9zaWduYXR1cmUYBSABKA' + 'xSFGt5YmVyUHJlS2V5U2lnbmF0dXJlGqwEChBVcGxvYWRQcWNQcmVLZXlzEi8KFGVjY19zaWdu' + 'ZWRfcHJla2V5X2lkGAEgASgDUhFlY2NTaWduZWRQcmVrZXlJZBIqChFlY2Nfc2lnbmVkX3ByZW' + 'tleRgCIAEoDFIPZWNjU2lnbmVkUHJla2V5Ej0KG2VjY19zaWduZWRfcHJla2V5X3NpZ25hdHVy' + 'ZRgDIAEoDFIYZWNjU2lnbmVkUHJla2V5U2lnbmF0dXJlEjMKFmt5YmVyX3NpZ25lZF9wcmVrZX' + 'lfaWQYBCABKANSE2t5YmVyU2lnbmVkUHJla2V5SWQSLgoTa3liZXJfc2lnbmVkX3ByZWtleRgF' + 'IAEoDFIRa3liZXJTaWduZWRQcmVrZXkSQQoda3liZXJfc2lnbmVkX3ByZWtleV9zaWduYXR1cm' + 'UYBiABKAxSGmt5YmVyU2lnbmVkUHJla2V5U2lnbmF0dXJlEkUKB3ByZWtleXMYByADKAsyKy5j' + 'bGllbnRfdG9fc2VydmVyLkFwcGxpY2F0aW9uRGF0YS5QcWNQcmVLZXlSB3ByZWtleXMSMwoTcH' + 'VibGljX2lkZW50aXR5X2tleRgIIAEoDEgAUhFwdWJsaWNJZGVudGl0eUtleYgBARIsCg9yZWdp' + 'c3RyYXRpb25faWQYCSABKANIAVIOcmVnaXN0cmF0aW9uSWSIAQFCFgoUX3B1YmxpY19pZGVudG' + 'l0eV9rZXlCEgoQX3JlZ2lzdHJhdGlvbl9pZBo1CgxEb3dubG9hZERvbmUSJQoOZG93bmxvYWRf' + 'dG9rZW4YASABKAxSDWRvd25sb2FkVG9rZW4aTgoKUmVwb3J0VXNlchIoChByZXBvcnRlZF91c2' + 'VyX2lkGAEgASgDUg5yZXBvcnRlZFVzZXJJZBIWCgZyZWFzb24YAiABKAlSBnJlYXNvbhpxCgtJ' + 'UEFQdXJjaGFzZRIdCgpwcm9kdWN0X2lkGAEgASgJUglwcm9kdWN0SWQSFgoGc291cmNlGAIgAS' + 'gJUgZzb3VyY2USKwoRdmVyaWZpY2F0aW9uX2RhdGEYAyABKAlSEHZlcmlmaWNhdGlvbkRhdGEa' + 'DwoNSVBBRm9yY2VDaGVjaxoPCg1EZWxldGVBY2NvdW50GiwKEUFkZEFkZGl0aW9uYWxVc2VyEh' + 'cKB3VzZXJfaWQYASABKANSBnVzZXJJZBowCg1TZXRMb2dpblRva2VuEh8KC2xvZ2luX3Rva2Vu' + 'GAEgASgMUgpsb2dpblRva2VuGo4BChxSZWdpc3RlclBhc3N3b3JkTGVzc1JlY292ZXJ5Ei4KEm' + 'VuY3J5cHRlZFNlcnZlcktleRgBIAEoDFISZW5jcnlwdGVkU2VydmVyS2V5EisKDnBpblVubG9j' + 'a1Rva2VuGAIgASgMSABSDnBpblVubG9ja1Rva2VuiAEBQhEKD19waW5VbmxvY2tUb2tlbhpwCh' + 'hQYXNzd29yZGxlc3NOb3RpZmljYXRpb24SJwoPbm90aWZpY2F0aW9uX2lkGAEgASgJUg5ub3Rp' + 'ZmljYXRpb25JZBIrChFlbmNyeXB0ZWRfbWVzc2FnZRgCIAEoDFIQZW5jcnlwdGVkTWVzc2FnZR' + 'prChVSZXF1ZXN0TWVtb3JpZXNVcGxvYWQSEgoEc2l6ZRgBIAEoA1IEc2l6ZRIjCg1vcmlnaW5h' + 'bF9kYXRlGAIgASgDUgxvcmlnaW5hbERhdGUSGQoIbWVkaWFfaWQYAyABKAlSB21lZGlhSWQaMg' + 'oVQ29uZmlybU1lbW9yaWVzVXBsb2FkEhkKCG1lZGlhX2lkGAEgASgJUgdtZWRpYUlkGkgKD0dl' + 'dE1lbW9yaWVzTGlzdBIfCgtvZmZzZXRfZGF0ZRgBIAEoA1IKb2Zmc2V0RGF0ZRIUCgVsaW1pdB' + 'gCIAEoA1IFbGltaXQaSQoOR2V0TWVtb3JpZXNVcmwSGQoIbWVkaWFfaWQYASABKAlSB21lZGlh' + 'SWQSHAoJdGh1bWJuYWlsGAIgASgIUgl0aHVtYm5haWwaEgoQR2V0TWVtb3JpZXNVc2FnZRopCg' + 'xEZWxldGVNZW1vcnkSGQoIbWVkaWFfaWQYASABKAlSB21lZGlhSWQaFwoVRGlzYWJsZU1lbW9y' + 'aWVzQmFja3VwGhgKFlJlcXVlc3RQZW5kaW5nTWVzc2FnZXNCEQoPQXBwbGljYXRpb25EYXRhSg' + 'QICRAKSgQICxAMSgQIDRASSgQIExAUSgQIKRAqSgQIKhAr'); @$core.Deprecated('Use responseDescriptor instead') const Response$json = { @@ -1232,6 +1449,7 @@ const Response$json = { Response_PreKey$json, Response_Prekeys$json, Response_PqcPrekeys$json, + Response_AcknowledgedPendingMessages$json, Response_Ok$json ], '8': [ @@ -1278,6 +1496,14 @@ const Response_PqcPrekeys$json = { ], }; +@$core.Deprecated('Use responseDescriptor instead') +const Response_AcknowledgedPendingMessages$json = { + '1': 'AcknowledgedPendingMessages', + '2': [ + {'1': 'delivery_ids', '3': 1, '4': 3, '5': 3, '10': 'deliveryIds'}, + ], +}; + @$core.Deprecated('Use responseDescriptor instead') const Response_Ok$json = { '1': 'Ok', @@ -1301,6 +1527,15 @@ const Response_Ok$json = { '9': 0, '10': 'prekeysPqc' }, + { + '1': 'acknowledged_pending_messages', + '3': 4, + '4': 1, + '5': 11, + '6': '.client_to_server.Response.AcknowledgedPendingMessages', + '9': 0, + '10': 'acknowledgedPendingMessages' + }, ], '8': [ {'1': 'Ok'}, @@ -1314,7 +1549,11 @@ final $typed_data.Uint8List responseDescriptor = $convert.base64Decode( 'Eg4KAmlkGAEgASgDUgJpZBIWCgZwcmVrZXkYAiABKAxSBnByZWtleRpGCgdQcmVrZXlzEjsKB3' 'ByZWtleXMYASADKAsyIS5jbGllbnRfdG9fc2VydmVyLlJlc3BvbnNlLlByZUtleVIHcHJla2V5' 'cxpTCgpQcWNQcmVrZXlzEkUKB3ByZWtleXMYASADKAsyKy5jbGllbnRfdG9fc2VydmVyLkFwcG' - 'xpY2F0aW9uRGF0YS5QcWNQcmVLZXlSB3ByZWtleXMaqgEKAk9rEhQKBE5vbmUYASABKAhIAFIE' - 'Tm9uZRI+CgdwcmVrZXlzGAIgASgLMiIuY2xpZW50X3RvX3NlcnZlci5SZXNwb25zZS5QcmVrZX' - 'lzSABSB3ByZWtleXMSSAoLcHJla2V5c19wcWMYAyABKAsyJS5jbGllbnRfdG9fc2VydmVyLlJl' - 'c3BvbnNlLlBxY1ByZWtleXNIAFIKcHJla2V5c1BxY0IECgJPa0IKCghSZXNwb25zZQ=='); + 'xpY2F0aW9uRGF0YS5QcWNQcmVLZXlSB3ByZWtleXMaQAobQWNrbm93bGVkZ2VkUGVuZGluZ01l' + 'c3NhZ2VzEiEKDGRlbGl2ZXJ5X2lkcxgBIAMoA1ILZGVsaXZlcnlJZHMaqAIKAk9rEhQKBE5vbm' + 'UYASABKAhIAFIETm9uZRI+CgdwcmVrZXlzGAIgASgLMiIuY2xpZW50X3RvX3NlcnZlci5SZXNw' + 'b25zZS5QcmVrZXlzSABSB3ByZWtleXMSSAoLcHJla2V5c19wcWMYAyABKAsyJS5jbGllbnRfdG' + '9fc2VydmVyLlJlc3BvbnNlLlBxY1ByZWtleXNIAFIKcHJla2V5c1BxYxJ8Ch1hY2tub3dsZWRn' + 'ZWRfcGVuZGluZ19tZXNzYWdlcxgEIAEoCzI2LmNsaWVudF90b19zZXJ2ZXIuUmVzcG9uc2UuQW' + 'Nrbm93bGVkZ2VkUGVuZGluZ01lc3NhZ2VzSABSG2Fja25vd2xlZGdlZFBlbmRpbmdNZXNzYWdl' + 'c0IECgJPa0IKCghSZXNwb25zZQ=='); diff --git a/lib/src/model/protobuf/api/websocket/server_to_client.pb.dart b/lib/src/model/protobuf/api/websocket/server_to_client.pb.dart index 0d1053aa..3bef4bef 100644 --- a/lib/src/model/protobuf/api/websocket/server_to_client.pb.dart +++ b/lib/src/model/protobuf/api/websocket/server_to_client.pb.dart @@ -96,6 +96,7 @@ enum V0_Kind { error, newMessages, requestNewPqcPreKeys, + mailboxDrained, notSet } @@ -108,6 +109,7 @@ class V0 extends $pb.GeneratedMessage { $0.ErrorCode? error, NewMessages? newMessages, $core.bool? requestNewPqcPreKeys, + $core.bool? mailboxDrained, }) { final result = create(); if (seq != null) result.seq = seq; @@ -118,6 +120,7 @@ class V0 extends $pb.GeneratedMessage { if (newMessages != null) result.newMessages = newMessages; if (requestNewPqcPreKeys != null) result.requestNewPqcPreKeys = requestNewPqcPreKeys; + if (mailboxDrained != null) result.mailboxDrained = mailboxDrained; return result; } @@ -137,6 +140,7 @@ class V0 extends $pb.GeneratedMessage { 6: V0_Kind.error, 7: V0_Kind.newMessages, 8: V0_Kind.requestNewPqcPreKeys, + 11: V0_Kind.mailboxDrained, 0: V0_Kind.notSet }; static final $pb.BuilderInfo _i = $pb.BuilderInfo( @@ -144,7 +148,7 @@ class V0 extends $pb.GeneratedMessage { package: const $pb.PackageName(_omitMessageNames ? '' : 'server_to_client'), createEmptyInstance: create) - ..oo(0, [2, 3, 4, 6, 7, 8]) + ..oo(0, [2, 3, 4, 6, 7, 8, 11]) ..a<$fixnum.Int64>(1, _omitFieldNames ? '' : 'seq', $pb.PbFieldType.OU6, defaultOrMaker: $fixnum.Int64.ZERO) ..aOM(2, _omitFieldNames ? '' : 'response', @@ -159,6 +163,8 @@ class V0 extends $pb.GeneratedMessage { protoName: 'newMessages', subBuilder: NewMessages.create) ..aOB(8, _omitFieldNames ? '' : 'RequestNewPqcPreKeys', protoName: 'RequestNewPqcPreKeys') + ..aOB(11, _omitFieldNames ? '' : 'mailboxDrained', + protoName: 'mailboxDrained') ..hasRequiredFields = false; @$core.Deprecated('See https://github.com/google/protobuf.dart/issues/998.') @@ -185,6 +191,7 @@ class V0 extends $pb.GeneratedMessage { @$pb.TagNumber(6) @$pb.TagNumber(7) @$pb.TagNumber(8) + @$pb.TagNumber(11) V0_Kind whichKind() => _V0_KindByTag[$_whichOneof(0)]!; @$pb.TagNumber(2) @$pb.TagNumber(3) @@ -192,6 +199,7 @@ class V0 extends $pb.GeneratedMessage { @$pb.TagNumber(6) @$pb.TagNumber(7) @$pb.TagNumber(8) + @$pb.TagNumber(11) void clearKind() => $_clearField($_whichOneof(0)); @$pb.TagNumber(1) @@ -262,6 +270,15 @@ class V0 extends $pb.GeneratedMessage { $core.bool hasRequestNewPqcPreKeys() => $_has(6); @$pb.TagNumber(8) void clearRequestNewPqcPreKeys() => $_clearField(8); + + @$pb.TagNumber(11) + $core.bool get mailboxDrained => $_getBF(7); + @$pb.TagNumber(11) + set mailboxDrained($core.bool value) => $_setBool(7, value); + @$pb.TagNumber(11) + $core.bool hasMailboxDrained() => $_has(7); + @$pb.TagNumber(11) + void clearMailboxDrained() => $_clearField(11); } class NewMessage extends $pb.GeneratedMessage { diff --git a/lib/src/model/protobuf/api/websocket/server_to_client.pbjson.dart b/lib/src/model/protobuf/api/websocket/server_to_client.pbjson.dart index 42b93650..e72ab18b 100644 --- a/lib/src/model/protobuf/api/websocket/server_to_client.pbjson.dart +++ b/lib/src/model/protobuf/api/websocket/server_to_client.pbjson.dart @@ -96,6 +96,14 @@ const V0$json = { '9': 0, '10': 'error' }, + { + '1': 'mailboxDrained', + '3': 11, + '4': 1, + '5': 8, + '9': 0, + '10': 'mailboxDrained' + }, ], '8': [ {'1': 'Kind'}, @@ -110,7 +118,8 @@ final $typed_data.Uint8List v0Descriptor = $convert.base64Decode( '0uc2VydmVyX3RvX2NsaWVudC5OZXdNZXNzYWdlc0gAUgtuZXdNZXNzYWdlcxIuChFSZXF1ZXN0' 'TmV3UHJlS2V5cxgEIAEoCEgAUhFSZXF1ZXN0TmV3UHJlS2V5cxI0ChRSZXF1ZXN0TmV3UHFjUH' 'JlS2V5cxgIIAEoCEgAUhRSZXF1ZXN0TmV3UHFjUHJlS2V5cxIoCgVlcnJvchgGIAEoDjIQLmVy' - 'cm9yLkVycm9yQ29kZUgAUgVlcnJvckIGCgRLaW5k'); + 'cm9yLkVycm9yQ29kZUgAUgVlcnJvchIoCg5tYWlsYm94RHJhaW5lZBgLIAEoCEgAUg5tYWlsYm' + '94RHJhaW5lZEIGCgRLaW5k'); @$core.Deprecated('Use newMessageDescriptor instead') const NewMessage$json = { diff --git a/lib/src/services/api/api.service.dart b/lib/src/services/api/api.service.dart index 917cf8c6..49b9a3d8 100644 --- a/lib/src/services/api/api.service.dart +++ b/lib/src/services/api/api.service.dart @@ -1016,6 +1016,8 @@ class ApiService { Uint8List kyberSignedPreKey, Uint8List kyberSignedPreKeySignature, List prekeys, + Uint8List publicIdentityKey, + int registrationId, ) async { final get = ApplicationData_UploadPqcPreKeys() ..eccSignedPrekeyId = Int64(eccSignedPreKeyId) @@ -1024,7 +1026,9 @@ class ApiService { ..kyberSignedPrekeyId = Int64(kyberSignedPreKeyId) ..kyberSignedPrekey = kyberSignedPreKey ..kyberSignedPrekeySignature = kyberSignedPreKeySignature - ..prekeys.addAll(prekeys); + ..prekeys.addAll(prekeys) + ..publicIdentityKey = publicIdentityKey + ..registrationId = Int64(registrationId); final appData = ApplicationData()..uploadPqcPrekeys = get; final req = createClientToServerFromApplicationData(appData); return sendRequestSync(req); diff --git a/lib/src/services/api/messages.api.dart b/lib/src/services/api/messages.api.dart index 9c2aa47d..c84c90dc 100644 --- a/lib/src/services/api/messages.api.dart +++ b/lib/src/services/api/messages.api.dart @@ -18,6 +18,7 @@ import 'package:twonly/src/model/protobuf/client/generated/messages.pb.dart' import 'package:twonly/src/model/protobuf/client/generated/push_notification.pb.dart'; import 'package:twonly/src/services/notifications/pushkeys.notifications.dart'; import 'package:twonly/src/services/signal/encryption.signal.dart'; +import 'package:twonly/src/services/signal/protocol_state.signal.dart'; import 'package:twonly/src/services/signal/session.signal.dart'; import 'package:twonly/src/services/user.service.dart' show UserService; import 'package:twonly/src/services/user_discovery.service.dart'; @@ -26,8 +27,47 @@ import 'package:twonly/src/utils/misc.dart'; final lockRetransmission = Mutex(); +const int _outOfSyncRetryThreshold = 10; + +Future _resetStuckOutgoingSignalSessions() async { + final contactIds = await twonlyDB.receiptsDao + .getContactsWithStuckSignalMessages( + retryThreshold: _outOfSyncRetryThreshold, + ); + + for (final contactId in contactIds) { + if (!shouldAttemptResync(contactId)) continue; + + Log.warn( + 'Detected an out-of-sync outgoing Signal session with $contactId: ' + 'multiple messages exceeded $_outOfSyncRetryThreshold retries.', + ); + + var reset = false; + try { + reset = await resetSignalSession(contactId); + } catch (error) { + Log.error( + 'Could not automatically reset session with $contactId: $error', + ); + } + + if (!reset) { + recordResyncAttempt(contactId, success: false); + continue; + } + + // Avoid resetting the same healthy replacement session on the next + // retransmission pass. Outstanding messages will now be encrypted again + // using the freshly established session. + await twonlyDB.receiptsDao.resetMessageRetryCountsForContact(contactId); + } +} + Future retransmitAllMessages() async { return lockRetransmission.protect(() async { + await _resetStuckOutgoingSignalSessions(); + final receipts = await twonlyDB.receiptsDao.getReceiptsForRetransmission(); if (receipts.isEmpty) return; @@ -159,36 +199,24 @@ Future<(Uint8List, Uint8List?)?> _tryToSendCompleteMessageInternal({ .writeToBuffer(); } - if (message.type == pb.Message_Type.CIPHERTEXT) { - final encryptResult = await signalEncryptMessage( - receipt.contactId, - Uint8List.fromList(message.encryptedContent), - ); + if (message.type == pb.Message_Type.CIPHERTEXT || + message.type == pb.Message_Type.CIPHERTEXT_V2) { + final useV2 = contact.signalVersion == SignalVersion.v2; + final encryptResult = useV2 + ? await signalEncryptMessageV2( + receipt.contactId, + Uint8List.fromList(message.encryptedContent), + ) + : await signalEncryptMessage( + receipt.contactId, + Uint8List.fromList(message.encryptedContent), + ); + if (encryptResult == null) { Log.error( - '[${receipt.receiptId}] Could not encrypt the message for user ${receipt.contactId}. Aborting and trying again.', - ); - if (receipt.messageId != null) { - await twonlyDB.messagesDao.handleMessageAckByServer( - receipt.contactId, - receipt.messageId!, - clock.now(), - ); - } - await twonlyDB.receiptsDao.deleteReceipt(receipt.receiptId); - return null; - } - message - ..encryptedContent = encryptResult.ciphertext - ..type = encryptResult.type; - } else if (message.type == pb.Message_Type.CIPHERTEXT_V2) { - final encryptResult = await signalEncryptMessageV2( - receipt.contactId, - Uint8List.fromList(message.encryptedContent), - ); - if (encryptResult == null) { - Log.error( - '[${receipt.receiptId}] Could not encrypt the message (V2) for user ${receipt.contactId}. Aborting and trying again.', + '[${receipt.receiptId}] Could not encrypt the message ' + '(${useV2 ? 'V2' : 'V1'}) for user ${receipt.contactId}. ' + 'Aborting and trying again.', ); if (receipt.messageId != null) { await twonlyDB.messagesDao.handleMessageAckByServer( @@ -200,6 +228,7 @@ Future<(Uint8List, Uint8List?)?> _tryToSendCompleteMessageInternal({ await twonlyDB.receiptsDao.deleteReceipt(receipt.receiptId); return null; } + message ..encryptedContent = encryptResult.ciphertext ..type = encryptResult.type; diff --git a/lib/src/services/api/server_messages.api.dart b/lib/src/services/api/server_messages.api.dart index 712f28b0..aaff31e8 100644 --- a/lib/src/services/api/server_messages.api.dart +++ b/lib/src/services/api/server_messages.api.dart @@ -39,8 +39,27 @@ import 'package:twonly/src/services/signal/session.signal.dart'; import 'package:twonly/src/utils/log.dart'; import 'package:twonly/src/utils/misc.dart'; +const _signalUpgradeMailboxIdleDelay = Duration(milliseconds: 500); +Timer? _signalUpgradeMailboxIdleTimer; + +void _scheduleSignalUpgradeAfterMailboxIdle() { + _signalUpgradeMailboxIdleTimer?.cancel(); + _signalUpgradeMailboxIdleTimer = Timer(_signalUpgradeMailboxIdleDelay, () { + _signalUpgradeMailboxIdleTimer = null; + unawaited(upgradeSignalSessionsToV2()); + }); +} + +void _upgradeSignalSessionsAfterMailboxDrain() { + _signalUpgradeMailboxIdleTimer?.cancel(); + _signalUpgradeMailboxIdleTimer = null; + unawaited(upgradeSignalSessionsToV2()); +} + Future handleServerMessage(server.ServerToClient msg) async { Log.info('Processing a message from the server.'); + var receivedIncomingMessages = false; + var mailboxDrained = false; /// Returns means, that the server can delete the message from the server. final ok = client.Response_Ok()..none = true; @@ -54,6 +73,7 @@ Future handleServerMessage(server.ServerToClient msg) async { } else if (msg.v0.hasNewMessage()) { Log.info('Got 1 message from the server.'); await handleClient2ClientMessage(msg.v0.newMessage); + receivedIncomingMessages = true; } else if (msg.v0.hasNewMessages()) { Log.info( 'Got ${msg.v0.newMessages.newMessages.length} messages from the server.', @@ -69,6 +89,10 @@ Future handleServerMessage(server.ServerToClient msg) async { Log.error(e); } } + receivedIncomingMessages = true; + } else if (msg.v0.hasMailboxDrained()) { + Log.info('All pending messages have been downloaded.'); + mailboxDrained = true; } else { Log.error('Unknown server message: $msg'); } @@ -90,6 +114,15 @@ Future handleServerMessage(server.ServerToClient msg) async { } AppState.gotMessageFromServer = true; Log.info('All messages from the server processed.'); + + if (mailboxDrained) { + _upgradeSignalSessionsAfterMailboxDrain(); + } else if (receivedIncomingMessages) { + // Released servers do not send a mailbox-drained marker. They wait for + // this ACK before sending the next batch, so a short, resettable idle + // window approximates the queue boundary without blocking delivery. + _scheduleSignalUpgradeAfterMailboxIdle(); + } } DateTime lastPushKeyRequest = clock.now().subtract(const Duration(hours: 1)); diff --git a/lib/src/services/signal/identity.signal.dart b/lib/src/services/signal/identity.signal.dart index 6e018a63..08a9a93b 100644 --- a/lib/src/services/signal/identity.signal.dart +++ b/lib/src/services/signal/identity.signal.dart @@ -60,6 +60,8 @@ class SignalIdentityService { bundle.kyberPreKeyPublic, bundle.kyberPreKeySignature, [], + bundle.identityKey, + bundle.registrationId, ); if (pqcRes.isError) { diff --git a/lib/src/services/signal/session.signal.dart b/lib/src/services/signal/session.signal.dart index 6a715e0b..9837fbe5 100644 --- a/lib/src/services/signal/session.signal.dart +++ b/lib/src/services/signal/session.signal.dart @@ -2,6 +2,7 @@ import 'dart:typed_data'; import 'package:drift/drift.dart' as drift; import 'package:libsignal_protocol_dart/libsignal_protocol_dart.dart'; +import 'package:mutex/mutex.dart'; import 'package:twonly/core/bridge/wrapper/signal.dart'; import 'package:twonly/core/signal/engine.dart'; import 'package:twonly/locator.dart'; @@ -13,12 +14,83 @@ import 'package:twonly/src/services/signal/protocol_state.signal.dart'; import 'package:twonly/src/services/signal/utils.signal.dart'; import 'package:twonly/src/utils/log.dart'; +final Mutex _upgradeSignalSessionsLock = Mutex(); + Future processSignalUserData(Response_UserData userData) async { return lockingSignalProtocol.protect(() async { return _processSignalUserData(userData); }); } +/// Upgrades every known v1 contact that currently publishes a PQXDH bundle. +/// +/// The legacy session is deliberately retained: messages that were already in +/// flight before the upgrade can still be decrypted, while newly queued +/// messages use the contact's updated v2 session. +Future upgradeSignalSessionsToV2() async { + if (_upgradeSignalSessionsLock.isLocked) return; + + try { + await _upgradeSignalSessionsLock.protect(() async { + final contacts = await twonlyDB.contactsDao.getAllContacts(); + final legacyContacts = contacts.where( + (contact) => + contact.signalVersion == SignalVersion.v1 && + !contact.accountDeleted, + ); + + for (final contact in legacyContacts) { + try { + final userData = await apiService.getUserById(contact.userId); + if (userData == null || !userData.hasPqcBundle()) continue; + + if (await processSignalUserData(userData)) { + Log.info('Upgraded Signal session with ${contact.userId} to v2.'); + } + } catch (error) { + // Migration is best-effort. A contact that is temporarily + // unavailable remains on v1 and is retried after a later message + // queue drain. + Log.warn( + 'Could not upgrade Signal session with ${contact.userId}: $error', + ); + } + } + }); + } catch (error) { + // This function is started without awaiting it from the message loop. + Log.warn('Could not start Signal session upgrades: $error'); + } +} + +/// Replaces the current session with a freshly downloaded pre-key bundle. +Future resetSignalSession(int contactId) async { + final userData = await apiService.getUserById(contactId); + if (userData == null) return false; + + return lockingSignalProtocol.protect(() async { + if (userData.hasPqcBundle()) { + await RustSignal.resetContactSession(contactId: contactId); + } else { + final signalStore = await getSignalStore(); + await signalStore?.deleteAllSessions(contactId.toString()); + } + final reset = await _processSignalUserData(userData); + if (reset) recordResyncAttempt(contactId, success: true); + return reset; + }); +} + +ECPublicKey _decodePublicKey(List bytes, String fieldName) { + if (bytes.length != 33 || bytes.first != Curve.djbType) { + throw InvalidKeyException( + '$fieldName must be a serialized 33-byte Curve25519 public key ' + '(received ${bytes.length} bytes)', + ); + } + return Curve.decodePoint(Uint8List.fromList(bytes), 0); +} + Future _processSignalUserData(Response_UserData userData) async { if (userData.hasPqcBundle()) { return _processSignalUserDataV2(userData); @@ -29,17 +101,20 @@ Future _processSignalUserData(Response_UserData userData) async { Future _processSignalUserDataV2(Response_UserData userData) async { try { final tempIdentityKey = IdentityKey( - Curve.decodePoint( - DjbECPublicKey( - Uint8List.fromList(userData.publicIdentityKey), - ).serialize(), - 1, - ), + _decodePublicKey(userData.publicIdentityKey, 'identity key'), ); - final signalStore = await getSignalStore(); - if (signalStore != null) { - final existingIdentity = await signalStore.getIdentity( + final contact = await twonlyDB.contactsDao.getContactById( + userData.userId.toInt(), + ); + + // Only the one-time V1 -> V2 migration needs to compare against the + // legacy Dart identity store. Established V2 contacts are authenticated + // by the identity retained in the Rust store; consulting the obsolete + // Dart store here can make a valid V2 reset fail on legacy/corrupt data. + if (contact?.signalVersion != SignalVersion.v2) { + final signalStore = await getSignalStore(); + final existingIdentity = await signalStore?.getIdentity( SignalProtocolAddress(userData.userId.toString(), defaultDeviceId), ); @@ -60,11 +135,9 @@ Future _processSignalUserDataV2(Response_UserData userData) async { ); } else if (userData.prekeys.isNotEmpty) { tempEccPreKeyId = userData.prekeys.first.id.toInt(); - tempEccPreKeyPublic = Curve.decodePoint( - DjbECPublicKey( - Uint8List.fromList(userData.prekeys.first.prekey), - ).serialize(), - 1, + tempEccPreKeyPublic = _decodePublicKey( + userData.prekeys.first.prekey, + 'prekey', ).serialize(); } @@ -102,9 +175,6 @@ Future _processSignalUserDataV2(Response_UserData userData) async { bundle: rustBundle, ); - final contact = await twonlyDB.contactsDao.getContactById( - userData.userId.toInt(), - ); if (contact != null && contact.signalVersion != SignalVersion.v2) { await twonlyDB.contactsDao.updateContact( contact.userId, @@ -120,64 +190,57 @@ Future _processSignalUserDataV2(Response_UserData userData) async { } Future _processSignalUserDataV1(Response_UserData userData) async { - final SignalProtocolStore? signalStore = await getSignalStore(); - - if (signalStore == null) { - return false; - } - - final targetAddress = getSignalAddress(userData.userId.toInt()); - - final sessionBuilder = SessionBuilder.fromSignalStore( - signalStore, - targetAddress, - ); - - ECPublicKey? tempPrePublicKey; - int? tempPreKeyId; - - if (userData.prekeys.isNotEmpty) { - tempPrePublicKey = Curve.decodePoint( - DjbECPublicKey( - Uint8List.fromList(userData.prekeys.first.prekey), - ).serialize(), - 1, - ); - tempPreKeyId = userData.prekeys.first.id.toInt(); - } - - final tempSignedPreKeyId = userData.signedPrekeyId.toInt(); - - final tempSignedPreKeyPublic = Curve.decodePoint( - DjbECPublicKey(Uint8List.fromList(userData.signedPrekey)).serialize(), - 1, - ); - - final tempSignedPreKeySignature = Uint8List.fromList( - userData.signedPrekeySignature, - ); - - final tempIdentityKey = IdentityKey( - Curve.decodePoint( - DjbECPublicKey( - Uint8List.fromList(userData.publicIdentityKey), - ).serialize(), - 1, - ), - ); - - final preKeyBundle = PreKeyBundle( - userData.registrationId.toInt(), - defaultDeviceId, - tempPreKeyId, - tempPrePublicKey, - tempSignedPreKeyId, - tempSignedPreKeyPublic, - tempSignedPreKeySignature, - tempIdentityKey, - ); - try { + final SignalProtocolStore? signalStore = await getSignalStore(); + + if (signalStore == null) { + return false; + } + + final targetAddress = getSignalAddress(userData.userId.toInt()); + + final sessionBuilder = SessionBuilder.fromSignalStore( + signalStore, + targetAddress, + ); + + ECPublicKey? tempPrePublicKey; + int? tempPreKeyId; + + if (userData.prekeys.isNotEmpty) { + tempPrePublicKey = _decodePublicKey( + userData.prekeys.first.prekey, + 'prekey', + ); + tempPreKeyId = userData.prekeys.first.id.toInt(); + } + + final tempSignedPreKeyId = userData.signedPrekeyId.toInt(); + + final tempSignedPreKeyPublic = _decodePublicKey( + userData.signedPrekey, + 'signed prekey', + ); + + final tempSignedPreKeySignature = Uint8List.fromList( + userData.signedPrekeySignature, + ); + + final tempIdentityKey = IdentityKey( + _decodePublicKey(userData.publicIdentityKey, 'identity key'), + ); + + final preKeyBundle = PreKeyBundle( + userData.registrationId.toInt(), + defaultDeviceId, + tempPreKeyId, + tempPrePublicKey, + tempSignedPreKeyId, + tempSignedPreKeyPublic, + tempSignedPreKeySignature, + tempIdentityKey, + ); + await sessionBuilder.processPreKeyBundle(preKeyBundle); return true; } catch (e) { diff --git a/lib/src/visual/views/camera/camera_preview_components/main_camera_controller.dart b/lib/src/visual/views/camera/camera_preview_components/main_camera_controller.dart index 797f7a56..c06b9a99 100644 --- a/lib/src/visual/views/camera/camera_preview_components/main_camera_controller.dart +++ b/lib/src/visual/views/camera/camera_preview_components/main_camera_controller.dart @@ -127,8 +127,10 @@ class MainCameraController { try { await cameraControllerTemp?.stopImageStream(); // ignore: empty_catches - } catch (e) {} - await cameraControllerTemp?.dispose(); + } catch (e) { + } finally { + await cameraControllerTemp?.dispose(); + } }, ); initCameraStarted = false; diff --git a/lib/src/visual/views/contact/contact.view.dart b/lib/src/visual/views/contact/contact.view.dart index fe28593f..beb34996 100644 --- a/lib/src/visual/views/contact/contact.view.dart +++ b/lib/src/visual/views/contact/contact.view.dart @@ -10,6 +10,7 @@ import 'package:twonly/src/database/daos/contacts.dao.dart'; import 'package:twonly/src/database/tables/contacts.table.dart'; import 'package:twonly/src/database/twonly.db.dart'; import 'package:twonly/src/services/signal/session.signal.dart'; +import 'package:twonly/src/utils/log.dart'; import 'package:twonly/src/utils/misc.dart'; import 'package:twonly/src/visual/components/alert.dialog.dart'; import 'package:twonly/src/visual/components/avatar_icon.comp.dart'; @@ -38,6 +39,7 @@ class ContactView extends StatefulWidget { class _ContactViewState extends State { Contact? _contact; List _memberOfGroups = []; + bool _isResettingSignalSession = false; late StreamSubscription _streamContact; late StreamSubscription> _streamMemberOfGroups; @@ -136,6 +138,37 @@ class _ContactViewState extends State { } } + Future handleResetSignalSession(Contact contact) async { + if (_isResettingSignalSession) return; + + final displayName = getContactDisplayName(contact); + final confirmed = await showAlertDialog( + context, + context.lang.contactResetSessionTitle(displayName), + context.lang.contactResetSessionBody, + customOk: context.lang.contactResetSessionConfirm, + ); + if (!confirmed || !mounted) return; + + setState(() => _isResettingSignalSession = true); + var success = false; + try { + success = await resetSignalSession(contact.userId); + } catch (error) { + Log.error('Could not reset Signal session: $error'); + } + if (!mounted) return; + + setState(() => _isResettingSignalSession = false); + showSnackbar( + context, + success + ? context.lang.contactResetSessionSuccess(displayName) + : context.lang.contactResetSessionFailed, + level: success ? SnackbarLevel.success : SnackbarLevel.error, + ); + } + @override Widget build(BuildContext context) { if (_contact == null) return Container(); @@ -268,6 +301,19 @@ class _ContactViewState extends State { text: context.lang.contactBlock, onTap: () => handleUserBlockRequest(contact), ), + BetterListTile( + icon: FontAwesomeIcons.screwdriverWrench, + text: context.lang.contactTroubleshoot, + trailing: _isResettingSignalSession + ? const SizedBox.square( + dimension: 20, + child: CircularProgressIndicator(strokeWidth: 2), + ) + : null, + onTap: _isResettingSignalSession + ? null + : () => handleResetSignalSession(contact), + ), BetterListTile( icon: FontAwesomeIcons.userMinus, iconSize: 16, diff --git a/pubspec.yaml b/pubspec.yaml index 5e838714..1482b415 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -3,7 +3,7 @@ description: "twonly, a privacy-friendly way to connect with friends through sec publish_to: 'none' -version: 0.5.2+172 +version: 0.5.3+173 environment: sdk: ^3.11.0 diff --git a/rust/src/backup/backup_archive.rs b/rust/src/backup/backup_archive.rs index ca631223..faa41b9e 100644 --- a/rust/src/backup/backup_archive.rs +++ b/rust/src/backup/backup_archive.rs @@ -13,6 +13,65 @@ use zip::{CompressionMethod, ZipArchive, ZipWriter}; pub(crate) struct BackupArchive {} impl BackupArchive { + async fn reset_restored_signal_sessions( + file_name: &str, + database_path: &Path, + encryption_key: Option<&str>, + ) -> Result<()> { + let table = match file_name { + "twonly.sqlite" => "signal_session_stores", + "rust_db.sqlite" => "signal_sessions", + _ => return Ok(()), + }; + + let database = + Database::new(&database_path.display().to_string(), encryption_key, false).await?; + + let table_exists: bool = sqlx::query_scalar( + "SELECT EXISTS(SELECT 1 FROM sqlite_master WHERE type = 'table' AND name = ?)", + ) + .bind(table) + .fetch_one(&database.pool) + .await?; + + if table_exists { + match file_name { + "twonly.sqlite" => { + sqlx::query("DELETE FROM signal_session_stores") + .execute(&database.pool) + .await?; + } + "rust_db.sqlite" => { + sqlx::query("DELETE FROM signal_sessions") + .execute(&database.pool) + .await?; + } + _ => unreachable!(), + } + } + + // The hotfix chooses the encryption implementation from this column. + // A restored v2 marker without its restored session would make the + // first outgoing message fail before the normal migration can rebuild + // it. Mark contacts as v1; the post-download migration upgrades every + // peer that currently publishes a PQXDH bundle. + if file_name == "twonly.sqlite" { + let signal_version_exists: bool = sqlx::query_scalar( + "SELECT EXISTS(SELECT 1 FROM pragma_table_info('contacts') WHERE name = 'signal_version')", + ) + .fetch_one(&database.pool) + .await?; + if signal_version_exists { + sqlx::query("UPDATE contacts SET signal_version = 'v1'") + .execute(&database.pool) + .await?; + } + } + + database.pool.close().await; + Ok(()) + } + #[allow(clippy::type_complexity)] fn get_backup_files( ctx: &Context, @@ -147,9 +206,19 @@ impl BackupArchive { } } - for (file_name, target_dir, is_db, _) in Self::get_backup_files(ctx, &key_manager)? { + for (file_name, target_dir, is_db, mut encryption_key) in + Self::get_backup_files(ctx, &key_manager)? + { let src = restore_temp_dir.join(file_name); if src.exists() { + if is_db { + Self::reset_restored_signal_sessions( + file_name, + &src, + encryption_key.as_deref(), + ) + .await?; + } std::fs::create_dir_all(&target_dir)?; let dst = target_dir.join(file_name); if is_db { @@ -161,6 +230,7 @@ impl BackupArchive { std::fs::copy(src, dst)?; } + encryption_key.zeroize(); } std::fs::remove_dir_all(&restore_temp_dir)?; @@ -210,6 +280,18 @@ mod tests { ReceivedMessage::insert(&db.pool, 1, b"original message") .await .unwrap(); + sqlx::query( + "INSERT INTO signal_identities(name, identity_key, timestamp) VALUES('restored-peer', x'040506', 1)", + ) + .execute(&db.pool) + .await + .unwrap(); + sqlx::query( + "INSERT INTO signal_sessions(name, device_id, record_bytes) VALUES('restored-peer', 1, x'010203')", + ) + .execute(&db.pool) + .await + .unwrap(); // Add a file let config_file = PathBuf::from(&config.data_dir).join("user_discovery_config.json"); @@ -217,6 +299,39 @@ mod tests { key_manager.main_key.get_login_token() }; + // The hotfix still carries the legacy Dart Signal store alongside the + // Rust PQXDH store. Both session stores are part of an archive. + { + let config = ctx.get_config().unwrap(); + let app_db_path = PathBuf::from(&config.database_dir).join("twonly.sqlite"); + let app_db = Database::new(&app_db_path.display().to_string(), None, false) + .await + .unwrap(); + sqlx::query( + "CREATE TABLE contacts(user_id INTEGER PRIMARY KEY, signal_version TEXT NOT NULL)", + ) + .execute(&app_db.pool) + .await + .unwrap(); + sqlx::query( + "CREATE TABLE signal_session_stores(device_id INTEGER NOT NULL, name TEXT NOT NULL, session_record BLOB NOT NULL, PRIMARY KEY(device_id, name))", + ) + .execute(&app_db.pool) + .await + .unwrap(); + sqlx::query("INSERT INTO contacts(user_id, signal_version) VALUES(7, 'v2')") + .execute(&app_db.pool) + .await + .unwrap(); + sqlx::query( + "INSERT INTO signal_session_stores(device_id, name, session_record) VALUES(1, '7', x'070809')", + ) + .execute(&app_db.pool) + .await + .unwrap(); + app_db.pool.close().await; + } + // 2. Create backup let backup_path = BackupArchive::create_backup(&ctx).await.unwrap(); assert!(backup_path.exists()); @@ -266,6 +381,44 @@ mod tests { assert_eq!(messages[0].sender_id, 1); assert_eq!(messages[0].content, b"original message"); + let session_count: i64 = sqlx::query_scalar("SELECT COUNT(*) FROM signal_sessions") + .fetch_one(&db.pool) + .await + .unwrap(); + assert_eq!(session_count, 0, "restored PQXDH sessions must be reset"); + + let identity_key: Vec = sqlx::query_scalar( + "SELECT identity_key FROM signal_identities WHERE name = 'restored-peer'", + ) + .fetch_one(&db.pool) + .await + .unwrap(); + assert_eq!( + identity_key, + vec![4, 5, 6], + "trusted contact identities must survive session reset" + ); + + let app_db_path = PathBuf::from(&config.database_dir).join("twonly.sqlite"); + let app_db = Database::new(&app_db_path.display().to_string(), None, false) + .await + .unwrap(); + let legacy_session_count: i64 = + sqlx::query_scalar("SELECT COUNT(*) FROM signal_session_stores") + .fetch_one(&app_db.pool) + .await + .unwrap(); + assert_eq!( + legacy_session_count, 0, + "restored legacy Signal sessions must be reset" + ); + let signal_version: String = + sqlx::query_scalar("SELECT signal_version FROM contacts WHERE user_id = 7") + .fetch_one(&app_db.pool) + .await + .unwrap(); + assert_eq!(signal_version, "v1"); + let config_file = PathBuf::from(&config.data_dir).join("user_discovery_config.json"); let config_content = std::fs::read_to_string(config_file).unwrap(); assert_eq!(config_content, "original config"); diff --git a/rust/src/bridge/wrapper/signal.rs b/rust/src/bridge/wrapper/signal.rs index 3c163117..61de6c81 100644 --- a/rust/src/bridge/wrapper/signal.rs +++ b/rust/src/bridge/wrapper/signal.rs @@ -29,6 +29,17 @@ impl RustSignal { engine.process_prekey_bundle(name, device_id, bundle).await } + /// Removes the current v2 session for a contact. The Dart hotfix layer + /// immediately establishes its replacement from a freshly fetched bundle. + pub async fn reset_contact_session(contact_id: i64) -> Result<()> { + let guard = get_twonly_flutter()?.signal_engine.lock().await; + let engine = guard.as_ref().ok_or(TwonlyError::Initialization)?; + engine + .reset_session(&contact_id.to_string(), 1, true) + .await?; + Ok(()) + } + pub async fn encrypt(name: String, device_id: u32, plaintext: Vec) -> Result> { let guard = get_twonly_flutter()?.signal_engine.lock().await; let engine = guard.as_ref().ok_or(TwonlyError::Initialization)?; diff --git a/rust/src/frb_generated.rs b/rust/src/frb_generated.rs index de359247..c072ac77 100644 --- a/rust/src/frb_generated.rs +++ b/rust/src/frb_generated.rs @@ -40,7 +40,7 @@ flutter_rust_bridge::frb_generated_boilerplate!( default_rust_auto_opaque = RustAutoOpaqueMoi, ); pub(crate) const FLUTTER_RUST_BRIDGE_CODEGEN_VERSION: &str = "2.12.0"; -pub(crate) const FLUTTER_RUST_BRIDGE_CODEGEN_CONTENT_HASH: i32 = 1109927244; +pub(crate) const FLUTTER_RUST_BRIDGE_CODEGEN_CONTENT_HASH: i32 = -2042422055; // Section: executor @@ -915,6 +915,46 @@ fn wire__crate__bridge__wrapper__signal__rust_signal_process_prekey_bundle_impl( }, ) } +fn wire__crate__bridge__wrapper__signal__rust_signal_reset_contact_session_impl( + port_: flutter_rust_bridge::for_generated::MessagePort, + ptr_: flutter_rust_bridge::for_generated::PlatformGeneralizedUint8ListPtr, + rust_vec_len_: i32, + data_len_: i32, +) { + FLUTTER_RUST_BRIDGE_HANDLER.wrap_async::( + flutter_rust_bridge::for_generated::TaskInfo { + debug_name: "rust_signal_reset_contact_session", + port: Some(port_), + mode: flutter_rust_bridge::for_generated::FfiCallMode::Normal, + }, + move || { + let message = unsafe { + flutter_rust_bridge::for_generated::Dart2RustMessageSse::from_wire( + ptr_, + rust_vec_len_, + data_len_, + ) + }; + let mut deserializer = + flutter_rust_bridge::for_generated::SseDeserializer::new(message); + let api_contact_id = ::sse_decode(&mut deserializer); + deserializer.end(); + move |context| async move { + transform_result_sse::<_, flutter_rust_bridge::for_generated::anyhow::Error>( + (move || async move { + let output_ok = + crate::bridge::wrapper::signal::RustSignal::reset_contact_session( + api_contact_id, + ) + .await?; + Ok(output_ok) + })() + .await, + ) + } + }, + ) +} fn wire__crate__bridge__wrapper__rust_utils_generate_shares_impl( port_: flutter_rust_bridge::for_generated::MessagePort, ptr_: flutter_rust_bridge::for_generated::PlatformGeneralizedUint8ListPtr, @@ -2201,24 +2241,25 @@ fn pde_ffi_dispatcher_primary_impl( 34 => wire__crate__bridge__wrapper__signal__rust_signal_generate_bundle_impl(port, ptr, rust_vec_len, data_len), 35 => wire__crate__bridge__wrapper__signal__rust_signal_generate_pqc_prekeys_impl(port, ptr, rust_vec_len, data_len), 36 => wire__crate__bridge__wrapper__signal__rust_signal_process_prekey_bundle_impl(port, ptr, rust_vec_len, data_len), -37 => wire__crate__bridge__wrapper__rust_utils_generate_shares_impl(port, ptr, rust_vec_len, data_len), -38 => wire__crate__bridge__wrapper__rust_utils_recover_secret_impl(port, ptr, rust_vec_len, data_len), -39 => wire__crate__bridge__callbacks__user_discovery__user_discovery_store_flutter_get_announced_user_by_public_id_impl(port, ptr, rust_vec_len, data_len), -40 => wire__crate__bridge__callbacks__user_discovery__user_discovery_store_flutter_get_config_impl(port, ptr, rust_vec_len, data_len), -41 => wire__crate__bridge__callbacks__user_discovery__user_discovery_store_flutter_get_contact_promotion_impl(port, ptr, rust_vec_len, data_len), -42 => wire__crate__bridge__callbacks__user_discovery__user_discovery_store_flutter_get_contact_version_impl(port, ptr, rust_vec_len, data_len), -43 => wire__crate__bridge__callbacks__user_discovery__user_discovery_store_flutter_get_other_promotions_by_public_id_impl(port, ptr, rust_vec_len, data_len), -44 => wire__crate__bridge__callbacks__user_discovery__user_discovery_store_flutter_get_own_promotions_after_version_impl(port, ptr, rust_vec_len, data_len), -45 => wire__crate__bridge__callbacks__user_discovery__user_discovery_store_flutter_get_share_for_contact_impl(port, ptr, rust_vec_len, data_len), -46 => wire__crate__bridge__callbacks__user_discovery__user_discovery_store_flutter_push_new_user_relation_impl(port, ptr, rust_vec_len, data_len), -47 => wire__crate__bridge__callbacks__user_discovery__user_discovery_store_flutter_push_own_promotion_and_clear_old_version_impl(port, ptr, rust_vec_len, data_len), -48 => wire__crate__bridge__callbacks__user_discovery__user_discovery_store_flutter_set_contact_version_impl(port, ptr, rust_vec_len, data_len), -49 => wire__crate__bridge__callbacks__user_discovery__user_discovery_store_flutter_set_shares_impl(port, ptr, rust_vec_len, data_len), -50 => wire__crate__bridge__callbacks__user_discovery__user_discovery_store_flutter_store_other_promotion_impl(port, ptr, rust_vec_len, data_len), -51 => wire__crate__bridge__callbacks__user_discovery__user_discovery_store_flutter_update_config_impl(port, ptr, rust_vec_len, data_len), -52 => wire__crate__bridge__callbacks__user_discovery__user_discovery_utils_flutter_sign_data_impl(port, ptr, rust_vec_len, data_len), -53 => wire__crate__bridge__callbacks__user_discovery__user_discovery_utils_flutter_verify_signature_impl(port, ptr, rust_vec_len, data_len), -54 => wire__crate__bridge__callbacks__user_discovery__user_discovery_utils_flutter_verify_stored_pubkey_impl(port, ptr, rust_vec_len, data_len), +37 => wire__crate__bridge__wrapper__signal__rust_signal_reset_contact_session_impl(port, ptr, rust_vec_len, data_len), +38 => wire__crate__bridge__wrapper__rust_utils_generate_shares_impl(port, ptr, rust_vec_len, data_len), +39 => wire__crate__bridge__wrapper__rust_utils_recover_secret_impl(port, ptr, rust_vec_len, data_len), +40 => wire__crate__bridge__callbacks__user_discovery__user_discovery_store_flutter_get_announced_user_by_public_id_impl(port, ptr, rust_vec_len, data_len), +41 => wire__crate__bridge__callbacks__user_discovery__user_discovery_store_flutter_get_config_impl(port, ptr, rust_vec_len, data_len), +42 => wire__crate__bridge__callbacks__user_discovery__user_discovery_store_flutter_get_contact_promotion_impl(port, ptr, rust_vec_len, data_len), +43 => wire__crate__bridge__callbacks__user_discovery__user_discovery_store_flutter_get_contact_version_impl(port, ptr, rust_vec_len, data_len), +44 => wire__crate__bridge__callbacks__user_discovery__user_discovery_store_flutter_get_other_promotions_by_public_id_impl(port, ptr, rust_vec_len, data_len), +45 => wire__crate__bridge__callbacks__user_discovery__user_discovery_store_flutter_get_own_promotions_after_version_impl(port, ptr, rust_vec_len, data_len), +46 => wire__crate__bridge__callbacks__user_discovery__user_discovery_store_flutter_get_share_for_contact_impl(port, ptr, rust_vec_len, data_len), +47 => wire__crate__bridge__callbacks__user_discovery__user_discovery_store_flutter_push_new_user_relation_impl(port, ptr, rust_vec_len, data_len), +48 => wire__crate__bridge__callbacks__user_discovery__user_discovery_store_flutter_push_own_promotion_and_clear_old_version_impl(port, ptr, rust_vec_len, data_len), +49 => wire__crate__bridge__callbacks__user_discovery__user_discovery_store_flutter_set_contact_version_impl(port, ptr, rust_vec_len, data_len), +50 => wire__crate__bridge__callbacks__user_discovery__user_discovery_store_flutter_set_shares_impl(port, ptr, rust_vec_len, data_len), +51 => wire__crate__bridge__callbacks__user_discovery__user_discovery_store_flutter_store_other_promotion_impl(port, ptr, rust_vec_len, data_len), +52 => wire__crate__bridge__callbacks__user_discovery__user_discovery_store_flutter_update_config_impl(port, ptr, rust_vec_len, data_len), +53 => wire__crate__bridge__callbacks__user_discovery__user_discovery_utils_flutter_sign_data_impl(port, ptr, rust_vec_len, data_len), +54 => wire__crate__bridge__callbacks__user_discovery__user_discovery_utils_flutter_verify_signature_impl(port, ptr, rust_vec_len, data_len), +55 => wire__crate__bridge__callbacks__user_discovery__user_discovery_utils_flutter_verify_stored_pubkey_impl(port, ptr, rust_vec_len, data_len), _ => unreachable!(), } } diff --git a/rust/src/signal/engine.rs b/rust/src/signal/engine.rs index debfa415..a282ce68 100644 --- a/rust/src/signal/engine.rs +++ b/rust/src/signal/engine.rs @@ -2,8 +2,8 @@ use crate::error::{Result, TwonlyError}; use libsignal_protocol::{ message_encrypt, process_prekey_bundle, CiphertextMessageType, DeviceId, GenericSignedPreKey, IdentityKey, IdentityKeyPair, IdentityKeyStore, KyberPreKeyId, KyberPreKeyStore, PreKeyBundle, - PreKeyId, PreKeySignalMessage, PreKeyStore, ProtocolAddress, PublicKey, SignalMessage, - SignedPreKeyId, SignedPreKeyStore, Timestamp, + PreKeyId, PreKeySignalMessage, PreKeyStore, ProtocolAddress, PublicKey, SessionStore, + SignalMessage, SignedPreKeyId, SignedPreKeyStore, Timestamp, }; use std::sync::Arc; use std::time::{SystemTime, UNIX_EPOCH}; @@ -414,6 +414,58 @@ impl RustSignalEngine { Ok(()) } + /// Retires the session with a peer so a fresh one can replace it. + /// + /// A hard reset deletes the complete record, including archived states. + /// A soft reset archives only the current state so in-flight messages can + /// still be decrypted while a replacement session is established. + /// + /// Returns whether a session existed. + pub async fn reset_session(&self, name: &str, device_id: u32, hard: bool) -> Result { + let mut store_guard = self.store.lock().await; + let store = &mut *store_guard; + + let d_id = DeviceId::try_from(device_id) + .map_err(|_| TwonlyError::Generic(format!("Invalid device id: {}", device_id)))?; + let address = ProtocolAddress::new(name.to_owned(), d_id); + + if hard { + let deleted = + sqlx::query("DELETE FROM signal_sessions WHERE name = ? AND device_id = ?") + .bind(name) + .bind(device_id) + .execute(&store.pool) + .await? + .rows_affected(); + tracing::warn!(name, device_id, "deleted the signal session"); + return Ok(deleted != 0); + } + + let Some(mut record) = store + .session_store + .load_session(&address) + .assert_send() + .await + .map_err(|e| TwonlyError::Signal(e.to_string()))? + else { + return Ok(false); + }; + + record + .archive_current_state() + .map_err(|e| TwonlyError::Signal(e.to_string()))?; + + store + .session_store + .store_session(&address, &record) + .assert_send() + .await + .map_err(|e| TwonlyError::Signal(e.to_string()))?; + + tracing::warn!(name, device_id, "archived the signal session for a reset"); + Ok(true) + } + pub async fn encrypt_message( &self, name: String, @@ -618,4 +670,40 @@ mod tests { assert_eq!(plaintext.to_vec(), decrypted); } + + #[tokio::test] + async fn hard_reset_deletes_and_allows_rebuilding_a_contact_session() { + let (alice_engine, _alice_dir) = create_test_engine("alice").await; + let (bob_engine, _bob_dir) = create_test_engine("bob").await; + + alice_engine + .process_prekey_bundle( + "bob".to_string(), + 1, + bob_engine.generate_bundle().await.unwrap(), + ) + .await + .unwrap(); + + assert!(alice_engine.reset_session("bob", 1, true).await.unwrap()); + assert!(!alice_engine.reset_session("bob", 1, true).await.unwrap()); + assert!(alice_engine + .encrypt_message("bob".to_string(), 1, b"missing".to_vec()) + .await + .is_err()); + + alice_engine + .process_prekey_bundle( + "bob".to_string(), + 1, + bob_engine.generate_bundle().await.unwrap(), + ) + .await + .unwrap(); + + assert!(alice_engine + .encrypt_message("bob".to_string(), 1, b"rebuilt".to_vec()) + .await + .is_ok()); + } } diff --git a/test/model/mailbox_drained_proto_test.dart b/test/model/mailbox_drained_proto_test.dart new file mode 100644 index 00000000..c7eecfc3 --- /dev/null +++ b/test/model/mailbox_drained_proto_test.dart @@ -0,0 +1,13 @@ +import 'package:flutter_test/flutter_test.dart'; +import 'package:twonly/src/model/protobuf/api/websocket/server_to_client.pb.dart'; + +void main() { + test('decodes the server mailbox-drained signal', () { + // ServerToClient.V0 (field 1) containing V0.mailboxDrained (field 11). + final message = ServerToClient.fromBuffer([0x0a, 0x02, 0x58, 0x01]); + + expect(message.v0.hasMailboxDrained(), isTrue); + expect(message.v0.mailboxDrained, isTrue); + expect(message.v0.whichKind(), V0_Kind.mailboxDrained); + }); +} diff --git a/test/model/pqc_upload_proto_test.dart b/test/model/pqc_upload_proto_test.dart new file mode 100644 index 00000000..4477b4cb --- /dev/null +++ b/test/model/pqc_upload_proto_test.dart @@ -0,0 +1,27 @@ +import 'package:fixnum/fixnum.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:twonly/src/model/protobuf/api/websocket/client_to_server.pb.dart'; + +void main() { + test('PQC upload preserves migration identity fields on the wire', () { + final request = ApplicationData_UploadPqcPreKeys( + eccSignedPrekeyId: Int64(11), + eccSignedPrekey: [5, 6], + eccSignedPrekeySignature: [7, 8], + kyberSignedPrekeyId: Int64(12), + kyberSignedPrekey: [9, 10], + kyberSignedPrekeySignature: [11, 12], + publicIdentityKey: List.filled(33, 1), + registrationId: Int64(42), + ); + + final decoded = ApplicationData_UploadPqcPreKeys.fromBuffer( + request.writeToBuffer(), + ); + + expect(decoded.publicIdentityKey, hasLength(33)); + expect(decoded.registrationId, Int64(42)); + expect(decoded.hasPublicIdentityKey(), isTrue); + expect(decoded.hasRegistrationId(), isTrue); + }); +} diff --git a/test/services/receipts_out_of_sync_detection_test.dart b/test/services/receipts_out_of_sync_detection_test.dart new file mode 100644 index 00000000..1d88e4d7 --- /dev/null +++ b/test/services/receipts_out_of_sync_detection_test.dart @@ -0,0 +1,86 @@ +import 'package:drift/drift.dart'; +import 'package:drift/native.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:twonly/src/database/twonly.db.dart'; + +void main() { + late TwonlyDB db; + + setUp(() async { + db = TwonlyDB.forTesting( + DatabaseConnection( + NativeDatabase.memory(), + closeStreamsSynchronously: true, + ), + ); + + for (final contactId in [10, 11, 12]) { + await db.contactsDao.insertContact( + ContactsCompanion.insert( + userId: Value(contactId), + username: 'contact-$contactId', + ), + ); + } + }); + + tearDown(() => db.close()); + + Future addReceipt( + int contactId, + int retryCount, { + bool mediaUpload = false, + }) async { + await db.receiptsDao.insertReceipt( + ReceiptsCompanion( + contactId: Value(contactId), + message: Value(Uint8List.fromList([contactId])), + retryCount: Value(retryCount), + willBeRetriedByMediaUpload: Value(mediaUpload), + ), + ); + } + + test( + 'detects multiple message retries above the ratchet threshold', + () async { + await addReceipt(10, 21); + await addReceipt(10, 30); + await addReceipt(11, 21); + await addReceipt(11, 20); + await addReceipt(12, 25, mediaUpload: true); + await addReceipt(12, 25, mediaUpload: true); + + final contactIds = await db.receiptsDao + .getContactsWithStuckSignalMessages( + retryThreshold: 20, + ); + + expect(contactIds, [10]); + }, + ); + + test('clears message counters after a successful session reset', () async { + await addReceipt(10, 21); + await addReceipt(10, 30); + await addReceipt(10, 25, mediaUpload: true); + + await db.receiptsDao.resetMessageRetryCountsForContact(10); + + final receipts = await (db.select( + db.receipts, + )..where((receipt) => receipt.contactId.equals(10))).get(); + final messageReceipts = receipts.where( + (receipt) => !receipt.willBeRetriedByMediaUpload, + ); + final mediaReceipt = receipts.singleWhere( + (receipt) => receipt.willBeRetriedByMediaUpload, + ); + + expect( + messageReceipts.map((receipt) => receipt.retryCount), + everyElement(0), + ); + expect(mediaReceipt.retryCount, 25); + }); +}