diff --git a/android/app/build.gradle b/android/app/build.gradle index d0c98e4..802810e 100644 --- a/android/app/build.gradle +++ b/android/app/build.gradle @@ -33,7 +33,7 @@ android { defaultConfig { // TODO: Specify your own unique Application ID (https://developer.android.com/studio/build/application-id.html). - applicationId = "eu.twonly.testng" + applicationId = "eu.twonly.testing" multiDexEnabled true // You can update the following values to match your application needs. // For more information, see: https://flutter.dev/to/review-gradle-config. diff --git a/ios/Runner.xcodeproj/project.pbxproj b/ios/Runner.xcodeproj/project.pbxproj index 4f701a7..12712ce 100644 --- a/ios/Runner.xcodeproj/project.pbxproj +++ b/ios/Runner.xcodeproj/project.pbxproj @@ -12,10 +12,10 @@ 331C808B294A63AB00263BE5 /* RunnerTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 331C807B294A618700263BE5 /* RunnerTests.swift */; }; 3B3967161E833CAA004F5970 /* AppFrameworkInfo.plist in Resources */ = {isa = PBXBuildFile; fileRef = 3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */; }; 74858FAF1ED2DC5600515810 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 74858FAE1ED2DC5600515810 /* AppDelegate.swift */; }; + 951DB0F2008EB94699D02555 /* libPods-Runner.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 5F3BB8E3AC9CEA61248BD989 /* libPods-Runner.a */; }; 97C146FC1CF9000F007C117D /* Main.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FA1CF9000F007C117D /* Main.storyboard */; }; 97C146FE1CF9000F007C117D /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FD1CF9000F007C117D /* Assets.xcassets */; }; 97C147011CF9000F007C117D /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FF1CF9000F007C117D /* LaunchScreen.storyboard */; }; - A8E4DD3B3139A6996AC817E0 /* libPods-Runner.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 5F3BB8E3AC9CEA61248BD989 /* libPods-Runner.a */; }; F3C66D726A2EB28484DF0B10 /* GoogleService-Info.plist in Resources */ = {isa = PBXBuildFile; fileRef = 16FBC6F5B58E1C6646F5D447 /* GoogleService-Info.plist */; }; /* End PBXBuildFile section */ @@ -83,7 +83,7 @@ isa = PBXFrameworksBuildPhase; buildActionMask = 2147483647; files = ( - A8E4DD3B3139A6996AC817E0 /* libPods-Runner.a in Frameworks */, + 951DB0F2008EB94699D02555 /* libPods-Runner.a in Frameworks */, ); runOnlyForDeploymentPostprocessing = 0; }; diff --git a/l10n.yaml b/l10n.yaml index d480072..5852ac9 100644 --- a/l10n.yaml +++ b/l10n.yaml @@ -1,3 +1,4 @@ arb-dir: lib/src/localization template-arb-file: app_en.arb output-localization-file: app_localizations.dart +untranslated-messages-file: build/l10n.log diff --git a/lib/src/database/daos/messages_dao.dart b/lib/src/database/daos/messages_dao.dart index 4f552d8..65de60c 100644 --- a/lib/src/database/daos/messages_dao.dart +++ b/lib/src/database/daos/messages_dao.dart @@ -144,11 +144,14 @@ class MessagesDao extends DatabaseAccessor return (delete(messages)..where((t) => t.messageId.equals(messageId))).go(); } - Future containsOtherMessageId(int messageOtherId) async { + Future containsOtherMessageId( + int fromUserId, int messageOtherId) async { final query = select(messages) - ..where((t) => t.messageOtherId.equals(messageOtherId)); - final entry = await query.getSingleOrNull(); - return entry != null; + ..where((t) => + t.messageOtherId.equals(messageOtherId) & + t.contactId.equals(fromUserId)); + final entry = await query.get(); + return entry.isNotEmpty; } SingleOrNullSelectable getMessageByMessageId(int messageId) { diff --git a/lib/src/localization/app_de.arb b/lib/src/localization/app_de.arb index ddff01d..1b5db41 100644 --- a/lib/src/localization/app_de.arb +++ b/lib/src/localization/app_de.arb @@ -43,6 +43,7 @@ "contextMenuVerifyUser": "Kontakt verifizieren", "contextMenuOpenChat": "Chat öffnen", "contextMenuSendImage": "Bild senden", + "mediaViewerAuthReason": "Bitte authentifiziere dich, um diesen twonly zu sehen!", "messageSendState_Received": "Empfangen", "messageSendState_Opened": "Geöffnet", "messageSendState_Send": "Gesendet", diff --git a/lib/src/localization/app_en.arb b/lib/src/localization/app_en.arb index f24d3be..d9c572b 100644 --- a/lib/src/localization/app_en.arb +++ b/lib/src/localization/app_en.arb @@ -1,40 +1,79 @@ { "@@locale": "en", "registerTitle": "Welcome to twonly!", + "@registerTitle": {}, "registerSlogan": "twonly, a privacy friendly way to connect with friends through secure, spontaneous image sharing", + "@registerSlogan": {}, "onboardingWelcomeTitle": "Welcome to twonly!", + "@onboardingWelcomeTitle": {}, "onboardingWelcomeBody": "Experience a private and secure way to stay in touch with friends by sharing instant pictures.", + "@onboardingWelcomeBody": {}, "onboardingE2eTitle": "Carefree sharing", + "@onboardingE2eTitle": {}, "onboardingE2eBody": "With end-to-end encryption, enjoy the peace of mind that only you and your friends can see the moments you share.", + "@onboardingE2eBody": {}, "onboardingFocusTitle": "Focus on sharing moments", + "@onboardingFocusTitle": {}, "onboardingFocusBody": "Say goodbye to addictive features! twonly was created for sharing moments, free from useless distractions or ads.", + "@onboardingFocusBody": {}, "onboardingSendTwonliesTitle": "Send twonlies", + "@onboardingSendTwonliesTitle": {}, "onboardingSendTwonliesBody": "Share moments securely with your partner. twonly ensures that only your partner can open it, keeping your moments with your partner a two(o)nly thing!", + "@onboardingSendTwonliesBody": {}, "onboardingNotProductTitle": "You are not the product!", + "@onboardingNotProductTitle": {}, "onboardingNotProductBody": "twonly is financed by a small monthly fee and not by selling your data.", + "@onboardingNotProductBody": {}, "onboardingBuyOneGetTwoTitle": "Buy one get two", + "@onboardingBuyOneGetTwoTitle": {}, "onboardingBuyOneGetTwoBody": "twonly always requires at least two people, which is why you receive a second free license for your twonly partner with your purchase.", + "@onboardingBuyOneGetTwoBody": {}, "onboardingGetStartedTitle": "Let's go!", + "@onboardingGetStartedTitle": {}, "onboardingGetStartedBody": "You can test twonly free of charge for 14 days, after that it costs either 1€/month or 9€/year.", + "@onboardingGetStartedBody": {}, "onboardingTryForFree": "Try for free", + "@onboardingTryForFree": {}, "registerUsernameSlogan": "Please select a username so others can find you!", + "@registerUsernameSlogan": {}, "registerUsernameDecoration": "Username", + "@registerUsernameDecoration": {}, "registerUsernameLimits": "Username must be 3 to 12 characters long, consisting only of letters (a-z) and numbers (0-9).", + "@registerUsernameLimits": {}, "registerSubmitButton": "Register now!", + "@registerSubmitButton": {}, "newMessageTitle": "New message", + "@newMessageTitle": {}, "chatsTapToSend": "Click to send your first image", + "@chatsTapToSend": {}, "shareImageTitle": "Share with", + "@shareImageTitle": {}, "shareImageBestFriends": "Best friends", + "@shareImageBestFriends": {}, "shareImagedEditorSendImage": "Send", + "@shareImagedEditorSendImage": {}, "shareImagedEditorShareWith": "Share with", + "@shareImagedEditorShareWith": {}, "shareImagedEditorSaveImage": "Save", + "@shareImagedEditorSaveImage": {}, "shareImagedEditorSavedImage": "Saved", + "@shareImagedEditorSavedImage": {}, "shareImageAllUsers": "All contacts", + "@shareImageAllUsers": {}, "shareImageAllTwonlyWarning": "twonlies can only be send to verified contacts!", + "@shareImageAllTwonlyWarning": {}, "searchUsernameInput": "Username", + "@searchUsernameInput": {}, "searchUsernameTitle": "Search username", + "@searchUsernameTitle": {}, "searchUsernameNotFound": "Username not found", - "searchUsernameNotFoundBody": "There is no user with the username \"{username}\" registered.", + "@searchUsernameNotFound": {}, + "searchUsernameNotFoundBody": "There is no user with the username \"{username}\" registered", + "@searchUsernameNotFoundBody": { + "placeholders": { + "username": {} + } + }, "searchUsernameNewFollowerTitle": "Follow requests", "searchUsernameQrCodeBtn": "Scan QR code", "chatListViewSearchUserNameBtn": "Add your first twonly contact!", diff --git a/lib/src/providers/api/server_messages.dart b/lib/src/providers/api/server_messages.dart index b04d948..abfe0fd 100644 --- a/lib/src/providers/api/server_messages.dart +++ b/lib/src/providers/api/server_messages.dart @@ -162,147 +162,145 @@ Future handleDownloadData(DownloadData data) async { Future handleNewMessage(int fromUserId, Uint8List body) async { MessageJson? message = await SignalHelper.getDecryptedText(fromUserId, body); - if (message != null) { - switch (message.kind) { - case MessageKind.contactRequest: - Result username = await apiProvider.getUsername(fromUserId); - if (username.isSuccess) { - Uint8List name = username.value.userdata.username; + if (message == null) { + Logger("server_messages") + .info("Got invalid cypher text from $fromUserId. Deleting it."); + // Message is not valid, so server can delete it + var ok = client.Response_Ok()..none = true; + return client.Response()..ok = ok; + } - int added = - await twonlyDatabase.contactsDao.insertContact(ContactsCompanion( - username: Value(utf8.decode(name)), - userId: Value(fromUserId), - requested: Value(true), - )); - if (added > 0) { - localPushNotificationNewMessage( - fromUserId.toInt(), - message, - 999999, - ); - } + switch (message.kind) { + case MessageKind.contactRequest: + return handleContactRequest(fromUserId, message); + + case MessageKind.opened: + final update = MessagesCompanion(openedAt: Value(message.timestamp)); + await twonlyDatabase.messagesDao.updateMessageByOtherUser( + fromUserId, + message.messageId!, + update, + ); + break; + + case MessageKind.rejectRequest: + await twonlyDatabase.contactsDao.deleteContactByUserId(fromUserId); + break; + + case MessageKind.acceptRequest: + final update = ContactsCompanion(accepted: Value(true)); + await twonlyDatabase.contactsDao.updateContact(fromUserId, update); + localPushNotificationNewMessage(fromUserId.toInt(), message, 8888888); + notifyContactsAboutProfileChange(); + break; + + case MessageKind.profileChange: + var content = message.content; + if (content is ProfileContent) { + final update = ContactsCompanion( + avatarSvg: Value(content.avatarSvg), + displayName: Value(content.displayName), + ); + twonlyDatabase.contactsDao.updateContact(fromUserId, update); + } + break; + + case MessageKind.ack: + final update = MessagesCompanion(acknowledgeByUser: Value(true)); + await twonlyDatabase.messagesDao.updateMessageByOtherUser( + fromUserId, + message.messageId!, + update, + ); + break; + + default: + if (message.kind != MessageKind.textMessage && + message.kind != MessageKind.media && + message.kind != MessageKind.storedMediaFile) { + Logger("handleServerMessages") + .shout("Got unknown MessageKind $message"); + } else if (message.content == null || message.messageId == null) { + Logger("handleServerMessages") + .shout("Content or messageid not defined $message"); + } else { + // when a message is received doubled ignore it... + if ((await twonlyDatabase.messagesDao + .containsOtherMessageId(fromUserId, message.messageId!))) { + var ok = client.Response_Ok()..none = true; + return client.Response()..ok = ok; } - break; - case MessageKind.opened: - final update = MessagesCompanion(openedAt: Value(message.timestamp)); - await twonlyDatabase.messagesDao.updateMessageByOtherUser( - fromUserId, - message.messageId!, + + String content = jsonEncode(message.content!.toJson()); + + bool acknowledgeByUser = false; + DateTime? openedAt; + + if (message.kind == MessageKind.storedMediaFile) { + acknowledgeByUser = true; + openedAt = DateTime.now(); + } + + int? responseToMessageId; + final textContent = message.content!; + if (textContent is TextMessageContent) { + responseToMessageId = textContent.responseToMessageId; + } + + final update = MessagesCompanion( + contactId: Value(fromUserId), + kind: Value(message.kind), + messageOtherId: Value(message.messageId), + contentJson: Value(content), + acknowledgeByServer: Value(true), + acknowledgeByUser: Value(acknowledgeByUser), + responseToMessageId: Value(responseToMessageId), + openedAt: Value(openedAt), + downloadState: Value(message.kind == MessageKind.media + ? DownloadState.pending + : DownloadState.downloaded), + sendAt: Value(message.timestamp), + ); + + final messageId = await twonlyDatabase.messagesDao.insertMessage( update, ); - break; - case MessageKind.rejectRequest: - await twonlyDatabase.contactsDao.deleteContactByUserId(fromUserId); - break; - case MessageKind.acceptRequest: - final update = ContactsCompanion(accepted: Value(true)); - await twonlyDatabase.contactsDao.updateContact(fromUserId, update); - localPushNotificationNewMessage(fromUserId.toInt(), message, 8888888); - notifyContactsAboutProfileChange(); - break; - case MessageKind.profileChange: - var content = message.content; - if (content is ProfileContent) { - final update = ContactsCompanion( - avatarSvg: Value(content.avatarSvg), - displayName: Value(content.displayName), - ); - twonlyDatabase.contactsDao.updateContact(fromUserId, update); + + if (messageId == null) { + return client.Response()..error = ErrorCode.InternalError; } - break; - case MessageKind.ack: - final update = MessagesCompanion(acknowledgeByUser: Value(true)); - await twonlyDatabase.messagesDao.updateMessageByOtherUser( - fromUserId, + + encryptAndSendMessage( message.messageId!, - update, + fromUserId, + MessageJson( + kind: MessageKind.ack, + messageId: message.messageId!, + content: MessageContent(), + timestamp: DateTime.now(), + ), ); - break; - default: - if (message.kind != MessageKind.textMessage && - message.kind != MessageKind.media && - message.kind != MessageKind.storedMediaFile) { - Logger("handleServerMessages") - .shout("Got unknown MessageKind $message"); - } else if (message.content != null && message.messageId != null) { - String content = jsonEncode(message.content!.toJson()); - bool acknowledgeByUser = false; - DateTime? openedAt; - if (message.kind == MessageKind.storedMediaFile) { - acknowledgeByUser = true; - openedAt = DateTime.now(); - } - - int? responseToMessageId; - final textContent = message.content!; - if (textContent is TextMessageContent) { - responseToMessageId = textContent.responseToMessageId; - } - - // when a message is received doubled ignore it... - if ((await twonlyDatabase.messagesDao - .containsOtherMessageId(message.messageId!))) { - var ok = client.Response_Ok()..none = true; - return client.Response()..ok = ok; - } - - final update = MessagesCompanion( - contactId: Value(fromUserId), - kind: Value(message.kind), - messageOtherId: Value(message.messageId), - contentJson: Value(content), - acknowledgeByServer: Value(true), - acknowledgeByUser: Value(acknowledgeByUser), - responseToMessageId: Value(responseToMessageId), - openedAt: Value(openedAt), - downloadState: Value(message.kind == MessageKind.media - ? DownloadState.pending - : DownloadState.downloaded), - sendAt: Value(message.timestamp), - ); - - final messageId = await twonlyDatabase.messagesDao.insertMessage( - update, - ); - - if (messageId == null) { - return client.Response()..error = ErrorCode.InternalError; - } - - encryptAndSendMessage( - message.messageId!, + if (message.kind == MessageKind.media) { + twonlyDatabase.contactsDao.incFlameCounter( fromUserId, - MessageJson( - kind: MessageKind.ack, - messageId: message.messageId!, - content: MessageContent(), - timestamp: DateTime.now(), - ), + true, + message.timestamp, ); - if (message.kind == MessageKind.media) { - twonlyDatabase.contactsDao.incFlameCounter( - fromUserId, - true, - message.timestamp, - ); - - if (!globalIsAppInBackground) { - final content = message.content; - if (content is MediaMessageContent) { - tryDownloadMedia( - messageId, - fromUserId, - content, - ); - } + if (!globalIsAppInBackground) { + final content = message.content; + if (content is MediaMessageContent) { + tryDownloadMedia( + messageId, + fromUserId, + content, + ); } } - localPushNotificationNewMessage(fromUserId, message, messageId); } - } + localPushNotificationNewMessage(fromUserId, message, messageId); + } } var ok = client.Response_Ok()..none = true; return client.Response()..ok = ok; @@ -321,3 +319,31 @@ Future handleRequestNewPreKey() async { var ok = client.Response_Ok()..prekeys = prekeys; return client.Response()..ok = ok; } + +Future handleContactRequest( + int fromUserId, MessageJson message) async { + // request the username by the server so an attacker can not + // forge the displayed username in the contact request + Result username = await apiProvider.getUsername(fromUserId); + if (username.isSuccess) { + Uint8List name = username.value.userdata.username; + + int added = await twonlyDatabase.contactsDao.insertContact( + ContactsCompanion( + username: Value(utf8.decode(name)), + userId: Value(fromUserId), + requested: Value(true), + ), + ); + + if (added > 0) { + localPushNotificationNewMessage( + fromUserId, + message, + 999999, + ); + } + } + var ok = client.Response_Ok()..none = true; + return client.Response()..ok = ok; +} diff --git a/lib/src/utils/signal.dart b/lib/src/utils/signal.dart index 4adc12f..44745ff 100644 --- a/lib/src/utils/signal.dart +++ b/lib/src/utils/signal.dart @@ -279,9 +279,6 @@ Future getDecryptedText(int source, Uint8List msg) async { if (msgs == null) return null; Uint8List body = msgs[0]; int type = bytesToInt(msgs[1]); - - // gzip.decode(body); - Uint8List plaintext; if (type == CiphertextMessage.prekeyType) { PreKeySignalMessage pre = PreKeySignalMessage(body); diff --git a/pubspec.lock b/pubspec.lock index c178ad0..c6d511a 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -426,7 +426,7 @@ packages: source: hosted version: "3.10.4" fixnum: - dependency: transitive + dependency: "direct main" description: name: fixnum sha256: b6dc7065e46c974bc7c5f143080a6764ec7a4be6da1285ececdc37be96de53be diff --git a/pubspec.yaml b/pubspec.yaml index 3b8be38..d177579 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -55,6 +55,7 @@ dependencies: avatar_maker: ^0.2.0 flutter_svg: ^2.0.17 flutter_volume_controller: ^1.3.3 + fixnum: ^1.1.1 # avatar_maker # avatar_maker: # path: ./dependencies/avatar_maker/