Encrypted messaging app
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

230 lines
6.7 KiB

  1. import 'dart:convert';
  2. import 'dart:io';
  3. import 'dart:typed_data';
  4. import 'package:Envelope/database/repositories/friends_repository.dart';
  5. import 'package:mime/mime.dart';
  6. import 'package:pointycastle/export.dart';
  7. import 'package:uuid/uuid.dart';
  8. import '/database/repositories/conversation_users_repository.dart';
  9. import '/database/models/messages.dart';
  10. import '/database/models/text_messages.dart';
  11. import '/database/models/conversation_users.dart';
  12. import '/database/models/friends.dart';
  13. import '/database/models/my_profile.dart';
  14. import '/utils/encryption/aes_helper.dart';
  15. import '/utils/encryption/crypto_utils.dart';
  16. import '/utils/storage/database.dart';
  17. import '/utils/storage/write_file.dart';
  18. enum ConversationStatus {
  19. complete,
  20. pending,
  21. error,
  22. }
  23. class Conversation {
  24. String id;
  25. String userId;
  26. String symmetricKey;
  27. bool admin;
  28. String name;
  29. bool twoUser;
  30. ConversationStatus status;
  31. bool isRead;
  32. String messageExpiryDefault = 'no_expiry';
  33. bool adminAddMembers = true;
  34. bool adminEditInfo = true;
  35. bool adminSendMessages = false;
  36. DateTime createdAt;
  37. DateTime updatedAt;
  38. File? icon;
  39. Conversation({
  40. required this.id,
  41. required this.userId,
  42. required this.symmetricKey,
  43. required this.admin,
  44. required this.name,
  45. required this.twoUser,
  46. required this.status,
  47. required this.isRead,
  48. required this.messageExpiryDefault,
  49. required this.adminAddMembers,
  50. required this.adminEditInfo,
  51. required this.adminSendMessages,
  52. required this.createdAt,
  53. required this.updatedAt,
  54. this.icon,
  55. });
  56. factory Conversation.fromJson(Map<String, dynamic> json, RSAPrivateKey privKey) {
  57. var symmetricKeyDecrypted = CryptoUtils.rsaDecrypt(
  58. base64.decode(json['symmetric_key']),
  59. privKey,
  60. );
  61. var id = AesHelper.aesDecrypt(
  62. symmetricKeyDecrypted,
  63. base64.decode(json['conversation_detail_id']),
  64. );
  65. var admin = AesHelper.aesDecrypt(
  66. symmetricKeyDecrypted,
  67. base64.decode(json['admin']),
  68. );
  69. return Conversation(
  70. id: id,
  71. userId: json['user_id'],
  72. symmetricKey: base64.encode(symmetricKeyDecrypted),
  73. admin: admin == 'true',
  74. name: 'Unknown',
  75. twoUser: false,
  76. status: ConversationStatus.complete,
  77. isRead: true,
  78. messageExpiryDefault: 'no_expiry',
  79. adminAddMembers: true,
  80. adminEditInfo: true,
  81. adminSendMessages: false,
  82. createdAt: DateTime.now(),
  83. updatedAt: DateTime.now(),
  84. );
  85. }
  86. Future<Map<String, dynamic>> payloadJson({ bool includeUsers = true }) async {
  87. MyProfile profile = await MyProfile.getProfile();
  88. var symKey = base64.decode(symmetricKey);
  89. if (!includeUsers) {
  90. return {
  91. 'id': id,
  92. 'name': AesHelper.aesEncrypt(symKey, Uint8List.fromList(name.codeUnits)),
  93. 'users': await ConversationUsersRepository.getEncryptedConversationUsers(this, symKey),
  94. };
  95. }
  96. List<ConversationUser> users = await ConversationUsersRepository.getConversationUsers(this);
  97. List<Object> userConversations = [];
  98. for (ConversationUser user in users) {
  99. RSAPublicKey pubKey = profile.publicKey!;
  100. String newId = id;
  101. if (profile.id != user.userId) {
  102. Friend friend = await FriendsRepository.getFriendByFriendId(user.userId);
  103. pubKey = friend.publicKey;
  104. newId = (const Uuid()).v4();
  105. }
  106. userConversations.add({
  107. 'id': newId,
  108. 'user_id': user.userId,
  109. 'conversation_detail_id': AesHelper.aesEncrypt(symKey, Uint8List.fromList(id.codeUnits)),
  110. 'admin': AesHelper.aesEncrypt(symKey, Uint8List.fromList((user.admin ? 'true' : 'false').codeUnits)),
  111. 'symmetric_key': base64.encode(CryptoUtils.rsaEncrypt(symKey, pubKey)),
  112. });
  113. }
  114. Map<String, dynamic> returnData = {
  115. 'id': id,
  116. 'name': AesHelper.aesEncrypt(symKey, Uint8List.fromList(name.codeUnits)),
  117. 'users': await ConversationUsersRepository.getEncryptedConversationUsers(this, symKey),
  118. 'two_user': AesHelper.aesEncrypt(symKey, Uint8List.fromList((twoUser ? 'true' : 'false').codeUnits)),
  119. 'message_expiry': messageExpiryDefault,
  120. 'admin_add_members': AesHelper.aesEncrypt(symKey, Uint8List.fromList((adminAddMembers ? 'true' : 'false').codeUnits)),
  121. 'admin_edit_info': AesHelper.aesEncrypt(symKey, Uint8List.fromList((adminEditInfo ? 'true' : 'false').codeUnits)),
  122. 'admin_send_messages': AesHelper.aesEncrypt(symKey, Uint8List.fromList((adminSendMessages ? 'true' : 'false').codeUnits)),
  123. 'user_conversations': userConversations,
  124. };
  125. if (icon != null) {
  126. returnData['attachment'] = {
  127. 'data': AesHelper.aesEncrypt(symmetricKey, Uint8List.fromList(icon!.readAsBytesSync())),
  128. 'mimetype': lookupMimeType(icon!.path),
  129. 'extension': getExtension(icon!.path),
  130. };
  131. }
  132. return returnData;
  133. }
  134. Map<String, dynamic> payloadImageJson() {
  135. if (icon == null) {
  136. return {};
  137. }
  138. return {
  139. 'data': AesHelper.aesEncrypt(symmetricKey, Uint8List.fromList(icon!.readAsBytesSync())),
  140. 'mimetype': lookupMimeType(icon!.path),
  141. 'extension': getExtension(icon!.path),
  142. };
  143. }
  144. Map<String, dynamic> toMap() {
  145. return {
  146. 'id': id,
  147. 'user_id': userId,
  148. 'symmetric_key': symmetricKey,
  149. 'admin': admin ? 1 : 0,
  150. 'name': name,
  151. 'two_user': twoUser ? 1 : 0,
  152. 'status': status.index,
  153. 'is_read': isRead ? 1 : 0,
  154. 'file': icon != null ? icon!.path : null,
  155. 'message_expiry': messageExpiryDefault,
  156. 'admin_add_members': adminAddMembers ? 1 : 0,
  157. 'admin_edit_info': adminEditInfo ? 1 : 0,
  158. 'admin_send_messages': adminSendMessages ? 1 : 0,
  159. 'created_at': createdAt.toIso8601String(),
  160. 'updated_at': updatedAt.toIso8601String(),
  161. };
  162. }
  163. @override
  164. String toString() {
  165. return '''
  166. id: $id
  167. userId: $userId
  168. name: $name
  169. admin: $admin''';
  170. }
  171. Future<Message?> getRecentMessage() async {
  172. final db = await getDatabaseConnection();
  173. final List<Map<String, dynamic>> maps = await db.rawQuery(
  174. '''
  175. SELECT * FROM messages WHERE association_key IN (
  176. SELECT association_key FROM conversation_users WHERE conversation_id = ?
  177. )
  178. ORDER BY created_at DESC
  179. LIMIT 1;
  180. ''',
  181. [ id ],
  182. );
  183. if (maps.isEmpty) {
  184. return null;
  185. }
  186. return TextMessage(
  187. id: maps[0]['id'],
  188. symmetricKey: maps[0]['symmetric_key'],
  189. userSymmetricKey: maps[0]['user_symmetric_key'],
  190. text: maps[0]['data'] ?? 'Image',
  191. senderId: maps[0]['sender_id'],
  192. senderUsername: maps[0]['sender_username'],
  193. associationKey: maps[0]['association_key'],
  194. createdAt: maps[0]['created_at'],
  195. failedToSend: maps[0]['failed_to_send'] == 1,
  196. );
  197. }
  198. }