From 2cafc3163ca585132dce202fa7eb4aecf12c26d4 Mon Sep 17 00:00:00 2001 From: afonso Date: Tue, 23 Apr 2024 11:12:18 +0100 Subject: [PATCH] [PD1] small changes --- Projs/PD1/.ignore | 1 + Projs/PD1/internal/client/client.go | 57 +++++++++++++----- Projs/PD1/internal/client/interface.go | 17 ++---- Projs/PD1/internal/protocol/protocol.go | 41 ++++++++++++- Projs/PD1/internal/server/datastore.go | 27 +++++---- Projs/PD1/internal/server/interface.go | 5 +- Projs/PD1/internal/server/server.go | 46 ++++++++++---- .../internal/utils/cryptoUtils/cryptoUtils.go | 23 +++++-- Projs/PD1/server.db | Bin 45056 -> 49152 bytes Projs/PD1/tokefile.toml | 14 ++--- 10 files changed, 160 insertions(+), 71 deletions(-) create mode 100644 Projs/PD1/.ignore diff --git a/Projs/PD1/.ignore b/Projs/PD1/.ignore new file mode 100644 index 0000000..b229014 --- /dev/null +++ b/Projs/PD1/.ignore @@ -0,0 +1 @@ +certs diff --git a/Projs/PD1/internal/client/client.go b/Projs/PD1/internal/client/client.go index 3769cb2..43bd84e 100644 --- a/Projs/PD1/internal/client/client.go +++ b/Projs/PD1/internal/client/client.go @@ -20,7 +20,7 @@ func Run() { panic("No command provided. Use 'help' for instructions.") } //Get user KeyStore - password := AskUserPassword() + password := readStdin("Insert keystore passphrase") clientKeyStore := cryptoUtils.LoadKeyStore(userFile, password) command := flag.Arg(0) @@ -49,7 +49,14 @@ func Run() { if !cl.Connection.Send(sendMsgPacket) { return } - cl.Connection.Conn.Close() + answerSendMsg, active := cl.Connection.Receive() + if !active { + return + } + if answerSendMsg.Flag == protocol.FlagReportError { + reportError := protocol.UnmarshalReportError(answerSendMsg.Body) + log.Println(reportError.ErrorMessage) + } case "askqueue": pageInput := flag.Arg(1) @@ -69,7 +76,7 @@ func Run() { cl := networking.NewClient[protocol.Packet](&clientKeyStore) defer cl.Connection.Conn.Close() - askQueue(cl,clientKeyStore, page, pageSize) + askQueue(cl, clientKeyStore, page, pageSize) case "getmsg": if flag.NArg() < 2 { @@ -89,6 +96,11 @@ func Run() { if !active { return } + if receivedMsgPacket.Flag == protocol.FlagReportError { + reportError := protocol.UnmarshalReportError(receivedMsgPacket.Body) + log.Println(reportError.ErrorMessage) + return + } answerGetMsg := protocol.UnmarshalAnswerGetMsg(receivedMsgPacket.Body) senderCert := getUserCert(cl, answerGetMsg.FromUID) decSubjectBytes := clientKeyStore.DecryptMessageContent(senderCert, answerGetMsg.Subject) @@ -117,6 +129,11 @@ func getUserCert(cl networking.Client[protocol.Packet], uid string) *x509.Certif if !active { return nil } + if answerGetUserCertPacket.Flag == protocol.FlagReportError { + reportError := protocol.UnmarshalReportError(answerGetUserCertPacket.Body) + log.Println(reportError.ErrorMessage) + return nil + } answerGetUserCert := protocol.UnmarshalAnswerGetUserCert(answerGetUserCertPacket.Body) userCert, err := x509.ParseCertificate(answerGetUserCert.Certificate) if err != nil { @@ -130,6 +147,11 @@ func getManyMessagesInfo(cl networking.Client[protocol.Packet]) (protocol.Answer if !active { return protocol.NewAnswerGetUnreadMsgsInfo(0, 0, nil), nil } + if answerGetUnreadMsgsInfoPacket.Flag == protocol.FlagReportError { + reportError := protocol.UnmarshalReportError(answerGetUnreadMsgsInfoPacket.Body) + log.Println(reportError.ErrorMessage) + return protocol.NewAnswerGetUnreadMsgsInfo(0, 0, nil), nil + } answerGetUnreadMsgsInfo := protocol.UnmarshalAnswerGetUnreadMsgsInfo(answerGetUnreadMsgsInfoPacket.Body) //Create Set of needed certificates @@ -146,7 +168,7 @@ func getManyMessagesInfo(cl networking.Client[protocol.Packet]) (protocol.Answer return answerGetUnreadMsgsInfo, certificatesMap } -func askQueue(cl networking.Client[protocol.Packet],clientKeyStore cryptoUtils.KeyStore, page int, pageSize int) { +func askQueue(cl networking.Client[protocol.Packet], clientKeyStore cryptoUtils.KeyStore, page int, pageSize int) { requestUnreadMsgsQueuePacket := protocol.NewGetUnreadMsgsInfoPacket(page, pageSize) if !cl.Connection.Send(requestUnreadMsgsQueuePacket) { return @@ -156,8 +178,13 @@ func askQueue(cl networking.Client[protocol.Packet],clientKeyStore cryptoUtils.K for _, message := range unreadMsgsInfo.MessagesInfo { senderCert, ok := certificates[message.FromUID] if ok { - decryptedSubjectBytes := clientKeyStore.DecryptMessageContent(senderCert, message.Subject) - subject := Unmarshal(decryptedSubjectBytes) + var subject string + if senderCert != nil { + decryptedSubjectBytes := clientKeyStore.DecryptMessageContent(senderCert, message.Subject) + subject = Unmarshal(decryptedSubjectBytes) + } else { + subject = "" + } clientMessage := newClientMessageInfo(message.Num, message.FromUID, subject, message.Timestamp) clientMessages = append(clientMessages, clientMessage) } @@ -167,13 +194,13 @@ func askQueue(cl networking.Client[protocol.Packet],clientKeyStore cryptoUtils.K return clientMessages[i].Num > clientMessages[j].Num }) - action := showMessagesInfo(unreadMsgsInfo.Page, unreadMsgsInfo.NumPages, clientMessages) - switch action { - case -1: - askQueue(cl, clientKeyStore , max(1,unreadMsgsInfo.Page-1) , pageSize) - case 0: - return - case 1: - askQueue(cl, clientKeyStore , max(1,unreadMsgsInfo.Page+1) , pageSize) - } + action := showMessagesInfo(unreadMsgsInfo.Page, unreadMsgsInfo.NumPages, clientMessages) + switch action { + case -1: + askQueue(cl, clientKeyStore, max(1, unreadMsgsInfo.Page-1), pageSize) + case 0: + return + case 1: + askQueue(cl, clientKeyStore, max(1, unreadMsgsInfo.Page+1), pageSize) + } } diff --git a/Projs/PD1/internal/client/interface.go b/Projs/PD1/internal/client/interface.go index 82f2d8a..21551c1 100644 --- a/Projs/PD1/internal/client/interface.go +++ b/Projs/PD1/internal/client/interface.go @@ -11,15 +11,6 @@ func readStdin(message string) string { fmt.Println(message) scanner := bufio.NewScanner(os.Stdin) scanner.Scan() - // FIX: make sure this doesnt die - return scanner.Text() -} - -func AskUserPassword() string { - fmt.Println("Enter key store password") - scanner := bufio.NewScanner(os.Stdin) - scanner.Scan() - // FIX: make sure this doesnt die return scanner.Text() } @@ -43,9 +34,13 @@ func showMessagesInfo(page int, numPages int, messages []ClientMessageInfo) int return 0 } for _, message := range messages { + if message.Subject == "" { + fmt.Printf("ERROR DECRYPTING MESSAGE %v IN QUEUE FROM UID %v\n", message.Num, message.FromUID) + continue + } fmt.Printf("%v:%v:%v:%v\n", message.Num, message.FromUID, message.Timestamp, message.Subject) } - fmt.Printf("Page %v/%v\n",page,numPages) + fmt.Printf("Page %v/%v\n", page, numPages) return messagesInfoPageNavigation(page, numPages) } @@ -89,11 +84,9 @@ func messagesInfoPageNavigation(page int, numPages int) int { return 0 } - func showMessage(message ClientMessage) { fmt.Printf("From: %s\n", message.FromUID) fmt.Printf("To: %s\n", message.ToUID) fmt.Printf("Subject: %s\n", message.Subject) fmt.Printf("Body: %s\n", message.Body) } - diff --git a/Projs/PD1/internal/protocol/protocol.go b/Projs/PD1/internal/protocol/protocol.go index 680c2b5..0e57d08 100644 --- a/Projs/PD1/internal/protocol/protocol.go +++ b/Projs/PD1/internal/protocol/protocol.go @@ -29,6 +29,12 @@ const ( // Server sends requested message FlagAnswerGetMsg + + // Server tells the client that the message was successfully sent + FlagAnswerSendMsg + + // Report an error + FlagReportError ) type ( @@ -76,6 +82,10 @@ type ( Body []byte `json:"body"` Timestamp time.Time `json:"timestamp"` } + + ReportError struct { + ErrorMessage string `json:"error"` + } ) type PacketBody interface{} @@ -127,7 +137,7 @@ func NewAnswerGetUserCert(uid string, certificate []byte) AnswerGetUserCert { } func NewAnswerGetUnreadMsgsInfo(page int, numPages int, messagesInfo []MsgInfo) AnswerGetUnreadMsgsInfo { - return AnswerGetUnreadMsgsInfo{Page:page,NumPages:numPages,MessagesInfo: messagesInfo} + return AnswerGetUnreadMsgsInfo{Page: page, NumPages: numPages, MessagesInfo: messagesInfo} } func NewMsgInfo(num int, fromUID string, subject []byte, timestamp time.Time) MsgInfo { return MsgInfo{ @@ -148,6 +158,12 @@ func NewAnswerGetMsg(fromUID, toUID string, subject []byte, body []byte, timesta } } +func NewReportError(errorMessage string) ReportError { + return ReportError{ + ErrorMessage: errorMessage, + } +} + func NewGetUserCertPacket(UID string) Packet { return NewPacket(FlagGetUserCert, NewGetUserCert(UID)) } @@ -169,13 +185,22 @@ func NewAnswerGetUserCertPacket(uid string, certificate []byte) Packet { } func NewAnswerGetUnreadMsgsInfoPacket(page int, numPages int, messagesInfo []MsgInfo) Packet { - return NewPacket(FlagAnswerGetUnreadMsgsInfo, NewAnswerGetUnreadMsgsInfo(page,numPages,messagesInfo)) + return NewPacket(FlagAnswerGetUnreadMsgsInfo, NewAnswerGetUnreadMsgsInfo(page, numPages, messagesInfo)) } func NewAnswerGetMsgPacket(fromUID, toUID string, subject []byte, body []byte, timestamp time.Time, last bool) Packet { return NewPacket(FlagAnswerGetMsg, NewAnswerGetMsg(fromUID, toUID, subject, body, timestamp, last)) } +func NewAnswerSendMsgPacket() Packet{ + //This packet has no body + return NewPacket(FlagAnswerSendMsg,nil) +} + +func NewReportErrorPacket(errorMessage string) Packet { + return NewPacket(FlagReportError, NewReportError(errorMessage)) +} + func UnmarshalGetUserCert(data PacketBody) GetUserCert { jsonData, err := json.Marshal(data) if err != nil { @@ -270,3 +295,15 @@ func UnmarshalAnswerGetMsg(data PacketBody) AnswerGetMsg { } return packet } + +func UnmarshalReportError(data PacketBody) ReportError { + jsonData, err := json.Marshal(data) + if err != nil { + panic(fmt.Errorf("failed to marshal data: %v", err)) + } + var packet ReportError + if err := json.Unmarshal(jsonData, &packet); err != nil { + panic(fmt.Errorf("failed to unmarshal into AnswerGetMsg: %v", err)) + } + return packet +} diff --git a/Projs/PD1/internal/server/datastore.go b/Projs/PD1/internal/server/datastore.go index c2466c1..b578630 100644 --- a/Projs/PD1/internal/server/datastore.go +++ b/Projs/PD1/internal/server/datastore.go @@ -88,8 +88,10 @@ func (ds DataStore) GetMessage(toUID string, position int) protocol.Packet { // Execute the query row := ds.db.QueryRow(query, toUID, position) err := row.Scan(&serverMessage.FromUID, &serverMessage.ToUID, &serverMessage.Subject, &serverMessage.Body, &serverMessage.Timestamp) - if err != nil { - log.Printf("Error getting the message in position %v from UID %v: %v", position, toUID, err) + if err == sql.ErrNoRows { + log.Printf("No message with NUM %v for UID %v\n", position, toUID) + errorMessage := fmt.Sprintf("No message with NUM %v", position) + return protocol.NewReportErrorPacket(errorMessage) } return protocol.NewAnswerGetMsgPacket(serverMessage.FromUID, serverMessage.ToUID, serverMessage.Subject, serverMessage.Body, serverMessage.Timestamp, true) @@ -119,8 +121,9 @@ func (ds DataStore) GetUnreadMsgsInfo(toUID string, page int, pageSize int) prot // Retrieve the total count of unread messages var totalCount int err := ds.db.QueryRow("SELECT COUNT(*) FROM messages WHERE toUID = ? AND status = 0", toUID).Scan(&totalCount) - if err != nil { - log.Printf("Error getting total count of unread messages for UID %v: %v", toUID, err) + if err == sql.ErrNoRows { + log.Printf("No unread messages for UID %v: %v", toUID, err) + return protocol.NewAnswerGetUnreadMsgsInfoPacket(0, 0, []protocol.MsgInfo{}) } // Query to retrieve all messages from the user's queue @@ -143,7 +146,7 @@ func (ds DataStore) GetUnreadMsgsInfo(toUID string, page int, pageSize int) prot // Execute the query rows, err := ds.db.Query(query, toUID, pageSize, (page-1)*pageSize) if err != nil { - log.Printf("Error getting all messages for UID %v: %v", toUID, err) + log.Printf("Error getting unread messages for UID %v: %v", toUID, err) } defer rows.Close() @@ -161,6 +164,7 @@ func (ds DataStore) GetUnreadMsgsInfo(toUID string, page int, pageSize int) prot } if err := rows.Err(); err != nil { log.Printf("Error when getting messages for UID %v: %v", toUID, err) + return protocol.NewReportErrorPacket(err.Error()) } numberOfPages := (totalCount + pageSize - 1) / pageSize @@ -168,7 +172,7 @@ func (ds DataStore) GetUnreadMsgsInfo(toUID string, page int, pageSize int) prot return protocol.NewAnswerGetUnreadMsgsInfoPacket(currentPage, numberOfPages, messageInfoPackets) } -func (ds DataStore) AddMessageToQueue(fromUID string, message protocol.SendMsg) { +func (ds DataStore) AddMessageToQueue(fromUID string, message protocol.SendMsg) protocol.Packet { query := ` INSERT INTO messages (fromUID, toUID, subject, body, timestamp, status) VALUES (?, ?, ?, ?, ?, 0) @@ -179,7 +183,9 @@ func (ds DataStore) AddMessageToQueue(fromUID string, message protocol.SendMsg) _, err := ds.db.Exec(query, fromUID, message.ToUID, message.Subject, message.Body, currentTime) if err != nil { log.Printf("Error adding message to UID %v: %v", fromUID, err) + return protocol.NewReportErrorPacket(err.Error()) } + return protocol.NewAnswerSendMsgPacket() } func (ds DataStore) GetUserCertificate(uid string) protocol.Packet { @@ -193,12 +199,10 @@ func (ds DataStore) GetUserCertificate(uid string) protocol.Packet { var userCertBytes []byte err := ds.db.QueryRow(query, uid).Scan(&userCertBytes) if err == sql.ErrNoRows { - log.Panicf("No certificate for UID %v found in the database", uid) + errorMessage := fmt.Sprintf("No certificate for UID %v found in the database", uid) + log.Println(errorMessage) + return protocol.NewReportErrorPacket(errorMessage) } - //userCert,err := x509.ParseCertificate(userCertBytes) - //if err!=nil { - // log.Panicf("Error parsing certificate for UID %v",uid) - //} return protocol.NewAnswerGetUserCertPacket(uid, userCertBytes) } @@ -224,7 +228,6 @@ func (ds DataStore) userExists(uid string) bool { func (ds DataStore) storeUserCertIfNotExists(uid string, cert x509.Certificate) { // Check if the user already exists if ds.userExists(uid) { - log.Printf("User certificate for UID %s already exists.\n", uid) return } diff --git a/Projs/PD1/internal/server/interface.go b/Projs/PD1/internal/server/interface.go index f593c69..40e156f 100644 --- a/Projs/PD1/internal/server/interface.go +++ b/Projs/PD1/internal/server/interface.go @@ -6,10 +6,9 @@ import ( "os" ) -func AskServerPassword() string { - fmt.Println("Enter key store password") +func readStdin(message string) string { + fmt.Println(message) scanner := bufio.NewScanner(os.Stdin) scanner.Scan() - // FIX: make sure this doesnt die return scanner.Text() } diff --git a/Projs/PD1/internal/server/server.go b/Projs/PD1/internal/server/server.go index 1f8def5..ffd002c 100644 --- a/Projs/PD1/internal/server/server.go +++ b/Projs/PD1/internal/server/server.go @@ -4,12 +4,10 @@ import ( "PD1/internal/protocol" "PD1/internal/utils/cryptoUtils" "PD1/internal/utils/networking" + "log" ) -//TODO: CREATE SERVER SIDE CHECKS FOR EVERYTHING //TODO: LOGGING SYSTEM -//TODO: TELL THE USER THAT THE MESSAGE HAS BEEN RECEIVED BY THE SERVER -//TODO: ERROR PACKET TO SEND BACK TO USER func clientHandler(connection networking.Connection[protocol.Packet], dataStore DataStore) { defer connection.Conn.Close() @@ -18,10 +16,16 @@ func clientHandler(connection networking.Connection[protocol.Packet], dataStore clientCert := connection.GetPeerCertificate() //Get the OID values oidMap := cryptoUtils.ExtractAllOIDValues(clientCert) + //Check if certificate usage is MSG SERVICE + usage := oidMap["2.5.4.11"] + if usage == "" { + log.Println("User certificate does not have the correct usage") + return + } //Get the UID of this user UID := oidMap["2.5.4.65"] if UID == "" { - panic("User certificate does not specify it's PSEUDONYM") + log.Println("User certificate does not specify it's PSEUDONYM") } dataStore.storeUserCertIfNotExists(UID, *clientCert) F: @@ -34,31 +38,47 @@ F: case protocol.FlagGetUserCert: reqUserCert := protocol.UnmarshalGetUserCert(pac.Body) userCertPacket := dataStore.GetUserCertificate(reqUserCert.UID) - if active := connection.Send(userCertPacket); !active { + if !connection.Send(userCertPacket) { break F } case protocol.FlagGetUnreadMsgsInfo: getUnreadMsgsInfo := protocol.UnmarshalGetUnreadMsgsInfo(pac.Body) - messages := dataStore.GetUnreadMsgsInfo(UID,getUnreadMsgsInfo.Page,getUnreadMsgsInfo.PageSize) + var messages protocol.Packet + if getUnreadMsgsInfo.Page <= 0 || getUnreadMsgsInfo.PageSize <= 0 { + messages = protocol.NewReportErrorPacket("Page and PageSize need to be >= 1") + } else { + messages = dataStore.GetUnreadMsgsInfo(UID, getUnreadMsgsInfo.Page, getUnreadMsgsInfo.PageSize) + } if !connection.Send(messages) { break F } case protocol.FlagGetMsg: reqMsg := protocol.UnmarshalGetMsg(pac.Body) - message := dataStore.GetMessage(UID, reqMsg.Num) - if active := connection.Send(message); !active { + var message protocol.Packet + if reqMsg.Num <= 0 { + message = protocol.NewReportErrorPacket("Message NUM needs to be >= 1") + } else { + message = dataStore.GetMessage(UID, reqMsg.Num) + } + if !connection.Send(message) { break F } dataStore.MarkMessageInQueueAsRead(UID, reqMsg.Num) case protocol.FlagSendMsg: submitMsg := protocol.UnmarshalSendMsg(pac.Body) - if submitMsg.ToUID != UID && dataStore.userExists(submitMsg.ToUID) { - dataStore.AddMessageToQueue(UID, submitMsg) + var answerSendMsgPacket protocol.Packet + if submitMsg.ToUID == UID { + answerSendMsgPacket = protocol.NewReportErrorPacket("Cannot message yourself") + } else if !dataStore.userExists(submitMsg.ToUID) { + answerSendMsgPacket = protocol.NewReportErrorPacket("Message receiver does not exist in database") + } else { + answerSendMsgPacket = dataStore.AddMessageToQueue(UID, submitMsg) + } + if !connection.Send(answerSendMsgPacket) { + break F } - } } - } func Run(port int) { @@ -69,7 +89,7 @@ func Run(port int) { //FIX: Get the server's keystore path instead of hardcoding it //Read server keystore - password := AskServerPassword() + password := readStdin("Insert keystore passphrase") serverKeyStore := cryptoUtils.LoadKeyStore("certs/server/server.p12", password) //Create server listener diff --git a/Projs/PD1/internal/utils/cryptoUtils/cryptoUtils.go b/Projs/PD1/internal/utils/cryptoUtils/cryptoUtils.go index ca93591..38c8d89 100644 --- a/Projs/PD1/internal/utils/cryptoUtils/cryptoUtils.go +++ b/Projs/PD1/internal/utils/cryptoUtils/cryptoUtils.go @@ -10,7 +10,6 @@ import ( "encoding/binary" "errors" - //"errors" "log" "os" @@ -94,9 +93,25 @@ func (k *KeyStore) GetServerTLSConfig() *tls.Config { caCertPool.AddCert(caCert) } tlsConfig.ClientCAs = caCertPool - //FIX: SERVER ACCEPTS CONNECTIONS WITH UNMATCHING OR - // NO CERTIFICATE, NEEDS TO BE CHANGED SOMEHOW - tlsConfig.ClientAuth = tls.RequireAndVerifyClientCert + tlsConfig.ClientAuth = tls.RequestClientCert + tlsConfig.VerifyPeerCertificate = func(rawCerts [][]byte, _ [][]*x509.Certificate) error { + // Verify the peer's certificate + opts := x509.VerifyOptions{ + Roots: caCertPool, + } + for _, certBytes := range rawCerts { + cert, err := x509.ParseCertificate(certBytes) + if err != nil { + return err + } + // Check if the certificate is signed by the specified CA + _, err = cert.Verify(opts) + if err != nil { + return errors.New("certificate not signed by trusted CA") + } + } + return nil + } return tlsConfig } diff --git a/Projs/PD1/server.db b/Projs/PD1/server.db index e4212f7faea935ea26f6e1f6f5e3d0a7016810de..96db2305cfb1831434738c281f4befdbfaa4e6bf 100644 GIT binary patch delta 2371 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