From 1a9f76311279956963e9444c282adbf4418afab3 Mon Sep 17 00:00:00 2001 From: Tovi Jaeschke-Rogers Date: Wed, 8 Jun 2022 20:39:50 +0930 Subject: [PATCH] Start adding routes for mobile interaction Add data seeders for testing --- Backend/Api/Auth/Login.go | 1 - Backend/Api/Auth/Logout.go | 34 ++++ Backend/Api/Friends/EncryptedFriendsList.go | 41 +++++ Backend/Api/Friends/FriendRequest.go | 59 +++++++ Backend/Api/Friends/Friends.go | 32 ++++ Backend/Api/Messages/MessageThread.go | 43 +++++ Backend/Api/Routes.go | 39 ++--- Backend/Database/Friends.go | 57 ++++++ Backend/Database/Init.go | 2 +- Backend/Database/Messages.go | 52 ++++++ Backend/Database/Seeder/FriendSeeder.go | 67 ++++++++ Backend/Database/Seeder/MessageSeeder.go | 136 +++++++++++++++ Backend/Database/Seeder/Seed.go | 50 ++++++ Backend/Database/Seeder/UserSeeder.go | 68 ++++++++ Backend/Models/Friends.go | 15 +- Backend/Models/Messages.go | 21 ++- Backend/Util/Bytes.go | 21 +++ Backend/Util/UserHelper.go | 51 ++++++ Backend/go.mod | 2 +- Backend/main.go | 15 ++ mobile/lib/models/conversations.dart | 30 ++++ mobile/lib/utils/storage/encryption_keys.dart | 5 + mobile/lib/views/authentication/login.dart | 2 - .../lib/views/main/conversation_detail.dart | 162 ++++++++++++++++++ mobile/lib/views/main/conversation_list.dart | 113 +++++++++--- .../views/main/conversation_list_item.dart | 60 +++++++ mobile/lib/views/main/home.dart | 3 +- mobile/lib/views/main/profile.dart | 64 +++++++ 28 files changed, 1182 insertions(+), 63 deletions(-) create mode 100644 Backend/Api/Auth/Logout.go create mode 100644 Backend/Api/Friends/EncryptedFriendsList.go create mode 100644 Backend/Api/Friends/FriendRequest.go create mode 100644 Backend/Api/Friends/Friends.go create mode 100644 Backend/Api/Messages/MessageThread.go create mode 100644 Backend/Database/Friends.go create mode 100644 Backend/Database/Messages.go create mode 100644 Backend/Database/Seeder/FriendSeeder.go create mode 100644 Backend/Database/Seeder/MessageSeeder.go create mode 100644 Backend/Database/Seeder/Seed.go create mode 100644 Backend/Database/Seeder/UserSeeder.go create mode 100644 Backend/Util/Bytes.go create mode 100644 Backend/Util/UserHelper.go create mode 100644 mobile/lib/models/conversations.dart create mode 100644 mobile/lib/views/main/conversation_detail.dart create mode 100644 mobile/lib/views/main/conversation_list_item.dart create mode 100644 mobile/lib/views/main/profile.dart diff --git a/Backend/Api/Auth/Login.go b/Backend/Api/Auth/Login.go index 4d3e3f2..d3e5116 100644 --- a/Backend/Api/Auth/Login.go +++ b/Backend/Api/Auth/Login.go @@ -48,7 +48,6 @@ func makeLoginResponse(w http.ResponseWriter, code int, message, pubKey, privKey // Return updated json w.WriteHeader(code) w.Write(returnJson) - } func Login(w http.ResponseWriter, r *http.Request) { diff --git a/Backend/Api/Auth/Logout.go b/Backend/Api/Auth/Logout.go new file mode 100644 index 0000000..822b21d --- /dev/null +++ b/Backend/Api/Auth/Logout.go @@ -0,0 +1,34 @@ +package Auth + +import ( + "net/http" + "time" +) + +func Logout(w http.ResponseWriter, r *http.Request) { + var ( + c *http.Cookie + sessionToken string + err error + ) + + c, err = r.Cookie("session_token") + if err != nil { + if err == http.ErrNoCookie { + w.WriteHeader(http.StatusUnauthorized) + return + } + w.WriteHeader(http.StatusBadRequest) + return + } + + sessionToken = c.Value + + delete(Sessions, sessionToken) + + http.SetCookie(w, &http.Cookie{ + Name: "session_token", + Value: "", + Expires: time.Now(), + }) +} diff --git a/Backend/Api/Friends/EncryptedFriendsList.go b/Backend/Api/Friends/EncryptedFriendsList.go new file mode 100644 index 0000000..5179c9a --- /dev/null +++ b/Backend/Api/Friends/EncryptedFriendsList.go @@ -0,0 +1,41 @@ +package Friends + +import ( + "encoding/json" + "net/http" + + "git.tovijaeschke.xyz/tovi/Envelope/Backend/Api/Auth" + "git.tovijaeschke.xyz/tovi/Envelope/Backend/Database" + "git.tovijaeschke.xyz/tovi/Envelope/Backend/Models" +) + +func EncryptedFriendList(w http.ResponseWriter, r *http.Request) { + var ( + userSession Auth.Session + friends []Models.Friend + returnJson []byte + err error + ) + + userSession, err = Auth.CheckCookie(r) + + if err != nil { + http.Error(w, "Forbidden", http.StatusUnauthorized) + return + } + + friends, err = Database.GetFriendsByUserId(userSession.UserID) + 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) +} diff --git a/Backend/Api/Friends/FriendRequest.go b/Backend/Api/Friends/FriendRequest.go new file mode 100644 index 0000000..078bdcb --- /dev/null +++ b/Backend/Api/Friends/FriendRequest.go @@ -0,0 +1,59 @@ +package Friends + +import ( + "encoding/json" + "io/ioutil" + "net/http" + + "git.tovijaeschke.xyz/tovi/Envelope/Backend/Database" + "git.tovijaeschke.xyz/tovi/Envelope/Backend/Models" + "git.tovijaeschke.xyz/tovi/Envelope/Backend/Util" +) + +func FriendRequest(w http.ResponseWriter, r *http.Request) { + var ( + user Models.User + requestBody []byte + requestJson map[string]interface{} + friendID []byte + friendRequest Models.Friend + err error + ) + + user, err = Util.GetUserById(w, r) + if err != nil { + http.Error(w, "Not Found", http.StatusNotFound) + return + } + + requestBody, err = ioutil.ReadAll(r.Body) + if err != nil { + http.Error(w, "Not Found", http.StatusNotFound) + return + } + + json.Unmarshal(requestBody, &requestJson) + if requestJson["id"] == nil { + http.Error(w, "Invalid Data", http.StatusBadRequest) + return + } + + friendID, err = Util.ToBytes(requestJson["id"]) + if requestJson["id"] == nil { + http.Error(w, "Error", http.StatusInternalServerError) + return + } + + friendRequest = Models.Friend{ + UserID: user.ID, + FriendID: friendID, + } + + err = Database.CreateFriendRequest(&friendRequest) + if requestJson["id"] == nil { + http.Error(w, "Error", http.StatusInternalServerError) + return + } + + w.WriteHeader(http.StatusNoContent) +} diff --git a/Backend/Api/Friends/Friends.go b/Backend/Api/Friends/Friends.go new file mode 100644 index 0000000..0706e14 --- /dev/null +++ b/Backend/Api/Friends/Friends.go @@ -0,0 +1,32 @@ +package Friends + +import ( + "encoding/json" + "net/http" + + "git.tovijaeschke.xyz/tovi/Envelope/Backend/Models" + "git.tovijaeschke.xyz/tovi/Envelope/Backend/Util" +) + +func Friend(w http.ResponseWriter, r *http.Request) { + var ( + userData Models.User + returnJson []byte + err error + ) + + userData, err = Util.GetUserById(w, r) + if err != nil { + http.Error(w, "Not Found", http.StatusNotFound) + return + } + + returnJson, err = json.MarshalIndent(userData, "", " ") + if err != nil { + http.Error(w, "Error", http.StatusInternalServerError) + return + } + + w.WriteHeader(http.StatusOK) + w.Write(returnJson) +} diff --git a/Backend/Api/Messages/MessageThread.go b/Backend/Api/Messages/MessageThread.go new file mode 100644 index 0000000..cd2e08e --- /dev/null +++ b/Backend/Api/Messages/MessageThread.go @@ -0,0 +1,43 @@ +package Messages + +import ( + "encoding/json" + "net/http" + + "git.tovijaeschke.xyz/tovi/Envelope/Backend/Database" + "git.tovijaeschke.xyz/tovi/Envelope/Backend/Models" + "github.com/gorilla/mux" +) + +func MessageThread(w http.ResponseWriter, r *http.Request) { + var ( + messages []Models.Message + urlVars map[string]string + threadID string + returnJson []byte + ok bool + err error + ) + + urlVars = mux.Vars(r) + threadID, ok = urlVars["threadID"] + if !ok { + http.Error(w, "Not Found", http.StatusNotFound) + return + } + + messages, err = Database.GetMessagesByThreadId(threadID) + if !ok { + http.Error(w, "Not Found", http.StatusNotFound) + return + } + + returnJson, err = json.MarshalIndent(messages, "", " ") + if err != nil { + http.Error(w, "Error", http.StatusInternalServerError) + return + } + + w.WriteHeader(http.StatusOK) + w.Write(returnJson) +} diff --git a/Backend/Api/Routes.go b/Backend/Api/Routes.go index ad4d271..9715739 100644 --- a/Backend/Api/Routes.go +++ b/Backend/Api/Routes.go @@ -5,6 +5,8 @@ import ( "net/http" "git.tovijaeschke.xyz/tovi/Envelope/Backend/Api/Auth" + "git.tovijaeschke.xyz/tovi/Envelope/Backend/Api/Friends" + "git.tovijaeschke.xyz/tovi/Envelope/Backend/Api/Messages" "github.com/gorilla/mux" ) @@ -12,8 +14,7 @@ import ( func loggingMiddleware(next http.Handler) http.Handler { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { log.Printf( - "%s %s %s, Content Length: %d", - r.RemoteAddr, + "%s %s, Content Length: %d", r.Method, r.RequestURI, r.ContentLength, @@ -26,50 +27,44 @@ func loggingMiddleware(next http.Handler) http.Handler { func authenticationMiddleware(next http.Handler) http.Handler { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { var ( - //userSession Auth.Session - //err error + err error ) - http.Error(w, "Forbidden", http.StatusUnauthorized) - return - - /** - userSession, err = Auth.CheckCookie(r) + _, err = Auth.CheckCookie(r) if err != nil { http.Error(w, "Forbidden", http.StatusUnauthorized) return } - log.Printf( - "Authenticated user: %s (%s)", - userSession.Email, - userSession.UserID, - ) - next.ServeHTTP(w, r) - */ }) } func InitApiEndpoints(router *mux.Router) { var ( - api *mux.Router - adminApi *mux.Router + api *mux.Router + authApi *mux.Router ) log.Println("Initializing API routes...") api = router.PathPrefix("/api/v1/").Subrouter() - api.Use(loggingMiddleware) // Define routes for authentication api.HandleFunc("/signup", Auth.Signup).Methods("POST") api.HandleFunc("/login", Auth.Login).Methods("POST") - // api.HandleFunc("/logout", Auth.Logout).Methods("GET") + api.HandleFunc("/logout", Auth.Logout).Methods("GET") + + authApi = api.PathPrefix("/auth/").Subrouter() + authApi.Use(authenticationMiddleware) - adminApi = api.PathPrefix("/message/").Subrouter() + // Define routes for friends and friend requests + authApi.HandleFunc("/friends", Friends.EncryptedFriendList).Methods("GET") + authApi.HandleFunc("/friend/{userID}", Friends.Friend).Methods("GET") + authApi.HandleFunc("/friend/{userID}/request", Friends.FriendRequest).Methods("POST") - adminApi.Use(authenticationMiddleware) + // Define routes for messages + authApi.HandleFunc("/messages/{threadID}", Messages.MessageThread).Methods("GET") } diff --git a/Backend/Database/Friends.go b/Backend/Database/Friends.go new file mode 100644 index 0000000..946a1d3 --- /dev/null +++ b/Backend/Database/Friends.go @@ -0,0 +1,57 @@ +package Database + +import ( + "git.tovijaeschke.xyz/tovi/Envelope/Backend/Models" + "github.com/gofrs/uuid" + + "gorm.io/gorm" + "gorm.io/gorm/clause" +) + +func GetFriendById(id string) (Models.Friend, error) { + var ( + friend Models.Friend + err error + ) + + err = DB.Preload(clause.Associations). + First(&friend, "id = ?", id). + Error + + return friend, err +} + +func GetFriendsByUserId(userID string) ([]Models.Friend, error) { + var ( + friends []Models.Friend + err error + ) + + err = DB.Model(Models.Friend{}). + Where("user_id = ?", userID). + Find(&friends). + Error + + return friends, err +} + +func CreateFriendRequest(friend *Models.Friend) error { + var ( + err error + ) + + friend.ThreadID, err = uuid.NewV1() + if err != nil { + return err + } + + return DB.Session(&gorm.Session{FullSaveAssociations: true}). + Create(friend). + Error +} + +func DeleteFriend(friend *Models.Friend) error { + return DB.Session(&gorm.Session{FullSaveAssociations: true}). + Delete(friend). + Error +} diff --git a/Backend/Database/Init.go b/Backend/Database/Init.go index b9de172..7918085 100644 --- a/Backend/Database/Init.go +++ b/Backend/Database/Init.go @@ -21,7 +21,7 @@ func GetModels() []interface{} { &Models.User{}, &Models.Friend{}, &Models.MessageData{}, - &Models.MessageKey{}, + &Models.Message{}, } } diff --git a/Backend/Database/Messages.go b/Backend/Database/Messages.go new file mode 100644 index 0000000..1d17b09 --- /dev/null +++ b/Backend/Database/Messages.go @@ -0,0 +1,52 @@ +package Database + +import ( + "git.tovijaeschke.xyz/tovi/Envelope/Backend/Models" + + "gorm.io/gorm" + "gorm.io/gorm/clause" +) + +func GetMessageById(id string) (Models.Message, error) { + var ( + message Models.Message + err error + ) + + err = DB.Preload(clause.Associations). + First(&message, "id = ?", id). + Error + + return message, err +} + +func GetMessagesByThreadId(id string) ([]Models.Message, error) { + var ( + messages []Models.Message + err error + ) + + err = DB.Preload(clause.Associations). + Find(&messages, "thread_id = ?", id). + Error + + return messages, err +} + +func CreateMessage(message *Models.Message) error { + var ( + err error + ) + + err = DB.Session(&gorm.Session{FullSaveAssociations: true}). + Create(message). + Error + + return err +} + +func DeleteMessage(message *Models.Message) error { + return DB.Session(&gorm.Session{FullSaveAssociations: true}). + Delete(message). + Error +} diff --git a/Backend/Database/Seeder/FriendSeeder.go b/Backend/Database/Seeder/FriendSeeder.go new file mode 100644 index 0000000..771dcad --- /dev/null +++ b/Backend/Database/Seeder/FriendSeeder.go @@ -0,0 +1,67 @@ +package Seeder + +import ( + "time" + + "git.tovijaeschke.xyz/tovi/Envelope/Backend/Database" + "git.tovijaeschke.xyz/tovi/Envelope/Backend/Models" +) + +func seedFriend(user, friendUser Models.User) error { + var ( + friend Models.Friend + err error + ) + + friend = Models.Friend{ + UserID: user.ID, + FriendID: encryptWithPublicKey(friendUser.ID.Bytes(), decodedPublicKey), + AcceptedAt: time.Now(), + } + + err = Database.CreateFriendRequest(&friend) + if err != nil { + return err + } + + friend = Models.Friend{ + UserID: friendUser.ID, + FriendID: encryptWithPublicKey(user.ID.Bytes(), decodedPublicKey), + AcceptedAt: time.Now(), + } + + return Database.CreateFriendRequest(&friend) +} + +func SeedFriends() { + var ( + primaryUser Models.User + secondaryUser Models.User + i int + err error + ) + + primaryUser, err = Database.GetUserByUsername("testUser") + if err != nil { + panic(err) + } + + secondaryUser, err = Database.GetUserByUsername("testUser2") + if err != nil { + panic(err) + } + + err = seedFriend(primaryUser, secondaryUser) + + for i = 0; i <= 3; i++ { + secondaryUser, err = Database.GetUserByUsername(userNames[i]) + if err != nil { + panic(err) + } + + err = seedFriend(primaryUser, secondaryUser) + if err != nil { + panic(err) + } + } +} diff --git a/Backend/Database/Seeder/MessageSeeder.go b/Backend/Database/Seeder/MessageSeeder.go new file mode 100644 index 0000000..4b36671 --- /dev/null +++ b/Backend/Database/Seeder/MessageSeeder.go @@ -0,0 +1,136 @@ +package Seeder + +import ( + "bytes" + "crypto/aes" + "crypto/cipher" + "crypto/rand" + "crypto/rsa" + "crypto/sha512" + "encoding/pem" + "hash" + + "git.tovijaeschke.xyz/tovi/Envelope/Backend/Database" + "git.tovijaeschke.xyz/tovi/Envelope/Backend/Models" + "github.com/gofrs/uuid" +) + +// EncryptWithPublicKey encrypts data with public key +func encryptWithPublicKey(msg []byte, pub *rsa.PublicKey) []byte { + var ( + hash hash.Hash + ) + + hash = sha512.New() + ciphertext, err := rsa.EncryptOAEP(hash, rand.Reader, pub, msg, nil) + if err != nil { + panic(err) + } + return ciphertext +} + +func PKCS5Padding(ciphertext []byte, blockSize int, after int) []byte { + var ( + padding int + padtext []byte + ) + padding = (blockSize - len(ciphertext)%blockSize) + padtext = bytes.Repeat([]byte{byte(padding)}, padding) + return append(ciphertext, padtext...) +} + +func seedMessage(primaryUser, secondaryUser Models.User, threadID uuid.UUID, i int) error { + var ( + messageKey Models.Message + messageData Models.MessageData + + block cipher.Block + mode cipher.BlockMode + pemBlock *pem.Block + + plaintext string + ciphertext []byte + + bKey []byte + bIV []byte + bPlaintext []byte + + err error + ) + + plaintext = "Test Message" + + bKey = make([]byte, 32) + _, err = rand.Read(bKey) + if err != nil { + panic(err) + } + bIV = make([]byte, 16) + _, err = rand.Read(bIV) + if err != nil { + panic(err) + } + bPlaintext = PKCS5Padding([]byte(plaintext), aes.BlockSize, len(plaintext)) + + pemBlock = &pem.Block{ + Type: "AES KEY", + Bytes: bKey, + } + + block, err = aes.NewCipher(bKey) + if err != nil { + panic(err) + } + + ciphertext = make([]byte, len(bPlaintext)) + + mode = cipher.NewCBCEncrypter(block, bIV) + + mode.CryptBlocks(ciphertext, bPlaintext) + + messageData = Models.MessageData{ + Data: ciphertext, + } + + messageKey = Models.Message{ + UserID: primaryUser.ID, + MessageData: messageData, + MessageType: "sender", + RelationalUserId: encryptWithPublicKey(secondaryUser.ID.Bytes(), decodedPublicKey), + SymmetricKey: string(pem.EncodeToMemory(pemBlock)), + } + + return Database.CreateMessage(&messageKey) +} + +func SeedMessages() { + var ( + primaryUser Models.User + secondaryUser Models.User + threadID uuid.UUID + i int + err error + ) + + primaryUser, err = Database.GetUserByUsername("testUser") + if err != nil { + panic(err) + } + + secondaryUser, err = Database.GetUserByUsername("testUser2") + if err != nil { + panic(err) + } + + threadID, err = uuid.NewV4() + if err != nil { + panic(err) + } + + for i = 0; i <= 20; i++ { + err = seedMessage(primaryUser, secondaryUser, threadID, i) + if err != nil { + panic(err) + } + } +} diff --git a/Backend/Database/Seeder/Seed.go b/Backend/Database/Seeder/Seed.go new file mode 100644 index 0000000..e1d2531 --- /dev/null +++ b/Backend/Database/Seeder/Seed.go @@ -0,0 +1,50 @@ +package Seeder + +import ( + "crypto/rsa" + "crypto/x509" + "encoding/pem" + "log" +) + +const ( + // Encrypted with "password" + encryptedPrivateKey string = ` +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 +` + + publicKey string = ` +-----BEGIN PUBLIC KEY----- +MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAidHsO9H0fMd6cu9dQiOwu/qtaDq+lG59kudoHGx8WZhucpOSp2df95UzjnXmMw1fz0Mx5fjgBaunG7u1MVBy+7IdYwXp+iz3xZulVy1Yv3e+GMzgQhAuxhz/8wsHkFkUTweDZCrCZPhkTXlCrxDIuKykQ0el+RnpSjyyljOsAWAdTmrz0a2Nh9FOmF1v49pWy3Z3aJG58xl1dmFkpXjT3m36GB4Z30iR1uOMnNdrtfwIfLQAc7nmle2LxCHeEMYzlA9y6JChm5kGI6FmglSKYTxvDm40jA5mYyDCPADeCodbIw4Mtm0nwrM3QqKWj+jlaL8BY7/jjaosmz7VK2do4wIDAQAB +-----END PUBLIC KEY----- +` +) + +var ( + decodedPublicKey *rsa.PublicKey +) + +func Seed() { + var ( + block *pem.Block + decKey any + err error + ) + + // TODO: Fix this parsing + block, _ = pem.Decode([]byte(publicKey)) + decKey, err = x509.ParsePKIXPublicKey(block.Bytes) + if err != nil { + panic(err) + } + decodedPublicKey = decKey.(*rsa.PublicKey) + + log.Println("Seeding users...") + SeedUsers() + + log.Println("Seeding friend connections...") + SeedFriends() + + log.Println("Seeding messages...") + SeedMessages() +} diff --git a/Backend/Database/Seeder/UserSeeder.go b/Backend/Database/Seeder/UserSeeder.go new file mode 100644 index 0000000..fd7cc7e --- /dev/null +++ b/Backend/Database/Seeder/UserSeeder.go @@ -0,0 +1,68 @@ +package Seeder + +import ( + "git.tovijaeschke.xyz/tovi/Envelope/Backend/Api/Auth" + "git.tovijaeschke.xyz/tovi/Envelope/Backend/Database" + "git.tovijaeschke.xyz/tovi/Envelope/Backend/Models" +) + +var userNames = []string{ + "assuredcoot", + "quotesteeve", + "blueberriessiemens", + "eliteexaggerate", + "twotrice", + "moderagged", + "duleelderly", + "stringdetailed", + "nodesanymore", + "sacredpolitical", + "pajamasenergy", +} + +func createUser(username string) (Models.User, error) { + var ( + userData Models.User + password string + err error + ) + + password, err = Auth.HashPassword("password") + if err != nil { + return Models.User{}, err + } + + userData = Models.User{ + Username: username, + Password: password, + AsymmetricPrivateKey: encryptedPrivateKey, + AsymmetricPublicKey: publicKey, + } + + err = Database.CreateUser(&userData) + return userData, err +} + +func SeedUsers() { + var ( + i int + err error + ) + + // Seed users used for conversation seeding + _, err = createUser("testUser") + if err != nil { + panic(err) + } + _, err = createUser("testUser2") + if err != nil { + panic(err) + } + + for i = 0; i <= 10; i++ { + _, err = createUser(userNames[i]) + if err != nil { + panic(err) + } + } +} diff --git a/Backend/Models/Friends.go b/Backend/Models/Friends.go index c0b9abd..b56b8ab 100644 --- a/Backend/Models/Friends.go +++ b/Backend/Models/Friends.go @@ -1,9 +1,18 @@ package Models -import "github.com/gofrs/uuid" +import ( + "time" + "github.com/gofrs/uuid" +) + +// Set with User being the requestee, and FriendID being the requester type Friend struct { Base - UserID uuid.UUID `gorm:"type:uuid;column:user_id;not null;" json:"user_id"` - FriendId string `gorm:"column:friend_id;not null" json:"friend_id"` // Stored encrypted + UserID uuid.UUID `gorm:"type:uuid;column:user_id;not null;" json:"user_id"` + User User `json:"user"` + FriendID []byte `gorm:"not null" json:"friend_id"` // Stored encrypted + ThreadID uuid.UUID `gorm:"type:uuid;column:thread_id;not null;" json:"thread_id"` + Thread []Message `gorm:"foreignKey:thread_id" json:"-"` + AcceptedAt time.Time `json:"accepted_at"` } diff --git a/Backend/Models/Messages.go b/Backend/Models/Messages.go index ce2fc1b..35e903d 100644 --- a/Backend/Models/Messages.go +++ b/Backend/Models/Messages.go @@ -2,14 +2,25 @@ package Models import "github.com/gofrs/uuid" +const ( + MessageTypeSender = "sender" + MessageTypeReceiver = "reciever" +) + type MessageData struct { Base - Data string `gorm:"not null" json:"data"` // Stored encrypted + Data []byte `gorm:"not null" json:"data"` // Stored encrypted } -type MessageKey struct { +// TODO: Rename this to something better +type Message struct { Base - UserID uuid.UUID `gorm:"type:uuid;column:user_id;not null;" json:"user_id"` - MessageDataID uuid.UUID `gorm:"type:uuid;column:message_data_id;not null;" json:"message_data_id"` - SymmetricKey string `gorm:"not null" json:"symmetric_key"` // Stored encrypted + ThreadID uuid.UUID `gorm:"not null" json:"thread_id"` + UserID uuid.UUID `json:"-"` + User User `json:"user"` + MessageDataID uuid.UUID `json:"-"` + MessageData MessageData `json:"message_data"` + MessageType string `gorm:"not null" json:"message_type"` // sender / reciever + RelationalUserId []byte `gorm:"not null" json:"relational_user_id"` // Stored encrypted. UserID for the user this message is in relation to + SymmetricKey string `gorm:"not null" json:"symmetric_key"` // Stored encrypted } diff --git a/Backend/Util/Bytes.go b/Backend/Util/Bytes.go new file mode 100644 index 0000000..cfef327 --- /dev/null +++ b/Backend/Util/Bytes.go @@ -0,0 +1,21 @@ +package Util + +import ( + "bytes" + "encoding/gob" +) + +func ToBytes(key interface{}) ([]byte, error) { + var ( + buf bytes.Buffer + enc *gob.Encoder + err error + ) + enc = gob.NewEncoder(&buf) + err = enc.Encode(key) + if err != nil { + return nil, err + } + return buf.Bytes(), nil + +} diff --git a/Backend/Util/UserHelper.go b/Backend/Util/UserHelper.go new file mode 100644 index 0000000..32616a6 --- /dev/null +++ b/Backend/Util/UserHelper.go @@ -0,0 +1,51 @@ +package Util + +import ( + "errors" + "log" + "net/http" + + "git.tovijaeschke.xyz/tovi/Envelope/Backend/Database" + "git.tovijaeschke.xyz/tovi/Envelope/Backend/Models" + + "github.com/gorilla/mux" +) + +func GetUserId(r *http.Request) (string, error) { + var ( + urlVars map[string]string + id string + ok bool + ) + + urlVars = mux.Vars(r) + id, ok = urlVars["userID"] + if !ok { + return id, errors.New("Could not get id") + } + return id, nil +} + +func GetUserById(w http.ResponseWriter, r *http.Request) (Models.User, error) { + var ( + postData Models.User + id string + err error + ) + + id, err = GetUserId(r) + if err != nil { + log.Printf("Error encountered getting id\n") + http.Error(w, "Error", http.StatusInternalServerError) + return postData, err + } + + postData, err = Database.GetUserById(id) + if err != nil { + log.Printf("Could not find user with id %s\n", id) + http.Error(w, "Error", http.StatusInternalServerError) + return postData, err + } + + return postData, nil +} diff --git a/Backend/go.mod b/Backend/go.mod index b67be65..127bb75 100644 --- a/Backend/go.mod +++ b/Backend/go.mod @@ -6,6 +6,7 @@ require ( github.com/Kangaroux/go-map-schema v0.6.1 github.com/gofrs/uuid v4.2.0+incompatible github.com/gorilla/mux v1.8.0 + golang.org/x/crypto v0.0.0-20210921155107-089bfa567519 gorm.io/driver/postgres v1.3.4 gorm.io/gorm v1.23.4 ) @@ -21,6 +22,5 @@ require ( github.com/jackc/pgx/v4 v4.15.0 // indirect github.com/jinzhu/inflection v1.0.0 // indirect github.com/jinzhu/now v1.1.4 // indirect - golang.org/x/crypto v0.0.0-20210921155107-089bfa567519 // indirect golang.org/x/text v0.3.7 // indirect ) diff --git a/Backend/main.go b/Backend/main.go index 009e8ae..2978a6f 100644 --- a/Backend/main.go +++ b/Backend/main.go @@ -1,16 +1,26 @@ package main import ( + "flag" "net/http" "git.tovijaeschke.xyz/tovi/Envelope/Backend/Api" "git.tovijaeschke.xyz/tovi/Envelope/Backend/Database" + "git.tovijaeschke.xyz/tovi/Envelope/Backend/Database/Seeder" "github.com/gorilla/mux" ) +var ( + seed bool +) + func init() { Database.Init() + + flag.BoolVar(&seed, "seed", false, "Seed database for development") + + flag.Parse() } func main() { @@ -18,6 +28,11 @@ func main() { router *mux.Router ) + if seed { + Seeder.Seed() + return + } + router = mux.NewRouter() Api.InitApiEndpoints(router) diff --git a/mobile/lib/models/conversations.dart b/mobile/lib/models/conversations.dart new file mode 100644 index 0000000..89a5373 --- /dev/null +++ b/mobile/lib/models/conversations.dart @@ -0,0 +1,30 @@ +const messageTypeSender = 'sender'; +const messageTypeReceiver = 'receiver'; + +class Message { + String id; + String conversationId; + String symmetricKey; + String data; + String messageType; + String? decryptedData; + Message({ + required this.id, + required this.conversationId, + required this.symmetricKey, + required this.data, + required this.messageType, + this.decryptedData, + }); +} + +class Conversation { + String id; + String friendId; + String recentMessageId; + Conversation({ + required this.id, + required this.friendId, + required this.recentMessageId, + }); +} diff --git a/mobile/lib/utils/storage/encryption_keys.dart b/mobile/lib/utils/storage/encryption_keys.dart index d27bfc7..edcd727 100644 --- a/mobile/lib/utils/storage/encryption_keys.dart +++ b/mobile/lib/utils/storage/encryption_keys.dart @@ -11,6 +11,11 @@ void setPrivateKey(RSAPrivateKey key) async { prefs.setString(rsaPrivateKeyName, keyPem); } +void unsetPrivateKey() async { + final prefs = await SharedPreferences.getInstance(); + await prefs.remove(rsaPrivateKeyName); +} + Future getPrivateKey() async { final prefs = await SharedPreferences.getInstance(); String? keyPem = prefs.getString(rsaPrivateKeyName); diff --git a/mobile/lib/views/authentication/login.dart b/mobile/lib/views/authentication/login.dart index a2d6595..98a3ce9 100644 --- a/mobile/lib/views/authentication/login.dart +++ b/mobile/lib/views/authentication/login.dart @@ -2,7 +2,6 @@ import 'dart:convert'; import 'package:flutter/material.dart'; import 'package:http/http.dart' as http; import 'package:shared_preferences/shared_preferences.dart'; -import '/views/main/conversation_list.dart'; import '/utils/encryption/rsa_key_helper.dart'; import '/utils/encryption/aes_helper.dart'; import '/utils/storage/encryption_keys.dart'; @@ -73,7 +72,6 @@ class Login extends StatelessWidget { //`true` if you want Flutter to automatically add Back Button when needed, //or `false` if you want to force your own back button every where leading: IconButton(icon: const Icon(Icons.arrow_back), - //onPressed:() => Navigator.pop(context, false), onPressed:() => { Navigator.pop(context) } diff --git a/mobile/lib/views/main/conversation_detail.dart b/mobile/lib/views/main/conversation_detail.dart new file mode 100644 index 0000000..8732f80 --- /dev/null +++ b/mobile/lib/views/main/conversation_detail.dart @@ -0,0 +1,162 @@ +import 'package:flutter/material.dart'; +import '/models/conversations.dart'; + +class ConversationDetail extends StatefulWidget{ + const ConversationDetail({Key? key}) : super(key: key); + + @override + _ConversationDetailState createState() => _ConversationDetailState(); +} + +class _ConversationDetailState extends State { + Conversation conversation = Conversation( + id: '777', + friendId: 'abc', + recentMessageId: '111', + ); + + List messages = [ + Message( + id: '444', + conversationId: '777', + symmetricKey: '', + data: 'This is a message', + messageType: messageTypeSender, + ), + Message( + id: '444', + conversationId: '777', + symmetricKey: '', + data: 'This is a message', + messageType: messageTypeReceiver, + ), + Message( + id: '444', + conversationId: '777', + symmetricKey: '', + data: 'This is a message', + messageType: messageTypeSender + ), + Message( + id: '444', + conversationId: '777', + symmetricKey: '', + data: 'This is a message', + messageType: messageTypeReceiver, + ), + ]; + + @override + Widget build(BuildContext context) { + return Scaffold( + appBar: AppBar( + elevation: 0, + automaticallyImplyLeading: false, + backgroundColor: Colors.white, + flexibleSpace: SafeArea( + child: Container( + padding: const EdgeInsets.only(right: 16), + child: Row( + children: [ + IconButton( + onPressed: (){ + Navigator.pop(context); + }, + icon: const Icon(Icons.arrow_back,color: Colors.black,), + ), + const SizedBox(width: 2,), + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + mainAxisAlignment: MainAxisAlignment.center, + children: const [ + Text("Kriss Benwat",style: TextStyle( fontSize: 16 ,fontWeight: FontWeight.w600),), + ], + ), + ), + const Icon(Icons.settings,color: Colors.black54,), + ], + ), + ), + ), + ), + body: Stack( + children: [ + ListView.builder( + itemCount: messages.length, + shrinkWrap: true, + padding: const EdgeInsets.only(top: 10,bottom: 10), + physics: const NeverScrollableScrollPhysics(), + itemBuilder: (context, index) { + return Container( + padding: const EdgeInsets.only(left: 14,right: 14,top: 10,bottom: 10), + child: Align( + alignment: (messages[index].messageType == messageTypeReceiver ? Alignment.topLeft:Alignment.topRight), + child: Container( + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(20), + color: (messages[index].messageType == messageTypeReceiver ? Colors.grey.shade200:Colors.blue[200]), + ), + padding: const EdgeInsets.all(16), + child: Text(messages[index].data, style: const TextStyle(fontSize: 15)), + ), + ), + ); + }, + ), + Align( + alignment: Alignment.bottomLeft, + child: ConstrainedBox( + constraints: const BoxConstraints( + maxHeight: 200.0, + ), + child: Container( + padding: const EdgeInsets.only(left: 10,bottom: 10,top: 10), + // height: 60, + width: double.infinity, + color: Colors.white, + child: Row( + children: [ + GestureDetector( + onTap: (){ + }, + child: Container( + height: 30, + width: 30, + decoration: BoxDecoration( + color: Colors.lightBlue, + borderRadius: BorderRadius.circular(30), + ), + child: const Icon(Icons.add, color: Colors.white, size: 20, ), + ), + ), + const SizedBox(width: 15,), + const Expanded( + child: TextField( + decoration: InputDecoration( + hintText: "Write message...", + hintStyle: TextStyle(color: Colors.black54), + border: InputBorder.none, + ), + maxLines: null, + ), + ), + const SizedBox(width: 15,), + FloatingActionButton( + onPressed: () { + }, + child: const Icon(Icons.send,color: Colors.white,size: 18,), + backgroundColor: Colors.blue, + elevation: 0, + ), + ], + + ), + ), + ), + ), + ], + ), + ); + } +} diff --git a/mobile/lib/views/main/conversation_list.dart b/mobile/lib/views/main/conversation_list.dart index 1f414d4..b47e678 100644 --- a/mobile/lib/views/main/conversation_list.dart +++ b/mobile/lib/views/main/conversation_list.dart @@ -1,5 +1,6 @@ import 'package:flutter/material.dart'; -import 'package:shared_preferences/shared_preferences.dart'; +import '/models/conversations.dart'; +import '/views/main/conversation_list_item.dart'; class ConversationList extends StatefulWidget { const ConversationList({Key? key}) : super(key: key); @@ -9,35 +10,41 @@ class ConversationList extends StatefulWidget { } class _ConversationListState extends State { - final _suggestions = []; - final _biggerFont = const TextStyle(fontSize: 18); + List messages = [ + Message( + id: '123', + conversationId: 'xyz', + data: '', + symmetricKey: '', + messageType: 'reciever', + ), + ]; + + List friends = [ + Conversation( + id: 'xyz', + friendId: 'abc', + recentMessageId: '123', + ), + ]; Widget list() { - if (_suggestions.isEmpty) { + if (friends.isEmpty) { return const Center( child: Text('No Conversations'), ); } return ListView.builder( - itemCount: _suggestions.length, - padding: const EdgeInsets.all(16.0), - itemBuilder: /*1*/ (context, i) { - //if (i >= _suggestions.length) { - // TODO: Check for more conversations here. Remove the itemCount to use this section - //_suggestions.addAll(generateWordPairs().take(10)); /*4*/ - //} - return Column( - children: [ - ListTile( - title: Text( - _suggestions[i], - style: _biggerFont, - ), - ), - const Divider(), - ] + itemCount: friends.length, + shrinkWrap: true, + padding: const EdgeInsets.only(top: 16), + physics: const NeverScrollableScrollPhysics(), + itemBuilder: (context, i) { + return ConversationListItem( + id: friends[i].id, + username: 'Test', ); }, ); @@ -46,11 +53,63 @@ class _ConversationListState extends State { @override Widget build(BuildContext context) { return Scaffold( - appBar: AppBar( - title: const Text('Envelope'), - ), - body: list(), - ); - + body: SingleChildScrollView( + physics: const BouncingScrollPhysics(), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + SafeArea( + child: Padding( + padding: const EdgeInsets.only(left: 16,right: 16,top: 10), + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + const Text("Conversations",style: TextStyle(fontSize: 32,fontWeight: FontWeight.bold),), + Container( + padding: const EdgeInsets.only(left: 8,right: 8,top: 2,bottom: 2), + height: 30, + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(30), + color: Colors.pink[50], + ), + child: Row( + children: const [ + Icon(Icons.add,color: Colors.pink,size: 20,), + SizedBox(width: 2,), + Text("Add",style: TextStyle(fontSize: 14,fontWeight: FontWeight.bold),), + ], + ), + ) + ], + ), + ), + ), + Padding( + padding: const EdgeInsets.only(top: 16,left: 16,right: 16), + child: TextField( + decoration: InputDecoration( + hintText: "Search...", + hintStyle: TextStyle(color: Colors.grey.shade600), + prefixIcon: Icon(Icons.search,color: Colors.grey.shade600, size: 20,), + filled: true, + fillColor: Colors.grey.shade100, + contentPadding: const EdgeInsets.all(8), + enabledBorder: OutlineInputBorder( + borderRadius: BorderRadius.circular(20), + borderSide: BorderSide( + color: Colors.grey.shade100 + ) + ), + ), + ), + ), + Padding( + padding: const EdgeInsets.only(top: 16,left: 16,right: 16), + child: list(), + ), + ], + ), + ), + ); } } diff --git a/mobile/lib/views/main/conversation_list_item.dart b/mobile/lib/views/main/conversation_list_item.dart new file mode 100644 index 0000000..e8b6c23 --- /dev/null +++ b/mobile/lib/views/main/conversation_list_item.dart @@ -0,0 +1,60 @@ +import 'package:flutter/material.dart'; +import '/views/main/conversation_detail.dart'; + +class ConversationListItem extends StatefulWidget{ + final String id; + final String username; + const ConversationListItem({ + Key? key, + required this.id, + required this.username, + }) : super(key: key); + + @override + _ConversationListItemState createState() => _ConversationListItemState(); +} + +class _ConversationListItemState extends State { + @override + Widget build(BuildContext context) { + return GestureDetector( + onTap: () { + Navigator.push(context, MaterialPageRoute(builder: (context){ + return ConversationDetail(); + })); + }, + child: Container( + padding: const EdgeInsets.only(left: 16,right: 16,top: 10,bottom: 10), + child: Row( + children: [ + Expanded( + child: Row( + children: [ + // CircleAvatar( + // backgroundImage: NetworkImage(widget.imageUrl), + // maxRadius: 30, + // ), + //const SizedBox(width: 16), + Expanded( + child: Container( + color: Colors.transparent, + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text(widget.username, style: const TextStyle(fontSize: 16)), + const SizedBox(height: 6), + //Text(widget.messageText,style: TextStyle(fontSize: 13,color: Colors.grey.shade600, fontWeight: widget.isMessageRead?FontWeight.bold:FontWeight.normal),), + const Divider(), + ], + ), + ), + ), + ], + ), + ), + ], + ), + ), + ); + } +} diff --git a/mobile/lib/views/main/home.dart b/mobile/lib/views/main/home.dart index c157055..85b3ecc 100644 --- a/mobile/lib/views/main/home.dart +++ b/mobile/lib/views/main/home.dart @@ -2,6 +2,7 @@ import 'package:flutter/material.dart'; import 'package:shared_preferences/shared_preferences.dart'; import '/views/main/conversation_list.dart'; import '/views/main/friend_list.dart'; +import '/views/main/profile.dart'; class Home extends StatefulWidget { const Home({Key? key}) : super(key: key); @@ -28,7 +29,7 @@ class _HomeState extends State { static const List _widgetOptions = [ ConversationList(), FriendList(), - Text('Not Implemented'), + Profile(), ]; void _onItemTapped(int index) { diff --git a/mobile/lib/views/main/profile.dart b/mobile/lib/views/main/profile.dart new file mode 100644 index 0000000..488d1b6 --- /dev/null +++ b/mobile/lib/views/main/profile.dart @@ -0,0 +1,64 @@ +import 'package:flutter/material.dart'; +import 'package:shared_preferences/shared_preferences.dart'; +import '/utils/storage/encryption_keys.dart'; + +class Profile extends StatefulWidget { + const Profile({Key? key}) : super(key: key); + + @override + State createState() => _ProfileState(); +} + +class _ProfileState extends State { + @override + Widget build(BuildContext context) { + return Scaffold( + body: SingleChildScrollView( + physics: const BouncingScrollPhysics(), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + SafeArea( + child: Padding( + padding: const EdgeInsets.only(left: 16,right: 16,top: 10), + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + const Text("Profile",style: TextStyle(fontSize: 32,fontWeight: FontWeight.bold),), + Container( + padding: const EdgeInsets.only(left: 8,right: 8,top: 2,bottom: 2), + height: 30, + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(30), + color: Colors.pink[50], + ), + child: GestureDetector( + onTap: () async { + final preferences = await SharedPreferences.getInstance(); + preferences.setBool('islogin', false); + preferences.remove(rsaPrivateKeyName); + Navigator.pushNamedAndRemoveUntil(context, '/landing', ModalRoute.withName('/landing')); + }, + child: Row( + children: const [ + Icon(Icons.logout, color: Colors.pink, size: 20,), + SizedBox(width: 2,), + Text("Logout",style: TextStyle(fontSize: 14,fontWeight: FontWeight.bold),), + ], + ), + ), + ) + ], + ), + ), + ), + const Padding( + padding: EdgeInsets.only(top: 16,left: 16,right: 16), + child: Text('Test'), + ), + ], + ), + ), + ); + } +}