This commit is contained in:
otsmr 2025-03-31 21:59:57 +02:00
parent b756682f7c
commit bf34920350
10 changed files with 207 additions and 139 deletions

View file

@ -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.

View file

@ -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;
};

View file

@ -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

View file

@ -144,11 +144,14 @@ class MessagesDao extends DatabaseAccessor<TwonlyDatabase>
return (delete(messages)..where((t) => t.messageId.equals(messageId))).go();
}
Future<bool> containsOtherMessageId(int messageOtherId) async {
Future<bool> 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<Message> getMessageByMessageId(int messageId) {

View file

@ -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",

View file

@ -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!",

View file

@ -162,147 +162,145 @@ Future<client.Response> handleDownloadData(DownloadData data) async {
Future<client.Response> 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<client.Response> handleRequestNewPreKey() async {
var ok = client.Response_Ok()..prekeys = prekeys;
return client.Response()..ok = ok;
}
Future<client.Response> 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;
}

View file

@ -279,9 +279,6 @@ Future<MessageJson?> 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);

View file

@ -426,7 +426,7 @@ packages:
source: hosted
version: "3.10.4"
fixnum:
dependency: transitive
dependency: "direct main"
description:
name: fixnum
sha256: b6dc7065e46c974bc7c5f143080a6764ec7a4be6da1285ececdc37be96de53be

View file

@ -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/