From c89dcf10eca66b3173b13d52fd5afb39d5298f9a Mon Sep 17 00:00:00 2001 From: Tovi Jaeschke-Rogers Date: Fri, 5 Aug 2022 18:35:02 +0930 Subject: [PATCH] Fix messages doubling up due to mismatched ids --- Backend/Api/Messages/CreateConversation.go | 8 +- Backend/Api/Messages/MessageThread.go | 16 +- Backend/Api/Messages/UpdateConversation.go | 8 +- Backend/Database/ConversationDetailUsers.go | 41 ++ Backend/Database/Init.go | 1 + Backend/Database/Messages.go | 10 +- Backend/Database/Seeder/MessageSeeder.go | 158 ++++--- Backend/Models/Conversations.go | 33 ++ Backend/Models/Messages.go | 16 - Backend/main.go | 4 +- README.md | 23 +- mobile/lib/models/conversation_users.dart | 102 +++- mobile/lib/models/conversations.dart | 35 +- mobile/lib/models/friends.dart | 46 +- mobile/lib/models/messages.dart | 14 +- mobile/lib/utils/storage/conversations.dart | 11 +- mobile/lib/utils/storage/database.dart | 5 +- mobile/lib/utils/storage/messages.dart | 6 +- mobile/lib/views/authentication/login.dart | 20 +- mobile/lib/views/authentication/signup.dart | 2 +- .../main/conversation/create_add_users.dart | 185 ++++---- .../lib/views/main/conversation/detail.dart | 444 +++++++++--------- .../lib/views/main/conversation/settings.dart | 372 +++++++-------- mobile/lib/views/main/friend/list.dart | 132 +++--- 24 files changed, 932 insertions(+), 760 deletions(-) create mode 100644 Backend/Database/ConversationDetailUsers.go create mode 100644 Backend/Models/Conversations.go diff --git a/Backend/Api/Messages/CreateConversation.go b/Backend/Api/Messages/CreateConversation.go index 505305a..5241995 100644 --- a/Backend/Api/Messages/CreateConversation.go +++ b/Backend/Api/Messages/CreateConversation.go @@ -11,10 +11,10 @@ import ( ) type RawCreateConversationData struct { - ID string `json:"id"` - Name string `json:"name"` - Users string `json:"users"` - UserConversations []Models.UserConversation `json:"user_conversations"` + ID string `json:"id"` + Name string `json:"name"` + Users []Models.ConversationDetailUser `json:"users"` + UserConversations []Models.UserConversation `json:"user_conversations"` } func CreateConversation(w http.ResponseWriter, r *http.Request) { diff --git a/Backend/Api/Messages/MessageThread.go b/Backend/Api/Messages/MessageThread.go index 7eb3b27..b9c4ee7 100644 --- a/Backend/Api/Messages/MessageThread.go +++ b/Backend/Api/Messages/MessageThread.go @@ -12,22 +12,22 @@ import ( func Messages(w http.ResponseWriter, r *http.Request) { var ( - messages []Models.Message - urlVars map[string]string - threadKey string - returnJson []byte - ok bool - err error + messages []Models.Message + urlVars map[string]string + associationKey string + returnJson []byte + ok bool + err error ) urlVars = mux.Vars(r) - threadKey, ok = urlVars["threadKey"] + associationKey, ok = urlVars["threadKey"] if !ok { http.Error(w, "Not Found", http.StatusNotFound) return } - messages, err = Database.GetMessagesByThreadKey(threadKey) + messages, err = Database.GetMessagesByAssociationKey(associationKey) if !ok { http.Error(w, "Not Found", http.StatusNotFound) return diff --git a/Backend/Api/Messages/UpdateConversation.go b/Backend/Api/Messages/UpdateConversation.go index f1073a2..93b5215 100644 --- a/Backend/Api/Messages/UpdateConversation.go +++ b/Backend/Api/Messages/UpdateConversation.go @@ -11,10 +11,10 @@ import ( ) type RawUpdateConversationData struct { - ID string `json:"id"` - Name string `json:"name"` - Users string `json:"users"` - UserConversations []Models.UserConversation `json:"user_conversations"` + ID string `json:"id"` + Name string `json:"name"` + Users []Models.ConversationDetailUser `json:"users"` + UserConversations []Models.UserConversation `json:"user_conversations"` } func UpdateConversation(w http.ResponseWriter, r *http.Request) { diff --git a/Backend/Database/ConversationDetailUsers.go b/Backend/Database/ConversationDetailUsers.go new file mode 100644 index 0000000..6396acb --- /dev/null +++ b/Backend/Database/ConversationDetailUsers.go @@ -0,0 +1,41 @@ +package Database + +import ( + "git.tovijaeschke.xyz/tovi/Envelope/Backend/Models" + + "gorm.io/gorm" + "gorm.io/gorm/clause" +) + +func GetConversationDetailUserById(id string) (Models.ConversationDetailUser, error) { + var ( + messageThread Models.ConversationDetailUser + err error + ) + + err = DB.Preload(clause.Associations). + Where("id = ?", id). + First(&messageThread). + Error + + return messageThread, err +} + +func CreateConversationDetailUser(messageThread *Models.ConversationDetailUser) error { + return DB.Session(&gorm.Session{FullSaveAssociations: true}). + Create(messageThread). + Error +} + +func UpdateConversationDetailUser(messageThread *Models.ConversationDetailUser) error { + return DB.Session(&gorm.Session{FullSaveAssociations: true}). + Where("id = ?", messageThread.ID). + Updates(messageThread). + Error +} + +func DeleteConversationDetailUser(messageThread *Models.ConversationDetailUser) error { + return DB.Session(&gorm.Session{FullSaveAssociations: true}). + Delete(messageThread). + Error +} diff --git a/Backend/Database/Init.go b/Backend/Database/Init.go index 6241fdb..4124949 100644 --- a/Backend/Database/Init.go +++ b/Backend/Database/Init.go @@ -24,6 +24,7 @@ func GetModels() []interface{} { &Models.MessageData{}, &Models.Message{}, &Models.ConversationDetail{}, + &Models.ConversationDetailUser{}, &Models.UserConversation{}, } } diff --git a/Backend/Database/Messages.go b/Backend/Database/Messages.go index 0affa34..67cf8d3 100644 --- a/Backend/Database/Messages.go +++ b/Backend/Database/Messages.go @@ -20,7 +20,7 @@ func GetMessageById(id string) (Models.Message, error) { return message, err } -func GetMessagesByThreadKey(associationKey string) ([]Models.Message, error) { +func GetMessagesByAssociationKey(associationKey string) ([]Models.Message, error) { var ( messages []Models.Message err error @@ -34,9 +34,7 @@ func GetMessagesByThreadKey(associationKey string) ([]Models.Message, error) { } func CreateMessage(message *Models.Message) error { - var ( - err error - ) + var err error err = DB.Session(&gorm.Session{FullSaveAssociations: true}). Create(message). @@ -46,9 +44,7 @@ func CreateMessage(message *Models.Message) error { } func CreateMessages(messages *[]Models.Message) error { - var ( - err error - ) + var err error err = DB.Session(&gorm.Session{FullSaveAssociations: true}). Create(messages). diff --git a/Backend/Database/Seeder/MessageSeeder.go b/Backend/Database/Seeder/MessageSeeder.go index 0aa750f..dc9a221 100644 --- a/Backend/Database/Seeder/MessageSeeder.go +++ b/Backend/Database/Seeder/MessageSeeder.go @@ -2,7 +2,6 @@ package Seeder import ( "encoding/base64" - "fmt" "git.tovijaeschke.xyz/tovi/Envelope/Backend/Database" "git.tovijaeschke.xyz/tovi/Envelope/Backend/Models" @@ -115,39 +114,19 @@ func seedConversationDetail(key aesKey) (Models.ConversationDetail, error) { return messageThread, err } -func seedUpdateUserConversation( - userJson string, - key aesKey, - messageThread Models.ConversationDetail, -) (Models.ConversationDetail, error) { - var ( - usersCiphertext []byte - err error - ) - - usersCiphertext, err = key.aesEncrypt([]byte(userJson)) - if err != nil { - return messageThread, err - } - - messageThread.Users = base64.StdEncoding.EncodeToString(usersCiphertext) - err = Database.UpdateConversationDetail(&messageThread) - return messageThread, err -} - func seedUserConversation( user Models.User, threadID uuid.UUID, key aesKey, ) (Models.UserConversation, error) { var ( - messageThreadUser Models.UserConversation - threadIdCiphertext []byte - adminCiphertext []byte - err error + messageThreadUser Models.UserConversation + conversationDetailIDCiphertext []byte + adminCiphertext []byte + err error ) - threadIdCiphertext, err = key.aesEncrypt([]byte(threadID.String())) + conversationDetailIDCiphertext, err = key.aesEncrypt([]byte(threadID.String())) if err != nil { return messageThreadUser, err } @@ -159,7 +138,7 @@ func seedUserConversation( messageThreadUser = Models.UserConversation{ UserID: user.ID, - ConversationDetailID: base64.StdEncoding.EncodeToString(threadIdCiphertext), + ConversationDetailID: base64.StdEncoding.EncodeToString(conversationDetailIDCiphertext), Admin: base64.StdEncoding.EncodeToString(adminCiphertext), SymmetricKey: base64.StdEncoding.EncodeToString( encryptWithPublicKey(key.Key, decodedPublicKey), @@ -170,16 +149,78 @@ func seedUserConversation( return messageThreadUser, err } +func seedConversationDetailUser( + user Models.User, + conversationDetail Models.ConversationDetail, + associationKey uuid.UUID, + admin bool, + key aesKey, +) (Models.ConversationDetailUser, error) { + var ( + conversationDetailUser Models.ConversationDetailUser + + adminString string = "false" + + userIdCiphertext []byte + usernameCiphertext []byte + adminCiphertext []byte + associationKeyCiphertext []byte + publicKeyCiphertext []byte + + err error + ) + + if admin { + adminString = "true" + } + + userIdCiphertext, err = key.aesEncrypt([]byte(user.ID.String())) + if err != nil { + return conversationDetailUser, err + } + + usernameCiphertext, err = key.aesEncrypt([]byte(user.Username)) + if err != nil { + return conversationDetailUser, err + } + + adminCiphertext, err = key.aesEncrypt([]byte(adminString)) + if err != nil { + return conversationDetailUser, err + } + + associationKeyCiphertext, err = key.aesEncrypt([]byte(associationKey.String())) + if err != nil { + return conversationDetailUser, err + } + + publicKeyCiphertext, err = key.aesEncrypt([]byte(user.AsymmetricPublicKey)) + if err != nil { + return conversationDetailUser, err + } + + conversationDetailUser = Models.ConversationDetailUser{ + ConversationDetailID: conversationDetail.ID, + UserID: base64.StdEncoding.EncodeToString(userIdCiphertext), + Username: base64.StdEncoding.EncodeToString(usernameCiphertext), + Admin: base64.StdEncoding.EncodeToString(adminCiphertext), + AssociationKey: base64.StdEncoding.EncodeToString(associationKeyCiphertext), + PublicKey: base64.StdEncoding.EncodeToString(publicKeyCiphertext), + } + + err = Database.CreateConversationDetailUser(&conversationDetailUser) + + return conversationDetailUser, err +} + func SeedMessages() { var ( - messageThread Models.ConversationDetail + conversationDetail Models.ConversationDetail key aesKey primaryUser Models.User primaryUserAssociationKey uuid.UUID secondaryUser Models.User secondaryUserAssociationKey uuid.UUID - userJson string - id1, id2 uuid.UUID i int err error ) @@ -188,7 +229,7 @@ func SeedMessages() { if err != nil { panic(err) } - messageThread, err = seedConversationDetail(key) + conversationDetail, err = seedConversationDetail(key) primaryUserAssociationKey, err = uuid.NewV4() if err != nil { @@ -206,7 +247,7 @@ func SeedMessages() { _, err = seedUserConversation( primaryUser, - messageThread.ID, + conversationDetail.ID, key, ) if err != nil { @@ -220,53 +261,34 @@ func SeedMessages() { _, err = seedUserConversation( secondaryUser, - messageThread.ID, + conversationDetail.ID, key, ) - - id1, err = uuid.NewV4() if err != nil { panic(err) } - id2, err = uuid.NewV4() + + _, err = seedConversationDetailUser( + primaryUser, + conversationDetail, + primaryUserAssociationKey, + true, + key, + ) if err != nil { panic(err) } - userJson = fmt.Sprintf( - ` -[ - { - "id": "%s", - "user_id": "%s", - "username": "%s", - "admin": "true", - "association_key": "%s" - }, - { - "id": "%s", - "user_id": "%s", - "username": "%s", - "admin": "false", - "association_key": "%s" - } -] - `, - id1.String(), - primaryUser.ID.String(), - primaryUser.Username, - primaryUserAssociationKey.String(), - id2.String(), - secondaryUser.ID.String(), - secondaryUser.Username, - secondaryUserAssociationKey.String(), - ) - - messageThread, err = seedUpdateUserConversation( - userJson, + _, err = seedConversationDetailUser( + secondaryUser, + conversationDetail, + secondaryUserAssociationKey, + false, key, - messageThread, ) + if err != nil { + panic(err) + } for i = 0; i <= 20; i++ { err = seedMessage( diff --git a/Backend/Models/Conversations.go b/Backend/Models/Conversations.go new file mode 100644 index 0000000..586ff98 --- /dev/null +++ b/Backend/Models/Conversations.go @@ -0,0 +1,33 @@ +package Models + +import ( + "github.com/gofrs/uuid" +) + +type ConversationDetail struct { + Base + Name string `gorm:"not null" json:"name"` // Stored encrypted + Users []ConversationDetailUser ` json:"users"` +} + +type ConversationDetailUser struct { + Base + ConversationDetailID uuid.UUID `gorm:"not null" json:"conversation_detail_id"` + ConversationDetail ConversationDetail `gorm:"not null" json:"conversation"` + UserID string `gorm:"not null" json:"user_id"` // Stored encrypted + Username string `gorm:"not null" json:"username"` // Stored encrypted + Admin string `gorm:"not null" json:"admin"` // Stored encrypted + AssociationKey string `gorm:"not null" json:"association_key"` // Stored encrypted + PublicKey string `gorm:"not null" json:"public_key"` // Stored encrypted +} + +// Used to link the current user to their conversations +type UserConversation struct { + Base + UserID uuid.UUID `gorm:"type:uuid;column:user_id;not null;" json:"user_id"` + User User ` json:"user"` + ConversationDetailID string `gorm:"not null" json:"conversation_detail_id"` // Stored encrypted + Admin string `gorm:"not null" json:"admin"` // Bool if user is admin of thread, stored encrypted + SymmetricKey string `gorm:"not null" json:"symmetric_key"` // Stored encrypted + // TODO: Add association_key here +} diff --git a/Backend/Models/Messages.go b/Backend/Models/Messages.go index de32fca..663d72d 100644 --- a/Backend/Models/Messages.go +++ b/Backend/Models/Messages.go @@ -22,19 +22,3 @@ type Message struct { AssociationKey string `json:"association_key" gorm:"not null"` // TODO: This links all encrypted messages for a user in a thread together. Find a way to fix this CreatedAt time.Time `json:"created_at" gorm:"not null"` } - -type ConversationDetail struct { - Base - Name string `gorm:"not null" json:"name"` // Stored encrypted - Users string ` json:"users"` // Stored as encrypted JSON -} - -type UserConversation struct { - Base - UserID uuid.UUID `gorm:"type:uuid;column:user_id;not null;" json:"user_id"` - User User ` json:"user"` - ConversationDetailID string `gorm:"not null" json:"conversation_detail_id"` // Stored encrypted - Admin string `gorm:"not null" json:"admin"` // Bool if user is admin of thread, stored encrypted - SymmetricKey string `gorm:"not null" json:"symmetric_key"` // Stored encrypted - // TODO: Add association_key here -} diff --git a/Backend/main.go b/Backend/main.go index 00db155..634cac0 100644 --- a/Backend/main.go +++ b/Backend/main.go @@ -2,7 +2,7 @@ package main import ( "flag" - "fmt" + "log" "net/http" "git.tovijaeschke.xyz/tovi/Envelope/Backend/Api" @@ -37,7 +37,7 @@ func main() { Api.InitApiEndpoints(router) - fmt.Println("Listening on port :8080") + log.Println("Listening on port :8080") err = http.ListenAndServe(":8080", router) if err != nil { panic(err) diff --git a/README.md b/README.md index d1cb191..d52d837 100644 --- a/README.md +++ b/README.md @@ -4,14 +4,15 @@ Encrypted messaging app ## TODO -- Fix adding users to conversations -- Fix users recieving messages -- Fix the admin checks on conversation settings page -- Add admin checks to conversation settings page -- Add admin checks on backend -- Add errors to login / signup page -- Add errors when updating conversations -- Refactor the update conversations function -- Finish the friends list page -- Allow adding friends -- Finish the disappearing messages functionality +[x] Fix adding users to conversations +[x] Fix users recieving messages +[x] Fix the admin checks on conversation settings page +[x] Fix sending messages in a conversation that includes users that are not the current users friend +[x] Add admin checks to conversation settings page +[ ] Add admin checks on backend +[ ] Add errors to login / signup page +[ ] Add errors when updating conversations +[ ] Refactor the update conversations function +[ ] Finish the friends list page +[ ] Allow adding friends +[ ] Finish the disappearing messages functionality diff --git a/mobile/lib/models/conversation_users.dart b/mobile/lib/models/conversation_users.dart index fe68f58..73ab7ac 100644 --- a/mobile/lib/models/conversation_users.dart +++ b/mobile/lib/models/conversation_users.dart @@ -1,3 +1,10 @@ +import 'dart:convert'; +import 'dart:typed_data'; + +import 'package:Envelope/utils/encryption/aes_helper.dart'; +import 'package:Envelope/utils/encryption/crypto_utils.dart'; +import 'package:pointycastle/impl.dart'; + import '/models/conversations.dart'; import '/utils/storage/database.dart'; @@ -20,6 +27,7 @@ Future getConversationUser(Conversation conversation, String u conversationId: maps[0]['conversation_id'], username: maps[0]['username'], associationKey: maps[0]['association_key'], + publicKey: CryptoUtils.rsaPublicKeyFromPem(maps[0]['asymmetric_public_key']), admin: maps[0]['admin'] == 1, ); @@ -33,19 +41,60 @@ Future> getConversationUsers(Conversation conversation) a 'conversation_users', where: 'conversation_id = ?', whereArgs: [conversation.id], - orderBy: 'admin', + orderBy: 'username', ); - return List.generate(maps.length, (i) { + List conversationUsers = List.generate(maps.length, (i) { return ConversationUser( id: maps[i]['id'], userId: maps[i]['user_id'], conversationId: maps[i]['conversation_id'], username: maps[i]['username'], associationKey: maps[i]['association_key'], + publicKey: CryptoUtils.rsaPublicKeyFromPem(maps[i]['asymmetric_public_key']), admin: maps[i]['admin'] == 1, ); }); + + int index = 0; + List finalConversationUsers = []; + + for (ConversationUser conversationUser in conversationUsers) { + if (!conversationUser.admin) { + finalConversationUsers.add(conversationUser); + continue; + } + + finalConversationUsers.insert(index, conversationUser); + index++; + } + + return finalConversationUsers; +} + +Future>> getEncryptedConversationUsers(Conversation conversation, Uint8List symKey) async { + final db = await getDatabaseConnection(); + + final List> maps = await db.query( + 'conversation_users', + where: 'conversation_id = ?', + whereArgs: [conversation.id], + orderBy: 'username', + ); + + List> conversationUsers = List.generate(maps.length, (i) { + return { + 'id': maps[i]['id'], + 'conversation_id': maps[i]['conversation_id'], + 'user_id': AesHelper.aesEncrypt(symKey, Uint8List.fromList(maps[i]['user_id'].codeUnits)), + 'username': AesHelper.aesEncrypt(symKey, Uint8List.fromList(maps[i]['username'].codeUnits)), + 'association_key': AesHelper.aesEncrypt(symKey, Uint8List.fromList(maps[i]['association_key'].codeUnits)), + 'public_key': AesHelper.aesEncrypt(symKey, Uint8List.fromList(maps[i]['asymmetric_public_key'].codeUnits)), + 'admin': AesHelper.aesEncrypt(symKey, Uint8List.fromList((maps[i]['admin'] == 1 ? 'true' : 'false').codeUnits)), + }; + }); + + return conversationUsers; } class ConversationUser{ @@ -54,6 +103,7 @@ class ConversationUser{ String conversationId; String username; String associationKey; + RSAPublicKey publicKey; bool admin; ConversationUser({ required this.id, @@ -61,17 +111,47 @@ class ConversationUser{ required this.conversationId, required this.username, required this.associationKey, + required this.publicKey, required this.admin, }); - factory ConversationUser.fromJson(Map json, String conversationId) { + factory ConversationUser.fromJson(Map json, Uint8List symmetricKey) { + + String userId = AesHelper.aesDecrypt( + symmetricKey, + base64.decode(json['user_id']), + ); + + String username = AesHelper.aesDecrypt( + symmetricKey, + base64.decode(json['username']), + ); + + String associationKey = AesHelper.aesDecrypt( + symmetricKey, + base64.decode(json['association_key']), + ); + + String admin = AesHelper.aesDecrypt( + symmetricKey, + base64.decode(json['admin']), + ); + + String publicKeyString = AesHelper.aesDecrypt( + symmetricKey, + base64.decode(json['public_key']), + ); + + RSAPublicKey publicKey = CryptoUtils.rsaPublicKeyFromPem(publicKeyString); + return ConversationUser( id: json['id'], - userId: json['user_id'], - conversationId: conversationId, - username: json['username'], - associationKey: json['association_key'], - admin: json['admin'] == 'true', + conversationId: json['conversation_detail_id'], + userId: userId, + username: username, + associationKey: associationKey, + publicKey: publicKey, + admin: admin == 'true', ); } @@ -81,6 +161,7 @@ class ConversationUser{ 'user_id': userId, 'username': username, 'association_key': associationKey, + 'asymmetric_public_key': publicKeyPem(), 'admin': admin ? 'true' : 'false', }; } @@ -92,7 +173,12 @@ class ConversationUser{ 'conversation_id': conversationId, 'username': username, 'association_key': associationKey, + 'asymmetric_public_key': publicKeyPem(), 'admin': admin ? 1 : 0, }; } + + String publicKeyPem() { + return CryptoUtils.encodeRSAPublicKeyToPem(publicKey); + } } diff --git a/mobile/lib/models/conversations.dart b/mobile/lib/models/conversations.dart index 6e656da..6b10460 100644 --- a/mobile/lib/models/conversations.dart +++ b/mobile/lib/models/conversations.dart @@ -21,14 +21,12 @@ Future createConversation(String title, List friends) asyn var uuid = const Uuid(); final String conversationId = uuid.v4(); - final String conversationDetailId = uuid.v4(); Uint8List symmetricKey = AesHelper.deriveKey(generateRandomString(32)); Conversation conversation = Conversation( id: conversationId, userId: profile.id, - conversationDetailId: conversationDetailId, symmetricKey: base64.encode(symmetricKey), admin: true, name: title, @@ -50,6 +48,7 @@ Future createConversation(String title, List friends) asyn conversationId: conversationId, username: profile.username, associationKey: uuid.v4(), + publicKey: profile.publicKey!, admin: true, ).toMap(), conflictAlgorithm: ConflictAlgorithm.fail, @@ -64,6 +63,7 @@ Future createConversation(String title, List friends) asyn conversationId: conversationId, username: friend.username, associationKey: uuid.v4(), + publicKey: friend.publicKey, admin: false, ).toMap(), conflictAlgorithm: ConflictAlgorithm.replace, @@ -88,6 +88,7 @@ Future addUsersToConversation(Conversation conversation, List addUsersToConversation(Conversation conversation, List conversations, String id) { for (var conversation in conversations) { - if (conversation.conversationDetailId == id) { + if (conversation.id == id) { return conversation; } } @@ -123,7 +124,6 @@ Future getConversationById(String id) async { 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'], @@ -142,7 +142,6 @@ Future> getConversations() async { 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'], @@ -156,7 +155,6 @@ Future> getConversations() async { class Conversation { String id; String userId; - String conversationDetailId; String symmetricKey; bool admin; String name; @@ -166,7 +164,6 @@ class Conversation { Conversation({ required this.id, required this.userId, - required this.conversationDetailId, required this.symmetricKey, required this.admin, required this.name, @@ -181,7 +178,7 @@ class Conversation { privKey, ); - var detailId = AesHelper.aesDecrypt( + var id = AesHelper.aesDecrypt( symmetricKeyDecrypted, base64.decode(json['conversation_detail_id']), ); @@ -192,9 +189,8 @@ class Conversation { ); return Conversation( - id: json['id'], + id: id, userId: json['user_id'], - conversationDetailId: detailId, symmetricKey: base64.encode(symmetricKeyDecrypted), admin: admin == 'true', name: 'Unknown', @@ -208,16 +204,16 @@ class Conversation { var symKey = base64.decode(symmetricKey); - List users = await getConversationUsers(this); - if (!includeUsers) { return { - 'id': conversationDetailId, + 'id': id, 'name': AesHelper.aesEncrypt(symKey, Uint8List.fromList(name.codeUnits)), - 'users': AesHelper.aesEncrypt(symKey, Uint8List.fromList(jsonEncode(users).codeUnits)), + 'users': await getEncryptedConversationUsers(this, symKey), }; } + List users = await getConversationUsers(this); + List userConversations = []; for (ConversationUser user in users) { @@ -227,23 +223,23 @@ class Conversation { if (profile.id != user.userId) { Friend friend = await getFriendByFriendId(user.userId); - pubKey = CryptoUtils.rsaPublicKeyFromPem(friend.asymmetricPublicKey); + pubKey = friend.publicKey; 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)), + 'conversation_detail_id': AesHelper.aesEncrypt(symKey, Uint8List.fromList(id.codeUnits)), + 'admin': AesHelper.aesEncrypt(symKey, Uint8List.fromList((user.admin ? 'true' : 'false').codeUnits)), 'symmetric_key': base64.encode(CryptoUtils.rsaEncrypt(symKey, pubKey)), }); } return { - 'id': conversationDetailId, + 'id': id, 'name': AesHelper.aesEncrypt(symKey, Uint8List.fromList(name.codeUnits)), - 'users': AesHelper.aesEncrypt(symKey, Uint8List.fromList(jsonEncode(users).codeUnits)), + 'users': await getEncryptedConversationUsers(this, symKey), 'user_conversations': userConversations, }; } @@ -252,7 +248,6 @@ class Conversation { return { 'id': id, 'user_id': userId, - 'conversation_detail_id': conversationDetailId, 'symmetric_key': symmetricKey, 'admin': admin ? 1 : 0, 'name': name, diff --git a/mobile/lib/models/friends.dart b/mobile/lib/models/friends.dart index 86d1537..5711e3a 100644 --- a/mobile/lib/models/friends.dart +++ b/mobile/lib/models/friends.dart @@ -21,7 +21,7 @@ class Friend{ String username; String friendId; String friendSymmetricKey; - String asymmetricPublicKey; + RSAPublicKey publicKey; String acceptedAt; bool? selected; Friend({ @@ -30,39 +30,41 @@ class Friend{ required this.username, required this.friendId, required this.friendSymmetricKey, - required this.asymmetricPublicKey, + required this.publicKey, required this.acceptedAt, this.selected, }); factory Friend.fromJson(Map json, RSAPrivateKey privKey) { - Uint8List friendIdDecrypted = CryptoUtils.rsaDecrypt( + Uint8List idDecrypted = CryptoUtils.rsaDecrypt( base64.decode(json['friend_id']), privKey, ); - Uint8List friendUsername = CryptoUtils.rsaDecrypt( + Uint8List username = CryptoUtils.rsaDecrypt( base64.decode(json['friend_username']), privKey, ); - Uint8List friendSymmetricKeyDecrypted = CryptoUtils.rsaDecrypt( + Uint8List symmetricKeyDecrypted = CryptoUtils.rsaDecrypt( base64.decode(json['symmetric_key']), privKey, ); - String asymmetricPublicKey = AesHelper.aesDecrypt( - friendSymmetricKeyDecrypted, + String publicKeyString = AesHelper.aesDecrypt( + symmetricKeyDecrypted, base64.decode(json['asymmetric_public_key']) ); + RSAPublicKey publicKey = CryptoUtils.rsaPublicKeyFromPem(publicKeyString); + return Friend( id: json['id'], userId: json['user_id'], - username: String.fromCharCodes(friendUsername), - friendId: String.fromCharCodes(friendIdDecrypted), - friendSymmetricKey: base64.encode(friendSymmetricKeyDecrypted), - asymmetricPublicKey: asymmetricPublicKey, + username: String.fromCharCodes(username), + friendId: String.fromCharCodes(idDecrypted), + friendSymmetricKey: base64.encode(symmetricKeyDecrypted), + publicKey: publicKey, acceptedAt: json['accepted_at'], ); } @@ -86,10 +88,14 @@ class Friend{ 'username': username, 'friend_id': friendId, 'symmetric_key': friendSymmetricKey, - 'asymmetric_public_key': asymmetricPublicKey, + 'asymmetric_public_key': publicKeyPem(), 'accepted_at': acceptedAt, }; } + + String publicKeyPem() { + return CryptoUtils.encodeRSAPublicKeyToPem(publicKey); + } } @@ -105,7 +111,7 @@ Future> getFriends() async { userId: maps[i]['user_id'], friendId: maps[i]['friend_id'], friendSymmetricKey: maps[i]['symmetric_key'], - asymmetricPublicKey: maps[i]['asymmetric_public_key'], + publicKey: CryptoUtils.rsaPublicKeyFromPem(maps[i]['asymmetric_public_key']), acceptedAt: maps[i]['accepted_at'], username: maps[i]['username'], ); @@ -126,12 +132,12 @@ Future getFriendByFriendId(String userId) async { } return Friend( - id: maps[0]['id'], - userId: maps[0]['user_id'], - friendId: maps[0]['friend_id'], - friendSymmetricKey: maps[0]['symmetric_key'], - asymmetricPublicKey: maps[0]['asymmetric_public_key'], - acceptedAt: maps[0]['accepted_at'], - username: maps[0]['username'], + id: maps[0]['id'], + userId: maps[0]['user_id'], + friendId: maps[0]['friend_id'], + friendSymmetricKey: maps[0]['symmetric_key'], + publicKey: CryptoUtils.rsaPublicKeyFromPem(maps[0]['asymmetric_public_key']), + acceptedAt: maps[0]['accepted_at'], + username: maps[0]['username'], ); } diff --git a/mobile/lib/models/messages.dart b/mobile/lib/models/messages.dart index f19f78c..e88594f 100644 --- a/mobile/lib/models/messages.dart +++ b/mobile/lib/models/messages.dart @@ -2,6 +2,7 @@ import 'dart:convert'; import 'dart:typed_data'; import 'package:pointycastle/export.dart'; +import 'package:uuid/uuid.dart'; import '/models/conversation_users.dart'; import '/models/conversations.dart'; @@ -101,20 +102,20 @@ class Message { ); } - Future toJson(Conversation conversation, String messageDataId) async { + Future payloadJson(Conversation conversation, String messageId) async { MyProfile profile = await MyProfile.getProfile(); if (profile.publicKey == null) { throw Exception('Could not get profile.publicKey'); } + RSAPublicKey publicKey = profile.publicKey!; + final String messageDataId = (const Uuid()).v4(); + final userSymmetricKey = AesHelper.deriveKey(generateRandomString(32)); final symmetricKey = AesHelper.deriveKey(generateRandomString(32)); List> messages = []; - - String id = ''; - List conversationUsers = await getConversationUsers(conversation); for (var i = 0; i < conversationUsers.length; i++) { @@ -124,6 +125,7 @@ class Message { id = user.id; messages.add({ + 'id': messageId, 'message_data_id': messageDataId, 'symmetric_key': base64.encode(CryptoUtils.rsaEncrypt( userSymmetricKey, @@ -135,8 +137,8 @@ class Message { continue; } - Friend friend = await getFriendByFriendId(user.userId); - RSAPublicKey friendPublicKey = CryptoUtils.rsaPublicKeyFromPem(friend.asymmetricPublicKey); + ConversationUser conversationUser = await getConversationUser(conversation, user.userId); + RSAPublicKey friendPublicKey = conversationUser.publicKey; messages.add({ 'message_data_id': messageDataId, diff --git a/mobile/lib/utils/storage/conversations.dart b/mobile/lib/utils/storage/conversations.dart index a7fe2c0..b5aa0b9 100644 --- a/mobile/lib/utils/storage/conversations.dart +++ b/mobile/lib/utils/storage/conversations.dart @@ -41,7 +41,7 @@ Future updateConversations() async { privKey, ); conversations.add(conversation); - conversationsDetailIds.add(conversation.conversationDetailId); + conversationsDetailIds.add(conversation.id); } Map params = {}; @@ -78,17 +78,12 @@ Future updateConversations() async { conflictAlgorithm: ConflictAlgorithm.replace, ); - List usersData = json.decode( - AesHelper.aesDecrypt( - base64.decode(conversation.symmetricKey), - base64.decode(conversationDetailJson['users']), - ) - ); + List usersData = conversationDetailJson['users']; for (var i = 0; i < usersData.length; i++) { ConversationUser conversationUser = ConversationUser.fromJson( usersData[i] as Map, - conversation.id, + base64.decode(conversation.symmetricKey), ); await db.insert( diff --git a/mobile/lib/utils/storage/database.dart b/mobile/lib/utils/storage/database.dart index 4466f6d..9814bf8 100644 --- a/mobile/lib/utils/storage/database.dart +++ b/mobile/lib/utils/storage/database.dart @@ -35,7 +35,6 @@ Future getDatabaseConnection() async { CREATE TABLE IF NOT EXISTS conversations( id TEXT PRIMARY KEY, user_id TEXT, - conversation_detail_id TEXT, symmetric_key TEXT, admin INTEGER, name TEXT, @@ -51,9 +50,9 @@ Future getDatabaseConnection() async { user_id TEXT, conversation_id TEXT, username TEXT, - data TEXT, association_key TEXT, - admin INTEGER + admin INTEGER, + asymmetric_public_key TEXT ); '''); diff --git a/mobile/lib/utils/storage/messages.dart b/mobile/lib/utils/storage/messages.dart index d5978d8..b551672 100644 --- a/mobile/lib/utils/storage/messages.dart +++ b/mobile/lib/utils/storage/messages.dart @@ -16,12 +16,12 @@ Future sendMessage(Conversation conversation, String data) async { MyProfile profile = await MyProfile.getProfile(); var uuid = const Uuid(); - final String messageDataId = uuid.v4(); + final String messageId = uuid.v4(); ConversationUser currentUser = await getConversationUser(conversation, profile.id); Message message = Message( - id: messageDataId, + id: messageId, symmetricKey: '', userSymmetricKey: '', senderId: currentUser.userId, @@ -42,7 +42,7 @@ Future sendMessage(Conversation conversation, String data) async { String sessionCookie = await getSessionCookie(); - message.toJson(conversation, messageDataId) + message.payloadJson(conversation, messageId) .then((messageJson) { return http.post( Uri.parse('${dotenv.env["SERVER_URL"]}api/v1/auth/message'), diff --git a/mobile/lib/views/authentication/login.dart b/mobile/lib/views/authentication/login.dart index c1c7fb7..b608703 100644 --- a/mobile/lib/views/authentication/login.dart +++ b/mobile/lib/views/authentication/login.dart @@ -8,28 +8,28 @@ import '/utils/storage/session_cookie.dart'; class LoginResponse { final String status; final String message; - final String asymmetricPublicKey; - final String asymmetricPrivateKey; + final String publicKey; + final String privateKey; final String userId; final String username; const LoginResponse({ required this.status, required this.message, - required this.asymmetricPublicKey, - required this.asymmetricPrivateKey, + required this.publicKey, + required this.privateKey, required this.userId, required this.username, }); factory LoginResponse.fromJson(Map json) { return LoginResponse( - status: json['status'], - message: json['message'], - asymmetricPublicKey: json['asymmetric_public_key'], - asymmetricPrivateKey: json['asymmetric_private_key'], - userId: json['user_id'], - username: json['username'], + status: json['status'], + message: json['message'], + publicKey: json['asymmetric_public_key'], + privateKey: json['asymmetric_private_key'], + userId: json['user_id'], + username: json['username'], ); } } diff --git a/mobile/lib/views/authentication/signup.dart b/mobile/lib/views/authentication/signup.dart index 0847bf7..50ef4f0 100644 --- a/mobile/lib/views/authentication/signup.dart +++ b/mobile/lib/views/authentication/signup.dart @@ -28,7 +28,7 @@ Future signUp(context, String username, String password, String var rsaPubPem = CryptoUtils.encodeRSAPublicKeyToPem(keyPair.publicKey); var rsaPrivPem = CryptoUtils.encodeRSAPrivateKeyToPem(keyPair.privateKey); - var encRsaPriv = AesHelper.aesEncrypt(password, Uint8List.fromList(rsaPrivPem.codeUnits)); + String encRsaPriv = AesHelper.aesEncrypt(password, Uint8List.fromList(rsaPrivPem.codeUnits)); // TODO: Check for timeout here final resp = await http.post( diff --git a/mobile/lib/views/main/conversation/create_add_users.dart b/mobile/lib/views/main/conversation/create_add_users.dart index 1b9186f..e1ddf59 100644 --- a/mobile/lib/views/main/conversation/create_add_users.dart +++ b/mobile/lib/views/main/conversation/create_add_users.dart @@ -1,10 +1,7 @@ import 'package:flutter/material.dart'; -import '/models/conversations.dart'; import '/models/friends.dart'; -import '/utils/storage/conversations.dart'; import '/views/main/conversation/create_add_users_list.dart'; -import '/views/main/conversation/detail.dart'; class ConversationAddFriendsList extends StatefulWidget { final List friends; @@ -26,81 +23,81 @@ class _ConversationAddFriendsListState extends State @override Widget build(BuildContext context) { return Scaffold( - appBar: AppBar( - elevation: 0, - automaticallyImplyLeading: false, - flexibleSpace: SafeArea( - child: Container( - padding: const EdgeInsets.only(right: 16), - child: Row( - children: [ - IconButton( - onPressed: (){ - Navigator.pop(context); - }, - icon: const Icon(Icons.arrow_back), - ), - const SizedBox(width: 2,), - Expanded( - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - mainAxisAlignment: MainAxisAlignment.center, - children: [ - Text( - friendsSelected.isEmpty ? - 'Select Friends' : - '${friendsSelected.length} Friends Selected', - style: const TextStyle( - fontSize: 16, - fontWeight: FontWeight.w600 - ), - ), - ], - ), - ), - ], - ), + appBar: AppBar( + elevation: 0, + automaticallyImplyLeading: false, + flexibleSpace: SafeArea( + child: Container( + padding: const EdgeInsets.only(right: 16), + child: Row( + children: [ + IconButton( + onPressed: (){ + Navigator.pop(context); + }, + icon: const Icon(Icons.arrow_back), ), - ), - ), - body: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Padding( - padding: const EdgeInsets.only(top: 20,left: 16,right: 16), - child: TextField( - decoration: const InputDecoration( - hintText: "Search...", - prefixIcon: Icon( - Icons.search, - size: 20 - ), + const SizedBox(width: 2,), + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Text( + friendsSelected.isEmpty ? + 'Select Friends' : + '${friendsSelected.length} Friends Selected', + style: const TextStyle( + fontSize: 16, + fontWeight: FontWeight.w600 ), - onChanged: (value) => filterSearchResults(value.toLowerCase()) - ), - ), - Padding( - padding: const EdgeInsets.only(top: 0,left: 16,right: 16), - child: list(), + ), + ], + ), ), - ], + ], + ), + ), ), - floatingActionButton: Padding( - padding: const EdgeInsets.only(right: 10, bottom: 10), - child: FloatingActionButton( - onPressed: () { - widget.saveCallback(friendsSelected); - - setState(() { - friendsSelected = []; - }); - }, - backgroundColor: Theme.of(context).colorScheme.primary, - child: friendsSelected.isEmpty ? - const Text('Skip') : - const Icon(Icons.add, size: 30), + ), + body: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Padding( + padding: const EdgeInsets.only(top: 20,left: 16,right: 16), + child: TextField( + decoration: const InputDecoration( + hintText: "Search...", + prefixIcon: Icon( + Icons.search, + size: 20 + ), + ), + onChanged: (value) => filterSearchResults(value.toLowerCase()) ), + ), + Padding( + padding: const EdgeInsets.only(top: 0,left: 16,right: 16), + child: list(), + ), + ], + ), + floatingActionButton: Padding( + padding: const EdgeInsets.only(right: 10, bottom: 10), + child: FloatingActionButton( + onPressed: () { + widget.saveCallback(friendsSelected); + + setState(() { + friendsSelected = []; + }); + }, + backgroundColor: Theme.of(context).colorScheme.primary, + child: friendsSelected.isEmpty ? + const Text('Skip') : + const Icon(Icons.add, size: 30), ), + ), ); } @@ -111,10 +108,10 @@ class _ConversationAddFriendsListState extends State if(query.isNotEmpty) { List dummyListData = []; for (Friend friend in dummySearchList) { - if (friend.username.toLowerCase().contains(query)) { - dummyListData.add(friend); - } + if (friend.username.toLowerCase().contains(query)) { + dummyListData.add(friend); } + } setState(() { friends.clear(); friends.addAll(dummyListData); @@ -138,30 +135,30 @@ class _ConversationAddFriendsListState extends State Widget list() { if (friends.isEmpty) { return const Center( - child: Text('No Friends'), + child: Text('No Friends'), ); } return ListView.builder( - itemCount: friends.length, - shrinkWrap: true, - padding: const EdgeInsets.only(top: 16), - physics: const BouncingScrollPhysics(), - itemBuilder: (context, i) { - return ConversationAddFriendItem( - friend: friends[i], - isSelected: (bool value) { - setState(() { - widget.friends[i].selected = value; - if (value) { - friendsSelected.add(friends[i]); - return; - } - friendsSelected.remove(friends[i]); - }); + itemCount: friends.length, + shrinkWrap: true, + padding: const EdgeInsets.only(top: 16), + physics: const BouncingScrollPhysics(), + itemBuilder: (context, i) { + return ConversationAddFriendItem( + friend: friends[i], + isSelected: (bool value) { + setState(() { + widget.friends[i].selected = value; + if (value) { + friendsSelected.add(friends[i]); + return; } - ); - }, + friendsSelected.remove(friends[i]); + }); + } + ); + }, ); } } diff --git a/mobile/lib/views/main/conversation/detail.dart b/mobile/lib/views/main/conversation/detail.dart index cdee4ac..922bb66 100644 --- a/mobile/lib/views/main/conversation/detail.dart +++ b/mobile/lib/views/main/conversation/detail.dart @@ -28,212 +28,136 @@ class _ConversationDetailState extends State { @override Widget build(BuildContext context) { return Scaffold( - appBar: AppBar( - elevation: 0, - automaticallyImplyLeading: false, - flexibleSpace: SafeArea( - child: Container( - padding: const EdgeInsets.only(right: 16), - child: Row( - children: [ - IconButton( - onPressed: (){ - Navigator.pop(context); - }, - icon: Icon( - Icons.arrow_back, - color: Theme.of(context).appBarTheme.iconTheme?.color, - ), - ), - const SizedBox(width: 2,), - Expanded( - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - mainAxisAlignment: MainAxisAlignment.center, - children: [ - Text( - widget.conversation.name, - style: TextStyle( - fontSize: 16, - fontWeight: FontWeight.w600, - color: Theme.of(context).appBarTheme.toolbarTextStyle?.color - ), - ), - ], - ), - ), - IconButton( - onPressed: (){ - Navigator.of(context).push( - MaterialPageRoute(builder: (context) => ConversationSettings(conversation: widget.conversation)), - ); - }, - icon: Icon( - Icons.settings, - color: Theme.of(context).appBarTheme.iconTheme?.color, - ), - ), - ], - ), - ), + appBar: AppBar( + elevation: 0, + automaticallyImplyLeading: false, + flexibleSpace: SafeArea( + child: Container( + padding: const EdgeInsets.only(right: 16), + child: Row( + children: [ + IconButton( + onPressed: (){ + Navigator.pop(context); + }, + icon: Icon( + Icons.arrow_back, + color: Theme.of(context).appBarTheme.iconTheme?.color, ), ), - body: Stack( + const SizedBox(width: 2,), + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + mainAxisAlignment: MainAxisAlignment.center, children: [ - ListView.builder( - itemCount: messages.length, - shrinkWrap: true, - padding: const EdgeInsets.only(top: 10,bottom: 90), - reverse: true, - itemBuilder: (context, index) { - return Container( - padding: const EdgeInsets.only(left: 14,right: 14,top: 0,bottom: 0), - child: Align( - alignment: ( - messages[index].senderUsername == profile.username ? - Alignment.topRight : - Alignment.topLeft - ), - child: Column( - crossAxisAlignment: messages[index].senderUsername == profile.username ? - CrossAxisAlignment.end : - CrossAxisAlignment.start, - children: [ - Container( - decoration: BoxDecoration( - borderRadius: BorderRadius.circular(20), - color: ( - messages[index].senderUsername == profile.username ? - Theme.of(context).colorScheme.primary : - Theme.of(context).colorScheme.tertiary - ), - ), - padding: const EdgeInsets.all(12), - child: Text( - messages[index].data, - style: TextStyle( - fontSize: 15, - color: messages[index].senderUsername == profile.username ? - Theme.of(context).colorScheme.onPrimary : - Theme.of(context).colorScheme.onTertiary, - ) - ), - ), - const SizedBox(height: 1.5), - Row( - mainAxisAlignment: messages[index].senderUsername == profile.username ? - MainAxisAlignment.end : - MainAxisAlignment.start, - children: [ - const SizedBox(width: 10), - usernameOrFailedToSend(index), - ], - ), - const SizedBox(height: 1.5), - Row( - mainAxisAlignment: messages[index].senderUsername == profile.username ? - MainAxisAlignment.end : - MainAxisAlignment.start, - children: [ - const SizedBox(width: 10), - Text( - convertToAgo(messages[index].createdAt), - textAlign: messages[index].senderUsername == profile.username ? - TextAlign.left : - TextAlign.right, - style: TextStyle( - fontSize: 12, - color: Colors.grey[500], - ), - ), - ], - ), - index != 0 ? - const SizedBox(height: 20) : - const SizedBox.shrink(), - ], - ) - ), - ); + Text( + widget.conversation.name, + style: TextStyle( + fontSize: 16, + fontWeight: FontWeight.w600, + color: Theme.of(context).appBarTheme.toolbarTextStyle?.color + ), + ), + ], + ), + ), + IconButton( + onPressed: (){ + Navigator.of(context).push( + MaterialPageRoute(builder: (context) => ConversationSettings(conversation: widget.conversation)), + ); + }, + icon: Icon( + Icons.settings, + color: Theme.of(context).appBarTheme.iconTheme?.color, + ), + ), + ], + ), + ), + ), + ), + body: Stack( + children: [ + messagesView(), + Align( + alignment: Alignment.bottomLeft, + child: ConstrainedBox( + constraints: const BoxConstraints( + maxHeight: 200.0, + ), + child: Container( + padding: const EdgeInsets.only(left: 10,bottom: 10,top: 10), + // height: 60, + width: double.infinity, + color: Theme.of(context).backgroundColor, + child: Row( + children: [ + GestureDetector( + onTap: (){ + }, + child: Container( + height: 30, + width: 30, + decoration: BoxDecoration( + color: Theme.of(context).primaryColor, + borderRadius: BorderRadius.circular(30), + ), + child: Icon( + Icons.add, + color: Theme.of(context).colorScheme.onPrimary, + size: 20 + ), + ), + ), + const SizedBox(width: 15,), + Expanded( + child: TextField( + decoration: InputDecoration( + hintText: "Write message...", + hintStyle: TextStyle( + color: Theme.of(context).hintColor, + ), + border: InputBorder.none, + ), + maxLines: null, + controller: msgController, + ), + ), + const SizedBox(width: 15), + Container( + width: 45, + height: 45, + child: FittedBox( + child: FloatingActionButton( + onPressed: () async { + if (msgController.text == '') { + return; + } + await sendMessage(widget.conversation, msgController.text); + messages = await getMessagesForThread(widget.conversation); + setState(() {}); + msgController.text = ''; }, + child: Icon( + Icons.send, + color: Theme.of(context).colorScheme.onPrimary, + size: 22 ), - Align( - alignment: Alignment.bottomLeft, - child: ConstrainedBox( - constraints: const BoxConstraints( - maxHeight: 200.0, - ), - child: Container( - padding: const EdgeInsets.only(left: 10,bottom: 10,top: 10), - // height: 60, - width: double.infinity, - color: Theme.of(context).backgroundColor, - child: Row( - children: [ - GestureDetector( - onTap: (){ - }, - child: Container( - height: 30, - width: 30, - decoration: BoxDecoration( - color: Theme.of(context).primaryColor, - borderRadius: BorderRadius.circular(30), - ), - child: Icon( - Icons.add, - color: Theme.of(context).colorScheme.onPrimary, - size: 20 - ), - ), - ), - const SizedBox(width: 15,), - Expanded( - child: TextField( - decoration: InputDecoration( - hintText: "Write message...", - hintStyle: TextStyle( - color: Theme.of(context).hintColor, - ), - border: InputBorder.none, - ), - maxLines: null, - controller: msgController, - ), - ), - const SizedBox(width: 15), - Container( - width: 45, - height: 45, - child: FittedBox( - child: FloatingActionButton( - onPressed: () async { - if (msgController.text == '') { - return; - } - await sendMessage(widget.conversation, msgController.text); - messages = await getMessagesForThread(widget.conversation); - setState(() {}); - msgController.text = ''; - }, - child: Icon( - Icons.send, - color: Theme.of(context).colorScheme.onPrimary, - size: 22 - ), - backgroundColor: Theme.of(context).primaryColor, - ), - ), - ), - const SizedBox(width: 10), - ], - ), - ), - ), + backgroundColor: Theme.of(context).primaryColor, + ), ), + ), + const SizedBox(width: 10), ], + ), + ), ), - ); + ), + ], + ), + ); } Future fetchMessages() async { @@ -251,32 +175,118 @@ class _ConversationDetailState extends State { Widget usernameOrFailedToSend(int index) { if (messages[index].senderUsername != profile.username) { return Text( - messages[index].senderUsername, - style: TextStyle( - fontSize: 12, - color: Colors.grey[300], - ), - ); + messages[index].senderUsername, + style: TextStyle( + fontSize: 12, + color: Colors.grey[300], + ), + ); } if (messages[index].failedToSend) { return Row( - mainAxisAlignment: MainAxisAlignment.end, - children: const [ - Icon( - Icons.warning_rounded, - color: Colors.red, - size: 20, - ), - Text( - 'Failed to send', - style: TextStyle(color: Colors.red, fontSize: 12), - textAlign: TextAlign.right, - ), - ], + mainAxisAlignment: MainAxisAlignment.end, + children: const [ + Icon( + Icons.warning_rounded, + color: Colors.red, + size: 20, + ), + Text( + 'Failed to send', + style: TextStyle(color: Colors.red, fontSize: 12), + textAlign: TextAlign.right, + ), + ], ); } return const SizedBox.shrink(); } + + Widget messagesView() { + if (messages.isEmpty) { + return const Center( + child: Text('No Messages'), + ); + } + + return ListView.builder( + itemCount: messages.length, + shrinkWrap: true, + padding: const EdgeInsets.only(top: 10,bottom: 90), + reverse: true, + itemBuilder: (context, index) { + return Container( + padding: const EdgeInsets.only(left: 14,right: 14,top: 0,bottom: 0), + child: Align( + alignment: ( + messages[index].senderUsername == profile.username ? + Alignment.topRight : + Alignment.topLeft + ), + child: Column( + crossAxisAlignment: messages[index].senderUsername == profile.username ? + CrossAxisAlignment.end : + CrossAxisAlignment.start, + children: [ + Container( + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(20), + color: ( + messages[index].senderUsername == profile.username ? + Theme.of(context).colorScheme.primary : + Theme.of(context).colorScheme.tertiary + ), + ), + padding: const EdgeInsets.all(12), + child: Text( + messages[index].data, + style: TextStyle( + fontSize: 15, + color: messages[index].senderUsername == profile.username ? + Theme.of(context).colorScheme.onPrimary : + Theme.of(context).colorScheme.onTertiary, + ) + ), + ), + const SizedBox(height: 1.5), + Row( + mainAxisAlignment: messages[index].senderUsername == profile.username ? + MainAxisAlignment.end : + MainAxisAlignment.start, + children: [ + const SizedBox(width: 10), + usernameOrFailedToSend(index), + ], + ), + const SizedBox(height: 1.5), + Row( + mainAxisAlignment: messages[index].senderUsername == profile.username ? + MainAxisAlignment.end : + MainAxisAlignment.start, + children: [ + const SizedBox(width: 10), + Text( + convertToAgo(messages[index].createdAt), + textAlign: messages[index].senderUsername == profile.username ? + TextAlign.left : + TextAlign.right, + style: TextStyle( + fontSize: 12, + color: Colors.grey[500], + ), + ), + ], + ), + index != 0 ? + const SizedBox(height: 20) : + const SizedBox.shrink(), + ], + ) + ), + ); + }, + ); + } } diff --git a/mobile/lib/views/main/conversation/settings.dart b/mobile/lib/views/main/conversation/settings.dart index cc6991b..cbc06bf 100644 --- a/mobile/lib/views/main/conversation/settings.dart +++ b/mobile/lib/views/main/conversation/settings.dart @@ -1,4 +1,5 @@ import 'package:Envelope/models/friends.dart'; +import 'package:Envelope/utils/encryption/crypto_utils.dart'; import 'package:Envelope/views/main/conversation/create_add_users.dart'; import 'package:flutter/material.dart'; @@ -31,111 +32,113 @@ class _ConversationSettingsState extends State { @override Widget build(BuildContext context) { return Scaffold( - appBar: AppBar( - elevation: 0, - automaticallyImplyLeading: false, - flexibleSpace: SafeArea( - child: Container( - padding: const EdgeInsets.only(right: 16), - child: Row( - children: [ - IconButton( - onPressed: (){ - Navigator.pop(context); - }, - icon: const Icon(Icons.arrow_back), - ), - const SizedBox(width: 2,), - Expanded( - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - mainAxisAlignment: MainAxisAlignment.center, - children: [ - Text( - widget.conversation.name + " Settings", - style: const TextStyle( - fontSize: 16, - fontWeight: FontWeight.w600 - ), - ), - ], - ), - ), - ], - ), + appBar: AppBar( + elevation: 0, + automaticallyImplyLeading: false, + flexibleSpace: SafeArea( + child: Container( + padding: const EdgeInsets.only(right: 16), + child: Row( + children: [ + IconButton( + onPressed: (){ + Navigator.pop(context); + }, + icon: const Icon(Icons.arrow_back), ), + const SizedBox(width: 2,), + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Text( + widget.conversation.name + " Settings", + style: const TextStyle( + fontSize: 16, + fontWeight: FontWeight.w600 + ), + ), + ], + ), + ), + ], + ), ), ), - body: Padding( - padding: const EdgeInsets.all(15), - child: Column( - children: [ - const SizedBox(height: 30), - conversationName(), - const SizedBox(height: 25), - widget.conversation.admin ? - sectionTitle('Settings') : - const SizedBox.shrink(), - widget.conversation.admin ? - settings() : - const SizedBox.shrink(), - widget.conversation.admin ? - const SizedBox(height: 25) : - const SizedBox.shrink(), - sectionTitle('Members', showUsersAdd: true), - usersList(), - const SizedBox(height: 25), - myAccess(), - ], + ), + body: Padding( + padding: const EdgeInsets.all(15), + child: SingleChildScrollView( + child: Column( + children: [ + const SizedBox(height: 30), + conversationName(), + const SizedBox(height: 25), + widget.conversation.admin ? + sectionTitle('Settings') : + const SizedBox.shrink(), + widget.conversation.admin ? + settings() : + const SizedBox.shrink(), + widget.conversation.admin ? + const SizedBox(height: 25) : + const SizedBox.shrink(), + sectionTitle('Members', showUsersAdd: true), + usersList(), + const SizedBox(height: 25), + myAccess(), + ], ), ), + ), ); } Widget conversationName() { return Row( - children: [ - const CustomCircleAvatar( - icon: Icon(Icons.people, size: 40), - imagePath: null, // TODO: Add image here - radius: 30, - ), - const SizedBox(width: 10), - Text( - widget.conversation.name, - style: const TextStyle( - fontSize: 25, - fontWeight: FontWeight.w500, - ), + children: [ + const CustomCircleAvatar( + icon: Icon(Icons.people, size: 40), + imagePath: null, // TODO: Add image here + radius: 30, + ), + const SizedBox(width: 10), + Text( + widget.conversation.name, + style: const TextStyle( + fontSize: 25, + fontWeight: FontWeight.w500, ), - widget.conversation.admin ? IconButton( - iconSize: 20, - icon: const Icon(Icons.edit), - padding: const EdgeInsets.all(5.0), - splashRadius: 25, - onPressed: () { - Navigator.of(context).push( - MaterialPageRoute(builder: (context) => ConversationEditDetails( - saveCallback: (String conversationName) async { - widget.conversation.name = conversationName; + ), + widget.conversation.admin ? IconButton( + iconSize: 20, + icon: const Icon(Icons.edit), + padding: const EdgeInsets.all(5.0), + splashRadius: 25, + onPressed: () { + Navigator.of(context).push( + MaterialPageRoute(builder: (context) => ConversationEditDetails( + saveCallback: (String conversationName) async { + widget.conversation.name = conversationName; - final db = await getDatabaseConnection(); - db.update( - 'conversations', - widget.conversation.toMap(), - where: 'id = ?', - whereArgs: [widget.conversation.id], - ); + final db = await getDatabaseConnection(); + db.update( + 'conversations', + widget.conversation.toMap(), + where: 'id = ?', + whereArgs: [widget.conversation.id], + ); - await updateConversation(widget.conversation, includeUsers: true); - setState(() {}); - Navigator.pop(context); - }, - conversation: widget.conversation, - )), - ).then(onGoBack); - }, - ) : const SizedBox.shrink(), + await updateConversation(widget.conversation, includeUsers: true); + setState(() {}); + Navigator.pop(context); + }, + conversation: widget.conversation, + )), + ).then(onGoBack); + }, + ) : const SizedBox.shrink(), ], ); } @@ -155,24 +158,24 @@ class _ConversationSettingsState extends State { Widget myAccess() { return Align( - alignment: Alignment.centerLeft, - child: Column( - crossAxisAlignment: CrossAxisAlignment.stretch, - children: [ - TextButton.icon( - label: const Text( - 'Leave Conversation', - style: TextStyle(fontSize: 16) - ), - icon: const Icon(Icons.exit_to_app), - style: const ButtonStyle( - alignment: Alignment.centerLeft, - ), - onPressed: () { - print('Leave Group'); - } + alignment: Alignment.centerLeft, + child: Column( + crossAxisAlignment: CrossAxisAlignment.stretch, + children: [ + TextButton.icon( + label: const Text( + 'Leave Conversation', + style: TextStyle(fontSize: 16) +), +icon: const Icon(Icons.exit_to_app), +style: const ButtonStyle( + alignment: Alignment.centerLeft, ), - ], + onPressed: () { + print('Leave Group'); + } + ), + ], ), ); } @@ -194,29 +197,29 @@ class _ConversationSettingsState extends State { ), ), !showUsersAdd ? - const SizedBox.shrink() : - IconButton( - icon: const Icon(Icons.add), - padding: const EdgeInsets.all(0), - onPressed: () async { - List friends = await unselectedFriends(); - Navigator.of(context).push( - MaterialPageRoute(builder: (context) => ConversationAddFriendsList( - friends: friends, - saveCallback: (List selectedFriends) async { - addUsersToConversation( - widget.conversation, - selectedFriends, - ); + const SizedBox.shrink() : + IconButton( + icon: const Icon(Icons.add), + padding: const EdgeInsets.all(0), + onPressed: () async { + List friends = await unselectedFriends(); + Navigator.of(context).push( + MaterialPageRoute(builder: (context) => ConversationAddFriendsList( + friends: friends, + saveCallback: (List selectedFriends) async { + addUsersToConversation( + widget.conversation, + selectedFriends, + ); - await updateConversation(widget.conversation, includeUsers: true); - await getUsers(); - Navigator.pop(context); - }, - )) - ); - }, - ), + await updateConversation(widget.conversation, includeUsers: true); + await getUsers(); + Navigator.pop(context); + }, + )) + ); + }, + ), ], ) ) @@ -225,64 +228,65 @@ class _ConversationSettingsState extends State { Widget settings() { return Align( - alignment: Alignment.centerLeft, - child: Column( - crossAxisAlignment: CrossAxisAlignment.stretch, - children: [ - const SizedBox(height: 5), - TextButton.icon( - label: const Text( - 'Disappearing Messages', - style: TextStyle(fontSize: 16) - ), - icon: const Icon(Icons.timer), - style: ButtonStyle( - alignment: Alignment.centerLeft, - foregroundColor: MaterialStateProperty.resolveWith( - (Set states) { - return Theme.of(context).colorScheme.onBackground; - }, - ) - ), - onPressed: () { - print('Disappearing Messages'); - } + alignment: Alignment.centerLeft, + child: Column( + crossAxisAlignment: CrossAxisAlignment.stretch, + children: [ + const SizedBox(height: 5), + TextButton.icon( + label: const Text( + 'Disappearing Messages', + style: TextStyle(fontSize: 16) ), - TextButton.icon( - label: const Text( - 'Permissions', - style: TextStyle(fontSize: 16) - ), - icon: const Icon(Icons.lock), - style: ButtonStyle( - alignment: Alignment.centerLeft, - foregroundColor: MaterialStateProperty.resolveWith( - (Set states) { - return Theme.of(context).colorScheme.onBackground; - }, - ) - ), - onPressed: () { - print('Permissions'); - } + icon: const Icon(Icons.timer), + style: ButtonStyle( + alignment: Alignment.centerLeft, + foregroundColor: MaterialStateProperty.resolveWith( + (Set states) { + return Theme.of(context).colorScheme.onBackground; + }, + ) ), - ], + onPressed: () { + print('Disappearing Messages'); + } + ), + TextButton.icon( + label: const Text( + 'Permissions', + style: TextStyle(fontSize: 16) + ), + icon: const Icon(Icons.lock), + style: ButtonStyle( + alignment: Alignment.centerLeft, +foregroundColor: MaterialStateProperty.resolveWith( +(Set states) { +return Theme.of(context).colorScheme.onBackground; +}, + ) + ), + onPressed: () { + print('Permissions'); + } + ), + ], ), ); } Widget usersList() { return ListView.builder( - itemCount: users.length, - shrinkWrap: true, - padding: const EdgeInsets.only(top: 5, bottom: 0), - itemBuilder: (context, i) { - return ConversationSettingsUserListItem( - user: users[i], - isAdmin: widget.conversation.admin, - profile: profile!, // TODO: Fix this - ); - } + itemCount: users.length, + shrinkWrap: true, + padding: const EdgeInsets.only(top: 5, bottom: 0), + physics: const NeverScrollableScrollPhysics(), + itemBuilder: (context, i) { + return ConversationSettingsUserListItem( + user: users[i], + isAdmin: widget.conversation.admin, + profile: profile!, // TODO: Fix this + ); + } ); } @@ -291,8 +295,8 @@ class _ConversationSettingsState extends State { List notInArgs = []; for (var user in users) { - notInArgs.add(user.userId); - } + notInArgs.add(user.userId); + } final List> maps = await db.query( 'friends', @@ -307,7 +311,7 @@ class _ConversationSettingsState extends State { userId: maps[i]['user_id'], friendId: maps[i]['friend_id'], friendSymmetricKey: maps[i]['symmetric_key'], - asymmetricPublicKey: maps[i]['asymmetric_public_key'], + publicKey: CryptoUtils.rsaPublicKeyFromPem(maps[i]['asymmetric_public_key']), acceptedAt: maps[i]['accepted_at'], username: maps[i]['username'], ); diff --git a/mobile/lib/views/main/friend/list.dart b/mobile/lib/views/main/friend/list.dart index 40b8fd3..4e79fb1 100644 --- a/mobile/lib/views/main/friend/list.dart +++ b/mobile/lib/views/main/friend/list.dart @@ -21,65 +21,65 @@ class _FriendListState extends State { @override Widget build(BuildContext context) { return Scaffold( - body: SingleChildScrollView( - physics: const BouncingScrollPhysics(), - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - SafeArea( - child: Padding( - padding: const EdgeInsets.only(left: 16,right: 16,top: 10), - child: Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - const Text("Friends",style: TextStyle(fontSize: 32,fontWeight: FontWeight.bold),), - Container( - padding: const EdgeInsets.only(left: 8,right: 8,top: 2,bottom: 2), - height: 30, - decoration: BoxDecoration( - borderRadius: BorderRadius.circular(20), - color: Theme.of(context).colorScheme.tertiary - ), - child: Row( - children: [ - Icon( - Icons.add, - color: Theme.of(context).primaryColor, - size: 20 - ), - const SizedBox(width: 2,), - const Text( - "Add", - style: TextStyle( - fontSize: 14, - fontWeight: FontWeight.bold - ) - ), - ], - ), - ) - ], + body: SingleChildScrollView( + physics: const BouncingScrollPhysics(), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + SafeArea( + child: Padding( + padding: const EdgeInsets.only(left: 16,right: 16,top: 10), + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + const Text("Friends",style: TextStyle(fontSize: 32,fontWeight: FontWeight.bold),), + Container( + padding: const EdgeInsets.only(left: 8,right: 8,top: 2,bottom: 2), + height: 30, + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(20), + color: Theme.of(context).colorScheme.tertiary + ), + child: Row( + children: [ + Icon( + Icons.add, + color: Theme.of(context).primaryColor, + size: 20 ), - ), - ), - Padding( - padding: const EdgeInsets.only(top: 16,left: 16,right: 16), - child: TextField( - decoration: const InputDecoration( - hintText: "Search...", - prefixIcon: Icon( - Icons.search, - size: 20 - ), - ), - onChanged: (value) => filterSearchResults(value.toLowerCase()) - ), + const SizedBox(width: 2,), + const Text( + "Add", + style: TextStyle( + fontSize: 14, + fontWeight: FontWeight.bold + ) + ), + ], + ), + ) + ], ), - Padding( - padding: const EdgeInsets.only(top: 16,left: 16,right: 16), - child: list(), + ), + ), + Padding( + padding: const EdgeInsets.only(top: 16,left: 16,right: 16), + child: TextField( + decoration: const InputDecoration( + hintText: "Search...", + prefixIcon: Icon( + Icons.search, + size: 20 + ), ), - ], + onChanged: (value) => filterSearchResults(value.toLowerCase()) + ), + ), + Padding( + padding: const EdgeInsets.only(top: 16,left: 16,right: 16), + child: list(), + ), + ], ), ), ); @@ -119,20 +119,20 @@ class _FriendListState extends State { Widget list() { if (friends.isEmpty) { return const Center( - child: Text('No Friends'), + child: Text('No Friends'), ); } return ListView.builder( - itemCount: friends.length, - shrinkWrap: true, - padding: const EdgeInsets.only(top: 16), - physics: const NeverScrollableScrollPhysics(), - itemBuilder: (context, i) { - return FriendListItem( - friend: friends[i], - ); - }, + itemCount: friends.length, + shrinkWrap: true, + padding: const EdgeInsets.only(top: 16), + physics: const NeverScrollableScrollPhysics(), + itemBuilder: (context, i) { + return FriendListItem( + friend: friends[i], + ); + }, ); } }