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.

312 lines
8.1 KiB

  1. import 'dart:convert';
  2. import 'dart:typed_data';
  3. import 'package:Envelope/models/messages.dart';
  4. import 'package:pointycastle/export.dart';
  5. import 'package:sqflite/sqflite.dart';
  6. import 'package:uuid/uuid.dart';
  7. import '/models/conversation_users.dart';
  8. import '/models/friends.dart';
  9. import '/models/my_profile.dart';
  10. import '/utils/encryption/aes_helper.dart';
  11. import '/utils/encryption/crypto_utils.dart';
  12. import '/utils/storage/database.dart';
  13. import '/utils/strings.dart';
  14. Future<Conversation> createConversation(String title, List<Friend> friends) async {
  15. final db = await getDatabaseConnection();
  16. MyProfile profile = await MyProfile.getProfile();
  17. var uuid = const Uuid();
  18. final String conversationId = uuid.v4();
  19. final String conversationDetailId = uuid.v4();
  20. Uint8List symmetricKey = AesHelper.deriveKey(generateRandomString(32));
  21. Conversation conversation = Conversation(
  22. id: conversationId,
  23. userId: profile.id,
  24. conversationDetailId: conversationDetailId,
  25. symmetricKey: base64.encode(symmetricKey),
  26. admin: true,
  27. name: title,
  28. status: ConversationStatus.pending,
  29. isRead: true,
  30. );
  31. await db.insert(
  32. 'conversations',
  33. conversation.toMap(),
  34. conflictAlgorithm: ConflictAlgorithm.replace,
  35. );
  36. await db.insert(
  37. 'conversation_users',
  38. ConversationUser(
  39. id: uuid.v4(),
  40. userId: profile.id,
  41. conversationId: conversationId,
  42. username: profile.username,
  43. associationKey: uuid.v4(),
  44. admin: true,
  45. ).toMap(),
  46. conflictAlgorithm: ConflictAlgorithm.fail,
  47. );
  48. for (Friend friend in friends) {
  49. await db.insert(
  50. 'conversation_users',
  51. ConversationUser(
  52. id: uuid.v4(),
  53. userId: friend.friendId,
  54. conversationId: conversationId,
  55. username: friend.username,
  56. associationKey: uuid.v4(),
  57. admin: false,
  58. ).toMap(),
  59. conflictAlgorithm: ConflictAlgorithm.replace,
  60. );
  61. }
  62. return conversation;
  63. }
  64. Future<Conversation> addUsersToConversation(Conversation conversation, List<Friend> friends) async {
  65. final db = await getDatabaseConnection();
  66. var uuid = const Uuid();
  67. for (Friend friend in friends) {
  68. await db.insert(
  69. 'conversation_users',
  70. ConversationUser(
  71. id: uuid.v4(),
  72. userId: friend.friendId,
  73. conversationId: conversation.id,
  74. username: friend.username,
  75. associationKey: uuid.v4(),
  76. admin: false,
  77. ).toMap(),
  78. conflictAlgorithm: ConflictAlgorithm.replace,
  79. );
  80. }
  81. return conversation;
  82. }
  83. Conversation findConversationByDetailId(List<Conversation> conversations, String id) {
  84. for (var conversation in conversations) {
  85. if (conversation.conversationDetailId == id) {
  86. return conversation;
  87. }
  88. }
  89. // Or return `null`.
  90. throw ArgumentError.value(id, "id", "No element with that id");
  91. }
  92. Future<Conversation> getConversationById(String id) async {
  93. final db = await getDatabaseConnection();
  94. final List<Map<String, dynamic>> maps = await db.query(
  95. 'conversations',
  96. where: 'id = ?',
  97. whereArgs: [id],
  98. );
  99. if (maps.length != 1) {
  100. throw ArgumentError('Invalid user id');
  101. }
  102. return Conversation(
  103. id: maps[0]['id'],
  104. userId: maps[0]['user_id'],
  105. conversationDetailId: maps[0]['conversation_detail_id'],
  106. symmetricKey: maps[0]['symmetric_key'],
  107. admin: maps[0]['admin'] == 1,
  108. name: maps[0]['name'],
  109. status: ConversationStatus.values[maps[0]['status']],
  110. isRead: maps[0]['is_read'] == 1,
  111. );
  112. }
  113. // A method that retrieves all the dogs from the dogs table.
  114. Future<List<Conversation>> getConversations() async {
  115. final db = await getDatabaseConnection();
  116. final List<Map<String, dynamic>> maps = await db.query('conversations');
  117. return List.generate(maps.length, (i) {
  118. return Conversation(
  119. id: maps[i]['id'],
  120. userId: maps[i]['user_id'],
  121. conversationDetailId: maps[i]['conversation_detail_id'],
  122. symmetricKey: maps[i]['symmetric_key'],
  123. admin: maps[i]['admin'] == 1,
  124. name: maps[i]['name'],
  125. status: ConversationStatus.values[maps[i]['status']],
  126. isRead: maps[i]['is_read'] == 1,
  127. );
  128. });
  129. }
  130. class Conversation {
  131. String id;
  132. String userId;
  133. String conversationDetailId;
  134. String symmetricKey;
  135. bool admin;
  136. String name;
  137. ConversationStatus status;
  138. bool isRead;
  139. Conversation({
  140. required this.id,
  141. required this.userId,
  142. required this.conversationDetailId,
  143. required this.symmetricKey,
  144. required this.admin,
  145. required this.name,
  146. required this.status,
  147. required this.isRead,
  148. });
  149. factory Conversation.fromJson(Map<String, dynamic> json, RSAPrivateKey privKey) {
  150. var symmetricKeyDecrypted = CryptoUtils.rsaDecrypt(
  151. base64.decode(json['symmetric_key']),
  152. privKey,
  153. );
  154. var detailId = AesHelper.aesDecrypt(
  155. symmetricKeyDecrypted,
  156. base64.decode(json['conversation_detail_id']),
  157. );
  158. var admin = AesHelper.aesDecrypt(
  159. symmetricKeyDecrypted,
  160. base64.decode(json['admin']),
  161. );
  162. return Conversation(
  163. id: json['id'],
  164. userId: json['user_id'],
  165. conversationDetailId: detailId,
  166. symmetricKey: base64.encode(symmetricKeyDecrypted),
  167. admin: admin == 'true',
  168. name: 'Unknown',
  169. status: ConversationStatus.complete,
  170. isRead: true,
  171. );
  172. }
  173. Future<Map<String, dynamic>> payloadJson({ bool includeUsers = true }) async {
  174. MyProfile profile = await MyProfile.getProfile();
  175. var symKey = base64.decode(symmetricKey);
  176. List<ConversationUser> users = await getConversationUsers(this);
  177. if (!includeUsers) {
  178. return {
  179. 'id': conversationDetailId,
  180. 'name': AesHelper.aesEncrypt(symKey, Uint8List.fromList(name.codeUnits)),
  181. 'users': AesHelper.aesEncrypt(symKey, Uint8List.fromList(jsonEncode(users).codeUnits)),
  182. };
  183. }
  184. List<Object> userConversations = [];
  185. for (ConversationUser user in users) {
  186. RSAPublicKey pubKey = profile.publicKey!;
  187. String newId = id;
  188. if (profile.id != user.userId) {
  189. Friend friend = await getFriendByFriendId(user.userId);
  190. pubKey = CryptoUtils.rsaPublicKeyFromPem(friend.asymmetricPublicKey);
  191. newId = (const Uuid()).v4();
  192. }
  193. userConversations.add({
  194. 'id': newId,
  195. 'user_id': user.userId,
  196. 'conversation_detail_id': AesHelper.aesEncrypt(symKey, Uint8List.fromList(conversationDetailId.codeUnits)),
  197. 'admin': AesHelper.aesEncrypt(symKey, Uint8List.fromList((admin ? 'true' : 'false').codeUnits)),
  198. 'symmetric_key': base64.encode(CryptoUtils.rsaEncrypt(symKey, pubKey)),
  199. });
  200. }
  201. return {
  202. 'id': conversationDetailId,
  203. 'name': AesHelper.aesEncrypt(symKey, Uint8List.fromList(name.codeUnits)),
  204. 'users': AesHelper.aesEncrypt(symKey, Uint8List.fromList(jsonEncode(users).codeUnits)),
  205. 'user_conversations': userConversations,
  206. };
  207. }
  208. Map<String, dynamic> toMap() {
  209. return {
  210. 'id': id,
  211. 'user_id': userId,
  212. 'conversation_detail_id': conversationDetailId,
  213. 'symmetric_key': symmetricKey,
  214. 'admin': admin ? 1 : 0,
  215. 'name': name,
  216. 'status': status.index,
  217. 'is_read': isRead ? 1 : 0,
  218. };
  219. }
  220. @override
  221. String toString() {
  222. return '''
  223. id: $id
  224. userId: $userId
  225. name: $name
  226. admin: $admin''';
  227. }
  228. Future<Message?> getRecentMessage() async {
  229. final db = await getDatabaseConnection();
  230. final List<Map<String, dynamic>> maps = await db.rawQuery(
  231. '''
  232. SELECT * FROM messages WHERE association_key IN (
  233. SELECT association_key FROM conversation_users WHERE conversation_id = ?
  234. )
  235. ORDER BY created_at DESC
  236. LIMIT 1;
  237. ''',
  238. [id],
  239. );
  240. if (maps.isEmpty) {
  241. return null;
  242. }
  243. return Message(
  244. id: maps[0]['id'],
  245. symmetricKey: maps[0]['symmetric_key'],
  246. userSymmetricKey: maps[0]['user_symmetric_key'],
  247. data: maps[0]['data'],
  248. senderId: maps[0]['sender_id'],
  249. senderUsername: maps[0]['sender_username'],
  250. associationKey: maps[0]['association_key'],
  251. createdAt: maps[0]['created_at'],
  252. failedToSend: maps[0]['failed_to_send'] == 1,
  253. );
  254. }
  255. }
  256. enum ConversationStatus {
  257. complete,
  258. pending,
  259. error,
  260. }