Browse Source

Working conversations and messages

pull/1/head
Tovi Jaeschke-Rogers 2 years ago
parent
commit
d7af0a9ac9
17 changed files with 537 additions and 270 deletions
  1. +2
    -5
      Backend/Api/Messages/CreateConversation.go
  2. +56
    -0
      Backend/Api/Messages/UpdateConversation.go
  3. +1
    -0
      Backend/Api/Routes.go
  4. +22
    -11
      Backend/Database/Seeder/MessageSeeder.go
  5. +35
    -0
      Backend/Database/UserConversations.go
  6. +10
    -9
      Backend/Models/Messages.go
  7. +8
    -4
      Backend/main.go
  8. +15
    -1
      README.md
  9. +77
    -77
      mobile/lib/models/conversation_users.dart
  10. +36
    -5
      mobile/lib/models/conversations.dart
  11. +119
    -113
      mobile/lib/models/messages.dart
  12. +26
    -1
      mobile/lib/utils/storage/conversations.dart
  13. +2
    -1
      mobile/lib/utils/storage/messages.dart
  14. +4
    -12
      mobile/lib/views/main/conversation/create_add_users.dart
  15. +7
    -15
      mobile/lib/views/main/conversation/edit_details.dart
  16. +25
    -1
      mobile/lib/views/main/conversation/list.dart
  17. +92
    -15
      mobile/lib/views/main/conversation/settings.dart

+ 2
- 5
Backend/Api/Messages/CreateConversation.go View File

