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.

371 lines
9.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, bool twoUser) 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. twoUser: twoUser,
  27. status: ConversationStatus.pending,
  28. isRead: true,
  29. );
  30. await db.insert(
  31. 'conversations',
  32. conversation.toMap(),
  33. conflictAlgorithm: ConflictAlgorithm.replace,
  34. );
  35. await db.insert(
  36. 'conversation_users',
  37. ConversationUser(
  38. id: uuid.v4(),
  39. userId: profile.id,
  40. conversationId: conversationId,
  41. username: profile.username,
  42. associationKey: uuid.v4(),
  43. publicKey: profile.publicKey!,
  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. publicKey: friend.publicKey,
  58. admin: false,
  59. ).toMap(),
  60. conflictAlgorithm: ConflictAlgorithm.replace,
  61. );
  62. }
  63. return conversation;
  64. }
  65. Future<Conversation> addUsersToConversation(Conversation conversation, List<Friend> friends) async {
  66. final db = await getDatabaseConnection();
  67. var uuid = const Uuid();
  68. for (Friend friend in friends) {
  69. await db.insert(
  70. 'conversation_users',
  71. ConversationUser(
  72. id: uuid.v4(),
  73. userId: friend.friendId,
  74. conversationId: conversation.id,
  75. username: friend.username,
  76. associationKey: uuid.v4(),
  77. publicKey: friend.publicKey,
  78. admin: false,
  79. ).toMap(),
  80. conflictAlgorithm: ConflictAlgorithm.replace,
  81. );
  82. }
  83. return conversation;
  84. }
  85. Conversation findConversationByDetailId(List<Conversation> conversations, String id) {
  86. for (var conversation in conversations) {
  87. if (conversation.id == id) {
  88. return conversation;
  89. }
  90. }
  91. // Or return `null`.
  92. throw ArgumentError.value(id, 'id', 'No element with that id');
  93. }
  94. Future<Conversation> getConversationById(String id) async {
  95. final db = await getDatabaseConnection();
  96. final List<Map<String, dynamic>> maps = await db.query(
  97. 'conversations',
  98. where: 'id = ?',
  99. whereArgs: [id],
  100. );
  101. if (maps.length != 1) {
  102. throw ArgumentError('Invalid user id');
  103. }
  104. return Conversation(
  105. id: maps[0]['id'],
  106. userId: maps[0]['user_id'],
  107. symmetricKey: maps[0]['symmetric_key'],
  108. admin: maps[0]['admin'] == 1,
  109. name: maps[0]['name'],
  110. twoUser: maps[0]['two_user'] == 1,
  111. status: ConversationStatus.values[maps[0]['status']],
  112. isRead: maps[0]['is_read'] == 1,
  113. );
  114. }
  115. // A method that retrieves all the dogs from the dogs table.
  116. Future<List<Conversation>> getConversations() async {
  117. final db = await getDatabaseConnection();
  118. final List<Map<String, dynamic>> maps = await db.query('conversations');
  119. return List.generate(maps.length, (i) {
  120. return Conversation(
  121. id: maps[i]['id'],
  122. userId: maps[i]['user_id'],
  123. symmetricKey: maps[i]['symmetric_key'],
  124. admin: maps[i]['admin'] == 1,
  125. name: maps[i]['name'],
  126. twoUser: maps[i]['two_user'] == 1,
  127. status: ConversationStatus.values[maps[i]['status']],
  128. isRead: maps[i]['is_read'] == 1,
  129. );
  130. });
  131. }
  132. Future<Conversation?> getTwoUserConversation(String userId) async {
  133. final db = await getDatabaseConnection();
  134. MyProfile profile = await MyProfile.getProfile();
  135. print(userId);
  136. print(profile.id);
  137. final List<Map<String, dynamic>> maps = await db.rawQuery(
  138. '''
  139. SELECT conversations.* FROM conversations
  140. LEFT JOIN conversation_users ON conversation_users.conversation_id = conversations.id
  141. WHERE conversation_users.user_id = ?
  142. AND conversation_users.user_id != ?
  143. AND conversations.two_user = 1
  144. ''',
  145. [ userId, profile.id ],
  146. );
  147. if (maps.length != 1) {
  148. return null;
  149. }
  150. return Conversation(
  151. id: maps[0]['id'],
  152. userId: maps[0]['user_id'],
  153. symmetricKey: maps[0]['symmetric_key'],
  154. admin: maps[0]['admin'] == 1,
  155. name: maps[0]['name'],
  156. twoUser: maps[0]['two_user'] == 1,
  157. status: ConversationStatus.values[maps[0]['status']],
  158. isRead: maps[0]['is_read'] == 1,
  159. );
  160. }
  161. class Conversation {
  162. String id;
  163. String userId;
  164. String symmetricKey;
  165. bool admin;
  166. String name;
  167. bool twoUser;
  168. ConversationStatus status;
  169. bool isRead;
  170. Conversation({
  171. required this.id,
  172. required this.userId,
  173. required this.symmetricKey,
  174. required this.admin,
  175. required this.name,
  176. required this.twoUser,
  177. required this.status,
  178. required this.isRead,
  179. });
  180. factory Conversation.fromJson(Map<String, dynamic> json, RSAPrivateKey privKey) {
  181. var symmetricKeyDecrypted = CryptoUtils.rsaDecrypt(
  182. base64.decode(json['symmetric_key']),
  183. privKey,
  184. );
  185. var id = AesHelper.aesDecrypt(
  186. symmetricKeyDecrypted,
  187. base64.decode(json['conversation_detail_id']),
  188. );
  189. var admin = AesHelper.aesDecrypt(
  190. symmetricKeyDecrypted,
  191. base64.decode(json['admin']),
  192. );
  193. return Conversation(
  194. id: id,
  195. userId: json['user_id'],
  196. symmetricKey: base64.encode(symmetricKeyDecrypted),
  197. admin: admin == 'true',
  198. name: 'Unknown',
  199. twoUser: false,
  200. status: ConversationStatus.complete,
  201. isRead: true,
  202. );
  203. }
  204. Future<Map<String, dynamic>> payloadJson({ bool includeUsers = true }) async {
  205. MyProfile profile = await MyProfile.getProfile();
  206. var symKey = base64.decode(symmetricKey);
  207. if (!includeUsers) {
  208. return {
  209. 'id': id,
  210. 'name': AesHelper.aesEncrypt(symKey, Uint8List.fromList(name.codeUnits)),
  211. 'users': await getEncryptedConversationUsers(this, symKey),
  212. };
  213. }
  214. List<ConversationUser> users = await getConversationUsers(this);
  215. List<Object> userConversations = [];
  216. for (ConversationUser user in users) {
  217. RSAPublicKey pubKey = profile.publicKey!;
  218. String newId = id;
  219. if (profile.id != user.userId) {
  220. Friend friend = await getFriendByFriendId(user.userId);
  221. pubKey = friend.publicKey;
  222. newId = (const Uuid()).v4();
  223. }
  224. userConversations.add({
  225. 'id': newId,
  226. 'user_id': user.userId,
  227. 'conversation_detail_id': AesHelper.aesEncrypt(symKey, Uint8List.fromList(id.codeUnits)),
  228. 'admin': AesHelper.aesEncrypt(symKey, Uint8List.fromList((user.admin ? 'true' : 'false').codeUnits)),
  229. 'symmetric_key': base64.encode(CryptoUtils.rsaEncrypt(symKey, pubKey)),
  230. });
  231. }
  232. return {
  233. 'id': id,
  234. 'name': AesHelper.aesEncrypt(symKey, Uint8List.fromList(name.codeUnits)),
  235. 'users': await getEncryptedConversationUsers(this, symKey),
  236. 'user_conversations': userConversations,
  237. };
  238. }
  239. Map<String, dynamic> toMap() {
  240. return {
  241. 'id': id,
  242. 'user_id': userId,
  243. 'symmetric_key': symmetricKey,
  244. 'admin': admin ? 1 : 0,
  245. 'name': name,
  246. 'two_user': twoUser ? 1 : 0,
  247. 'status': status.index,
  248. 'is_read': isRead ? 1 : 0,
  249. };
  250. }
  251. @override
  252. String toString() {
  253. return '''
  254. id: $id
  255. userId: $userId
  256. name: $name
  257. admin: $admin''';
  258. }
  259. Future<Message?> getRecentMessage() async {
  260. final db = await getDatabaseConnection();
  261. final List<Map<String, dynamic>> maps = await db.rawQuery(
  262. '''
  263. SELECT * FROM messages WHERE association_key IN (
  264. SELECT association_key FROM conversation_users WHERE conversation_id = ?
  265. )
  266. ORDER BY created_at DESC
  267. LIMIT 1;
  268. ''',
  269. [id],
  270. );
  271. if (maps.isEmpty) {
  272. return null;
  273. }
  274. return Message(
  275. id: maps[0]['id'],
  276. symmetricKey: maps[0]['symmetric_key'],
  277. userSymmetricKey: maps[0]['user_symmetric_key'],
  278. data: maps[0]['data'],
  279. senderId: maps[0]['sender_id'],
  280. senderUsername: maps[0]['sender_username'],
  281. associationKey: maps[0]['association_key'],
  282. createdAt: maps[0]['created_at'],
  283. failedToSend: maps[0]['failed_to_send'] == 1,
  284. );
  285. }
  286. Future<String> getName() async {
  287. if (!twoUser) {
  288. return name;
  289. }
  290. MyProfile profile = await MyProfile.getProfile();
  291. final db = await getDatabaseConnection();
  292. List<Map<String, dynamic>> maps = await db.query(
  293. 'conversation_users',
  294. where: 'conversation_id = ? AND user_id != ?',
  295. whereArgs: [ id, profile.id ],
  296. );
  297. if (maps.length != 1) {
  298. throw ArgumentError('Invalid user id');
  299. }
  300. return maps[0]['username'];
  301. }
  302. }
  303. enum ConversationStatus {
  304. complete,
  305. pending,
  306. error,
  307. }