Skip to content

Commit

Permalink
Feature/encryption (#103)
Browse files Browse the repository at this point in the history
* fix(messaging):
missing chatId attribute
sending localId of the message

* feature(chatting): encryption

* correctly encrypt and decrypt

---------

Co-authored-by: Marwan Alhameedy <[email protected]>
  • Loading branch information
Ahmed-Aladdiin and marwan2232004 authored Dec 20, 2024
1 parent ad3d2b0 commit 3e50099
Show file tree
Hide file tree
Showing 10 changed files with 236 additions and 82 deletions.
24 changes: 22 additions & 2 deletions lib/core/models/chat_model.dart
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,10 @@ class ChatModel {
final DateTime? muteUntil; // Add this field
@HiveField(16)
final bool messagingPermission; //1->anyone 0->admins
@HiveField(17)
final String? encryptionKey;
@HiveField(18)
final String? initializationVector;



Expand All @@ -68,6 +72,8 @@ class ChatModel {
required this.messages,
this.muteUntil, // Initialize this field
this.messagingPermission = true,
this.encryptionKey,
this.initializationVector,
});

Future<void> _setPhotoBytes() async {
Expand Down Expand Up @@ -109,7 +115,10 @@ class ChatModel {
other.draft == draft &&
other.isMentioned == isMentioned &&
other.messages == messages&&
other.messagingPermission == messagingPermission;
other.messagingPermission == messagingPermission &&
other.encryptionKey == encryptionKey &&
other.initializationVector == initializationVector
;
}

@override
Expand All @@ -128,7 +137,10 @@ class ChatModel {
creators.hashCode ^
isMentioned.hashCode ^
messages.hashCode^ // Include messages in hashCode
messagingPermission.hashCode;
messagingPermission.hashCode ^
encryptionKey.hashCode ^
initializationVector.hashCode
;
}

@override
Expand All @@ -149,6 +161,8 @@ class ChatModel {
'isMentioned: $isMentioned,\n'
'messages: $messages,\n' // Add messages to the string representation
'messagingPermission: $messagingPermission,\n'
'encryptionKey: $encryptionKey,\n'
'initializationVecot: $initializationVector,\n'
')');
}

Expand All @@ -170,6 +184,8 @@ class ChatModel {
bool? isMentioned,
List<MessageModel>? messages,
bool? messagingPermission,
String? encryptionKey,
String? initializationVector
}) {
return ChatModel(
title: title ?? this.title,
Expand All @@ -189,6 +205,8 @@ class ChatModel {
isMentioned: isMentioned ?? this.isMentioned,
messages: messages ?? this.messages,
messagingPermission: messagingPermission ?? this.messagingPermission,
encryptionKey: encryptionKey ?? this.encryptionKey,
initializationVector: initializationVector ?? this.initializationVector
);
}

Expand All @@ -210,6 +228,8 @@ class ChatModel {
'isMentioned': isMentioned,
'messagingPermission': messagingPermission,
'messages': messages.map((message) => message.toMap()).toList(),
'encryptionKey': encryptionKey,
'initializationVector': initializationVector
};
}

