diff --git a/Backend/Api/Messages/CreateMessage.go b/Backend/Api/Messages/CreateMessage.go new file mode 100644 index 0000000..d73977c --- /dev/null +++ b/Backend/Api/Messages/CreateMessage.go @@ -0,0 +1,23 @@ +package Messages + +import ( + "encoding/json" + "fmt" + "net/http" + + "git.tovijaeschke.xyz/tovi/Envelope/Backend/Models" +) + +func CreateMessage(w http.ResponseWriter, r *http.Request) { + var ( + message Models.Message + err error + ) + + err = json.NewDecoder(r.Body).Decode(&message) + if err != nil { + return + } + + fmt.Println(message) +} diff --git a/Backend/Api/Messages/MessageThread.go b/Backend/Api/Messages/MessageThread.go index cd2e08e..8a86209 100644 --- a/Backend/Api/Messages/MessageThread.go +++ b/Backend/Api/Messages/MessageThread.go @@ -4,6 +4,7 @@ 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" "github.com/gorilla/mux" @@ -11,28 +12,35 @@ import ( func MessageThread(w http.ResponseWriter, r *http.Request) { var ( - messages []Models.Message - urlVars map[string]string - threadID string - returnJson []byte - ok bool - err error + userData Models.User + messageThread Models.MessageThread + urlVars map[string]string + threadKey string + returnJson []byte + ok bool + err error ) + userData, err = Auth.CheckCookieCurrentUser(w, r) + if !ok { + http.Error(w, "Forbidden", http.StatusUnauthorized) + return + } + urlVars = mux.Vars(r) - threadID, ok = urlVars["threadID"] + threadKey, ok = urlVars["threadKey"] if !ok { http.Error(w, "Not Found", http.StatusNotFound) return } - messages, err = Database.GetMessagesByThreadId(threadID) + messageThread, err = Database.GetMessageThreadById(threadKey, userData) if !ok { http.Error(w, "Not Found", http.StatusNotFound) return } - returnJson, err = json.MarshalIndent(messages, "", " ") + returnJson, err = json.MarshalIndent(messageThread, "", " ") if err != nil { http.Error(w, "Error", http.StatusInternalServerError) return diff --git a/Backend/Api/Routes.go b/Backend/Api/Routes.go index 9715739..adc2f4a 100644 --- a/Backend/Api/Routes.go +++ b/Backend/Api/Routes.go @@ -66,5 +66,5 @@ func InitApiEndpoints(router *mux.Router) { authApi.HandleFunc("/friend/{userID}/request", Friends.FriendRequest).Methods("POST") // Define routes for messages - authApi.HandleFunc("/messages/{threadID}", Messages.MessageThread).Methods("GET") + authApi.HandleFunc("/messages/{threadKey}", Messages.MessageThread).Methods("GET") } diff --git a/Backend/Database/Friends.go b/Backend/Database/Friends.go index 946a1d3..d82ccab 100644 --- a/Backend/Database/Friends.go +++ b/Backend/Database/Friends.go @@ -2,7 +2,6 @@ package Database import ( "git.tovijaeschke.xyz/tovi/Envelope/Backend/Models" - "github.com/gofrs/uuid" "gorm.io/gorm" "gorm.io/gorm/clause" @@ -36,15 +35,6 @@ func GetFriendsByUserId(userID string) ([]Models.Friend, error) { } 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 diff --git a/Backend/Database/Init.go b/Backend/Database/Init.go index 7918085..12f90ae 100644 --- a/Backend/Database/Init.go +++ b/Backend/Database/Init.go @@ -22,6 +22,8 @@ func GetModels() []interface{} { &Models.Friend{}, &Models.MessageData{}, &Models.Message{}, + &Models.MessageThread{}, + &Models.MessageThreadUser{}, } } diff --git a/Backend/Database/MessageThreadUsers.go b/Backend/Database/MessageThreadUsers.go new file mode 100644 index 0000000..842bc15 --- /dev/null +++ b/Backend/Database/MessageThreadUsers.go @@ -0,0 +1,39 @@ +package Database + +import ( + "git.tovijaeschke.xyz/tovi/Envelope/Backend/Models" + + "gorm.io/gorm" + "gorm.io/gorm/clause" +) + +func GetMessageThreadUserById(id string) (Models.MessageThreadUser, error) { + var ( + message Models.MessageThreadUser + err error + ) + + err = DB.Preload(clause.Associations). + First(&message, "id = ?", id). + Error + + return message, err +} + +func CreateMessageThreadUser(messageThreadUser *Models.MessageThreadUser) error { + var ( + err error + ) + + err = DB.Session(&gorm.Session{FullSaveAssociations: true}). + Create(messageThreadUser). + Error + + return err +} + +func DeleteMessageThreadUser(messageThreadUser *Models.MessageThreadUser) error { + return DB.Session(&gorm.Session{FullSaveAssociations: true}). + Delete(messageThreadUser). + Error +} diff --git a/Backend/Database/MessageThreads.go b/Backend/Database/MessageThreads.go new file mode 100644 index 0000000..be3ffc1 --- /dev/null +++ b/Backend/Database/MessageThreads.go @@ -0,0 +1,42 @@ +package Database + +import ( + "git.tovijaeschke.xyz/tovi/Envelope/Backend/Models" + + "gorm.io/gorm" + "gorm.io/gorm/clause" +) + +func GetMessageThreadById(id string, user Models.User) (Models.MessageThread, error) { + var ( + messageThread Models.MessageThread + err error + ) + + err = DB.Preload(clause.Associations). + Where("id = ?", id). + Where("user_id = ?", user.ID). + First(&messageThread). + Error + + return messageThread, err +} + +func CreateMessageThread(messageThread *Models.MessageThread) error { + return DB.Session(&gorm.Session{FullSaveAssociations: true}). + Create(messageThread). + Error +} + +func UpdateMessageThread(messageThread *Models.MessageThread) error { + return DB.Session(&gorm.Session{FullSaveAssociations: true}). + Where("id = ?", messageThread.ID). + Updates(messageThread). + Error +} + +func DeleteMessageThread(messageThread *Models.MessageThread) error { + return DB.Session(&gorm.Session{FullSaveAssociations: true}). + Delete(messageThread). + Error +} diff --git a/Backend/Database/Messages.go b/Backend/Database/Messages.go index 1d17b09..866cc40 100644 --- a/Backend/Database/Messages.go +++ b/Backend/Database/Messages.go @@ -20,19 +20,6 @@ func GetMessageById(id string) (Models.Message, 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 diff --git a/Backend/Database/Seeder/MessageSeeder.go b/Backend/Database/Seeder/MessageSeeder.go index 4b36671..6a7504d 100644 --- a/Backend/Database/Seeder/MessageSeeder.go +++ b/Backend/Database/Seeder/MessageSeeder.go @@ -8,10 +8,12 @@ import ( "crypto/rsa" "crypto/sha512" "encoding/pem" + "fmt" "hash" "git.tovijaeschke.xyz/tovi/Envelope/Backend/Database" "git.tovijaeschke.xyz/tovi/Envelope/Backend/Models" + "git.tovijaeschke.xyz/tovi/Envelope/Backend/Util" "github.com/gofrs/uuid" ) @@ -39,27 +41,17 @@ func PKCS5Padding(ciphertext []byte, blockSize int, after int) []byte { return append(ciphertext, padtext...) } -func seedMessage(primaryUser, secondaryUser Models.User, threadID uuid.UUID, i int) error { +func generateAesKey() (*pem.Block, cipher.BlockMode) { var ( - messageKey Models.Message - messageData Models.MessageData - - block cipher.Block - mode cipher.BlockMode pemBlock *pem.Block + block cipher.Block - plaintext string - ciphertext []byte - - bKey []byte - bIV []byte - bPlaintext []byte + bKey []byte + bIV []byte err error ) - plaintext = "Test Message" - bKey = make([]byte, 32) _, err = rand.Read(bKey) if err != nil { @@ -70,7 +62,6 @@ func seedMessage(primaryUser, secondaryUser Models.User, threadID uuid.UUID, i i if err != nil { panic(err) } - bPlaintext = PKCS5Padding([]byte(plaintext), aes.BlockSize, len(plaintext)) pemBlock = &pem.Block{ Type: "AES KEY", @@ -82,53 +73,256 @@ func seedMessage(primaryUser, secondaryUser Models.User, threadID uuid.UUID, i i panic(err) } - ciphertext = make([]byte, len(bPlaintext)) + return pemBlock, cipher.NewCBCEncrypter(block, bIV) +} + +func seedMessage( + primaryUser Models.User, + primaryUserThreadKey, secondaryUserThreadKey string, + thread Models.MessageThread, + i int, +) error { + var ( + message Models.Message + messageData Models.MessageData + + messagePemBlock *pem.Block + messageMode cipher.BlockMode + + plaintext string + dataCiphertext []byte + senderIdCiphertext []byte + + bPlaintext []byte + bSenderIdPlaintext []byte + + err error + ) + + plaintext = "Test Message" + bPlaintext = PKCS5Padding([]byte(plaintext), aes.BlockSize, len(plaintext)) + bSenderIdPlaintext = PKCS5Padding(primaryUser.ID.Bytes(), aes.BlockSize, len(primaryUser.ID.Bytes())) + + dataCiphertext = make([]byte, len(bPlaintext)) + senderIdCiphertext = make([]byte, len(bSenderIdPlaintext)) - mode = cipher.NewCBCEncrypter(block, bIV) + messagePemBlock, messageMode = generateAesKey() - mode.CryptBlocks(ciphertext, bPlaintext) + messageMode.CryptBlocks(dataCiphertext, bPlaintext) + messageMode.CryptBlocks(senderIdCiphertext, bSenderIdPlaintext) messageData = Models.MessageData{ - Data: ciphertext, + Data: dataCiphertext, + SenderID: senderIdCiphertext, } - messageKey = Models.Message{ - UserID: primaryUser.ID, + message = Models.Message{ MessageData: messageData, - MessageType: "sender", - RelationalUserId: encryptWithPublicKey(secondaryUser.ID.Bytes(), decodedPublicKey), - SymmetricKey: string(pem.EncodeToMemory(pemBlock)), + SymmetricKey: encryptWithPublicKey(pem.EncodeToMemory(messagePemBlock), decodedPublicKey), + MessageThreadKey: primaryUserThreadKey, + } + + err = Database.CreateMessage(&message) + if err != nil { + return err + } + + // The symmetric key would be encrypted with secondary users public key in production + // But due to using the same pub/priv key pair for all users, we will just duplicate it + message = Models.Message{ + MessageDataID: message.MessageDataID, + SymmetricKey: encryptWithPublicKey(pem.EncodeToMemory(messagePemBlock), decodedPublicKey), + MessageThreadKey: secondaryUserThreadKey, + } + + err = Database.CreateMessage(&message) + if err != nil { + return err } - return Database.CreateMessage(&messageKey) + return err +} + +func seedMessageThread(threadPemBlock *pem.Block, threadMode cipher.BlockMode) (Models.MessageThread, error) { + var ( + messageThread Models.MessageThread + + name string + bNamePlaintext []byte + nameCiphertext []byte + + err error + ) + + name = "Test Conversation" + + bNamePlaintext = PKCS5Padding([]byte(name), aes.BlockSize, len(name)) + nameCiphertext = make([]byte, len(bNamePlaintext)) + + threadMode.CryptBlocks(nameCiphertext, bNamePlaintext) + + messageThread = Models.MessageThread{ + Name: nameCiphertext, + } + + err = Database.CreateMessageThread(&messageThread) + return messageThread, err +} + +func seedUpdateMessageThreadUsers( + userJson string, + threadPemBlock *pem.Block, + threadMode cipher.BlockMode, + messageThread Models.MessageThread, +) (Models.MessageThread, error) { + var ( + bUsersPlaintext []byte + usersCiphertext []byte + err error + ) + + bUsersPlaintext = PKCS5Padding([]byte(userJson), aes.BlockSize, len(userJson)) + usersCiphertext = make([]byte, len(bUsersPlaintext)) + + threadMode.CryptBlocks(usersCiphertext, bUsersPlaintext) + + messageThread.Users = usersCiphertext + err = Database.UpdateMessageThread(&messageThread) + return messageThread, err +} + +func seedMessageThreadUser( + user Models.User, + threadID uuid.UUID, + messageThreadKey string, + threadPemBlock *pem.Block, + threadMode cipher.BlockMode, +) (Models.MessageThreadUser, error) { + var ( + messageThreadUser Models.MessageThreadUser + + bThreadIdPlaintext []byte + threadIdCiphertext []byte + + bKeyPlaintext []byte + keyCiphertext []byte + + bAdminPlaintext []byte + adminCiphertext []byte + + err error + ) + + bThreadIdPlaintext = PKCS5Padding(threadID.Bytes(), aes.BlockSize, len(threadID.String())) + threadIdCiphertext = make([]byte, len(bThreadIdPlaintext)) + + bKeyPlaintext = PKCS5Padding([]byte(messageThreadKey), aes.BlockSize, len(messageThreadKey)) + keyCiphertext = make([]byte, len(bKeyPlaintext)) + + bAdminPlaintext = PKCS5Padding([]byte("true"), aes.BlockSize, len("true")) + adminCiphertext = make([]byte, len(bAdminPlaintext)) + + threadMode.CryptBlocks(threadIdCiphertext, bThreadIdPlaintext) + threadMode.CryptBlocks(keyCiphertext, bKeyPlaintext) + threadMode.CryptBlocks(adminCiphertext, bAdminPlaintext) + + messageThreadUser = Models.MessageThreadUser{ + UserID: user.ID, + MessageThreadID: threadIdCiphertext, + MessageThreadKey: keyCiphertext, + Admin: adminCiphertext, + SymmetricKey: encryptWithPublicKey(pem.EncodeToMemory(threadPemBlock), decodedPublicKey), + } + + err = Database.CreateMessageThreadUser(&messageThreadUser) + return messageThreadUser, err } func SeedMessages() { var ( - primaryUser Models.User - secondaryUser Models.User - threadID uuid.UUID - i int - err error + messageThread Models.MessageThread + threadPemBlock *pem.Block + threadMode cipher.BlockMode + + primaryUser Models.User + primaryUserThreadKey string + secondaryUser Models.User + secondaryUserThreadKey string + + userJson string + + thread Models.MessageThread + i int + err error ) + threadPemBlock, threadMode = generateAesKey() + messageThread, err = seedMessageThread(threadPemBlock, threadMode) + primaryUserThreadKey = Util.RandomString(32) + secondaryUserThreadKey = Util.RandomString(32) + primaryUser, err = Database.GetUserByUsername("testUser") if err != nil { panic(err) } + _, err = seedMessageThreadUser( + primaryUser, + messageThread.ID, + primaryUserThreadKey, + threadPemBlock, + threadMode, + ) + secondaryUser, err = Database.GetUserByUsername("testUser2") if err != nil { panic(err) } - threadID, err = uuid.NewV4() - if err != nil { - panic(err) + _, err = seedMessageThreadUser( + secondaryUser, + messageThread.ID, + secondaryUserThreadKey, + threadPemBlock, + threadMode, + ) + + userJson = fmt.Sprintf( + ` +[ + { + "id": "%s", + "username": "%s", + "admin": "true" + }, + { + "id": "%s", + "username": "%s", + "admin": "true" } +] + `, + primaryUser.ID.String(), + primaryUser.Username, + secondaryUser.ID.String(), + secondaryUser.Username, + ) + + messageThread, err = seedUpdateMessageThreadUsers( + userJson, + threadPemBlock, + threadMode, + messageThread, + ) for i = 0; i <= 20; i++ { - err = seedMessage(primaryUser, secondaryUser, threadID, i) + err = seedMessage( + primaryUser, + primaryUserThreadKey, + secondaryUserThreadKey, + thread, + i, + ) if err != nil { panic(err) } diff --git a/Backend/Database/Seeder/Seed.go b/Backend/Database/Seeder/Seed.go index e1d2531..07b7248 100644 --- a/Backend/Database/Seeder/Seed.go +++ b/Backend/Database/Seeder/Seed.go @@ -31,7 +31,6 @@ func Seed() { err error ) - // TODO: Fix this parsing block, _ = pem.Decode([]byte(publicKey)) decKey, err = x509.ParsePKIXPublicKey(block.Bytes) if err != nil { diff --git a/Backend/Models/Friends.go b/Backend/Models/Friends.go index b56b8ab..b76d530 100644 --- a/Backend/Models/Friends.go +++ b/Backend/Models/Friends.go @@ -12,7 +12,5 @@ type Friend struct { 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 35e903d..6e1893e 100644 --- a/Backend/Models/Messages.go +++ b/Backend/Models/Messages.go @@ -2,25 +2,32 @@ package Models import "github.com/gofrs/uuid" -const ( - MessageTypeSender = "sender" - MessageTypeReceiver = "reciever" -) - type MessageData struct { Base - Data []byte `gorm:"not null" json:"data"` // Stored encrypted + Data []byte `gorm:"not null" json:"data"` // Stored encrypted + SenderID []byte `gorm:"not null" json:"sender_id"` } -// TODO: Rename this to something better type Message struct { Base - 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 + SymmetricKey []byte `gorm:"not null" json:"symmetric_key"` // Stored encrypted + MessageThreadKey string `gorm:"not null" json:"message_thread_key"` +} + +type MessageThread struct { + Base + Name []byte `gorm:"not null" json:"name"` + Users []byte `json:"users"` +} + +type MessageThreadUser struct { + Base + UserID uuid.UUID `gorm:"type:uuid;column:user_id;not null;" json:"user_id"` + User User `json:"user"` + MessageThreadID []byte `gorm:"not null" json:"message_thread_link_id"` + MessageThreadKey []byte `gorm:"not null" json:"message_thread_key"` + Admin []byte `gorm:"not null" json:"admin"` // Bool if user is admin of thread, stored encrypted + SymmetricKey []byte `gorm:"not null" json:"symmetric_key"` // Stored encrypted } diff --git a/Backend/Util/Strings.go b/Backend/Util/Strings.go new file mode 100644 index 0000000..a2d5d0f --- /dev/null +++ b/Backend/Util/Strings.go @@ -0,0 +1,21 @@ +package Util + +import ( + "math/rand" +) + +var ( + letterRunes = []rune("abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ") +) + +func RandomString(n int) string { + var ( + b []rune + i int + ) + b = make([]rune, n) + for i = range b { + b[i] = letterRunes[rand.Intn(len(letterRunes))] + } + return string(b) +}