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.5 KiB

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