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.

252 lines
6.6 KiB

  1. import 'dart:convert';
  2. import 'dart:typed_data';
  3. import 'package:Envelope/models/conversation_users.dart';
  4. import 'package:Envelope/models/friends.dart';
  5. import 'package:Envelope/models/my_profile.dart';
  6. import 'package:pointycastle/export.dart';
  7. import 'package:sqflite/sqflite.dart';
  8. import 'package:uuid/uuid.dart';
  9. import '/utils/encryption/crypto_utils.dart';
  10. import '/utils/encryption/aes_helper.dart';
  11. import '/utils/storage/database.dart';
  12. import '/utils/strings.dart';
  13. enum ConversationStatus {
  14. complete,
  15. pending,
  16. error,
  17. }
  18. class Conversation {
  19. String id;
  20. String userId;
  21. String conversationDetailId;
  22. String symmetricKey;
  23. bool admin;
  24. String name;
  25. ConversationStatus status;
  26. Conversation({
  27. required this.id,
  28. required this.userId,
  29. required this.conversationDetailId,
  30. required this.symmetricKey,
  31. required this.admin,
  32. required this.name,
  33. required this.status,
  34. });
  35. factory Conversation.fromJson(Map<String, dynamic> json, RSAPrivateKey privKey) {
  36. var symmetricKeyDecrypted = CryptoUtils.rsaDecrypt(
  37. base64.decode(json['symmetric_key']),
  38. privKey,
  39. );
  40. var detailId = AesHelper.aesDecrypt(
  41. symmetricKeyDecrypted,
  42. base64.decode(json['conversation_detail_id']),
  43. );
  44. var admin = AesHelper.aesDecrypt(
  45. symmetricKeyDecrypted,
  46. base64.decode(json['admin']),
  47. );
  48. return Conversation(
  49. id: json['id'],
  50. userId: json['user_id'],
  51. conversationDetailId: detailId,
  52. symmetricKey: base64.encode(symmetricKeyDecrypted),
  53. admin: admin == 'true',
  54. name: 'Unknown',
  55. status: ConversationStatus.complete,
  56. );
  57. }
  58. Future<Map<String, dynamic>> toJson() async {
  59. MyProfile profile = await MyProfile.getProfile();
  60. var symKey = base64.decode(symmetricKey);
  61. List<ConversationUser> users = await getConversationUsers(this);
  62. List<Object> userConversations = [];
  63. for (var x in users) {
  64. print(x.toMap());
  65. }
  66. for (ConversationUser user in users) {
  67. if (profile.id == user.userId) {
  68. userConversations.add({
  69. 'id': user.id,
  70. 'user_id': profile.id,
  71. 'conversation_detail_id': AesHelper.aesEncrypt(symKey, Uint8List.fromList(id.codeUnits)),
  72. 'admin': AesHelper.aesEncrypt(symKey, Uint8List.fromList((admin ? 'true' : 'false').codeUnits)),
  73. 'symmetric_key': base64.encode(CryptoUtils.rsaEncrypt(symKey, profile.publicKey!)),
  74. });
  75. continue;
  76. }
  77. Friend friend = await getFriendByFriendId(user.userId);
  78. RSAPublicKey pubKey = CryptoUtils.rsaPublicKeyFromPem(friend.asymmetricPublicKey);
  79. userConversations.add({
  80. 'id': user.id,
  81. 'user_id': friend.userId,
  82. 'conversation_detail_id': AesHelper.aesEncrypt(symKey, Uint8List.fromList(id.codeUnits)),
  83. 'admin': AesHelper.aesEncrypt(symKey, Uint8List.fromList((admin ? 'true' : 'false').codeUnits)),
  84. 'symmetric_key': base64.encode(CryptoUtils.rsaEncrypt(symKey, pubKey)),
  85. });
  86. }
  87. for (var x in userConversations) {
  88. print(x);
  89. }
  90. return {
  91. 'id': id,
  92. 'name': AesHelper.aesEncrypt(symKey, Uint8List.fromList(name.codeUnits)),
  93. 'users': AesHelper.aesEncrypt(symKey, Uint8List.fromList(jsonEncode(users).codeUnits)),
  94. 'user_conversations': userConversations,
  95. };
  96. }
  97. @override
  98. String toString() {
  99. return '''
  100. id: $id
  101. userId: $userId
  102. name: $name
  103. admin: $admin''';
  104. }
  105. Map<String, dynamic> toMap() {
  106. return {
  107. 'id': id,
  108. 'user_id': userId,
  109. 'conversation_detail_id': conversationDetailId,
  110. 'symmetric_key': symmetricKey,
  111. 'admin': admin ? 1 : 0,
  112. 'name': name,
  113. 'status': status.index,
  114. };
  115. }
  116. }
  117. Future<Conversation> createConversation(String title, List<Friend> friends) async {
  118. final db = await getDatabaseConnection();
  119. MyProfile profile = await MyProfile.getProfile();
  120. var uuid = const Uuid();
  121. final String conversationId = uuid.v4();
  122. Uint8List symmetricKey = AesHelper.deriveKey(generateRandomString(32));
  123. String associationKey = generateRandomString(32);
  124. Conversation conversation = Conversation(
  125. id: conversationId,
  126. userId: profile.id,
  127. conversationDetailId: '',
  128. symmetricKey: base64.encode(symmetricKey),
  129. admin: true,
  130. name: title,
  131. status: ConversationStatus.pending,
  132. );
  133. await db.insert(
  134. 'conversations',
  135. conversation.toMap(),
  136. conflictAlgorithm: ConflictAlgorithm.replace,
  137. );
  138. await db.insert(
  139. 'conversation_users',
  140. ConversationUser(
  141. id: uuid.v4(),
  142. userId: profile.id,
  143. conversationId: conversationId,
  144. username: profile.username,
  145. associationKey: associationKey,
  146. admin: true,
  147. ).toMap(),
  148. conflictAlgorithm: ConflictAlgorithm.replace,
  149. );
  150. for (Friend friend in friends) {
  151. await db.insert(
  152. 'conversation_users',
  153. ConversationUser(
  154. id: uuid.v4(),
  155. userId: friend.friendId,
  156. conversationId: conversationId,
  157. username: friend.username,
  158. associationKey: associationKey,
  159. admin: false,
  160. ).toMap(),
  161. conflictAlgorithm: ConflictAlgorithm.replace,
  162. );
  163. }
  164. return conversation;
  165. }
  166. Conversation findConversationByDetailId(List<Conversation> conversations, String id) {
  167. for (var conversation in conversations) {
  168. if (conversation.conversationDetailId == id) {
  169. return conversation;
  170. }
  171. }
  172. // Or return `null`.
  173. throw ArgumentError.value(id, "id", "No element with that id");
  174. }
  175. // A method that retrieves all the dogs from the dogs table.
  176. Future<List<Conversation>> getConversations() async {
  177. final db = await getDatabaseConnection();
  178. final List<Map<String, dynamic>> maps = await db.query('conversations');
  179. return List.generate(maps.length, (i) {
  180. return Conversation(
  181. id: maps[i]['id'],
  182. userId: maps[i]['user_id'],
  183. conversationDetailId: maps[i]['conversation_detail_id'],
  184. symmetricKey: maps[i]['symmetric_key'],
  185. admin: maps[i]['admin'] == 1,
  186. name: maps[i]['name'],
  187. status: ConversationStatus.values[maps[i]['status']],
  188. );
  189. });
  190. }
  191. Future<Conversation> getConversationById(String id) async {
  192. final db = await getDatabaseConnection();
  193. final List<Map<String, dynamic>> maps = await db.query(
  194. 'conversations',
  195. where: 'id = ?',
  196. whereArgs: [id],
  197. );
  198. if (maps.length != 1) {
  199. throw ArgumentError('Invalid user id');
  200. }
  201. return Conversation(
  202. id: maps[0]['id'],
  203. userId: maps[0]['user_id'],
  204. conversationDetailId: maps[0]['conversation_detail_id'],
  205. symmetricKey: maps[0]['symmetric_key'],
  206. admin: maps[0]['admin'] == 1,
  207. name: maps[0]['name'],
  208. status: ConversationStatus.values[maps[0]['status']],
  209. );
  210. }