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.

254 lines
6.6 KiB

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