Browse Source

Remove friends table due to unnecessary added complexity

WIP - Creating conversations
pull/1/head
Tovi Jaeschke-Rogers 2 years ago
parent
commit
b05b90e6d2
27 changed files with 499 additions and 517 deletions
  1. +0
    -38
      Backend/Api/Friends/EncryptedFriendsList.go
  2. +54
    -0
      Backend/Api/Messages/CreateConversation.go
  3. +3
    -5
      Backend/Api/Routes.go
  4. +1
    -1
      Backend/Database/ConversationDetails.go
  5. +0
    -74
      Backend/Database/Friends.go
  6. +5
    -6
      Backend/Database/Init.go
  7. +21
    -15
      Backend/Database/Seeder/FriendSeeder.go
  8. +11
    -36
      Backend/Database/Seeder/MessageSeeder.go
  9. +3
    -37
      Backend/Database/Seeder/UserSeeder.go
  10. +16
    -4
      Backend/Database/UserConversations.go
  11. +8
    -12
      Backend/Models/Friends.go
  12. +4
    -6
      Backend/Models/Users.go
  13. +19
    -25
      mobile/lib/models/conversation_users.dart
  14. +136
    -47
      mobile/lib/models/conversations.dart
  15. +34
    -24
      mobile/lib/models/friends.dart
  16. +8
    -3
      mobile/lib/models/my_profile.dart
  17. +88
    -68
      mobile/lib/utils/storage/conversations.dart
  18. +3
    -1
      mobile/lib/utils/storage/database.dart
  19. +0
    -27
      mobile/lib/utils/storage/encryption_keys.dart
  20. +13
    -47
      mobile/lib/utils/storage/friends.dart
  21. +13
    -17
      mobile/lib/utils/storage/messages.dart
  22. +6
    -2
      mobile/lib/views/main/conversation_create_add_users.dart
  23. +11
    -12
      mobile/lib/views/main/conversation_detail.dart
  24. +5
    -3
      mobile/lib/views/main/conversation_list.dart
  25. +21
    -6
      mobile/lib/views/main/conversation_list_item.dart
  26. +1
    -1
      mobile/lib/views/main/home.dart
  27. +15
    -0
      mobile/lib/views/main/profile.dart

+ 0
- 38
Backend/Api/Friends/EncryptedFriendsList.go View File

