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.

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