Expand Down
3 changes: 0 additions & 3 deletions lib/features/chat/models/message_event_models.dart
Original file line number Diff line number Diff line change
Expand Up @@ -146,15 +146,12 @@ class SendMessageEvent extends MessageEvent {
ackCallback: (res, timer, completer) {
try {
final response = res as Map<String, dynamic>;
debugPrint('### I got a response ${response['success'].toString()}');
print(response);
if (!completer.isCompleted) {
timer.cancel(); // Cancel the timer on acknowledgment
if (response['success'].toString() == 'true') {
final res = response['res'] as Map<String, dynamic>;
debugPrint('--- got the res');
final messageId = res['messageId'] as String;
debugPrint('--- got the id $messageId');

_controller!.updateMessageId(
msgId: messageId,
Expand Down
138 changes: 83 additions & 55 deletions lib/features/chat/repository/chat_remote_repository.dart
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import 'package:telware_cross_platform/core/models/user_model.dart';
import 'package:telware_cross_platform/features/chat/classes/message_content.dart';
import 'package:telware_cross_platform/features/chat/enum/chatting_enums.dart';
import 'package:telware_cross_platform/features/chat/enum/message_enums.dart';
import 'package:telware_cross_platform/features/chat/services/encryption_service.dart';
import 'package:telware_cross_platform/features/chat/utils/chat_utils.dart';

class ChatRemoteRepository {
Expand Down Expand Up @@ -34,7 +35,7 @@ class ChatRemoteRepository {
response.data['data']['lastMessages'] as List? ?? [];

Map<String, UserModel> userMap = {};
Map<String, MessageModel> lastMessageMap = {};
Map<String, Map> lastMessageMap = {};
for (var member in memberData) {
userMap[member['id']] = UserModel(
id: member['id'],
Expand Down Expand Up @@ -66,45 +67,7 @@ class ChatRemoteRepository {
continue;
}

Map<String, MessageState> userStates = {};
MessageContentType contentType =
MessageContentType.getType(lastMessage['contentType'] ?? 'text');
MessageContent? content;

final Map<String, String> userStatesMap =
lastMessage['userStates'] ?? {};

for (var entry in userStatesMap.entries) {
userStates[entry.key] = MessageState.getType(entry.value);
}

// TODO: needs to be modified to match the response fields
content = createMessageContent(
contentType: contentType,
text: lastMessage['content'],
fileName: lastMessage['fileName'],
mediaUrl: lastMessage['mediaUrl'],
);

final threadMessages = (lastMessage['threadMessages'] as List).map((e) => e as String).toList();

// todo(ahmed): add (parentMsgId)
// the connumicationType attribute is extra
lastMessageMap[message['chatId']] = MessageModel(
id: lastMessage['id'],
senderId: lastMessage['senderId'],
messageContentType: contentType,
messageType: MessageType.getType(lastMessage['type'] ?? 'unknown'),
content: content,
timestamp: lastMessage['timestamp'] != null
? DateTime.parse(lastMessage['timestamp'])
: DateTime.now(),
userStates: userStates,
isForward: lastMessage['isForward'] ?? false,
isPinned: lastMessage['isPinned'] ?? false,
isAnnouncement: lastMessage['isAnnouncement'],
threadMessages: threadMessages
);
lastMessageMap[message['chatId']] = lastMessage;
}

// Iterate through chats and map users
Expand All @@ -129,8 +92,16 @@ class ChatRemoteRepository {
// Create list of user IDs excluding current user
final otherUserIDs = members.where((id) => id != userID).toList();
final otherUsers = otherUserIDs.map((id) => userMap[id]).toList();
final List<MessageModel> messages =
lastMessageMap[chatID] == null ? [] : [lastMessageMap[chatID]!];
final List<MessageModel> messages = lastMessageMap[chatID] == null
? []
: [
_creataLastMsg(
lastMessage: lastMessageMap[chatID]!,
encryptionKey: chat['chat']['encryptionKey'],
initializationVector: chat['chat']['initializationVector'],
chatType: ChatType.getType(chat['chat']['type']),
)
];
String chatTitle = 'Invalid Chat';

bool messagingPermission = true;
Expand All @@ -147,8 +118,7 @@ class ChatRemoteRepository {
? otherUsers[0]!.username
: 'Private Chat';
}
}
else if (chat['chat']['type'] == 'group') {
} else if (chat['chat']['type'] == 'group') {
chatTitle = chat['chat']['name'] ?? 'Group Chat';
messagingPermission = chat['chat']['messagingPermission'];
} else if (chat['chat']['type'] == 'channel') {
Expand All @@ -161,17 +131,18 @@ class ChatRemoteRepository {
// todo(ahmed): store the creators list as well as the isSeen, isDeleted attributes
// should it be sent in the first place if it is deleted?
final chatModel = ChatModel(
id: chatID,
title: chatTitle,
userIds: members,
admins: admins,
type: ChatType.getType(chat['chat']['type']),
messages: messages,
draft: chat['draft'],
isMuted: chat['isMuted'],
creators: creators,
messagingPermission: messagingPermission
);
id: chatID,
title: chatTitle,
userIds: members,
admins: admins,
type: ChatType.getType(chat['chat']['type']),
messages: messages,
draft: chat['draft'],
isMuted: chat['isMuted'],
creators: creators,
messagingPermission: messagingPermission,
encryptionKey: chat['chat']['encryptionKey'],
initializationVector: chat['chat']['initializationVector']);

chats.add(chatModel);
}
Expand All @@ -189,6 +160,63 @@ class ChatRemoteRepository {
}
}

MessageModel _creataLastMsg({
required Map<dynamic, dynamic> lastMessage,
required String? encryptionKey,
required String? initializationVector,
required ChatType chatType,
}) {
Map<String, MessageState> userStates = {};
MessageContentType contentType =
MessageContentType.getType(lastMessage['contentType'] ?? 'text');
MessageContent? content;

final Map<String, String> userStatesMap = lastMessage['userStates'] ?? {};

for (var entry in userStatesMap.entries) {
userStates[entry.key] = MessageState.getType(entry.value);
}

final encryptionService = EncryptionService.instance;

final text = encryptionService.decrypt(
chatType: chatType,
msg: lastMessage['content'],
encryptionKey: encryptionKey,
initializationVector: initializationVector,
);

// TODO: needs to be modified to match the response fields
content = createMessageContent(
contentType: contentType,
text: text,
fileName: lastMessage['fileName'],
mediaUrl: lastMessage['mediaUrl'],
);

final threadMessages = (lastMessage['threadMessages'] as List)
.map((e) => e as String)
.toList();

// the connumicationType attribute is extra
return MessageModel(
id: lastMessage['id'],
senderId: lastMessage['senderId'],
messageContentType: contentType,
messageType: MessageType.getType(lastMessage['type'] ?? 'unknown'),
content: content,
timestamp: lastMessage['timestamp'] == null
? DateTime.parse(lastMessage['timestamp'])
: DateTime.now(),
userStates: userStates,
isForward: lastMessage['isForward'] ?? false,
isPinned: lastMessage['isPinned'] ?? false,
isAnnouncement: lastMessage['isAnnouncement'],
threadMessages: threadMessages,
parentMessage: lastMessage['parentMessageId'],
);
}

// Fetch user details
Future<UserModel?> getOtherUser(String sessionId, String userID) async {
debugPrint('!!! The Other userID: $userID');
Expand Down
66 changes: 66 additions & 0 deletions lib/features/chat/services/encryption_service.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
import 'dart:convert';

import 'package:telware_cross_platform/core/models/chat_model.dart';
import 'package:telware_cross_platform/features/chat/enum/chatting_enums.dart';
import 'package:encrypt/encrypt.dart';

class EncryptionService {
// Private constructor
EncryptionService._internal();

// Singleton instance
static EncryptionService? _instance;

// Getter for the singleton instance
static EncryptionService get instance {
return _instance ??= EncryptionService._internal();
}

String encrypt({
required String msg,
required String? encryptionKey,
required String? initializationVector,
required ChatType chatType,
}) {
if (chatType != ChatType.private ||
encryptionKey == null ||
initializationVector == null) {
print('((((((((((');
print(chatType.type);
print(encryptionKey);
print(initializationVector);
print('))))))))))');
return msg;
}

final key = Key.fromBase16(encryptionKey);
final iv = IV.fromBase16(initializationVector);
final encrypter = Encrypter(AES(key, mode: AESMode.cbc));

final encrypted = encrypter.encrypt(msg, iv: iv);

return encrypted.base16;
}

String decrypt({
required String msg,
required String? encryptionKey,
required String? initializationVector,
required ChatType chatType,
}) {
if (chatType != ChatType.private ||
encryptionKey == null ||
initializationVector == null) {
return msg;
}

final key = Key.fromBase16(encryptionKey);
final iv = IV.fromBase16(initializationVector);
final encrypter = Encrypter(AES(key, mode: AESMode.cbc));

final decryptedBytes =
encrypter.decryptBytes(Encrypted.fromBase16(msg), iv: iv);

return utf8.decode(decryptedBytes);
}
}
Loading

0 comments on commit 3e50099

Please sign in to comment.