import 'dart:convert'; import 'dart:typed_data'; import 'package:Envelope/models/messages.dart'; import 'package:pointycastle/export.dart'; import 'package:sqflite/sqflite.dart'; import 'package:uuid/uuid.dart'; import '/models/conversation_users.dart'; import '/models/friends.dart'; import '/models/my_profile.dart'; import '/utils/encryption/aes_helper.dart'; import '/utils/encryption/crypto_utils.dart'; import '/utils/storage/database.dart'; import '/utils/strings.dart'; Future createConversation(String title, List friends) async { final db = await getDatabaseConnection(); MyProfile profile = await MyProfile.getProfile(); var uuid = const Uuid(); final String conversationId = uuid.v4(); final String conversationDetailId = uuid.v4(); Uint8List symmetricKey = AesHelper.deriveKey(generateRandomString(32)); String associationKey = generateRandomString(32); Conversation conversation = Conversation( id: conversationId, userId: profile.id, conversationDetailId: conversationDetailId, symmetricKey: base64.encode(symmetricKey), admin: true, name: title, status: ConversationStatus.pending, isRead: true, ); await db.insert( 'conversations', conversation.toMap(), conflictAlgorithm: ConflictAlgorithm.replace, ); await db.insert( 'conversation_users', ConversationUser( id: uuid.v4(), userId: profile.id, conversationId: conversationId, username: profile.username, associationKey: associationKey, admin: true, ).toMap(), conflictAlgorithm: ConflictAlgorithm.fail, ); for (Friend friend in friends) { await db.insert( 'conversation_users', ConversationUser( id: uuid.v4(), userId: friend.friendId, conversationId: conversationId, username: friend.username, associationKey: associationKey, admin: false, ).toMap(), conflictAlgorithm: ConflictAlgorithm.replace, ); } return conversation; } Conversation findConversationByDetailId(List conversations, String id) { for (var conversation in conversations) { if (conversation.conversationDetailId == id) { return conversation; } } // Or return `null`. throw ArgumentError.value(id, "id", "No element with that id"); } Future getConversationById(String id) async { final db = await getDatabaseConnection(); final List> maps = await db.query( 'conversations', where: 'id = ?', whereArgs: [id], ); if (maps.length != 1) { throw ArgumentError('Invalid user id'); } return Conversation( id: maps[0]['id'], userId: maps[0]['user_id'], conversationDetailId: maps[0]['conversation_detail_id'], symmetricKey: maps[0]['symmetric_key'], admin: maps[0]['admin'] == 1, name: maps[0]['name'], status: ConversationStatus.values[maps[0]['status']], isRead: maps[0]['is_read'] == 1, ); } // A method that retrieves all the dogs from the dogs table. Future> getConversations() async { final db = await getDatabaseConnection(); final List> maps = await db.query('conversations'); return List.generate(maps.length, (i) { return Conversation( id: maps[i]['id'], userId: maps[i]['user_id'], conversationDetailId: maps[i]['conversation_detail_id'], symmetricKey: maps[i]['symmetric_key'], admin: maps[i]['admin'] == 1, name: maps[i]['name'], status: ConversationStatus.values[maps[i]['status']], isRead: maps[i]['is_read'] == 1, ); }); } class Conversation { String id; String userId; String conversationDetailId; String symmetricKey; bool admin; String name; ConversationStatus status; bool isRead; Conversation({ required this.id, required this.userId, required this.conversationDetailId, required this.symmetricKey, required this.admin, required this.name, required this.status, required this.isRead, }); factory Conversation.fromJson(Map json, RSAPrivateKey privKey) { var symmetricKeyDecrypted = CryptoUtils.rsaDecrypt( base64.decode(json['symmetric_key']), privKey, ); var detailId = AesHelper.aesDecrypt( symmetricKeyDecrypted, base64.decode(json['conversation_detail_id']), ); var admin = AesHelper.aesDecrypt( symmetricKeyDecrypted, base64.decode(json['admin']), ); return Conversation( id: json['id'], userId: json['user_id'], conversationDetailId: detailId, symmetricKey: base64.encode(symmetricKeyDecrypted), admin: admin == 'true', name: 'Unknown', status: ConversationStatus.complete, isRead: true, ); } Future> toJson() async { MyProfile profile = await MyProfile.getProfile(); var symKey = base64.decode(symmetricKey); List users = await getConversationUsers(this); List userConversations = []; for (ConversationUser user in users) { RSAPublicKey pubKey = profile.publicKey!; String newId = id; if (profile.id != user.userId) { Friend friend = await getFriendByFriendId(user.userId); pubKey = CryptoUtils.rsaPublicKeyFromPem(friend.asymmetricPublicKey); newId = (const Uuid()).v4(); } userConversations.add({ 'id': newId, 'user_id': user.userId, 'conversation_detail_id': AesHelper.aesEncrypt(symKey, Uint8List.fromList(conversationDetailId.codeUnits)), 'admin': AesHelper.aesEncrypt(symKey, Uint8List.fromList((admin ? 'true' : 'false').codeUnits)), 'symmetric_key': base64.encode(CryptoUtils.rsaEncrypt(symKey, pubKey)), }); } return { 'id': conversationDetailId, 'name': AesHelper.aesEncrypt(symKey, Uint8List.fromList(name.codeUnits)), 'users': AesHelper.aesEncrypt(symKey, Uint8List.fromList(jsonEncode(users).codeUnits)), 'user_conversations': userConversations, }; } Map toMap() { return { 'id': id, 'user_id': userId, 'conversation_detail_id': conversationDetailId, 'symmetric_key': symmetricKey, 'admin': admin ? 1 : 0, 'name': name, 'status': status.index, 'is_read': isRead ? 1 : 0, }; } @override String toString() { return ''' id: $id userId: $userId name: $name admin: $admin'''; } Future getRecentMessage() async { final db = await getDatabaseConnection(); final List> maps = await db.rawQuery( ''' SELECT * FROM messages WHERE association_key IN ( SELECT association_key FROM conversation_users WHERE conversation_id = ? ) ORDER BY created_at DESC LIMIT 1; ''', [id], ); if (maps.isEmpty) { return null; } return Message( id: maps[0]['id'], symmetricKey: maps[0]['symmetric_key'], userSymmetricKey: maps[0]['user_symmetric_key'], data: maps[0]['data'], senderId: maps[0]['sender_id'], senderUsername: maps[0]['sender_username'], associationKey: maps[0]['association_key'], createdAt: maps[0]['created_at'], failedToSend: maps[0]['failed_to_send'] == 1, ); } } enum ConversationStatus { complete, pending, error, }