started with #203

This commit is contained in:
otsmr 2025-06-05 17:28:36 +02:00
parent ab125042e2
commit 6c630c78b5
17 changed files with 1287 additions and 335 deletions

View file

@ -8,7 +8,6 @@ import 'package:twonly/src/services/api/media_send.dart';
import 'package:twonly/src/services/api.service.dart'; import 'package:twonly/src/services/api.service.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:twonly/src/providers/connection.provider.dart'; import 'package:twonly/src/providers/connection.provider.dart';
import 'package:twonly/src/utils/hive.dart';
import 'package:twonly/src/providers/settings.provider.dart'; import 'package:twonly/src/providers/settings.provider.dart';
import 'package:twonly/src/services/fcm.service.dart'; import 'package:twonly/src/services/fcm.service.dart';
import 'package:twonly/src/services/notification.service.dart'; import 'package:twonly/src/services/notification.service.dart';
@ -33,9 +32,9 @@ void main() async {
// Load the user's preferred theme while the splash screen is displayed. // Load the user's preferred theme while the splash screen is displayed.
// This prevents a sudden theme change when the app is first displayed. // This prevents a sudden theme change when the app is first displayed.
await settingsController.loadSettings(); await settingsController.loadSettings();
await SystemChrome.setPreferredOrientations([DeviceOrientation.portraitUp]);
await setupPushNotification(); setupPushNotification();
await initMediaStorage();
gCameras = await availableCameras(); gCameras = await availableCameras();
@ -48,8 +47,6 @@ void main() async {
purgeReceivedMediaFiles(); purgeReceivedMediaFiles();
purgeSendMediaFiles(); purgeSendMediaFiles();
await SystemChrome.setPreferredOrientations([DeviceOrientation.portraitUp]);
runApp( runApp(
MultiProvider( MultiProvider(
providers: [ providers: [

View file

@ -0,0 +1,44 @@
import 'package:drift/drift.dart';
import 'package:twonly/src/database/tables/message_retransmissions.dart';
import 'package:twonly/src/database/twonly_database.dart';
import 'package:twonly/src/utils/log.dart';
part 'message_retransmissions.dao.g.dart';
@DriftAccessor(tables: [MessageRetransmissions])
class MessageRetransmissionDao extends DatabaseAccessor<TwonlyDatabase>
with _$MessageRetransmissionDaoMixin {
// this constructor is required so that the main database can create an instance
// of this object.
MessageRetransmissionDao(super.db);
Future<int?> insertRetransmission(
MessageRetransmissionsCompanion message) async {
try {
return await into(messageRetransmissions).insert(message);
} catch (e) {
Log.error("Error while inserting message for retransmission: $e");
return null;
}
}
Future<List<int>> getRetransmitAbleMessages() async {
return (await (select(messageRetransmissions)
..where((t) => t.acknowledgeByServerAt.isNull()))
.get())
.map((msg) => msg.retransmissionId)
.toList();
}
SingleOrNullSelectable<MessageRetransmission> getRetransmissionById(
int retransmissionId) {
return select(messageRetransmissions)
..where((t) => t.retransmissionId.equals(retransmissionId));
}
Future deleteRetransmissionById(int retransmissionId) {
return (delete(messageRetransmissions)
..where((t) => t.retransmissionId.equals(retransmissionId)))
.go();
}
}

View file

@ -0,0 +1,11 @@
// GENERATED CODE - DO NOT MODIFY BY HAND
part of 'message_retransmissions.dao.dart';
// ignore_for_file: type=lint
mixin _$MessageRetransmissionDaoMixin on DatabaseAccessor<TwonlyDatabase> {
$ContactsTable get contacts => attachedDatabase.contacts;
$MessagesTable get messages => attachedDatabase.messages;
$MessageRetransmissionsTable get messageRetransmissions =>
attachedDatabase.messageRetransmissions;
}

View file

@ -0,0 +1,19 @@
import 'package:drift/drift.dart';
import 'package:twonly/src/database/tables/contacts_table.dart';
import 'package:twonly/src/database/tables/messages_table.dart';
@DataClassName('MessageRetransmission')
class MessageRetransmissions extends Table {
IntColumn get retransmissionId => integer().autoIncrement()();
IntColumn get contactId =>
integer().references(Contacts, #userId, onDelete: KeyAction.cascade)();
IntColumn get messageId => integer()
.nullable()
.references(Messages, #messageId, onDelete: KeyAction.cascade)();
BlobColumn get plaintextContent => blob()();
BlobColumn get pushData => blob().nullable()();
DateTimeColumn get acknowledgeByServerAt => dateTime().nullable()();
}

View file

@ -5,11 +5,13 @@ import 'package:path_provider/path_provider.dart';
import 'package:twonly/src/database/daos/contacts_dao.dart'; import 'package:twonly/src/database/daos/contacts_dao.dart';
import 'package:twonly/src/database/daos/media_downloads_dao.dart'; import 'package:twonly/src/database/daos/media_downloads_dao.dart';
import 'package:twonly/src/database/daos/media_uploads_dao.dart'; import 'package:twonly/src/database/daos/media_uploads_dao.dart';
import 'package:twonly/src/database/daos/message_retransmissions.dao.dart';
import 'package:twonly/src/database/daos/messages_dao.dart'; import 'package:twonly/src/database/daos/messages_dao.dart';
import 'package:twonly/src/database/daos/signal_dao.dart'; import 'package:twonly/src/database/daos/signal_dao.dart';
import 'package:twonly/src/database/tables/contacts_table.dart'; import 'package:twonly/src/database/tables/contacts_table.dart';
import 'package:twonly/src/database/tables/media_download_table.dart'; import 'package:twonly/src/database/tables/media_download_table.dart';
import 'package:twonly/src/database/tables/media_uploads_table.dart'; import 'package:twonly/src/database/tables/media_uploads_table.dart';
import 'package:twonly/src/database/tables/message_retransmissions.dart';
import 'package:twonly/src/database/tables/messages_table.dart'; import 'package:twonly/src/database/tables/messages_table.dart';
import 'package:twonly/src/database/tables/signal_contact_prekey_table.dart'; import 'package:twonly/src/database/tables/signal_contact_prekey_table.dart';
import 'package:twonly/src/database/tables/signal_contact_signed_prekey_table.dart'; import 'package:twonly/src/database/tables/signal_contact_signed_prekey_table.dart';
@ -32,13 +34,15 @@ part 'twonly_database.g.dart';
SignalSenderKeyStores, SignalSenderKeyStores,
SignalSessionStores, SignalSessionStores,
SignalContactPreKeys, SignalContactPreKeys,
SignalContactSignedPreKeys SignalContactSignedPreKeys,
MessageRetransmissions
], daos: [ ], daos: [
MessagesDao, MessagesDao,
ContactsDao, ContactsDao,
MediaUploadsDao, MediaUploadsDao,
MediaDownloadsDao, MediaDownloadsDao,
SignalDao SignalDao,
MessageRetransmissionDao
]) ])
class TwonlyDatabase extends _$TwonlyDatabase { class TwonlyDatabase extends _$TwonlyDatabase {
TwonlyDatabase([QueryExecutor? e]) TwonlyDatabase([QueryExecutor? e])

File diff suppressed because it is too large Load diff

View file

@ -26,9 +26,9 @@ import 'package:twonly/src/services/api/server_messages.dart';
import 'package:twonly/src/services/signal/identity.signal.dart'; import 'package:twonly/src/services/signal/identity.signal.dart';
import 'package:twonly/src/services/signal/prekeys.signal.dart'; import 'package:twonly/src/services/signal/prekeys.signal.dart';
import 'package:twonly/src/services/signal/utils.signal.dart'; import 'package:twonly/src/services/signal/utils.signal.dart';
import 'package:twonly/src/utils/hive.dart';
import 'package:twonly/src/services/fcm.service.dart'; import 'package:twonly/src/services/fcm.service.dart';
import 'package:twonly/src/services/flame.service.dart'; import 'package:twonly/src/services/flame.service.dart';
import 'package:twonly/src/utils/keyvalue.dart';
import 'package:twonly/src/utils/log.dart'; import 'package:twonly/src/utils/log.dart';
import 'package:twonly/src/utils/misc.dart'; import 'package:twonly/src/utils/misc.dart';
import 'package:twonly/src/utils/storage.dart'; import 'package:twonly/src/utils/storage.dart';
@ -38,6 +38,7 @@ import 'package:libsignal_protocol_dart/src/ecc/ed25519.dart';
import 'package:web_socket_channel/web_socket_channel.dart'; import 'package:web_socket_channel/web_socket_channel.dart';
final lockConnecting = Mutex(); final lockConnecting = Mutex();
final lockRetransStore = Mutex();
/// The ApiProvider is responsible for communicating with the server. /// The ApiProvider is responsible for communicating with the server.
/// It handles errors and does automatically tries to reconnect on /// It handles errors and does automatically tries to reconnect on
@ -203,39 +204,43 @@ class ApiService {
} }
Future<Map<String, dynamic>> getRetransmission() async { Future<Map<String, dynamic>> getRetransmission() async {
final box = await getMediaStorage(); return await KeyValueStore.get("rawbytes-to-retransmit") ?? {};
try {
return box.get("rawbytes-to-retransmit");
} catch (e) {
return {};
}
} }
Future retransmitRawBytes() async { Future retransmitRawBytes() async {
await lockRetransStore.protect(() async {
var retransmit = await getRetransmission(); var retransmit = await getRetransmission();
Log.info("retransmitting ${retransmit.keys.length} messages"); Log.info("retransmitting ${retransmit.keys.length} messages");
bool gotError = false;
for (final seq in retransmit.keys) { for (final seq in retransmit.keys) {
try { try {
_channel!.sink.add(base64Decode(retransmit[seq])); _channel!.sink.add(base64Decode(retransmit[seq]));
} catch (e) { } catch (e) {
gotError = true;
Log.error("$e"); Log.error("$e");
} }
} }
if (!gotError) {
KeyValueStore.put("rawbytes-to-retransmit", {});
}
});
} }
Future addToRetransmissionBuffer(Int64 seq, Uint8List bytes) async { Future addToRetransmissionBuffer(Int64 seq, Uint8List bytes) async {
await lockRetransStore.protect(() async {
var retransmit = await getRetransmission(); var retransmit = await getRetransmission();
retransmit[seq.toString()] = base64Encode(bytes); retransmit[seq.toString()] = base64Encode(bytes);
final box = await getMediaStorage(); KeyValueStore.put("rawbytes-to-retransmit", retransmit);
box.put("rawbytes-to-retransmit", retransmit); });
} }
Future removeFromRetransmissionBuffer(Int64 seq) async { Future removeFromRetransmissionBuffer(Int64 seq) async {
await lockRetransStore.protect(() async {
var retransmit = await getRetransmission(); var retransmit = await getRetransmission();
if (retransmit.isEmpty) return; if (retransmit.isEmpty) return;
retransmit.remove(seq.toString()); retransmit.remove(seq.toString());
final box = await getMediaStorage(); KeyValueStore.put("rawbytes-to-retransmit", retransmit);
box.put("rawbytes-to-retransmit", retransmit); });
} }
Future<Result> sendRequestSync( Future<Result> sendRequestSync(

View file

@ -1,8 +1,6 @@
import 'dart:convert'; import 'dart:convert';
import 'dart:math'; import 'dart:io';
import 'package:drift/drift.dart'; import 'package:drift/drift.dart';
import 'package:hive/hive.dart';
import 'package:mutex/mutex.dart';
import 'package:twonly/globals.dart'; import 'package:twonly/globals.dart';
import 'package:twonly/src/database/twonly_database.dart'; import 'package:twonly/src/database/twonly_database.dart';
import 'package:twonly/src/database/tables/messages_table.dart'; import 'package:twonly/src/database/tables/messages_table.dart';
@ -11,192 +9,79 @@ import 'package:twonly/src/model/json/userdata.dart';
import 'package:twonly/src/model/protobuf/api/error.pb.dart'; import 'package:twonly/src/model/protobuf/api/error.pb.dart';
import 'package:twonly/src/services/api/utils.dart'; import 'package:twonly/src/services/api/utils.dart';
import 'package:twonly/src/services/signal/encryption.signal.dart'; import 'package:twonly/src/services/signal/encryption.signal.dart';
import 'package:twonly/src/utils/hive.dart';
import 'package:twonly/src/services/notification.service.dart'; import 'package:twonly/src/services/notification.service.dart';
import 'package:twonly/src/utils/log.dart'; import 'package:twonly/src/utils/log.dart';
import 'package:twonly/src/utils/storage.dart'; import 'package:twonly/src/utils/storage.dart';
final lockSendingMessages = Mutex();
Future tryTransmitMessages() async { Future tryTransmitMessages() async {
lockSendingMessages.protect(() async { final retransIds =
Map<String, dynamic> retransmit = await getAllMessagesForRetransmitting(); await twonlyDB.messageRetransmissionDao.getRetransmitAbleMessages();
if (retransmit.isEmpty) return; for (final retransId in retransIds) {
sendRetransmitMessage(retransId);
}
}
Log.info("try sending messages: ${retransmit.length}"); Future sendRetransmitMessage(int retransId) async {
MessageRetransmission? retrans = await twonlyDB.messageRetransmissionDao
.getRetransmissionById(retransId)
.getSingleOrNull();
Map<String, dynamic> failed = {}; if (retrans == null) {
Log.error("$retransId not found in database");
return;
}
List<MapEntry<String, dynamic>> sortedList = retransmit.entries.toList() Uint8List? encryptedBytes = await signalEncryptMessage(
..sort((a, b) => int.parse(a.key).compareTo(int.parse(b.key))); retrans.contactId,
retrans.plaintextContent,
);
for (final element in sortedList) { if (encryptedBytes == null) {
RetransmitMessage msg = Log.error("Could not encrypt the message. Aborting and trying again.");
RetransmitMessage.fromJson(jsonDecode(element.value)); return;
}
Result resp = await apiService.sendTextMessage( Result resp = await apiService.sendTextMessage(
msg.userId, retrans.contactId,
msg.bytes, encryptedBytes,
msg.pushData, retrans.pushData,
); );
if (resp.isSuccess) {
if (msg.messageId != null) {
await twonlyDB.messagesDao.updateMessageByMessageId(
msg.messageId!,
MessagesCompanion(
acknowledgeByServer: Value(true),
),
);
}
} else {
failed[element.key] = element.value;
}
}
Box box = await getMediaStorage();
box.put("messages-to-retransmit", jsonEncode(failed));
});
}
class RetransmitMessage {
int? messageId;
int userId;
Uint8List bytes;
List<int>? pushData;
RetransmitMessage({
this.messageId,
required this.userId,
required this.bytes,
this.pushData,
});
// From JSON constructor
factory RetransmitMessage.fromJson(Map<String, dynamic> json) {
return RetransmitMessage(
messageId: json['messageId'],
userId: json['userId'],
bytes: base64Decode(json['bytes']),
pushData: json['pushData'] == null
? null
: List<int>.from(json['pushData'].map((item) => item as int)),
);
}
// To JSON method
Map<String, dynamic> toJson() {
return {
'messageId': messageId,
'userId': userId,
'bytes': base64Encode(bytes),
'pushData': pushData,
};
}
}
Future<Map<String, dynamic>> getAllMessagesForRetransmitting() async {
Box box = await getMediaStorage();
String? retransmitJson = box.get("messages-to-retransmit");
Map<String, dynamic> retransmit = {};
if (retransmitJson != null) {
try {
retransmit = jsonDecode(retransmitJson);
} catch (e) {
Log.error("Could not decode the retransmit messages: $e");
await box.delete("messages-to-retransmit");
}
}
return retransmit;
}
Future<Result> sendRetransmitMessage(
String stateId, RetransmitMessage msg) async {
Log.info("Sending ${msg.messageId}");
Result resp =
await apiService.sendTextMessage(msg.userId, msg.bytes, msg.pushData);
bool retry = true; bool retry = true;
if (resp.isError) { if (resp.isError) {
if (resp.error == ErrorCode.UserIdNotFound) { if (resp.error == ErrorCode.UserIdNotFound) {
retry = false; retry = false;
if (msg.messageId != null) { if (retrans.messageId != null) {
await twonlyDB.messagesDao.updateMessageByMessageId( await twonlyDB.messagesDao.updateMessageByMessageId(
msg.messageId!, retrans.messageId!,
MessagesCompanion(errorWhileSending: Value(true)), MessagesCompanion(errorWhileSending: Value(true)),
); );
} }
await twonlyDB.contactsDao.updateContact(
retrans.contactId,
ContactsCompanion(deleted: Value(true)),
);
} }
} }
if (resp.isSuccess) { if (resp.isSuccess) {
retry = false; retry = false;
if (msg.messageId != null) { if (retrans.messageId != null) {
await twonlyDB.messagesDao.updateMessageByMessageId( await twonlyDB.messagesDao.updateMessageByMessageId(
msg.messageId!, retrans.messageId!,
MessagesCompanion(acknowledgeByServer: Value(true)), MessagesCompanion(
acknowledgeByServer: Value(true),
errorWhileSending: Value(false),
),
); );
} }
} }
if (!retry) { if (!retry) {
{ await twonlyDB.messageRetransmissionDao.deleteRetransmissionById(retransId);
var retransmit = await getAllMessagesForRetransmitting();
retransmit.remove(stateId);
Box box = await getMediaStorage();
box.put("messages-to-retransmit", jsonEncode(retransmit));
} }
}
return resp;
}
// this functions ensures that the message is received by the server and in case of errors will try again later
Future<(String, RetransmitMessage)?> encryptMessage(
int? messageId, int userId, MessageJson msg,
{PushKind? pushKind}) async {
return await lockSendingMessages
.protect<(String, RetransmitMessage)?>(() async {
Uint8List? bytes = await signalEncryptMessage(userId, msg);
if (bytes == null) {
Log.error("Error encryption message!");
return null;
}
var retransmit = await getAllMessagesForRetransmitting();
int currentMaxStateId = messageId ?? 60000;
if (retransmit.isNotEmpty && messageId == null) {
currentMaxStateId = retransmit.keys.map((x) => int.parse(x)).reduce(max);
if (currentMaxStateId < 60000) {
currentMaxStateId = 60000;
}
}
String stateId = (currentMaxStateId + 1).toString();
Box box = await getMediaStorage();
List<int>? pushData;
if (pushKind != null) {
pushData = await getPushData(userId, pushKind);
}
RetransmitMessage encryptedMessage = RetransmitMessage(
messageId: messageId,
userId: userId,
bytes: bytes,
pushData: pushData,
);
{
retransmit[stateId] = jsonEncode(encryptedMessage.toJson());
box.put("messages-to-retransmit", jsonEncode(retransmit));
}
return (stateId, encryptedMessage);
});
} }
// encrypts and stores the message and then sends it in the background // encrypts and stores the message and then sends it in the background
@ -205,16 +90,38 @@ Future encryptAndSendMessageAsync(int? messageId, int userId, MessageJson msg,
if (gIsDemoUser) { if (gIsDemoUser) {
return; return;
} }
(String, RetransmitMessage)? stateData =
await encryptMessage(messageId, userId, msg, pushKind: pushKind); Uint8List? pushData;
if (stateData != null) { if (pushKind != null) {
final (stateId, message) = stateData; pushData = await getPushData(userId, pushKind);
sendRetransmitMessage(stateId, message);
} }
Uint8List plaintextContent =
Uint8List.fromList(gzip.encode(utf8.encode(jsonEncode(msg.toJson()))));
int? retransId = await twonlyDB.messageRetransmissionDao.insertRetransmission(
MessageRetransmissionsCompanion(
contactId: Value(userId),
messageId: Value(messageId),
plaintextContent: Value(plaintextContent),
pushData: Value(pushData),
),
);
if (retransId == null) {
Log.error("Could not insert the message into the retransmission database");
return;
}
// this can now be done in the background...
sendRetransmitMessage(retransId);
} }
Future sendTextMessage( Future sendTextMessage(
int target, TextMessageContent content, PushKind? pushKind) async { int target,
TextMessageContent content,
PushKind? pushKind,
) async {
DateTime messageSendAt = DateTime.now(); DateTime messageSendAt = DateTime.now();
int? messageId = await twonlyDB.messagesDao.insertMessage( int? messageId = await twonlyDB.messagesDao.insertMessage(

View file

@ -101,7 +101,8 @@ Future<client.Response> handleNewMessage(int fromUserId, Uint8List body) async {
if (message.messageId != null) { if (message.messageId != null) {
final update = MessagesCompanion( final update = MessagesCompanion(
openedAt: Value(message.timestamp), openedAt: Value(message.timestamp),
errorWhileSending: Value(false)); errorWhileSending: Value(false),
);
await twonlyDB.messagesDao.updateMessageByOtherUser( await twonlyDB.messagesDao.updateMessageByOtherUser(
fromUserId, fromUserId,
message.messageId!, message.messageId!,
@ -215,6 +216,9 @@ Future<client.Response> handleNewMessage(int fromUserId, Uint8List body) async {
responseToMessageId, responseToMessageId,
MessagesCompanion( MessagesCompanion(
errorWhileSending: Value(false), errorWhileSending: Value(false),
openedAt: Value(
DateTime.now(),
) // when a user reacted to the media file, it should be marked as opened
), ),
); );
} }
@ -259,16 +263,16 @@ Future<client.Response> handleNewMessage(int fromUserId, Uint8List body) async {
} }
} }
// await encryptAndSendMessageAsync( await encryptAndSendMessageAsync(
// message.messageId!, message.messageId!,
// fromUserId, fromUserId,
// MessageJson( MessageJson(
// kind: MessageKind.ack, kind: MessageKind.ack,
// messageId: message.messageId!, messageId: message.messageId!,
// content: MessageContent(), content: MessageContent(),
// timestamp: DateTime.now(), timestamp: DateTime.now(),
// ), ),
// ); );
// unarchive contact when receiving a new message // unarchive contact when receiving a new message
await twonlyDB.contactsDao.updateContact( await twonlyDB.contactsDao.updateContact(

View file

@ -238,7 +238,7 @@ class PushNotification {
/// this will trigger a push notification /// this will trigger a push notification
/// push notification only containing the message kind and username /// push notification only containing the message kind and username
Future<List<int>?> getPushData(int toUserId, PushKind kind) async { Future<Uint8List?> getPushData(int toUserId, PushKind kind) async {
final Map<int, PushUser> pushKeys = await getPushKeys("sendingPushKeys"); final Map<int, PushUser> pushKeys = await getPushKeys("sendingPushKeys");
List<int> key = "InsecureOnlyUsedForAddingContact".codeUnits; List<int> key = "InsecureOnlyUsedForAddingContact".codeUnits;
@ -278,8 +278,7 @@ Future<List<int>?> getPushData(int toUserId, PushKind kind) async {
cipherText: secretBox.cipherText, cipherText: secretBox.cipherText,
mac: secretBox.mac.bytes, mac: secretBox.mac.bytes,
); );
return Utf8Encoder().convert(jsonEncode(res.toJson()));
return jsonEncode(res.toJson()).codeUnits;
} }
Future<PushKind?> tryDecryptMessage( Future<PushKind?> tryDecryptMessage(

View file

@ -2,6 +2,7 @@ import 'dart:convert';
import 'dart:io'; import 'dart:io';
import 'dart:typed_data'; import 'dart:typed_data';
import 'package:libsignal_protocol_dart/libsignal_protocol_dart.dart'; import 'package:libsignal_protocol_dart/libsignal_protocol_dart.dart';
import 'package:mutex/mutex.dart';
import 'package:twonly/src/database/twonly_database.dart'; import 'package:twonly/src/database/twonly_database.dart';
import 'package:twonly/src/model/json/message.dart'; import 'package:twonly/src/model/json/message.dart';
import 'package:twonly/src/database/signal/connect_signal_protocol_store.dart'; import 'package:twonly/src/database/signal/connect_signal_protocol_store.dart';
@ -11,7 +12,12 @@ import 'package:twonly/src/services/signal/utils.signal.dart';
import 'package:twonly/src/utils/log.dart'; import 'package:twonly/src/utils/log.dart';
import 'package:twonly/src/utils/misc.dart'; import 'package:twonly/src/utils/misc.dart';
Future<Uint8List?> signalEncryptMessage(int target, MessageJson msg) async { /// This caused some troubles, so protection the encryption...
final lockingSignalEncryption = Mutex();
Future<Uint8List?> signalEncryptMessage(
int target, Uint8List plaintextContent) async {
return await lockingSignalEncryption.protect<Uint8List?>(() async {
try { try {
ConnectSignalProtocolStore signalStore = (await getSignalStore())!; ConnectSignalProtocolStore signalStore = (await getSignalStore())!;
final address = SignalProtocolAddress(target.toString(), defaultDeviceId); final address = SignalProtocolAddress(target.toString(), defaultDeviceId);
@ -19,7 +25,8 @@ Future<Uint8List?> signalEncryptMessage(int target, MessageJson msg) async {
SessionCipher session = SessionCipher.fromStore(signalStore, address); SessionCipher session = SessionCipher.fromStore(signalStore, address);
SignalContactPreKey? preKey = await getPreKeyByContactId(target); SignalContactPreKey? preKey = await getPreKeyByContactId(target);
SignalContactSignedPreKey? signedPreKey = await getSignedPreKeyByContactId( SignalContactSignedPreKey? signedPreKey =
await getSignedPreKeyByContactId(
target, target,
); );
@ -74,9 +81,7 @@ Future<Uint8List?> signalEncryptMessage(int target, MessageJson msg) async {
} }
} }
final ciphertext = await session.encrypt( final ciphertext = await session.encrypt(plaintextContent);
Uint8List.fromList(gzip.encode(utf8.encode(jsonEncode(msg.toJson())))),
);
var b = BytesBuilder(); var b = BytesBuilder();
b.add(ciphertext.serialize()); b.add(ciphertext.serialize());
@ -87,6 +92,7 @@ Future<Uint8List?> signalEncryptMessage(int target, MessageJson msg) async {
Log.error(e.toString()); Log.error(e.toString());
return null; return null;
} }
});
} }
Future<MessageJson?> signalDecryptMessage(int source, Uint8List msg) async { Future<MessageJson?> signalDecryptMessage(int source, Uint8List msg) async {
@ -115,7 +121,12 @@ Future<MessageJson?> signalDecryptMessage(int source, Uint8List msg) async {
return null; return null;
} }
return MessageJson.fromJson( return MessageJson.fromJson(
jsonDecode(utf8.decode(gzip.decode(plaintext)))); jsonDecode(
utf8.decode(
gzip.decode(plaintext),
),
),
);
} on InvalidKeyIdException catch (_) { } on InvalidKeyIdException catch (_) {
return null; // got the same message again return null; // got the same message again
} on DuplicateMessageException catch (_) { } on DuplicateMessageException catch (_) {

View file

@ -1,41 +0,0 @@
import 'dart:convert';
import 'package:flutter_secure_storage/flutter_secure_storage.dart';
import 'package:hive/hive.dart';
import 'package:path_provider/path_provider.dart';
import 'package:twonly/src/services/notification.service.dart';
import 'package:twonly/src/utils/log.dart';
Future initMediaStorage() async {
final storage = FlutterSecureStorage();
var containsEncryptionKey =
await storage.containsKey(key: 'hive_encryption_key');
if (!containsEncryptionKey) {
var key = Hive.generateSecureKey();
await storage.write(
key: 'hive_encryption_key',
value: base64UrlEncode(key),
);
}
final dir = await getApplicationSupportDirectory();
Hive.init(dir.path);
}
Future<Box> getMediaStorage() async {
try {
await initMediaStorage();
final storage = FlutterSecureStorage();
var encryptionKey =
base64Url.decode((await storage.read(key: 'hive_encryption_key'))!);
return await Hive.openBox(
'media_storage',
encryptionCipher: HiveAesCipher(encryptionKey),
);
} catch (e) {
await customLocalPushNotification("Secure Storage Error",
"Settings > Apps > twonly > Storage and Cache > Press clear on both");
Log.error(e);
throw Exception(e);
}
}

View file

@ -0,0 +1,44 @@
import 'dart:convert';
import 'dart:io';
import 'package:path_provider/path_provider.dart';
import 'package:twonly/src/utils/log.dart';
class KeyValueStore {
static Future<String> _getFilePath(String key) async {
final directory = await getApplicationSupportDirectory();
return '${directory.path}/keyvalue/$key.json';
}
static Future<Map<String, dynamic>?> get(String key) async {
try {
final filePath = await _getFilePath(key);
final file = File(filePath);
// Check if the file exists
if (await file.exists()) {
final contents = await file.readAsString();
return jsonDecode(contents);
} else {
return null; // File does not exist
}
} catch (e) {
Log.error('Error reading file: $e');
return null;
}
}
static Future<void> put(String key, Map<String, dynamic> value) async {
try {
final filePath = await _getFilePath(key);
final file = File(filePath);
// Create the directory if it doesn't exist
await file.parent.create(recursive: true);
// Write the JSON data to the file
await file.writeAsString(jsonEncode(value));
} catch (e) {
Log.error('Error writing file: $e');
}
}
}

View file

@ -146,8 +146,10 @@ class _ChatMessagesViewState extends State<ChatMessagesView> {
} }
if (openedMessageOtherIds.isNotEmpty) { if (openedMessageOtherIds.isNotEmpty) {
notifyContactAboutOpeningMessage( await notifyContactAboutOpeningMessage(
widget.contact.userId, openedMessageOtherIds); widget.contact.userId,
openedMessageOtherIds,
);
} }
twonlyDB.messagesDao.openedAllNonMediaMessages(widget.contact.userId); twonlyDB.messagesDao.openedAllNonMediaMessages(widget.contact.userId);

View file

@ -210,7 +210,7 @@ class _MediaViewerViewState extends State<MediaViewerView> {
} }
} }
notifyContactAboutOpeningMessage( await notifyContactAboutOpeningMessage(
current.contactId, current.contactId,
[current.messageOtherId!], [current.messageOtherId!],
); );

View file

@ -779,14 +779,6 @@ packages:
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "3.0.3" version: "3.0.3"
hive:
dependency: "direct main"
description:
name: hive
sha256: "8dcf6db979d7933da8217edcec84e9df1bdb4e4edc7fc77dbd5aa74356d6d941"
url: "https://pub.dev"
source: hosted
version: "2.2.3"
html: html:
dependency: transitive dependency: transitive
description: description:

View file

@ -30,7 +30,6 @@ dependencies:
font_awesome_flutter: ^10.8.0 font_awesome_flutter: ^10.8.0
gal: ^2.3.1 gal: ^2.3.1
hand_signature: ^3.0.3 hand_signature: ^3.0.3
hive: ^2.2.3
image: ^4.3.0 image: ^4.3.0
intl: ^0.20.2 intl: ^0.20.2
introduction_screen: ^3.1.14 introduction_screen: ^3.1.14