@ -2,7 +2,6 @@ package Messages
import ( import (
"encoding/json" "encoding/json"
"fmt"
"net/http" "net/http"
"github.com/gofrs/uuid" "github.com/gofrs/uuid"
@ -11,7 +10,7 @@ import (
"git.tovijaeschke.xyz/tovi/Envelope/Backend/Models" "git.tovijaeschke.xyz/tovi/Envelope/Backend/Models"
) )
type RawConversationData struct {
type RawCreateConversationData struct {
ID string `json:"id"` ID string `json:"id"`
Name string `json:"name"` Name string `json:"name"`
Users string `json:"users"` Users string `json:"users"`
@ -20,7 +19,7 @@ type RawConversationData struct {
func CreateConversation(w http.ResponseWriter, r *http.Request) { func CreateConversation(w http.ResponseWriter, r *http.Request) {
var ( var (
rawConversationData RawConversationData
rawConversationData RawCreateConversationData
messageThread Models.ConversationDetail messageThread Models.ConversationDetail
err error err error
) )
@ -45,8 +44,6 @@ func CreateConversation(w http.ResponseWriter, r *http.Request) {
return return
} }
fmt.Println(rawConversationData.UserConversations[0])
err = Database.CreateUserConversations(&rawConversationData.UserConversations) err = Database.CreateUserConversations(&rawConversationData.UserConversations)
if err != nil { if err != nil {
http.Error(w, "Error", http.StatusInternalServerError) http.Error(w, "Error", http.StatusInternalServerError)


+ 56
- 0
Backend/Api/Messages/UpdateConversation.go View File

@ -0,0 +1,56 @@
package Messages
import (
"encoding/json"
"net/http"
"github.com/gofrs/uuid"
"git.tovijaeschke.xyz/tovi/Envelope/Backend/Database"
"git.tovijaeschke.xyz/tovi/Envelope/Backend/Models"
)
type RawUpdateConversationData struct {
ID string `json:"id"`
Name string `json:"name"`
Users string `json:"users"`
UserConversations []Models.UserConversation `json:"user_conversations"`
}
func UpdateConversation(w http.ResponseWriter, r *http.Request) {
var (
rawConversationData RawCreateConversationData
messageThread Models.ConversationDetail
err error
)
err = json.NewDecoder(r.Body).Decode(&rawConversationData)
if err != nil {
http.Error(w, "Error", http.StatusInternalServerError)
return
}
messageThread = Models.ConversationDetail{
Base: Models.Base{
ID: uuid.FromStringOrNil(rawConversationData.ID),
},
Name: rawConversationData.Name,
Users: rawConversationData.Users,
}
err = Database.UpdateConversationDetail(&messageThread)
if err != nil {
http.Error(w, "Error", http.StatusInternalServerError)
return
}
if len(rawConversationData.UserConversations) > 0 {
err = Database.UpdateOrCreateUserConversations(&rawConversationData.UserConversations)
if err != nil {
http.Error(w, "Error", http.StatusInternalServerError)
return
}
}
w.WriteHeader(http.StatusOK)
}

+ 1
- 0
Backend/Api/Routes.go View File

@ -67,6 +67,7 @@ func InitApiEndpoints(router *mux.Router) {
authApi.HandleFunc("/conversation_details", Messages.EncryptedConversationDetailsList).Methods("GET") authApi.HandleFunc("/conversation_details", Messages.EncryptedConversationDetailsList).Methods("GET")
authApi.HandleFunc("/conversations", Messages.CreateConversation).Methods("POST") authApi.HandleFunc("/conversations", Messages.CreateConversation).Methods("POST")
authApi.HandleFunc("/conversations", Messages.UpdateConversation).Methods("PUT")
// Define routes for messages // Define routes for messages
authApi.HandleFunc("/message", Messages.CreateMessage).Methods("POST") authApi.HandleFunc("/message", Messages.CreateMessage).Methods("POST")


+ 22
- 11
Backend/Database/Seeder/MessageSeeder.go View File

@ -6,7 +6,6 @@ import (
"git.tovijaeschke.xyz/tovi/Envelope/Backend/Database" "git.tovijaeschke.xyz/tovi/Envelope/Backend/Database"
"git.tovijaeschke.xyz/tovi/Envelope/Backend/Models" "git.tovijaeschke.xyz/tovi/Envelope/Backend/Models"
"git.tovijaeschke.xyz/tovi/Envelope/Backend/Util"
"github.com/gofrs/uuid" "github.com/gofrs/uuid"
) )
@ -176,9 +175,9 @@ func SeedMessages() {
messageThread Models.ConversationDetail messageThread Models.ConversationDetail
key aesKey key aesKey
primaryUser Models.User primaryUser Models.User
primaryUserAssociationKey string
primaryUserAssociationKey uuid.UUID
secondaryUser Models.User secondaryUser Models.User
secondaryUserAssociationKey string
secondaryUserAssociationKey uuid.UUID
userJson string userJson string
id1, id2 uuid.UUID id1, id2 uuid.UUID
i int i int
@ -186,10 +185,19 @@ func SeedMessages() {
) )
key, err = generateAesKey() key, err = generateAesKey()
if err != nil {
panic(err)
}
messageThread, err = seedConversationDetail(key) messageThread, err = seedConversationDetail(key)
primaryUserAssociationKey = Util.RandomString(32)
secondaryUserAssociationKey = Util.RandomString(32)
primaryUserAssociationKey, err = uuid.NewV4()
if err != nil {
panic(err)
}
secondaryUserAssociationKey, err = uuid.NewV4()
if err != nil {
panic(err)
}
primaryUser, err = Database.GetUserByUsername("testUser") primaryUser, err = Database.GetUserByUsername("testUser")
if err != nil { if err != nil {
@ -201,6 +209,9 @@ func SeedMessages() {
messageThread.ID, messageThread.ID,
key, key,
) )
if err != nil {
panic(err)
}
secondaryUser, err = Database.GetUserByUsername("ATestUser2") secondaryUser, err = Database.GetUserByUsername("ATestUser2")
if err != nil { if err != nil {
@ -227,14 +238,14 @@ func SeedMessages() {
[ [
{ {
"id": "%s", "id": "%s",
"user_id": "%s",
"user_id": "%s",
"username": "%s", "username": "%s",
"admin": "true", "admin": "true",
"association_key": "%s" "association_key": "%s"
}, },
{ {
"id": "%s", "id": "%s",
"user_id": "%s",
"user_id": "%s",
"username": "%s", "username": "%s",
"admin": "false", "admin": "false",
"association_key": "%s" "association_key": "%s"
@ -244,11 +255,11 @@ func SeedMessages() {
id1.String(), id1.String(),
primaryUser.ID.String(), primaryUser.ID.String(),
primaryUser.Username, primaryUser.Username,
primaryUserAssociationKey,
primaryUserAssociationKey.String(),
id2.String(), id2.String(),
secondaryUser.ID.String(), secondaryUser.ID.String(),
secondaryUser.Username, secondaryUser.Username,
secondaryUserAssociationKey,
secondaryUserAssociationKey.String(),
) )
messageThread, err = seedUpdateUserConversation( messageThread, err = seedUpdateUserConversation(
@ -261,8 +272,8 @@ func SeedMessages() {
err = seedMessage( err = seedMessage(
primaryUser, primaryUser,
secondaryUser, secondaryUser,
primaryUserAssociationKey,
secondaryUserAssociationKey,
primaryUserAssociationKey.String(),
secondaryUserAssociationKey.String(),
i, i,
) )
if err != nil { if err != nil {


+ 35
- 0
Backend/Database/UserConversations.go View File

@ -4,6 +4,7 @@ import (
"git.tovijaeschke.xyz/tovi/Envelope/Backend/Models" "git.tovijaeschke.xyz/tovi/Envelope/Backend/Models"
"gorm.io/gorm" "gorm.io/gorm"
"gorm.io/gorm/clause"
) )
func GetUserConversationById(id string) (Models.UserConversation, error) { func GetUserConversationById(id string) (Models.UserConversation, error) {
@ -49,6 +50,40 @@ func CreateUserConversations(userConversations *[]Models.UserConversation) error
return err return err
} }
func UpdateUserConversation(userConversation *Models.UserConversation) error {
var err error
err = DB.Model(Models.UserConversation{}).
Updates(userConversation).
Error
return err
}
func UpdateUserConversations(userConversations *[]Models.UserConversation) error {
var err error
err = DB.Model(Models.UserConversation{}).
Updates(userConversations).
Error
return err
}
func UpdateOrCreateUserConversations(userConversations *[]Models.UserConversation) error {
var err error
err = DB.Model(Models.UserConversation{}).
Clauses(clause.OnConflict{
Columns: []clause.Column{{Name: "id"}},
DoUpdates: clause.AssignmentColumns([]string{"admin"}),
}).
Create(userConversations).
Error
return err
}
func DeleteUserConversation(userConversation *Models.UserConversation) error { func DeleteUserConversation(userConversation *Models.UserConversation) error {
return DB.Session(&gorm.Session{FullSaveAssociations: true}). return DB.Session(&gorm.Session{FullSaveAssociations: true}).
Delete(userConversation). Delete(userConversation).


+ 10
- 9
Backend/Models/Messages.go View File

@ -18,22 +18,23 @@ type Message struct {
Base Base
MessageDataID uuid.UUID `json:"message_data_id"` MessageDataID uuid.UUID `json:"message_data_id"`
MessageData MessageData `json:"message_data"` MessageData MessageData `json:"message_data"`
SymmetricKey string `gorm:"not null" json:"symmetric_key"` // Stored encrypted
AssociationKey string `gorm:"not null" json:"association_key"` // TODO: This links all encrypted messages for a user in a thread together. Find a way to fix this
CreatedAt time.Time `gorm:"not null" json:"created_at"`
SymmetricKey string `json:"symmetric_key" gorm:"not null"` // Stored encrypted
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 { type ConversationDetail struct {
Base Base
Name string `gorm:"not null" json:"name"` // Stored encrypted
Users string `json:"users"` // Stored as encrypted JSON
Name string `gorm:"not null" json:"name"` // Stored encrypted
Users string ` json:"users"` // Stored as encrypted JSON
} }
type UserConversation struct { type UserConversation struct {
Base Base
UserID uuid.UUID `gorm:"type:uuid;column:user_id;not null;" json:"user_id"` 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
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
} }

+ 8
- 4
Backend/main.go View File

@ -2,6 +2,7 @@ package main
import ( import (
"flag" "flag"
"fmt"
"net/http" "net/http"
"git.tovijaeschke.xyz/tovi/Envelope/Backend/Api" "git.tovijaeschke.xyz/tovi/Envelope/Backend/Api"
@ -11,9 +12,7 @@ import (
"github.com/gorilla/mux" "github.com/gorilla/mux"
) )
var (
seed bool
)
var seed bool
func init() { func init() {
Database.Init() Database.Init()
@ -26,6 +25,7 @@ func init() {
func main() { func main() {
var ( var (
router *mux.Router router *mux.Router
err error
) )
if seed { if seed {
@ -37,5 +37,9 @@ func main() {
Api.InitApiEndpoints(router) Api.InitApiEndpoints(router)
http.ListenAndServe(":8080", router)
fmt.Println("Listening on port :8080")
err = http.ListenAndServe(":8080", router)
if err != nil {
panic(err)
}
} }

+ 15
- 1
README.md View File

@ -1,3 +1,17 @@
# Envelope # Envelope
Encrypted messaging app
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

+ 77
- 77
mobile/lib/models/conversation_users.dart View File

@ -2,97 +2,97 @@ import '/models/conversations.dart';
import '/utils/storage/database.dart'; import '/utils/storage/database.dart';
Future<ConversationUser> getConversationUser(Conversation conversation, String userId) async { Future<ConversationUser> getConversationUser(Conversation conversation, String userId) async {
final db = await getDatabaseConnection();
final db = await getDatabaseConnection();
final List<Map<String, dynamic>> maps = await db.query(
'conversation_users',
where: 'conversation_id = ? AND user_id = ?',
whereArgs: [conversation.id, userId],
);
final List<Map<String, dynamic>> maps = await db.query(
'conversation_users',
where: 'conversation_id = ? AND user_id = ?',
whereArgs: [conversation.id, userId],
);
if (maps.length != 1) {
throw ArgumentError('Invalid conversation_id or username');
}
if (maps.length != 1) {
throw ArgumentError('Invalid conversation_id or username');
}
return ConversationUser(
id: maps[0]['id'],
userId: maps[0]['user_id'],
conversationId: maps[0]['conversation_id'],
username: maps[0]['username'],
associationKey: maps[0]['association_key'],
admin: maps[0]['admin'] == 1,
);
return ConversationUser(
id: maps[0]['id'],
userId: maps[0]['user_id'],
conversationId: maps[0]['conversation_id'],
username: maps[0]['username'],
associationKey: maps[0]['association_key'],
admin: maps[0]['admin'] == 1,
);
} }
// A method that retrieves all the dogs from the dogs table. // A method that retrieves all the dogs from the dogs table.
Future<List<ConversationUser>> getConversationUsers(Conversation conversation) async { Future<List<ConversationUser>> getConversationUsers(Conversation conversation) async {
final db = await getDatabaseConnection();
final db = await getDatabaseConnection();
final List<Map<String, dynamic>> maps = await db.query(
'conversation_users',
where: 'conversation_id = ?',
whereArgs: [conversation.id],
orderBy: 'admin',
);
final List<Map<String, dynamic>> maps = await db.query(
'conversation_users',
where: 'conversation_id = ?',
whereArgs: [conversation.id],
orderBy: 'admin',
);
return 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'],
admin: maps[i]['admin'] == 1,
);
});
return 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'],
admin: maps[i]['admin'] == 1,
);
});
} }
class ConversationUser{ class ConversationUser{
String id;
String userId;
String conversationId;
String username;
String associationKey;
bool admin;
ConversationUser({
required this.id,
required this.userId,
required this.conversationId,
required this.username,
required this.associationKey,
required this.admin,
});
String id;
String userId;
String conversationId;
String username;
String associationKey;
bool admin;
ConversationUser({
required this.id,
required this.userId,
required this.conversationId,
required this.username,
required this.associationKey,
required this.admin,
});
factory ConversationUser.fromJson(Map<String, dynamic> json, String conversationId) {
return ConversationUser(
id: json['id'],
userId: json['user_id'],
conversationId: conversationId,
username: json['username'],
associationKey: json['association_key'],
admin: json['admin'] == 'true',
);
}
factory ConversationUser.fromJson(Map<String, dynamic> json, String conversationId) {
return ConversationUser(
id: json['id'],
userId: json['user_id'],
conversationId: conversationId,
username: json['username'],
associationKey: json['association_key'],
admin: json['admin'] == 'true',
);
}
Map<String, dynamic> toJson() {
return {
'id': id,
'user_id': userId,
'username': username,
'association_key': associationKey,
'admin': admin ? 'true' : 'false',
};
}
Map<String, dynamic> toJson() {
return {
'id': id,
'user_id': userId,
'username': username,
'association_key': associationKey,
'admin': admin ? 'true' : 'false',
};
}
Map<String, dynamic> toMap() {
return {
'id': id,
'user_id': userId,
'conversation_id': conversationId,
'username': username,
'association_key': associationKey,
'admin': admin ? 1 : 0,
};
}
Map<String, dynamic> toMap() {
return {
'id': id,
'user_id': userId,
'conversation_id': conversationId,
'username': username,
'association_key': associationKey,
'admin': admin ? 1 : 0,
};
}
} }

+ 36
- 5
mobile/lib/models/conversations.dart View File

@ -25,8 +25,6 @@ Future<Conversation> createConversation(String title, List<Friend> friends) asyn
Uint8List symmetricKey = AesHelper.deriveKey(generateRandomString(32)); Uint8List symmetricKey = AesHelper.deriveKey(generateRandomString(32));
String associationKey = generateRandomString(32);
Conversation conversation = Conversation( Conversation conversation = Conversation(
id: conversationId, id: conversationId,
userId: profile.id, userId: profile.id,
@ -51,7 +49,7 @@ Future<Conversation> createConversation(String title, List<Friend> friends) asyn
userId: profile.id, userId: profile.id,
conversationId: conversationId, conversationId: conversationId,
username: profile.username, username: profile.username,
associationKey: associationKey,
associationKey: uuid.v4(),
admin: true, admin: true,
).toMap(), ).toMap(),
conflictAlgorithm: ConflictAlgorithm.fail, conflictAlgorithm: ConflictAlgorithm.fail,
@ -65,7 +63,31 @@ Future<Conversation> createConversation(String title, List<Friend> friends) asyn
userId: friend.friendId, userId: friend.friendId,
conversationId: conversationId, conversationId: conversationId,
username: friend.username, username: friend.username,
associationKey: associationKey,
associationKey: uuid.v4(),
admin: false,
).toMap(),
conflictAlgorithm: ConflictAlgorithm.replace,
);
}
return conversation;
}
Future<Conversation> addUsersToConversation(Conversation conversation, List<Friend> friends) async {
final db = await getDatabaseConnection();
var uuid = const Uuid();
for (Friend friend in friends) {
await db.insert(
'conversation_users',
ConversationUser(
id: uuid.v4(),
userId: friend.friendId,
conversationId: conversation.id,
username: friend.username,
associationKey: uuid.v4(),
admin: false, admin: false,
).toMap(), ).toMap(),
conflictAlgorithm: ConflictAlgorithm.replace, conflictAlgorithm: ConflictAlgorithm.replace,
@ -181,12 +203,21 @@ class Conversation {
); );
} }
Future<Map<String, dynamic>> toJson() async {
Future<Map<String, dynamic>> payloadJson({ bool includeUsers = true }) async {
MyProfile profile = await MyProfile.getProfile(); MyProfile profile = await MyProfile.getProfile();
var symKey = base64.decode(symmetricKey); var symKey = base64.decode(symmetricKey);
List<ConversationUser> users = await getConversationUsers(this); List<ConversationUser> users = await getConversationUsers(this);
if (!includeUsers) {
return {
'id': conversationDetailId,
'name': AesHelper.aesEncrypt(symKey, Uint8List.fromList(name.codeUnits)),
'users': AesHelper.aesEncrypt(symKey, Uint8List.fromList(jsonEncode(users).codeUnits)),
};
}
List<Object> userConversations = []; List<Object> userConversations = [];
for (ConversationUser user in users) { for (ConversationUser user in users) {


+ 119
- 113
mobile/lib/models/messages.dart View File

@ -1,17 +1,48 @@
import 'dart:convert'; import 'dart:convert';
import 'dart:typed_data'; import 'dart:typed_data';
import 'package:Envelope/models/conversation_users.dart';
import 'package:Envelope/models/conversations.dart';
import 'package:pointycastle/export.dart'; import 'package:pointycastle/export.dart';
import 'package:shared_preferences/shared_preferences.dart';
import '/utils/encryption/crypto_utils.dart';
import '/models/conversation_users.dart';
import '/models/conversations.dart';
import '/models/my_profile.dart';
import '/models/friends.dart';
import '/utils/encryption/aes_helper.dart'; import '/utils/encryption/aes_helper.dart';
import '/utils/encryption/crypto_utils.dart';
import '/utils/storage/database.dart'; import '/utils/storage/database.dart';
import '/utils/strings.dart'; import '/utils/strings.dart';
import '/models/friends.dart';
const messageTypeSender = 'sender';
const messageTypeReceiver = 'receiver'; const messageTypeReceiver = 'receiver';
const messageTypeSender = 'sender';
Future<List<Message>> getMessagesForThread(Conversation conversation) async {
final db = await getDatabaseConnection();
final List<Map<String, dynamic>> 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;
''',
[conversation.id]
);
return List.generate(maps.length, (i) {
return Message(
id: maps[i]['id'],
symmetricKey: maps[i]['symmetric_key'],
userSymmetricKey: maps[i]['user_symmetric_key'],
data: maps[i]['data'],
senderId: maps[i]['sender_id'],
senderUsername: maps[i]['sender_username'],
associationKey: maps[i]['association_key'],
createdAt: maps[i]['created_at'],
failedToSend: maps[i]['failed_to_send'] == 1,
);
});
}
class Message { class Message {
String id; String id;
@ -38,109 +69,99 @@ class Message {
factory Message.fromJson(Map<String, dynamic> json, RSAPrivateKey privKey) { factory Message.fromJson(Map<String, dynamic> json, RSAPrivateKey privKey) {
var userSymmetricKey = CryptoUtils.rsaDecrypt( var userSymmetricKey = CryptoUtils.rsaDecrypt(
base64.decode(json['symmetric_key']),
privKey,
base64.decode(json['symmetric_key']),
privKey,
); );
var symmetricKey = AesHelper.aesDecrypt( var symmetricKey = AesHelper.aesDecrypt(
userSymmetricKey,
base64.decode(json['message_data']['symmetric_key']),
userSymmetricKey,
base64.decode(json['message_data']['symmetric_key']),
); );
var senderId = AesHelper.aesDecrypt( var senderId = AesHelper.aesDecrypt(
base64.decode(symmetricKey),
base64.decode(json['message_data']['sender_id']),
base64.decode(symmetricKey),
base64.decode(json['message_data']['sender_id']),
); );
var data = AesHelper.aesDecrypt( var data = AesHelper.aesDecrypt(
base64.decode(symmetricKey),
base64.decode(json['message_data']['data']),
base64.decode(symmetricKey),
base64.decode(json['message_data']['data']),
); );
return Message( return Message(
id: json['id'],
symmetricKey: symmetricKey,
userSymmetricKey: base64.encode(userSymmetricKey),
data: data,
senderId: senderId,
senderUsername: 'Unknown',
associationKey: json['association_key'],
createdAt: json['created_at'],
failedToSend: false,
id: json['id'],
symmetricKey: symmetricKey,
userSymmetricKey: base64.encode(userSymmetricKey),
data: data,
senderId: senderId,
senderUsername: 'Unknown',
associationKey: json['association_key'],
createdAt: json['created_at'],
failedToSend: false,
); );
} }
Future<String> toJson(Conversation conversation, String messageDataId) async { Future<String> toJson(Conversation conversation, String messageDataId) async {
final preferences = await SharedPreferences.getInstance();
RSAPublicKey publicKey = CryptoUtils.rsaPublicKeyFromPem(preferences.getString('asymmetricPublicKey')!);
final userSymmetricKey = AesHelper.deriveKey(generateRandomString(32));
final symmetricKey = AesHelper.deriveKey(generateRandomString(32));
List<Map<String, String>> messages = [];
String id = '';
List<ConversationUser> conversationUsers = await getConversationUsers(conversation);
for (var i = 0; i < conversationUsers.length; i++) {
ConversationUser user = conversationUsers[i];
if (preferences.getString('username') == user.username) {
id = user.id;
messages.add({
'message_data_id': messageDataId,
'symmetric_key': base64.encode(CryptoUtils.rsaEncrypt(
userSymmetricKey,
publicKey,
)),
'association_key': user.associationKey,
});
continue;
}
Friend friend = await getFriendByFriendId(user.id);
RSAPublicKey friendPublicKey = CryptoUtils.rsaPublicKeyFromPem(friend.asymmetricPublicKey);
messages.add({
'message_data_id': messageDataId,
'symmetric_key': base64.encode(CryptoUtils.rsaEncrypt(
userSymmetricKey,
friendPublicKey,
)),
'association_key': user.associationKey,
});
}
MyProfile profile = await MyProfile.getProfile();
if (profile.publicKey == null) {
throw Exception('Could not get profile.publicKey');
}
RSAPublicKey publicKey = profile.publicKey!;
final userSymmetricKey = AesHelper.deriveKey(generateRandomString(32));
final symmetricKey = AesHelper.deriveKey(generateRandomString(32));
List<Map<String, String>> messages = [];
String id = '';
List<ConversationUser> conversationUsers = await getConversationUsers(conversation);
Map<String, String> messageData = {
'id': messageDataId,
'data': AesHelper.aesEncrypt(symmetricKey, Uint8List.fromList(data.codeUnits)),
'sender_id': AesHelper.aesEncrypt(symmetricKey, Uint8List.fromList(id.codeUnits)),
'symmetric_key': AesHelper.aesEncrypt(
for (var i = 0; i < conversationUsers.length; i++) {
ConversationUser user = conversationUsers[i];
if (profile.id == user.userId) {
id = user.id;
messages.add({
'message_data_id': messageDataId,
'symmetric_key': base64.encode(CryptoUtils.rsaEncrypt(
userSymmetricKey, userSymmetricKey,
Uint8List.fromList(base64.encode(symmetricKey).codeUnits),
),
};
publicKey,
)),
'association_key': user.associationKey,
});
return jsonEncode(<String, dynamic>{
'message_data': messageData,
'message': messages,
});
}
continue;
}
@override
String toString() {
return '''
Friend friend = await getFriendByFriendId(user.userId);
RSAPublicKey friendPublicKey = CryptoUtils.rsaPublicKeyFromPem(friend.asymmetricPublicKey);
messages.add({
'message_data_id': messageDataId,
'symmetric_key': base64.encode(CryptoUtils.rsaEncrypt(
userSymmetricKey,
friendPublicKey,
)),
'association_key': user.associationKey,
});
}
Map<String, String> messageData = {
'id': messageDataId,
'data': AesHelper.aesEncrypt(symmetricKey, Uint8List.fromList(data.codeUnits)),
'sender_id': AesHelper.aesEncrypt(symmetricKey, Uint8List.fromList(senderId.codeUnits)),
'symmetric_key': AesHelper.aesEncrypt(
userSymmetricKey,
Uint8List.fromList(base64.encode(symmetricKey).codeUnits),
),
};
id: $id
data: $data
senderId: $senderId
senderUsername: $senderUsername
associationKey: $associationKey
createdAt: $createdAt
''';
return jsonEncode(<String, dynamic>{
'message_data': messageData,
'message': messages,
});
} }
Map<String, dynamic> toMap() { Map<String, dynamic> toMap() {
@ -157,33 +178,18 @@ class Message {
}; };
} }
}
Future<List<Message>> getMessagesForThread(Conversation conversation) async {
final db = await getDatabaseConnection();
@override
String toString() {
return '''
final List<Map<String, dynamic>> 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;
''',
[conversation.id]
);
return List.generate(maps.length, (i) {
return Message(
id: maps[i]['id'],
symmetricKey: maps[i]['symmetric_key'],
userSymmetricKey: maps[i]['user_symmetric_key'],
data: maps[i]['data'],
senderId: maps[i]['sender_id'],
senderUsername: maps[i]['sender_username'],
associationKey: maps[i]['association_key'],
createdAt: maps[i]['created_at'],
failedToSend: maps[i]['failed_to_send'] == 1,
);
});
id: $id
data: $data
senderId: $senderId
senderUsername: $senderUsername
associationKey: $associationKey
createdAt: $createdAt
''';
}
} }

+ 26
- 1
mobile/lib/utils/storage/conversations.dart View File

@ -31,6 +31,10 @@ Future<void> updateConversations() async {
List<dynamic> conversationsJson = jsonDecode(resp.body); List<dynamic> conversationsJson = jsonDecode(resp.body);
if (conversationsJson.isEmpty) {
return;
}
for (var i = 0; i < conversationsJson.length; i++) { for (var i = 0; i < conversationsJson.length; i++) {
Conversation conversation = Conversation.fromJson( Conversation conversation = Conversation.fromJson(
conversationsJson[i] as Map<String, dynamic>, conversationsJson[i] as Map<String, dynamic>,
@ -102,7 +106,7 @@ Future<void> updateConversations() async {
Future<void> uploadConversation(Conversation conversation) async { Future<void> uploadConversation(Conversation conversation) async {
String sessionCookie = await getSessionCookie(); String sessionCookie = await getSessionCookie();
Map<String, dynamic> conversationJson = await conversation.toJson();
Map<String, dynamic> conversationJson = await conversation.payloadJson();
var x = await http.post( var x = await http.post(
Uri.parse('${dotenv.env["SERVER_URL"]}api/v1/auth/conversations'), Uri.parse('${dotenv.env["SERVER_URL"]}api/v1/auth/conversations'),
@ -113,5 +117,26 @@ Future<void> uploadConversation(Conversation conversation) async {
body: jsonEncode(conversationJson), body: jsonEncode(conversationJson),
); );
// TODO: Handle errors here
print(x.statusCode);
}
Future<void> updateConversation(Conversation conversation, { includeUsers = true } ) async {
String sessionCookie = await getSessionCookie();
Map<String, dynamic> conversationJson = await conversation.payloadJson(includeUsers: includeUsers);
var x = await http.put(
Uri.parse('${dotenv.env["SERVER_URL"]}api/v1/auth/conversations'),
headers: <String, String>{
'Content-Type': 'application/json; charset=UTF-8',
'cookie': sessionCookie,
},
body: jsonEncode(conversationJson),
);
// TODO: Handle errors here
print(x.statusCode); print(x.statusCode);
} }

+ 2
- 1
mobile/lib/utils/storage/messages.dart View File

@ -24,7 +24,7 @@ Future<void> sendMessage(Conversation conversation, String data) async {
id: messageDataId, id: messageDataId,
symmetricKey: '', symmetricKey: '',
userSymmetricKey: '', userSymmetricKey: '',
senderId: profile.id,
senderId: currentUser.userId,
senderUsername: profile.username, senderUsername: profile.username,
data: data, data: data,
associationKey: currentUser.associationKey, associationKey: currentUser.associationKey,
@ -66,6 +66,7 @@ Future<void> sendMessage(Conversation conversation, String data) async {
where: 'id = ?', where: 'id = ?',
whereArgs: [message.id], whereArgs: [message.id],
); );
throw exception;
}); });
} }


+ 4
- 12
mobile/lib/views/main/conversation/create_add_users.dart View File

@ -8,11 +8,11 @@ import '/views/main/conversation/detail.dart';
class ConversationAddFriendsList extends StatefulWidget { class ConversationAddFriendsList extends StatefulWidget {
final List<Friend> friends; final List<Friend> friends;
final String title;
final Function(List<Friend> friendsSelected) saveCallback;
const ConversationAddFriendsList({ const ConversationAddFriendsList({
Key? key, Key? key,
required this.friends, required this.friends,
required this.title,
required this.saveCallback,
}) : super(key: key); }) : super(key: key);
@override @override
@ -88,20 +88,12 @@ class _ConversationAddFriendsListState extends State<ConversationAddFriendsList>
floatingActionButton: Padding( floatingActionButton: Padding(
padding: const EdgeInsets.only(right: 10, bottom: 10), padding: const EdgeInsets.only(right: 10, bottom: 10),
child: FloatingActionButton( child: FloatingActionButton(
onPressed: () async {
Conversation conversation = await createConversation(widget.title, friendsSelected);
uploadConversation(conversation);
onPressed: () {
widget.saveCallback(friendsSelected);
setState(() { setState(() {
friendsSelected = []; friendsSelected = [];
}); });
Navigator.of(context).popUntil((route) => route.isFirst);
Navigator.push(context, MaterialPageRoute(builder: (context){
return ConversationDetail(
conversation: conversation,
);
}));
}, },
backgroundColor: Theme.of(context).colorScheme.primary, backgroundColor: Theme.of(context).colorScheme.primary,
child: friendsSelected.isEmpty ? child: friendsSelected.isEmpty ?


+ 7
- 15
mobile/lib/views/main/conversation/edit_details.dart View File

@ -1,20 +1,15 @@
import 'package:Envelope/models/conversation_users.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import '/components/custom_circle_avatar.dart'; import '/components/custom_circle_avatar.dart';
import '/models/conversations.dart'; import '/models/conversations.dart';
import '/models/friends.dart';
import '/views/main/conversation/create_add_users.dart';
class ConversationEditDetails extends StatefulWidget { class ConversationEditDetails extends StatefulWidget {
final Function(String conversationName) saveCallback;
final Conversation? conversation; final Conversation? conversation;
final List<Friend>? friends;
final List<ConversationUser>? users;
const ConversationEditDetails({ const ConversationEditDetails({
Key? key, Key? key,
required this.saveCallback,
this.conversation, this.conversation,
this.friends,
this.users,
}) : super(key: key); }) : super(key: key);
@override @override
@ -134,15 +129,12 @@ class _ConversationEditDetails extends State<ConversationEditDetails> {
ElevatedButton( ElevatedButton(
style: buttonStyle, style: buttonStyle,
onPressed: () { onPressed: () {
if (_formKey.currentState!.validate()) {
Navigator.of(context).push(
MaterialPageRoute(builder: (context) => ConversationAddFriendsList(
friends: widget.friends!,
title: conversationNameController.text,
)
)
);
if (!_formKey.currentState!.validate()) {
// TODO: Show error here
return;
} }
widget.saveCallback(conversationNameController.text);
}, },
child: const Text('Save'), child: const Text('Save'),
), ),


+ 25
- 1
mobile/lib/views/main/conversation/list.dart View File

@ -1,9 +1,12 @@
import 'package:Envelope/models/friends.dart'; import 'package:Envelope/models/friends.dart';
import 'package:Envelope/utils/storage/conversations.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import '/models/conversations.dart'; import '/models/conversations.dart';
import '/views/main/conversation/edit_details.dart'; import '/views/main/conversation/edit_details.dart';
import '/views/main/conversation/list_item.dart'; import '/views/main/conversation/list_item.dart';
import 'create_add_users.dart';
import 'detail.dart';
class ConversationList extends StatefulWidget { class ConversationList extends StatefulWidget {
final List<Conversation> conversations; final List<Conversation> conversations;
@ -73,7 +76,28 @@ class _ConversationListState extends State<ConversationList> {
onPressed: () { onPressed: () {
Navigator.of(context).push( Navigator.of(context).push(
MaterialPageRoute(builder: (context) => ConversationEditDetails( MaterialPageRoute(builder: (context) => ConversationEditDetails(
friends: friends,
saveCallback: (String conversationName) {
Navigator.of(context).push(
MaterialPageRoute(builder: (context) => ConversationAddFriendsList(
friends: friends,
saveCallback: (List<Friend> friendsSelected) async {
Conversation conversation = await createConversation(
conversationName,
friendsSelected
);
uploadConversation(conversation);
Navigator.of(context).popUntil((route) => route.isFirst);
Navigator.push(context, MaterialPageRoute(builder: (context){
return ConversationDetail(
conversation: conversation,
);
}));
},
))
);
},
)), )),
).then(onGoBack); ).then(onGoBack);
}, },


+ 92
- 15
mobile/lib/views/main/conversation/settings.dart View File

@ -1,11 +1,15 @@
import 'package:Envelope/components/custom_circle_avatar.dart';
import 'package:Envelope/views/main/conversation/edit_details.dart';
import 'package:Envelope/models/friends.dart';
import 'package:Envelope/views/main/conversation/create_add_users.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import '/models/conversation_users.dart'; import '/models/conversation_users.dart';
import '/models/conversations.dart'; import '/models/conversations.dart';
import '/models/my_profile.dart'; import '/models/my_profile.dart';
import '/views/main/conversation/settings_user_list_item.dart'; import '/views/main/conversation/settings_user_list_item.dart';
import '/views/main/conversation/edit_details.dart';
import '/components/custom_circle_avatar.dart';
import '/utils/storage/database.dart';
import '/utils/storage/conversations.dart';
class ConversationSettings extends StatefulWidget { class ConversationSettings extends StatefulWidget {
final Conversation conversation; final Conversation conversation;
@ -78,7 +82,7 @@ class _ConversationSettingsState extends State<ConversationSettings> {
widget.conversation.admin ? widget.conversation.admin ?
const SizedBox(height: 25) : const SizedBox(height: 25) :
const SizedBox.shrink(), const SizedBox.shrink(),
sectionTitle('Members'),
sectionTitle('Members', showUsersAdd: true),
usersList(), usersList(),
const SizedBox(height: 25), const SizedBox(height: 25),
myAccess(), myAccess(),
@ -112,9 +116,22 @@ class _ConversationSettingsState extends State<ConversationSettings> {
onPressed: () { onPressed: () {
Navigator.of(context).push( Navigator.of(context).push(
MaterialPageRoute(builder: (context) => ConversationEditDetails( MaterialPageRoute(builder: (context) => ConversationEditDetails(
users: users,
conversation: widget.conversation,
friends: null,
saveCallback: (String conversationName) async {
widget.conversation.name = conversationName;
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); ).then(onGoBack);
}, },
@ -160,16 +177,49 @@ class _ConversationSettingsState extends State<ConversationSettings> {
); );
} }
Widget sectionTitle(String title) {
Widget sectionTitle(String title, { bool showUsersAdd = false}) {
return Align( return Align(
alignment: Alignment.centerLeft,
child: Container(
padding: const EdgeInsets.only(left: 12),
child: Text(
title,
style: const TextStyle(fontSize: 20),
alignment: Alignment.centerLeft,
child: Padding(
padding: const EdgeInsets.only(right: 6),
child: Row(
children: [
Expanded(
child: Container(
padding: const EdgeInsets.only(left: 12),
child: Text(
title,
style: const TextStyle(fontSize: 20),
),
),
), ),
),
!showUsersAdd ?
const SizedBox.shrink() :
IconButton(
icon: const Icon(Icons.add),
padding: const EdgeInsets.all(0),
onPressed: () async {
List<Friend> friends = await unselectedFriends();
Navigator.of(context).push(
MaterialPageRoute(builder: (context) => ConversationAddFriendsList(
friends: friends,
saveCallback: (List<Friend> selectedFriends) async {
addUsersToConversation(
widget.conversation,
selectedFriends,
);
await updateConversation(widget.conversation, includeUsers: true);
await getUsers();
Navigator.pop(context);
},
))
);
},
),
],
)
)
); );
} }
@ -236,9 +286,36 @@ class _ConversationSettingsState extends State<ConversationSettings> {
); );
} }
Future<List<Friend>> unselectedFriends() async {
final db = await getDatabaseConnection();
List<String> notInArgs = [];
for (var user in users) {
notInArgs.add(user.userId);
}
final List<Map<String, dynamic>> maps = await db.query(
'friends',
where: 'friend_id not in (${List.filled(notInArgs.length, '?').join(',')})',
whereArgs: notInArgs,
orderBy: 'username',
);
return List.generate(maps.length, (i) {
return Friend(
id: maps[i]['id'],
userId: maps[i]['user_id'],
friendId: maps[i]['friend_id'],
friendSymmetricKey: maps[i]['symmetric_key'],
asymmetricPublicKey: maps[i]['asymmetric_public_key'],
acceptedAt: maps[i]['accepted_at'],
username: maps[i]['username'],
);
});
}
onGoBack(dynamic value) async { onGoBack(dynamic value) async {
nameController.text = widget.conversation.name; nameController.text = widget.conversation.name;
super.initState();
getUsers(); getUsers();
setState(() {}); setState(() {});
} }


Loading…
Cancel
Save