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.

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