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.

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