@ -3,8 +3,6 @@ package Friends
import ( import (
"encoding/json" "encoding/json"
"net/http" "net/http"
"net/url"
"strings"
"git.tovijaeschke.xyz/tovi/Envelope/Backend/Api/Auth" "git.tovijaeschke.xyz/tovi/Envelope/Backend/Api/Auth"
"git.tovijaeschke.xyz/tovi/Envelope/Backend/Database" "git.tovijaeschke.xyz/tovi/Envelope/Backend/Database"
@ -40,39 +38,3 @@ func EncryptedFriendRequestList(w http.ResponseWriter, r *http.Request) {
w.WriteHeader(http.StatusOK) w.WriteHeader(http.StatusOK)
w.Write(returnJson) w.Write(returnJson)
} }
func EncryptedFriendList(w http.ResponseWriter, r *http.Request) {
var (
friends []Models.Friend
query url.Values
friendIds []string
returnJson []byte
ok bool
err error
)
query = r.URL.Query()
friendIds, ok = query["friend_ids"]
if !ok {
http.Error(w, "Invalid Data", http.StatusBadGateway)
return
}
// TODO: Fix error handling here
friendIds = strings.Split(friendIds[0], ",")
friends, err = Database.GetFriendsByIds(friendIds)
if err != nil {
http.Error(w, "Error", http.StatusInternalServerError)
return
}
returnJson, err = json.MarshalIndent(friends, "", " ")
if err != nil {
http.Error(w, "Error", http.StatusInternalServerError)
return
}
w.WriteHeader(http.StatusOK)
w.Write(returnJson)
}

+ 54
- 0
Backend/Api/Messages/CreateConversation.go View File

@ -0,0 +1,54 @@
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 RawConversationData struct {
ID string `json:"id"`
Name string `json:"name"`
Users string `json:"users"`
UserConversations []Models.UserConversation `json:"user_conversations"`
}
func CreateConversation(w http.ResponseWriter, r *http.Request) {
var (
rawConversationData RawConversationData
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.CreateConversationDetail(&messageThread)
if err != nil {
http.Error(w, "Error", http.StatusInternalServerError)
return
}
err = Database.CreateUserConversations(&rawConversationData.UserConversations)
if err != nil {
panic(err)
http.Error(w, "Error", http.StatusInternalServerError)
return
}
w.WriteHeader(http.StatusOK)
}

+ 3
- 5
Backend/Api/Routes.go View File

@ -26,9 +26,7 @@ func loggingMiddleware(next http.Handler) http.Handler {
func authenticationMiddleware(next http.Handler) http.Handler { func authenticationMiddleware(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
var (
err error
)
var err error
_, err = Auth.CheckCookie(r) _, err = Auth.CheckCookie(r)
if err != nil { if err != nil {
@ -65,11 +63,11 @@ func InitApiEndpoints(router *mux.Router) {
authApi.HandleFunc("/friend_requests", Friends.EncryptedFriendRequestList).Methods("GET") authApi.HandleFunc("/friend_requests", Friends.EncryptedFriendRequestList).Methods("GET")
authApi.HandleFunc("/friend_request", Friends.CreateFriendRequest).Methods("POST") authApi.HandleFunc("/friend_request", Friends.CreateFriendRequest).Methods("POST")
authApi.HandleFunc("/friends", Friends.EncryptedFriendList).Methods("GET")
authApi.HandleFunc("/conversations", Messages.EncryptedConversationList).Methods("GET") authApi.HandleFunc("/conversations", Messages.EncryptedConversationList).Methods("GET")
authApi.HandleFunc("/conversation_details", Messages.EncryptedConversationDetailsList).Methods("GET") authApi.HandleFunc("/conversation_details", Messages.EncryptedConversationDetailsList).Methods("GET")
authApi.HandleFunc("/conversations", Messages.CreateConversation).Methods("POST")
// Define routes for messages // Define routes for messages
authApi.HandleFunc("/message", Messages.CreateMessage).Methods("POST") authApi.HandleFunc("/message", Messages.CreateMessage).Methods("POST")
authApi.HandleFunc("/messages/{threadKey}", Messages.Messages).Methods("GET") authApi.HandleFunc("/messages/{threadKey}", Messages.Messages).Methods("GET")


+ 1
- 1
Backend/Database/ConversationDetails.go View File

@ -28,7 +28,7 @@ func GetConversationDetailsByIds(id []string) ([]Models.ConversationDetail, erro
) )
err = DB.Preload(clause.Associations). err = DB.Preload(clause.Associations).
Where("id = ?", id).
Where("id IN ?", id).
First(&messageThread). First(&messageThread).
Error Error


+ 0
- 74
Backend/Database/Friends.go View File

@ -1,74 +0,0 @@
package Database
import (
"git.tovijaeschke.xyz/tovi/Envelope/Backend/Models"
"gorm.io/gorm"
"gorm.io/gorm/clause"
)
func GetFriendById(id string) (Models.Friend, error) {
var (
userData Models.Friend
err error
)
err = DB.Preload(clause.Associations).
First(&userData, "id = ?", id).
Error
return userData, err
}
func GetFriendsByIds(ids []string) ([]Models.Friend, error) {
var (
userData []Models.Friend
err error
)
err = DB.Preload(clause.Associations).
Find(&userData, ids).
Error
return userData, err
}
func CreateFriend(userData *Models.Friend) error {
var (
err error
)
err = DB.Session(&gorm.Session{FullSaveAssociations: true}).
Create(userData).
Error
return err
}
func UpdateFriend(id string, userData *Models.Friend) error {
var (
err error
)
err = DB.Model(&userData).
Omit("id").
Where("id = ?", id).
Updates(userData).
Error
if err != nil {
return err
}
err = DB.Model(Models.Friend{}).
Where("id = ?", id).
First(userData).
Error
return err
}
func DeleteFriend(userData *Models.Friend) error {
return DB.Session(&gorm.Session{FullSaveAssociations: true}).
Delete(userData).
Error
}

+ 5
- 6
Backend/Database/Init.go View File

@ -9,18 +9,17 @@ import (
"gorm.io/gorm" "gorm.io/gorm"
) )
const dbUrl = "postgres://postgres:@localhost:5432/envelope"
const dbTestUrl = "postgres://postgres:@localhost:5432/envelope_test"
var (
DB *gorm.DB
const (
dbUrl = "postgres://postgres:@localhost:5432/envelope"
dbTestUrl = "postgres://postgres:@localhost:5432/envelope_test"
) )
var DB *gorm.DB
func GetModels() []interface{} { func GetModels() []interface{} {
return []interface{}{ return []interface{}{
&Models.Session{}, &Models.Session{},
&Models.User{}, &Models.User{},
&Models.Friend{},
&Models.FriendRequest{}, &Models.FriendRequest{},
&Models.MessageData{}, &Models.MessageData{},
&Models.Message{}, &Models.Message{},


+ 21
- 15
Backend/Database/Seeder/FriendSeeder.go View File

@ -11,36 +11,42 @@ import (
func seedFriend(userRequestTo, userRequestFrom Models.User) error { func seedFriend(userRequestTo, userRequestFrom Models.User) error {
var ( var (
friendRequest Models.FriendRequest friendRequest Models.FriendRequest
decodedID []byte
id []byte
decodedSymKey []byte
symKey []byte
symKey aesKey
encPublicKey []byte
err error err error
) )
decodedID, err = base64.StdEncoding.DecodeString(userRequestFrom.FriendID)
if err != nil {
return err
}
id, err = decryptWithPrivateKey(decodedID, decodedPrivateKey)
symKey, err = generateAesKey()
if err != nil { if err != nil {
return err return err
} }
decodedSymKey, err = base64.StdEncoding.DecodeString(userRequestFrom.FriendSymmetricKey)
encPublicKey, err = symKey.aesEncrypt([]byte(publicKey))
if err != nil { if err != nil {
return err return err
} }
symKey, err = decryptWithPrivateKey(decodedSymKey, decodedPrivateKey)
friendRequest = Models.FriendRequest{ friendRequest = Models.FriendRequest{
UserID: userRequestTo.ID,
AcceptedAt: time.Now(),
UserID: userRequestTo.ID,
UserUsername: userRequestTo.Username,
AcceptedAt: time.Now(),
FriendID: base64.StdEncoding.EncodeToString( FriendID: base64.StdEncoding.EncodeToString(
encryptWithPublicKey(id, decodedPublicKey),
encryptWithPublicKey(
[]byte(userRequestFrom.ID.String()),
decodedPublicKey,
),
),
FriendUsername: base64.StdEncoding.EncodeToString(
encryptWithPublicKey(
[]byte(userRequestFrom.Username),
decodedPublicKey,
),
),
FriendPublicAsymmetricKey: base64.StdEncoding.EncodeToString(
encPublicKey,
), ),
SymmetricKey: base64.StdEncoding.EncodeToString( SymmetricKey: base64.StdEncoding.EncodeToString(
encryptWithPublicKey(symKey, decodedPublicKey),
encryptWithPublicKey(symKey.Key, decodedPublicKey),
), ),
} }


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

@ -24,7 +24,6 @@ func seedMessage(
plaintext string plaintext string
dataCiphertext []byte dataCiphertext []byte
senderIdCiphertext []byte senderIdCiphertext []byte
friendId []byte
err error err error
) )
@ -45,31 +44,13 @@ func seedMessage(
panic(err) panic(err)
} }
friendId, err = base64.StdEncoding.DecodeString(primaryUser.FriendID)
if err != nil {
panic(err)
}
friendId, err = decryptWithPrivateKey(friendId, decodedPrivateKey)
if err != nil {
panic(err)
}
senderIdCiphertext, err = key.aesEncrypt(friendId)
senderIdCiphertext, err = key.aesEncrypt([]byte(primaryUser.ID.String()))
if err != nil { if err != nil {
panic(err) panic(err)
} }
if i%2 == 0 { if i%2 == 0 {
friendId, err = base64.StdEncoding.DecodeString(secondaryUser.FriendID)
if err != nil {
panic(err)
}
friendId, err = decryptWithPrivateKey(friendId, decodedPrivateKey)
if err != nil {
panic(err)
}
senderIdCiphertext, err = key.aesEncrypt(friendId)
senderIdCiphertext, err = key.aesEncrypt([]byte(secondaryUser.ID.String()))
if err != nil { if err != nil {
panic(err) panic(err)
} }
@ -198,9 +179,8 @@ func SeedMessages() {
primaryUserAssociationKey string primaryUserAssociationKey string
secondaryUser Models.User secondaryUser Models.User
secondaryUserAssociationKey string secondaryUserAssociationKey string
primaryUserFriendId []byte
secondaryUserFriendId []byte
userJson string userJson string
id1, id2 uuid.UUID
i int i int
err error err error
) )
@ -233,20 +213,11 @@ func SeedMessages() {
key, key,
) )
primaryUserFriendId, err = base64.StdEncoding.DecodeString(primaryUser.FriendID)
if err != nil {
panic(err)
}
primaryUserFriendId, err = decryptWithPrivateKey(primaryUserFriendId, decodedPrivateKey)
if err != nil {
panic(err)
}
secondaryUserFriendId, err = base64.StdEncoding.DecodeString(secondaryUser.FriendID)
id1, err = uuid.NewV4()
if err != nil { if err != nil {
panic(err) panic(err)
} }
secondaryUserFriendId, err = decryptWithPrivateKey(secondaryUserFriendId, decodedPrivateKey)
id2, err = uuid.NewV4()
if err != nil { if err != nil {
panic(err) panic(err)
} }
@ -256,22 +227,26 @@ func SeedMessages() {
[ [
{ {
"id": "%s", "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",
"username": "%s", "username": "%s",
"admin": "false", "admin": "false",
"association_key": "%s" "association_key": "%s"
} }
] ]
`, `,
string(primaryUserFriendId),
id1.String(),
primaryUser.ID.String(),
primaryUser.Username, primaryUser.Username,
primaryUserAssociationKey, primaryUserAssociationKey,
string(secondaryUserFriendId),
id2.String(),
secondaryUser.ID.String(),
secondaryUser.Username, secondaryUser.Username,
secondaryUserAssociationKey, secondaryUserAssociationKey,
) )


+ 3
- 37
Backend/Database/Seeder/UserSeeder.go View File

@ -1,8 +1,6 @@
package Seeder package Seeder
import ( import (
"encoding/base64"
"git.tovijaeschke.xyz/tovi/Envelope/Backend/Api/Auth" "git.tovijaeschke.xyz/tovi/Envelope/Backend/Api/Auth"
"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"
@ -24,12 +22,9 @@ var userNames = []string{
func createUser(username string) (Models.User, error) { func createUser(username string) (Models.User, error) {
var ( var (
userData Models.User
key aesKey
publicUserData Models.Friend
password string
usernameCiphertext []byte
err error
userData Models.User
password string
err error
) )
password, err = Auth.HashPassword("password") password, err = Auth.HashPassword("password")
@ -37,40 +32,11 @@ func createUser(username string) (Models.User, error) {
return Models.User{}, err return Models.User{}, err
} }
key, err = generateAesKey()
if err != nil {
return Models.User{}, err
}
usernameCiphertext, err = key.aesEncrypt([]byte(username))
if err != nil {
return Models.User{}, err
}
publicUserData = Models.Friend{
Username: base64.StdEncoding.EncodeToString(usernameCiphertext),
AsymmetricPublicKey: publicKey,
}
err = Database.CreateFriend(&publicUserData)
if err != nil {
return userData, err
}
userData = Models.User{ userData = Models.User{
Username: username, Username: username,
Password: password, Password: password,
AsymmetricPrivateKey: encryptedPrivateKey, AsymmetricPrivateKey: encryptedPrivateKey,
AsymmetricPublicKey: publicKey, AsymmetricPublicKey: publicKey,
FriendID: base64.StdEncoding.EncodeToString(
encryptWithPublicKey(
[]byte(publicUserData.ID.String()),
decodedPublicKey,
),
),
FriendSymmetricKey: base64.StdEncoding.EncodeToString(
encryptWithPublicKey(key.Key, decodedPublicKey),
),
} }
err = Database.CreateUser(&userData) err = Database.CreateUser(&userData)


+ 16
- 4
Backend/Database/UserConversations.go View File

@ -30,20 +30,32 @@ func GetUserConversationsByUserId(id string) ([]Models.UserConversation, error)
return conversations, err return conversations, err
} }
func CreateUserConversation(messageThreadUser *Models.UserConversation) error {
func CreateUserConversation(userConversation *Models.UserConversation) error {
var ( var (
err error err error
) )
err = DB.Session(&gorm.Session{FullSaveAssociations: true}). err = DB.Session(&gorm.Session{FullSaveAssociations: true}).
Create(messageThreadUser).
Create(userConversation).
Error Error
return err return err
} }
func DeleteUserConversation(messageThreadUser *Models.UserConversation) error {
func CreateUserConversations(userConversations *[]Models.UserConversation) error {
var (
err error
)
err = DB.Session(&gorm.Session{FullSaveAssociations: true}).
Create(userConversations).
Error
return err
}
func DeleteUserConversation(userConversation *Models.UserConversation) error {
return DB.Session(&gorm.Session{FullSaveAssociations: true}). return DB.Session(&gorm.Session{FullSaveAssociations: true}).
Delete(messageThreadUser).
Delete(userConversation).
Error Error
} }

+ 8
- 12
Backend/Models/Friends.go View File

@ -6,19 +6,15 @@ import (
"github.com/gofrs/uuid" "github.com/gofrs/uuid"
) )
// TODO: Add profile picture
type Friend struct {
Base
Username string `gorm:"not null" json:"username"` // Stored encrypted
AsymmetricPublicKey string `gorm:"not null" json:"asymmetric_public_key"` // Stored encrypted
}
// Set with Friend being the requestee, and RequestFromID being the requester // Set with Friend being the requestee, and RequestFromID being the requester
type FriendRequest struct { type FriendRequest struct {
Base Base
UserID uuid.UUID `gorm:"type:uuid;column:user_id;not null;" json:"user_id"`
User User `json:"user"`
FriendID string `gorm:"not null" json:"friend_id"` // Stored encrypted
SymmetricKey string `gorm:"not null" json:"symmetric_key"` // Stored encrypted
AcceptedAt time.Time `json:"accepted_at"`
UserID uuid.UUID `gorm:"type:uuid;column:user_id;not null;" json:"user_id"`
User User ` json:"user"`
UserUsername string ` json:"user_username"`
FriendID string `gorm:"not null" json:"friend_id"` // Stored encrypted
FriendUsername string ` json:"friend_username"`
FriendPublicAsymmetricKey string ` json:"asymmetric_public_key"`
SymmetricKey string `gorm:"not null" json:"symmetric_key"` // Stored encrypted
AcceptedAt time.Time ` json:"accepted_at"`
} }

+ 4
- 6
Backend/Models/Users.go View File

@ -16,10 +16,8 @@ func (u *User) BeforeUpdate(tx *gorm.DB) (err error) {
type User struct { type User struct {
Base Base
Username string `gorm:"not null;unique" json:"username"` Username string `gorm:"not null;unique" json:"username"`
Password string `gorm:"not null" json:"password"`
ConfirmPassword string `gorm:"-" json:"confirm_password"`
AsymmetricPrivateKey string `gorm:"not null" json:"asymmetric_private_key"` // Stored encrypted
AsymmetricPublicKey string `gorm:"not null" json:"asymmetric_public_key"`
FriendID string `gorm:"not null" json:"public_user_id"` // Stored encrypted
FriendSymmetricKey string `gorm:"not null" json:"symmetric_key"` // Stored encrypted
Password string `gorm:"not null" json:"password"`
ConfirmPassword string `gorm:"-" json:"confirm_password"`
AsymmetricPrivateKey string `gorm:"not null" json:"asymmetric_private_key"` // Stored encrypted
AsymmetricPublicKey string `gorm:"not null" json:"asymmetric_public_key"`
} }

+ 19
- 25
mobile/lib/models/conversation_users.dart View File

@ -3,12 +3,14 @@ import '/models/conversations.dart';
class ConversationUser{ class ConversationUser{
String id; String id;
String userId;
String conversationId; String conversationId;
String username; String username;
String associationKey; String associationKey;
bool admin; bool admin;
ConversationUser({ ConversationUser({
required this.id, required this.id,
required this.userId,
required this.conversationId, required this.conversationId,
required this.username, required this.username,
required this.associationKey, required this.associationKey,
@ -18,6 +20,7 @@ class ConversationUser{
factory ConversationUser.fromJson(Map<String, dynamic> json, String conversationId) { factory ConversationUser.fromJson(Map<String, dynamic> json, String conversationId) {
return ConversationUser( return ConversationUser(
id: json['id'], id: json['id'],
userId: json['user_id'],
conversationId: conversationId, conversationId: conversationId,
username: json['username'], username: json['username'],
associationKey: json['association_key'], associationKey: json['association_key'],
@ -25,9 +28,20 @@ class ConversationUser{
); );
} }
Map<String, dynamic> toJson() {
return {
'id': id,
'user_id': userId,
'username': username,
'associationKey': associationKey,
'admin': admin ? 'true' : 'false',
};
}
Map<String, dynamic> toMap() { Map<String, dynamic> toMap() {
return { return {
'id': id, 'id': id,
'user_id': userId,
'conversation_id': conversationId, 'conversation_id': conversationId,
'username': username, 'username': username,
'association_key': associationKey, 'association_key': associationKey,
@ -50,6 +64,7 @@ Future<List<ConversationUser>> getConversationUsers(Conversation conversation) a
return List.generate(maps.length, (i) { return List.generate(maps.length, (i) {
return ConversationUser( return ConversationUser(
id: maps[i]['id'], id: maps[i]['id'],
userId: maps[i]['user_id'],
conversationId: maps[i]['conversation_id'], conversationId: maps[i]['conversation_id'],
username: maps[i]['username'], username: maps[i]['username'],
associationKey: maps[i]['association_key'], associationKey: maps[i]['association_key'],
@ -58,36 +73,14 @@ Future<List<ConversationUser>> getConversationUsers(Conversation conversation) a
}); });
} }
Future<ConversationUser> getConversationUserById(Conversation conversation, String id) 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 id = ?',
whereArgs: [conversation.id, id],
);
if (maps.length != 1) {
throw ArgumentError('Invalid conversation_id or id');
}
return ConversationUser(
id: maps[0]['id'],
conversationId: maps[0]['conversation_id'],
username: maps[0]['username'],
associationKey: maps[0]['association_key'],
admin: maps[0]['admin'] == 1,
);
}
Future<ConversationUser> getConversationUserByUsername(Conversation conversation, String username) async {
final db = await getDatabaseConnection();
final List<Map<String, dynamic>> maps = await db.query( final List<Map<String, dynamic>> maps = await db.query(
'conversation_users', 'conversation_users',
where: 'conversation_id = ? AND username = ?',
whereArgs: [conversation.id, username],
where: 'conversation_id = ? AND user_id = ?',
whereArgs: [conversation.id, userId],
); );
if (maps.length != 1) { if (maps.length != 1) {
@ -96,6 +89,7 @@ Future<ConversationUser> getConversationUserByUsername(Conversation conversation
return ConversationUser( return ConversationUser(
id: maps[0]['id'], id: maps[0]['id'],
userId: maps[0]['user_id'],
conversationId: maps[0]['conversation_id'], conversationId: maps[0]['conversation_id'],
username: maps[0]['username'], username: maps[0]['username'],
associationKey: maps[0]['association_key'], associationKey: maps[0]['association_key'],


+ 136
- 47
mobile/lib/models/conversations.dart View File

@ -11,14 +11,10 @@ import '/utils/encryption/aes_helper.dart';
import '/utils/storage/database.dart'; import '/utils/storage/database.dart';
import '/utils/strings.dart'; import '/utils/strings.dart';
Conversation findConversationByDetailId(List<Conversation> conversations, String id) {
for (var conversation in conversations) {
if (conversation.conversationDetailId == id) {
return conversation;
}
}
// Or return `null`.
throw ArgumentError.value(id, "id", "No element with that id");
enum ConversationStatus {
complete,
pending,
error,
} }
class Conversation { class Conversation {
@ -28,6 +24,7 @@ class Conversation {
String symmetricKey; String symmetricKey;
bool admin; bool admin;
String name; String name;
ConversationStatus status;
Conversation({ Conversation({
required this.id, required this.id,
@ -36,35 +33,86 @@ class Conversation {
required this.symmetricKey, required this.symmetricKey,
required this.admin, required this.admin,
required this.name, required this.name,
required this.status,
}); });
factory Conversation.fromJson(Map<String, dynamic> json, RSAPrivateKey privKey) { factory Conversation.fromJson(Map<String, dynamic> json, RSAPrivateKey privKey) {
var symmetricKeyDecrypted = CryptoUtils.rsaDecrypt( var symmetricKeyDecrypted = CryptoUtils.rsaDecrypt(
base64.decode(json['symmetric_key']),
privKey,
base64.decode(json['symmetric_key']),
privKey,
); );
var detailId = AesHelper.aesDecrypt( var detailId = AesHelper.aesDecrypt(
symmetricKeyDecrypted,
base64.decode(json['conversation_detail_id']),
symmetricKeyDecrypted,
base64.decode(json['conversation_detail_id']),
); );
var admin = AesHelper.aesDecrypt( var admin = AesHelper.aesDecrypt(
symmetricKeyDecrypted,
base64.decode(json['admin']),
symmetricKeyDecrypted,
base64.decode(json['admin']),
); );
return Conversation( return Conversation(
id: json['id'],
userId: json['user_id'],
conversationDetailId: detailId,
symmetricKey: base64.encode(symmetricKeyDecrypted),
admin: admin == 'true',
name: 'Unknown',
id: json['id'],
userId: json['user_id'],
conversationDetailId: detailId,
symmetricKey: base64.encode(symmetricKeyDecrypted),
admin: admin == 'true',
name: 'Unknown',
status: ConversationStatus.complete,
); );
} }
Future<Map<String, dynamic>> toJson() async {
MyProfile profile = await MyProfile.getProfile();
var symKey = base64.decode(symmetricKey);
List<ConversationUser> users = await getConversationUsers(this);
List<Object> userConversations = [];
for (var x in users) {
print(x.toMap());
}
for (ConversationUser user in users) {
if (profile.id == user.userId) {
userConversations.add({
'id': user.id,
'user_id': profile.id,
'conversation_detail_id': AesHelper.aesEncrypt(symKey, Uint8List.fromList(id.codeUnits)),
'admin': AesHelper.aesEncrypt(symKey, Uint8List.fromList((admin ? 'true' : 'false').codeUnits)),
'symmetric_key': base64.encode(CryptoUtils.rsaEncrypt(symKey, profile.publicKey!)),
});
continue;
}
Friend friend = await getFriendByFriendId(user.userId);
RSAPublicKey pubKey = CryptoUtils.rsaPublicKeyFromPem(friend.asymmetricPublicKey);
userConversations.add({
'id': user.id,
'user_id': friend.userId,
'conversation_detail_id': AesHelper.aesEncrypt(symKey, Uint8List.fromList(id.codeUnits)),
'admin': AesHelper.aesEncrypt(symKey, Uint8List.fromList((admin ? 'true' : 'false').codeUnits)),
'symmetric_key': base64.encode(CryptoUtils.rsaEncrypt(symKey, pubKey)),
});
}
for (var x in userConversations) {
print(x);
}
return {
'id': id,
'name': AesHelper.aesEncrypt(symKey, Uint8List.fromList(name.codeUnits)),
'users': AesHelper.aesEncrypt(symKey, Uint8List.fromList(jsonEncode(users).codeUnits)),
'user_conversations': userConversations,
};
}
@override @override
String toString() { String toString() {
return ''' return '''
@ -84,6 +132,7 @@ class Conversation {
'symmetric_key': symmetricKey, 'symmetric_key': symmetricKey,
'admin': admin ? 1 : 0, 'admin': admin ? 1 : 0,
'name': name, 'name': name,
'status': status.index,
}; };
} }
} }
@ -107,43 +156,57 @@ Future<Conversation> createConversation(String title, List<Friend> friends) asyn
symmetricKey: base64.encode(symmetricKey), symmetricKey: base64.encode(symmetricKey),
admin: true, admin: true,
name: title, name: title,
status: ConversationStatus.pending,
); );
await db.insert( await db.insert(
'conversations',
conversation.toMap(),
conflictAlgorithm: ConflictAlgorithm.replace,
'conversations',
conversation.toMap(),
conflictAlgorithm: ConflictAlgorithm.replace,
); );
await db.insert( await db.insert(
'conversation_users',
ConversationUser(
id: uuid.v4(),
conversationId: conversationId,
username: profile.username,
associationKey: associationKey,
admin: false,
).toMap(),
conflictAlgorithm: ConflictAlgorithm.replace,
'conversation_users',
ConversationUser(
id: uuid.v4(),
userId: profile.id,
conversationId: conversationId,
username: profile.username,
associationKey: associationKey,
admin: true,
).toMap(),
conflictAlgorithm: ConflictAlgorithm.replace,
); );
for (Friend friend in friends) { for (Friend friend in friends) {
await db.insert( await db.insert(
'conversation_users',
ConversationUser(
id: uuid.v4(),
conversationId: conversationId,
username: friend.username,
associationKey: associationKey,
admin: false,
).toMap(),
conflictAlgorithm: ConflictAlgorithm.replace,
'conversation_users',
ConversationUser(
id: uuid.v4(),
userId: friend.friendId,
conversationId: conversationId,
username: friend.username,
associationKey: associationKey,
admin: false,
).toMap(),
conflictAlgorithm: ConflictAlgorithm.replace,
); );
} }
return conversation; return conversation;
} }
Conversation findConversationByDetailId(List<Conversation> conversations, String id) {
for (var conversation in conversations) {
if (conversation.conversationDetailId == id) {
return conversation;
}
}
// Or return `null`.
throw ArgumentError.value(id, "id", "No element with that id");
}
// A method that retrieves all the dogs from the dogs table. // A method that retrieves all the dogs from the dogs table.
Future<List<Conversation>> getConversations() async { Future<List<Conversation>> getConversations() async {
final db = await getDatabaseConnection(); final db = await getDatabaseConnection();
@ -152,12 +215,38 @@ Future<List<Conversation>> getConversations() async {
return List.generate(maps.length, (i) { return List.generate(maps.length, (i) {
return Conversation( 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'],
id: maps[i]['id'],
userId: maps[i]['user_id'],
conversationDetailId: maps[i]['conversation_detail_id'],
symmetricKey: maps[i]['symmetric_key'],
admin: maps[i]['admin'] == 1,
name: maps[i]['name'],
status: ConversationStatus.values[maps[i]['status']],
); );
}); });
} }
Future<Conversation> getConversationById(String id) async {
final db = await getDatabaseConnection();
final List<Map<String, dynamic>> maps = await db.query(
'conversations',
where: 'id = ?',
whereArgs: [id],
);
if (maps.length != 1) {
throw ArgumentError('Invalid user id');
}
return Conversation(
id: maps[0]['id'],
userId: maps[0]['user_id'],
conversationDetailId: maps[0]['conversation_detail_id'],
symmetricKey: maps[0]['symmetric_key'],
admin: maps[0]['admin'] == 1,
name: maps[0]['name'],
status: ConversationStatus.values[maps[0]['status']],
);
}

+ 34
- 24
mobile/lib/models/friends.dart View File

@ -1,5 +1,7 @@
import 'dart:convert'; import 'dart:convert';
import 'dart:typed_data';
import "package:pointycastle/export.dart"; import "package:pointycastle/export.dart";
import '../utils/encryption/aes_helper.dart';
import '/utils/encryption/crypto_utils.dart'; import '/utils/encryption/crypto_utils.dart';
import '/utils/storage/database.dart'; import '/utils/storage/database.dart';
@ -34,23 +36,33 @@ class Friend{
}); });
factory Friend.fromJson(Map<String, dynamic> json, RSAPrivateKey privKey) { factory Friend.fromJson(Map<String, dynamic> json, RSAPrivateKey privKey) {
var friendIdDecrypted = CryptoUtils.rsaDecrypt(
Uint8List friendIdDecrypted = CryptoUtils.rsaDecrypt(
base64.decode(json['friend_id']), base64.decode(json['friend_id']),
privKey, privKey,
); );
var friendSymmetricKeyDecrypted = CryptoUtils.rsaDecrypt(
Uint8List friendUsername = CryptoUtils.rsaDecrypt(
base64.decode(json['friend_username']),
privKey,
);
Uint8List friendSymmetricKeyDecrypted = CryptoUtils.rsaDecrypt(
base64.decode(json['symmetric_key']), base64.decode(json['symmetric_key']),
privKey, privKey,
); );
String asymmetricPublicKey = AesHelper.aesDecrypt(
friendSymmetricKeyDecrypted,
base64.decode(json['asymmetric_public_key'])
);
return Friend( return Friend(
id: json['id'], id: json['id'],
userId: json['user_id'], userId: json['user_id'],
username: '',
username: String.fromCharCodes(friendUsername),
friendId: String.fromCharCodes(friendIdDecrypted), friendId: String.fromCharCodes(friendIdDecrypted),
friendSymmetricKey: base64.encode(friendSymmetricKeyDecrypted), friendSymmetricKey: base64.encode(friendSymmetricKeyDecrypted),
asymmetricPublicKey: '',
asymmetricPublicKey: asymmetricPublicKey,
acceptedAt: json['accepted_at'], acceptedAt: json['accepted_at'],
); );
} }
@ -101,27 +113,25 @@ Future<List<Friend>> getFriends() async {
} }
Future<Friend> getFriendByFriendId(String userId) async { Future<Friend> getFriendByFriendId(String userId) async {
final db = await getDatabaseConnection();
List<dynamic> whereArguments = [userId];
final db = await getDatabaseConnection();
final List<Map<String, dynamic>> maps = await db.query(
'friends',
where: 'friend_id = ?',
whereArgs: whereArguments,
);
final List<Map<String, dynamic>> maps = await db.query(
'friends',
where: 'friend_id = ?',
whereArgs: [userId],
);
if (maps.length != 1) {
throw ArgumentError('Invalid user id');
}
if (maps.length != 1) {
throw ArgumentError('Invalid user id');
}
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'],
);
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'],
);
} }

+ 8
- 3
mobile/lib/models/my_profile.dart View File

@ -7,6 +7,7 @@ import 'package:shared_preferences/shared_preferences.dart';
class MyProfile { class MyProfile {
String id; String id;
String username; String username;
String? friendId;
RSAPrivateKey? privateKey; RSAPrivateKey? privateKey;
RSAPublicKey? publicKey; RSAPublicKey? publicKey;
DateTime? loggedInAt; DateTime? loggedInAt;
@ -14,6 +15,7 @@ class MyProfile {
MyProfile({ MyProfile({
required this.id, required this.id,
required this.username, required this.username,
this.friendId,
this.privateKey, this.privateKey,
this.publicKey, this.publicKey,
this.loggedInAt, this.loggedInAt,
@ -25,11 +27,14 @@ class MyProfile {
loggedInAt = DateTime.parse(json['logged_in_at']); loggedInAt = DateTime.parse(json['logged_in_at']);
} }
RSAPrivateKey privateKey = CryptoUtils.rsaPrivateKeyFromPem(json['asymmetric_private_key']);
RSAPublicKey publicKey = CryptoUtils.rsaPublicKeyFromPem(json['asymmetric_public_key']);
return MyProfile( return MyProfile(
id: json['user_id'], id: json['user_id'],
username: json['username'], username: json['username'],
privateKey: CryptoUtils.rsaPrivateKeyFromPem(json['asymmetric_private_key']),
publicKey: CryptoUtils.rsaPublicKeyFromPem(json['asymmetric_public_key']),
privateKey: privateKey,
publicKey: publicKey,
loggedInAt: loggedInAt, loggedInAt: loggedInAt,
); );
} }
@ -95,7 +100,7 @@ class MyProfile {
); );
} }
Future<RSAPrivateKey> getPrivateKey() async {
static Future<RSAPrivateKey> getPrivateKey() async {
MyProfile profile = await MyProfile.getProfile(); MyProfile profile = await MyProfile.getProfile();
if (profile.privateKey == null) { if (profile.privateKey == null) {
throw Exception('Could not get privateKey'); throw Exception('Could not get privateKey');


+ 88
- 68
mobile/lib/utils/storage/conversations.dart View File

@ -3,97 +3,117 @@ import 'package:http/http.dart' as http;
import 'package:flutter_dotenv/flutter_dotenv.dart'; import 'package:flutter_dotenv/flutter_dotenv.dart';
import 'package:pointycastle/export.dart'; import 'package:pointycastle/export.dart';
import 'package:sqflite/sqflite.dart'; import 'package:sqflite/sqflite.dart';
import '/models/my_profile.dart';
import '/models/conversations.dart'; import '/models/conversations.dart';
import '/models/conversation_users.dart'; import '/models/conversation_users.dart';
import '/utils/storage/database.dart'; import '/utils/storage/database.dart';
import '/utils/storage/session_cookie.dart'; import '/utils/storage/session_cookie.dart';
import '/utils/storage/encryption_keys.dart';
import '/utils/encryption/aes_helper.dart'; import '/utils/encryption/aes_helper.dart';
// TODO: Refactor this function
Future<void> updateConversations() async { Future<void> updateConversations() async {
RSAPrivateKey privKey = await getPrivateKey();
try {
var resp = await http.get(
Uri.parse('${dotenv.env["SERVER_URL"]}api/v1/auth/conversations'),
headers: {
'cookie': await getSessionCookie(),
}
);
RSAPrivateKey privKey = await MyProfile.getPrivateKey();
if (resp.statusCode != 200) {
throw Exception(resp.body);
try {
var resp = await http.get(
Uri.parse('${dotenv.env["SERVER_URL"]}api/v1/auth/conversations'),
headers: {
'cookie': await getSessionCookie(),
} }
);
List<Conversation> conversations = [];
List<String> conversationsDetailIds = [];
List<dynamic> conversationsJson = jsonDecode(resp.body);
if (resp.statusCode != 200) {
throw Exception(resp.body);
}
for (var i = 0; i < conversationsJson.length; i++) {
Conversation conversation = Conversation.fromJson(
conversationsJson[i] as Map<String, dynamic>,
privKey,
);
conversations.add(conversation);
conversationsDetailIds.add(conversation.conversationDetailId);
}
List<Conversation> conversations = [];
List<String> conversationsDetailIds = [];
Map<String, String> params = {};
params['conversation_detail_ids'] = conversationsDetailIds.join(',');
var uri = Uri.parse('${dotenv.env["SERVER_URL"]}api/v1/auth/conversation_details');
uri = uri.replace(queryParameters: params);
List<dynamic> conversationsJson = jsonDecode(resp.body);
resp = await http.get(
uri,
headers: {
'cookie': await getSessionCookie(),
}
for (var i = 0; i < conversationsJson.length; i++) {
Conversation conversation = Conversation.fromJson(
conversationsJson[i] as Map<String, dynamic>,
privKey,
); );
conversations.add(conversation);
conversationsDetailIds.add(conversation.conversationDetailId);
}
if (resp.statusCode != 200) {
throw Exception(resp.body);
Map<String, String> params = {};
params['conversation_detail_ids'] = conversationsDetailIds.join(',');
var uri = Uri.parse('${dotenv.env["SERVER_URL"]}api/v1/auth/conversation_details');
uri = uri.replace(queryParameters: params);
resp = await http.get(
uri,
headers: {
'cookie': await getSessionCookie(),
} }
);
final db = await getDatabaseConnection();
if (resp.statusCode != 200) {
throw Exception(resp.body);
}
List<dynamic> conversationsDetailsJson = jsonDecode(resp.body);
for (var i = 0; i < conversationsDetailsJson.length; i++) {
var conversationDetailJson = conversationsDetailsJson[i] as Map<String, dynamic>;
var conversation = findConversationByDetailId(conversations, conversationDetailJson['id']);
final db = await getDatabaseConnection();
conversation.name = AesHelper.aesDecrypt(
base64.decode(conversation.symmetricKey),
base64.decode(conversationDetailJson['name']),
);
List<dynamic> conversationsDetailsJson = jsonDecode(resp.body);
for (var i = 0; i < conversationsDetailsJson.length; i++) {
var conversationDetailJson = conversationsDetailsJson[i] as Map<String, dynamic>;
var conversation = findConversationByDetailId(conversations, conversationDetailJson['id']);
await db.insert(
'conversations',
conversation.toMap(),
conflictAlgorithm: ConflictAlgorithm.replace,
);
conversation.name = AesHelper.aesDecrypt(
base64.decode(conversation.symmetricKey),
base64.decode(conversationDetailJson['name']),
);
List<dynamic> usersData = json.decode(
AesHelper.aesDecrypt(
base64.decode(conversation.symmetricKey),
base64.decode(conversationDetailJson['users']),
)
await db.insert(
'conversations',
conversation.toMap(),
conflictAlgorithm: ConflictAlgorithm.replace,
);
List<dynamic> usersData = json.decode(
AesHelper.aesDecrypt(
base64.decode(conversation.symmetricKey),
base64.decode(conversationDetailJson['users']),
)
);
for (var i = 0; i < usersData.length; i++) {
ConversationUser conversationUser = ConversationUser.fromJson(
usersData[i] as Map<String, dynamic>,
conversation.id,
); );
for (var i = 0; i < usersData.length; i++) {
ConversationUser conversationUser = ConversationUser.fromJson(
usersData[i] as Map<String, dynamic>,
conversation.id,
);
await db.insert(
'conversation_users',
conversationUser.toMap(),
conflictAlgorithm: ConflictAlgorithm.replace,
);
}
await db.insert(
'conversation_users',
conversationUser.toMap(),
conflictAlgorithm: ConflictAlgorithm.replace,
);
} }
} catch (SocketException) {
return;
} }
} catch (SocketException) {
return;
}
}
Future<void> uploadConversation(Conversation conversation) async {
String sessionCookie = await getSessionCookie();
Map<String, dynamic> conversationJson = await conversation.toJson();
print(conversationJson);
// var x = await http.post(
// 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),
// );
// print(x.statusCode);
} }

+ 3
- 1
mobile/lib/utils/storage/database.dart View File

@ -39,6 +39,7 @@ Future<Database> getDatabaseConnection() async {
symmetric_key TEXT, symmetric_key TEXT,
admin INTEGER, admin INTEGER,
name TEXT, name TEXT,
status INTEGER
); );
'''); ''');
@ -46,6 +47,7 @@ Future<Database> getDatabaseConnection() async {
''' '''
CREATE TABLE IF NOT EXISTS conversation_users( CREATE TABLE IF NOT EXISTS conversation_users(
id TEXT PRIMARY KEY, id TEXT PRIMARY KEY,
user_id TEXT,
conversation_id TEXT, conversation_id TEXT,
username TEXT, username TEXT,
data TEXT, data TEXT,
@ -72,7 +74,7 @@ Future<Database> getDatabaseConnection() async {
}, },
// Set the version. This executes the onCreate function and provides a // Set the version. This executes the onCreate function and provides a
// path to perform database upgrades and downgrades. // path to perform database upgrades and downgrades.
version: 1,
version: 2,
); );
return database; return database;


+ 0
- 27
mobile/lib/utils/storage/encryption_keys.dart View File

@ -1,27 +0,0 @@
import 'package:shared_preferences/shared_preferences.dart';
import "package:pointycastle/export.dart";
import '/utils/encryption/crypto_utils.dart';
const rsaPrivateKeyName = 'rsaPrivateKey';
void setPrivateKey(RSAPrivateKey key) async {
String keyPem = CryptoUtils.encodeRSAPrivateKeyToPem(key);
final prefs = await SharedPreferences.getInstance();
prefs.setString(rsaPrivateKeyName, keyPem);
}
void unsetPrivateKey() async {
final prefs = await SharedPreferences.getInstance();
await prefs.remove(rsaPrivateKeyName);
}
Future<RSAPrivateKey> getPrivateKey() async {
final prefs = await SharedPreferences.getInstance();
String? keyPem = prefs.getString(rsaPrivateKeyName);
if (keyPem == null) {
throw Exception('No RSA private key set');
}
return CryptoUtils.rsaPrivateKeyFromPem(keyPem);
}

+ 13
- 47
mobile/lib/utils/storage/friends.dart View File

@ -1,16 +1,17 @@
import 'dart:convert'; import 'dart:convert';
import 'package:flutter_dotenv/flutter_dotenv.dart';
import 'package:http/http.dart' as http; import 'package:http/http.dart' as http;
import 'package:pointycastle/export.dart'; import 'package:pointycastle/export.dart';
import 'package:sqflite/sqflite.dart'; import 'package:sqflite/sqflite.dart';
import 'package:flutter_dotenv/flutter_dotenv.dart';
import '/models/friends.dart'; import '/models/friends.dart';
import '/models/my_profile.dart';
import '/utils/storage/database.dart'; import '/utils/storage/database.dart';
import '/utils/storage/encryption_keys.dart';
import '/utils/storage/session_cookie.dart'; import '/utils/storage/session_cookie.dart';
import '/utils/encryption/aes_helper.dart';
Future<void> updateFriends() async { Future<void> updateFriends() async {
RSAPrivateKey privKey = await getPrivateKey();
RSAPrivateKey privKey = await MyProfile.getPrivateKey();
try { try {
var resp = await http.get( var resp = await http.get(
@ -24,59 +25,24 @@ Future<void> updateFriends() async {
throw Exception(resp.body); throw Exception(resp.body);
} }
List<Friend> friends = [];
List<String> friendIds = [];
final db = await getDatabaseConnection();
List<dynamic> friendsRequestJson = jsonDecode(resp.body); List<dynamic> friendsRequestJson = jsonDecode(resp.body);
for (var i = 0; i < friendsRequestJson.length; i++) { for (var i = 0; i < friendsRequestJson.length; i++) {
friends.add(
Friend.fromJson(
friendsRequestJson[i] as Map<String, dynamic>,
privKey,
)
);
friendIds.add(friends[i].friendId);
}
Map<String, String> params = {};
params['friend_ids'] = friendIds.join(',');
var uri = Uri.parse('${dotenv.env["SERVER_URL"]}api/v1/auth/friends');
uri = uri.replace(queryParameters: params);
resp = await http.get(
uri,
headers: {
'cookie': await getSessionCookie(),
}
);
if (resp.statusCode != 200) {
throw Exception(resp.body);
}
final db = await getDatabaseConnection();
List<dynamic> friendsJson = jsonDecode(resp.body);
for (var i = 0; i < friendsJson.length; i++) {
var friendJson = friendsJson[i] as Map<String, dynamic>;
var friend = findFriendByFriendId(friends, friendJson['id']);
friend.username = AesHelper.aesDecrypt(
base64.decode(friend.friendSymmetricKey),
base64.decode(friendJson['username']),
Friend friend = Friend.fromJson(
friendsRequestJson[i] as Map<String, dynamic>,
privKey,
); );
friend.asymmetricPublicKey = friendJson['asymmetric_public_key'];
await db.insert( await db.insert(
'friends',
friend.toMap(),
conflictAlgorithm: ConflictAlgorithm.replace,
'friends',
friend.toMap(),
conflictAlgorithm: ConflictAlgorithm.replace,
); );
} }
} catch (SocketException) { } catch (SocketException) {
return; return;
} }


+ 13
- 17
mobile/lib/utils/storage/messages.dart View File

@ -1,23 +1,19 @@
import 'dart:convert'; import 'dart:convert';
import 'package:Envelope/models/my_profile.dart';
import 'package:uuid/uuid.dart'; import 'package:uuid/uuid.dart';
import 'package:Envelope/models/conversation_users.dart'; import 'package:Envelope/models/conversation_users.dart';
import 'package:intl/intl.dart';
import 'package:http/http.dart' as http; import 'package:http/http.dart' as http;
import 'package:flutter_dotenv/flutter_dotenv.dart'; import 'package:flutter_dotenv/flutter_dotenv.dart';
import 'package:pointycastle/export.dart';
import 'package:sqflite/sqflite.dart'; import 'package:sqflite/sqflite.dart';
import 'package:shared_preferences/shared_preferences.dart'; import 'package:shared_preferences/shared_preferences.dart';
import '/utils/storage/session_cookie.dart'; import '/utils/storage/session_cookie.dart';
import '/utils/storage/encryption_keys.dart';
import '/utils/storage/database.dart'; import '/utils/storage/database.dart';
import '/models/conversations.dart'; import '/models/conversations.dart';
import '/models/messages.dart'; import '/models/messages.dart';
Future<void> updateMessageThread(Conversation conversation, {RSAPrivateKey? privKey}) async {
privKey ??= await getPrivateKey();
final preferences = await SharedPreferences.getInstance();
String username = preferences.getString('username')!;
ConversationUser currentUser = await getConversationUserByUsername(conversation, username);
Future<void> updateMessageThread(Conversation conversation, {MyProfile? profile}) async {
profile ??= await MyProfile.getProfile();
ConversationUser currentUser = await getConversationUser(conversation, profile.id);
var resp = await http.get( var resp = await http.get(
Uri.parse('${dotenv.env["SERVER_URL"]}api/v1/auth/messages/${currentUser.associationKey}'), Uri.parse('${dotenv.env["SERVER_URL"]}api/v1/auth/messages/${currentUser.associationKey}'),
@ -37,10 +33,10 @@ Future<void> updateMessageThread(Conversation conversation, {RSAPrivateKey? priv
for (var i = 0; i < messageThreadJson.length; i++) { for (var i = 0; i < messageThreadJson.length; i++) {
Message message = Message.fromJson( Message message = Message.fromJson(
messageThreadJson[i] as Map<String, dynamic>, messageThreadJson[i] as Map<String, dynamic>,
privKey,
profile.privateKey!,
); );
ConversationUser messageUser = await getConversationUserById(conversation, message.senderId);
ConversationUser messageUser = await getConversationUser(conversation, message.senderId);
message.senderUsername = messageUser.username; message.senderUsername = messageUser.username;
await db.insert( await db.insert(
@ -52,17 +48,17 @@ Future<void> updateMessageThread(Conversation conversation, {RSAPrivateKey? priv
} }
Future<void> updateMessageThreads({List<Conversation>? conversations}) async { Future<void> updateMessageThreads({List<Conversation>? conversations}) async {
try {
RSAPrivateKey privKey = await getPrivateKey();
// try {
MyProfile profile = await MyProfile.getProfile();
conversations ??= await getConversations(); conversations ??= await getConversations();
for (var i = 0; i < conversations.length; i++) { for (var i = 0; i < conversations.length; i++) {
await updateMessageThread(conversations[i], privKey: privKey);
await updateMessageThread(conversations[i], profile: profile);
} }
} catch(SocketException) {
return;
}
// } catch(SocketException) {
// return;
// }
} }
Future<void> sendMessage(Conversation conversation, String data) async { Future<void> sendMessage(Conversation conversation, String data) async {
@ -76,7 +72,7 @@ Future<void> sendMessage(Conversation conversation, String data) async {
var uuid = const Uuid(); var uuid = const Uuid();
final String messageDataId = uuid.v4(); final String messageDataId = uuid.v4();
ConversationUser currentUser = await getConversationUserByUsername(conversation, username);
ConversationUser currentUser = await getConversationUser(conversation, username);
Message message = Message( Message message = Message(
id: messageDataId, id: messageDataId,


+ 6
- 2
mobile/lib/views/main/conversation_create_add_users.dart View File

@ -1,9 +1,9 @@
import 'package:Envelope/models/conversations.dart'; import 'package:Envelope/models/conversations.dart';
import 'package:Envelope/utils/storage/conversations.dart';
import 'package:Envelope/views/main/conversation_create_add_users_list.dart'; import 'package:Envelope/views/main/conversation_create_add_users_list.dart';
import 'package:Envelope/views/main/conversation_detail.dart'; import 'package:Envelope/views/main/conversation_detail.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import '/models/friends.dart'; import '/models/friends.dart';
import '/views/main/friend_list_item.dart';
class ConversationAddFriendsList extends StatefulWidget { class ConversationAddFriendsList extends StatefulWidget {
final List<Friend> friends; final List<Friend> friends;
@ -150,8 +150,12 @@ class _ConversationAddFriendsListState extends State<ConversationAddFriendsList>
child: FloatingActionButton( child: FloatingActionButton(
onPressed: () async { onPressed: () async {
Conversation conversation = await createConversation(widget.title, friendsSelected); Conversation conversation = await createConversation(widget.title, friendsSelected);
uploadConversation(conversation);
setState(() {
friendsSelected = [];
});
friendsSelected = [];
Navigator.of(context).popUntil((route) => route.isFirst); Navigator.of(context).popUntil((route) => route.isFirst);
Navigator.push(context, MaterialPageRoute(builder: (context){ Navigator.push(context, MaterialPageRoute(builder: (context){
return ConversationDetail( return ConversationDetail(


+ 11
- 12
mobile/lib/views/main/conversation_detail.dart View File

@ -1,6 +1,6 @@
import 'package:Envelope/models/my_profile.dart';
import 'package:Envelope/views/main/conversation_settings.dart'; import 'package:Envelope/views/main/conversation_settings.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:shared_preferences/shared_preferences.dart';
import '/models/conversations.dart'; import '/models/conversations.dart';
import '/models/messages.dart'; import '/models/messages.dart';
import '/utils/storage/messages.dart'; import '/utils/storage/messages.dart';
@ -38,7 +38,7 @@ class ConversationDetail extends StatefulWidget{
class _ConversationDetailState extends State<ConversationDetail> { class _ConversationDetailState extends State<ConversationDetail> {
List<Message> messages = []; List<Message> messages = [];
String username = '';
MyProfile profile = MyProfile(id: '', username: '');
TextEditingController msgController = TextEditingController(); TextEditingController msgController = TextEditingController();
@ -49,14 +49,13 @@ class _ConversationDetailState extends State<ConversationDetail> {
} }
Future<void> fetchMessages() async { Future<void> fetchMessages() async {
final preferences = await SharedPreferences.getInstance();
username = preferences.getString('username')!;
profile = await MyProfile.getProfile();
messages = await getMessagesForThread(widget.conversation); messages = await getMessagesForThread(widget.conversation);
setState(() {}); setState(() {});
} }
Widget usernameOrFailedToSend(int index) { Widget usernameOrFailedToSend(int index) {
if (messages[index].senderUsername != username) {
if (messages[index].senderUsername != profile.username) {
return Text( return Text(
messages[index].senderUsername, messages[index].senderUsername,
style: TextStyle( style: TextStyle(
@ -152,12 +151,12 @@ class _ConversationDetailState extends State<ConversationDetail> {
padding: const EdgeInsets.only(left: 14,right: 14,top: 0,bottom: 0), padding: const EdgeInsets.only(left: 14,right: 14,top: 0,bottom: 0),
child: Align( child: Align(
alignment: ( alignment: (
messages[index].senderUsername == username ?
messages[index].senderUsername == profile.username ?
Alignment.topRight : Alignment.topRight :
Alignment.topLeft Alignment.topLeft
), ),
child: Column( child: Column(
crossAxisAlignment: messages[index].senderUsername == username ?
crossAxisAlignment: messages[index].senderUsername == profile.username ?
CrossAxisAlignment.end : CrossAxisAlignment.end :
CrossAxisAlignment.start, CrossAxisAlignment.start,
children: <Widget>[ children: <Widget>[
@ -165,7 +164,7 @@ class _ConversationDetailState extends State<ConversationDetail> {
decoration: BoxDecoration( decoration: BoxDecoration(
borderRadius: BorderRadius.circular(20), borderRadius: BorderRadius.circular(20),
color: ( color: (
messages[index].senderUsername == username ?
messages[index].senderUsername == profile.username ?
Theme.of(context).colorScheme.primary : Theme.of(context).colorScheme.primary :
Theme.of(context).colorScheme.tertiary Theme.of(context).colorScheme.tertiary
), ),
@ -175,7 +174,7 @@ class _ConversationDetailState extends State<ConversationDetail> {
messages[index].data, messages[index].data,
style: TextStyle( style: TextStyle(
fontSize: 15, fontSize: 15,
color: messages[index].senderUsername == username ?
color: messages[index].senderUsername == profile.username ?
Theme.of(context).colorScheme.onPrimary : Theme.of(context).colorScheme.onPrimary :
Theme.of(context).colorScheme.onTertiary, Theme.of(context).colorScheme.onTertiary,
) )
@ -183,7 +182,7 @@ class _ConversationDetailState extends State<ConversationDetail> {
), ),
const SizedBox(height: 1.5), const SizedBox(height: 1.5),
Row( Row(
mainAxisAlignment: messages[index].senderUsername == username ?
mainAxisAlignment: messages[index].senderUsername == profile.username ?
MainAxisAlignment.end : MainAxisAlignment.end :
MainAxisAlignment.start, MainAxisAlignment.start,
children: <Widget>[ children: <Widget>[
@ -193,14 +192,14 @@ class _ConversationDetailState extends State<ConversationDetail> {
), ),
const SizedBox(height: 1.5), const SizedBox(height: 1.5),
Row( Row(
mainAxisAlignment: messages[index].senderUsername == username ?
mainAxisAlignment: messages[index].senderUsername == profile.username ?
MainAxisAlignment.end : MainAxisAlignment.end :
MainAxisAlignment.start, MainAxisAlignment.start,
children: <Widget>[ children: <Widget>[
const SizedBox(width: 10), const SizedBox(width: 10),
Text( Text(
convertToAgo(messages[index].createdAt), convertToAgo(messages[index].createdAt),
textAlign: messages[index].senderUsername == username ?
textAlign: messages[index].senderUsername == profile.username ?
TextAlign.left : TextAlign.left :
TextAlign.right, TextAlign.right,
style: TextStyle( style: TextStyle(


+ 5
- 3
mobile/lib/views/main/conversation_list.dart View File

@ -35,11 +35,11 @@ class _ConversationListState extends State<ConversationList> {
if(query.isNotEmpty) { if(query.isNotEmpty) {
List<Conversation> dummyListData = []; List<Conversation> dummyListData = [];
dummySearchList.forEach((item) {
for (Conversation item in dummySearchList) {
if (item.name.toLowerCase().contains(query)) { if (item.name.toLowerCase().contains(query)) {
dummyListData.add(item); dummyListData.add(item);
} }
});
}
setState(() { setState(() {
conversations.clear(); conversations.clear();
conversations.addAll(dummyListData); conversations.addAll(dummyListData);
@ -135,7 +135,9 @@ class _ConversationListState extends State<ConversationList> {
); );
} }
onGoBack(dynamic value) {
onGoBack(dynamic value) async {
conversations = await getConversations();
friends = await getFriends();
setState(() {}); setState(() {});
} }
} }

+ 21
- 6
mobile/lib/views/main/conversation_list_item.dart View File

@ -15,27 +15,37 @@ class ConversationListItem extends StatefulWidget{
} }
class _ConversationListItemState extends State<ConversationListItem> { class _ConversationListItemState extends State<ConversationListItem> {
late Conversation conversation;
bool loaded = false;
@override
void initState() {
super.initState();
conversation = widget.conversation;
loaded = true;
setState(() {});
}
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return GestureDetector( return GestureDetector(
behavior: HitTestBehavior.opaque, behavior: HitTestBehavior.opaque,
onTap: () { onTap: () {
Navigator.push(context, MaterialPageRoute(builder: (context){
loaded ? Navigator.push(context, MaterialPageRoute(builder: (context){
return ConversationDetail( return ConversationDetail(
conversation: widget.conversation,
conversation: conversation,
); );
}));
})).then(onGoBack) : null;
}, },
child: Container( child: Container(
padding: const EdgeInsets.only(left: 16,right: 16,top: 10,bottom: 10), padding: const EdgeInsets.only(left: 16,right: 16,top: 10,bottom: 10),
child: Row(
child: !loaded ? null : Row(
children: <Widget>[ children: <Widget>[
Expanded( Expanded(
child: Row( child: Row(
children: <Widget>[ children: <Widget>[
CustomCircleAvatar( CustomCircleAvatar(
initials: widget.conversation.name[0].toUpperCase(),
initials: conversation.name[0].toUpperCase(),
imagePath: null, imagePath: null,
), ),
const SizedBox(width: 16), const SizedBox(width: 16),
@ -48,7 +58,7 @@ class _ConversationListItemState extends State<ConversationListItem> {
crossAxisAlignment: CrossAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start,
children: <Widget>[ children: <Widget>[
Text( Text(
widget.conversation.name,
conversation.name,
style: const TextStyle(fontSize: 16) style: const TextStyle(fontSize: 16)
), ),
//Text(widget.messageText,style: TextStyle(fontSize: 13,color: Colors.grey.shade600, fontWeight: widget.isMessageRead?FontWeight.bold:FontWeight.normal),), //Text(widget.messageText,style: TextStyle(fontSize: 13,color: Colors.grey.shade600, fontWeight: widget.isMessageRead?FontWeight.bold:FontWeight.normal),),
@ -65,4 +75,9 @@ class _ConversationListItemState extends State<ConversationListItem> {
), ),
); );
} }
onGoBack(dynamic value) async {
conversation = await getConversationById(widget.conversation.id);
setState(() {});
}
} }

+ 1
- 1
mobile/lib/views/main/home.dart View File

@ -1,5 +1,4 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:shared_preferences/shared_preferences.dart';
import 'package:http/http.dart' as http; import 'package:http/http.dart' as http;
import 'package:flutter_dotenv/flutter_dotenv.dart'; import 'package:flutter_dotenv/flutter_dotenv.dart';
import '/views/main/conversation_list.dart'; import '/views/main/conversation_list.dart';
@ -51,6 +50,7 @@ class _HomeState extends State<Home> {
if (!await checkLogin()) { if (!await checkLogin()) {
return; return;
} }
await updateFriends(); await updateFriends();
await updateConversations(); await updateConversations();
await updateMessageThreads(); await updateMessageThreads();


+ 15
- 0
mobile/lib/views/main/profile.dart View File

@ -1,4 +1,5 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter_dotenv/flutter_dotenv.dart';
import 'package:qr_flutter/qr_flutter.dart'; import 'package:qr_flutter/qr_flutter.dart';
import '/utils/storage/database.dart'; import '/utils/storage/database.dart';
import '/models/my_profile.dart'; import '/models/my_profile.dart';
@ -90,6 +91,7 @@ class _ProfileState extends State<Profile> {
} }
Widget logout() { Widget logout() {
bool isTesting = dotenv.env["ENVIRONMENT"] == 'development';
return Align( return Align(
alignment: Alignment.centerLeft, alignment: Alignment.centerLeft,
child: Column( child: Column(
@ -110,6 +112,19 @@ class _ProfileState extends State<Profile> {
Navigator.pushNamedAndRemoveUntil(context, '/landing', ModalRoute.withName('/landing')); Navigator.pushNamedAndRemoveUntil(context, '/landing', ModalRoute.withName('/landing'));
} }
), ),
isTesting ? TextButton.icon(
label: const Text(
'Delete Database',
style: TextStyle(fontSize: 16)
),
icon: const Icon(Icons.delete_forever),
style: const ButtonStyle(
alignment: Alignment.centerLeft,
),
onPressed: () {
deleteDb();
}
) : const SizedBox.shrink(),
], ],
), ),
); );


Loading…
Cancel
Save