diff --git a/backend/src/security-layer/container/authentication.go b/backend/src/security-layer/container/authentication.go new file mode 100644 index 0000000..05da4e7 --- /dev/null +++ b/backend/src/security-layer/container/authentication.go @@ -0,0 +1,230 @@ +package main + +import ( + //"context" + "crypto/aes" + "crypto/cipher" + //"sync" + + "crypto/rand" + "crypto/sha1" + "encoding/base64" + "encoding/hex" + //"encoding/json" + "errors" + "fmt" + + "io" + "log" + "net/http" + + //"os" + //"os/signal" + "strconv" + "strings" + "time" + + "github.com/golang-jwt/jwt/v5" + "github.com/gorilla/mux" +) + +// TODO: Dynamically get key? Need to research best way to store private keys between two devices. +var key = []byte("passphrasewhichneedstobe32bytes!") + +func createToken(username string) (string, error) { + token := jwt.NewWithClaims(jwt.SigningMethodHS256, + jwt.MapClaims{ + "username": username, + "exp": time.Now().Add(time.Hour * 24).Unix(), + }) + + jwtToken, err := token.SignedString(key) + if err != nil { + return "Error creating JWT.", err + } + return jwtToken, nil +} + +func verifyToken(tokenString string) error { + token, err := jwt.Parse(tokenString, func(token *jwt.Token) (interface{}, error) { + return key, nil + }) + + if err != nil { + return err + } + + if !token.Valid { + return fmt.Errorf("invalid token") + } + + return nil +} + +func hash(text string) string { + hasher := sha1.New() + hasher.Write([]byte(text)) + return base64.URLEncoding.EncodeToString(hasher.Sum(nil)) +} + +func encrypt() { + var newCommunication Communication + + text := []byte(newCommunication.Communication) + + c, err := aes.NewCipher(key) + if err != nil { + return + //gc.IndentedJSON(http.StatusBadRequest, gin.H{"message": err}) + } + + gcm, err := cipher.NewGCM(c) + if err != nil { + return + //gc.IndentedJSON(http.StatusBadRequest, gin.H{"message": err}) + } + + nonce := make([]byte, gcm.NonceSize()) + if _, err = io.ReadFull(rand.Reader, nonce); err != nil { + return + //gc.IndentedJSON(http.StatusBadRequest, gin.H{"message": err}) + } + + var b []byte = gcm.Seal(nonce, nonce, text, nil) + hex, err := convertBytesToHex(b) + if err != nil { + fmt.Println(err) + } + var test string = "{data: " + hex + ", hash: " + hash(string(text)) + "}" + log.Println("encrypted string:" + test) + //gc.IndentedJSON(http.StatusCreated, gin.H{"message": test}) +} + +func convertBytesToHex(b []byte) (string, error) { + // Handle nil pointer case + if b == nil { + return "", errors.New("nil pointer provided for hex string") + } + + // Split the hex string by spaces + var h string = hex.EncodeToString(b) + var builder strings.Builder + for i := 0; i < len(h); i += 2 { + end := i + 2 + if end > len(h) { + end = len(h) + } + chunk := h[i:end] + builder.WriteString(chunk) + if end < len(h) { + builder.WriteString(" ") + } + } + + // Return the slice of uint8 and any errors encountered + return builder.String(), nil +} + +func convertHexToBytes(hexString *string) ([]uint8, error) { + // Handle nil pointer case + if hexString == nil { + return nil, errors.New("nil pointer provided for hex string") + } + + // Split the hex string by spaces + hexBytes := strings.Fields(*hexString) + + // Initialize an empty slice for uint8 + data := make([]uint8, len(hexBytes)) + + // Iterate and convert each hex byte + for i, hexByte := range hexBytes { + // Convert each hex string to a uint8 value (handling errors) + value, err := strconv.ParseUint(hexByte, 16, 8) + if err != nil { + return nil, fmt.Errorf("error parsing hex byte '%s': %w", hexByte, err) + } + + // Assign the converted value to the slice + data[i] = uint8(value) + } + + // Return the slice of uint8 and any errors encountered + return data, nil +} + +func decrypt(input *Communication, output *Chat) bool { + // TODO: Add testing flag for easier manipulation. + //ciphertext, err := ioutil.ReadFile("myfile") + ciphertext, err := convertHexToBytes(&input.Communication) + + // if our program was unable to read the file + // print out the reason why it can't + if err != nil { + fmt.Println(err) + } + + c, err := aes.NewCipher(key) + if err != nil { + fmt.Println(err) + } + + gcm, err := cipher.NewGCM(c) + if err != nil { + fmt.Println(err) + } + + nonceSize := gcm.NonceSize() + if len(ciphertext) < nonceSize { + fmt.Println(err) + } + + nonce, ciphertext := ciphertext[:nonceSize], ciphertext[nonceSize:] + plaintext, err := gcm.Open(nil, nonce, ciphertext, nil) + if err != nil { + fmt.Println(err) + } + s := string(plaintext) + if validateHash(s, input.Hash) { + output.Content = s + return true + } + input.Communication = "DATA CORRUPTED OR TAMPERED" + return false +} + +// Validate there's no tampering with SHA-1 sum. The decrypted hash and the transmitted hash should be identical. +func validateHash(decrypted string, hash string) bool { + // Calculate the SHA256 sum of the decrypted request + hasher := sha1.New() + hasher.Write([]byte(decrypted)) + decryptedHashString := base64.URLEncoding.EncodeToString(hasher.Sum(nil)) + + // TODO: Add error handling if hash doesn't match. + return decryptedHashString == hash +} + +// Serve the authentication and encryption layer to a provided local port. +// Authentication takes place solely on the backend. +func serveAuthentication( + router *mux.Router, + port int, +) { + // TODO: Add error handling + authR := router.Host("http://localhost").Subrouter() + authSrv := &http.Server{ + Addr: "0.0.0.0:" + string(port), + WriteTimeout: time.Second * 15, + ReadTimeout: time.Second * 15, + IdleTimeout: time.Second * 60, + Handler: authR, + } + + go func() { + if err := authSrv.ListenAndServe(); err != nil && err != http.ErrServerClosed { + log.Println(err) + } + }() + + log.Println("Auth server is running on port " + string(port)) +} diff --git a/backend/src/security-layer/container/router.go b/backend/src/security-layer/container/router.go index e086c7f..717d25c 100644 --- a/backend/src/security-layer/container/router.go +++ b/backend/src/security-layer/container/router.go @@ -2,29 +2,17 @@ package main import ( "context" - "crypto/aes" - "crypto/cipher" - "sync" - - "crypto/rand" - "crypto/sha1" - "encoding/base64" - "encoding/hex" "encoding/json" - "errors" "fmt" - "io" "log" "net/http" - "os" "os/signal" - "strconv" "strings" + "sync" "time" - "github.com/golang-jwt/jwt/v5" "github.com/gorilla/mux" ) @@ -85,187 +73,10 @@ type ResponseWHistory struct { var chats []Chat -// TODO: Test if 256 bit key works. -// TODO: Dynamically get key? Need to research best way to store private keys between two devices. -var key = []byte("passphrasewhichneedstobe32bytes!") - -func createToken(username string) (string, error) { - token := jwt.NewWithClaims(jwt.SigningMethodHS256, - jwt.MapClaims { - "username": username, - "exp": time.Now().Add(time.Hour * 24).Unix(), - }) - - jwtToken, err := token.SignedString(key) - if err != nil { - return "Error creating JWT.", err - } - return jwtToken, nil -} - -func verifyToken(tokenString string) error { - token, err := jwt.Parse(tokenString, func(token *jwt.Token) (interface{}, error) { - return key, nil - }) - - if err != nil { - return err - } - - if !token.Valid { - return fmt.Errorf("invalid token") - } - - return nil -} - -func hash(text string) string { - hasher := sha1.New() - hasher.Write([]byte(text)) - return base64.URLEncoding.EncodeToString(hasher.Sum(nil)) -} - -func encrypt() { - var newCommunication Communication - - text := []byte(newCommunication.Communication) - - c, err := aes.NewCipher(key) - if err != nil { - return - //gc.IndentedJSON(http.StatusBadRequest, gin.H{"message": err}) - } - - gcm, err := cipher.NewGCM(c) - if err != nil { - return - //gc.IndentedJSON(http.StatusBadRequest, gin.H{"message": err}) - } - - nonce := make([]byte, gcm.NonceSize()) - if _, err = io.ReadFull(rand.Reader, nonce); err != nil { - return - //gc.IndentedJSON(http.StatusBadRequest, gin.H{"message": err}) - } - - var b []byte = gcm.Seal(nonce, nonce, text, nil) - hex, err := convertBytesToHex(b) - if err != nil { - fmt.Println(err) - } - var test string = "{data: " + hex + ", hash: " + hash(string(text)) + "}" - log.Println("encrypted string:" + test) - //gc.IndentedJSON(http.StatusCreated, gin.H{"message": test}) -} - -func convertBytesToHex(b []byte) (string, error) { - // Handle nil pointer case - if b == nil { - return "", errors.New("nil pointer provided for hex string") - } - - // Split the hex string by spaces - var h string = hex.EncodeToString(b) - var builder strings.Builder - for i := 0; i < len(h); i += 2 { - end := i + 2 - if end > len(h) { - end = len(h) - } - chunk := h[i:end] - builder.WriteString(chunk) - if end < len(h) { - builder.WriteString(" ") - } - } - - // Return the slice of uint8 and any errors encountered - return builder.String(), nil -} - -func convertHexToBytes(hexString *string) ([]uint8, error) { - // Handle nil pointer case - if hexString == nil { - return nil, errors.New("nil pointer provided for hex string") - } - - // Split the hex string by spaces - hexBytes := strings.Fields(*hexString) - - // Initialize an empty slice for uint8 - data := make([]uint8, len(hexBytes)) - - // Iterate and convert each hex byte - for i, hexByte := range hexBytes { - // Convert each hex string to a uint8 value (handling errors) - value, err := strconv.ParseUint(hexByte, 16, 8) - if err != nil { - return nil, fmt.Errorf("error parsing hex byte '%s': %w", hexByte, err) - } - - // Assign the converted value to the slice - data[i] = uint8(value) - } - - // Return the slice of uint8 and any errors encountered - return data, nil -} - -func decrypt(input *Communication, output *Chat) bool { - // TODO: Add testing flag for easier manipulation. - //ciphertext, err := ioutil.ReadFile("myfile") - ciphertext, err := convertHexToBytes(&input.Communication) - - // if our program was unable to read the file - // print out the reason why it can't - if err != nil { - fmt.Println(err) - } - - c, err := aes.NewCipher(key) - if err != nil { - fmt.Println(err) - } - - gcm, err := cipher.NewGCM(c) - if err != nil { - fmt.Println(err) - } - - nonceSize := gcm.NonceSize() - if len(ciphertext) < nonceSize { - fmt.Println(err) - } - - nonce, ciphertext := ciphertext[:nonceSize], ciphertext[nonceSize:] - plaintext, err := gcm.Open(nil, nonce, ciphertext, nil) - if err != nil { - fmt.Println(err) - } - s := string(plaintext) - if validateHash(s, input.Hash) { - output.Content = s - return true - } - input.Communication = "DATA CORRUPTED OR TAMPERED" - return false -} - -// Validate there's no tampering with SHA-1 sum. The decrypted hash and the transmitted hash should be identical. -func validateHash(decrypted string, hash string) bool { - // Calculate the SHA256 sum of the decrypted request - hasher := sha1.New() - hasher.Write([]byte(decrypted)) - decryptedHashString := base64.URLEncoding.EncodeToString(hasher.Sum(nil)) - - // TODO: Add error handling if hash doesn't match. - return decryptedHashString == hash -} - // User HTTP GET request has the prompt decrypted and verified with a SHA-1 checksum. // If there's been no data corruption, re-construct user prompt for ollama // TODO: Eventually, add additional logic that will allow for re-direction and contextual awareness (pre-processing) -func request(w http.ResponseWriter, r *http.Request) { +func chat(w http.ResponseWriter, r *http.Request) { model := "qwen:0.5b" // qwen:0.5b used for testing while hosting from my laptop. llama3 seems to be the best to use normally. input, err := io.ReadAll(r.Body) @@ -310,16 +121,14 @@ func request(w http.ResponseWriter, r *http.Request) { defer r.Body.Close() response := ResponseWHistory{} json.Unmarshal(output, &response) - log.Println(response) } else { - http.Error(w, "Decryption/Hash failed.", 401) return } } -func chat(w http.ResponseWriter, r *http.Request) { +func request(w http.ResponseWriter, r *http.Request) { input, err := io.ReadAll(r.Body) if err != nil { return @@ -361,16 +170,19 @@ func chat(w http.ResponseWriter, r *http.Request) { //c.IndentedJSON(http.StatusCreated, resp) } -func main() { +func serve() { + +} + +func init() { var wg sync.WaitGroup r := mux.NewRouter() - portfolioDir := "/Users/ashton/Documents/Development/portfolio/public_html/" // MacBook dir - //portfolioDir := "/home/violet/documents/development/portfolio/public_html/" // ThinkPad dir - r.PathPrefix("/").Handler(http.FileServer(http.Dir(portfolioDir))) + portfolioDir := "/home/violet/documents/development/portfolio/public_html/" + servePage(r, portfolioDir, "/", 1112) srv := &http.Server{ - Addr: "0.0.0.0:8080", + Addr: "0.0.0.0:1111", // Good practice to set timeouts to avoid Slowloris attacks. WriteTimeout: time.Second * 15, ReadTimeout: time.Second * 15, @@ -387,32 +199,7 @@ func main() { } }() - log.Println("Portfolio server is running on port 8080") - - // Routing for AI web app and API - apiR := r.Host("ai.joshashton.dev").Subrouter() - apiR.HandleFunc("/request", chat) - apiR.PathPrefix("/").Handler(http.FileServer(http.Dir("/home/violet/templates/construction/"))) - - apiSrv := &http.Server{ - Addr: "0.0.0.0:8081", - // Good practice to set timeouts to avoid Slowloris attacks. - WriteTimeout: time.Second * 15, - ReadTimeout: time.Second * 15, - IdleTimeout: time.Second * 60, - Handler: apiR, // Pass our instance of gorilla/mux in. - } - - // Run our server in a goroutine so that it doesn't block. - wg.Add(1) - go func() { - defer wg.Done() - if err := apiSrv.ListenAndServe(); err != nil && err != http.ErrServerClosed { - log.Println(err) - } - }() - - log.Println("API server is running on port 8081") + log.Println("Router is running on port 1111") c := make(chan os.Signal, 1) // We'll accept graceful shutdowns when quit via SIGINT (Ctrl+Shift+C) @@ -427,7 +214,6 @@ func main() { // Doesn't block if no connections, but will otherwise wait // until the timeout deadline. srv.Shutdown(ctx) - apiSrv.Shutdown(ctx) // Optionally, you could run srv.Shutdown in a goroutine and block on // <-ctx.Done() if your application should wait for other services // to finalize based on context cancellation. @@ -435,3 +221,8 @@ func main() { wg.Wait() os.Exit(0) } + +func main() { + // TODO: Authenticate user credentials to verify they are allowed to even access + serve() +} diff --git a/backend/src/security-layer/container/serveAPI.go b/backend/src/security-layer/container/serveAPI.go new file mode 100644 index 0000000..b9d478a --- /dev/null +++ b/backend/src/security-layer/container/serveAPI.go @@ -0,0 +1,42 @@ +package main + +import ( + "github.com/gorilla/mux" + "log" + "net/http" + "time" +) + +// Serve an API using mux.Router().Host({domain}).Subrouter(). +// Provide the router, domain, an array of endpoints and functions, and +// the port you would like the API accessible to. +func serveApi( + router *mux.Router, + domain string, + endpoint []string, + function []func(http.ResponseWriter, *http.Request), + port int, +) { + // TODO: Add error handling + apiR := router.Host(domain).Subrouter() + + for i := 0; i < len(endpoint); i++ { + apiR.HandleFunc(endpoint[i], function[i]) + } + + apiSrv := &http.Server{ + Addr: "0.0.0.0:" + string(port), + WriteTimeout: time.Second * 15, + ReadTimeout: time.Second * 15, + IdleTimeout: time.Second * 60, + Handler: apiR, + } + + go func() { + if err := apiSrv.ListenAndServe(); err != nil && err != http.ErrServerClosed { + log.Println(err) + } + }() + + log.Println("API server is running on port " + string(port)) +} diff --git a/backend/src/security-layer/container/servePage.go b/backend/src/security-layer/container/servePage.go new file mode 100644 index 0000000..84669f8 --- /dev/null +++ b/backend/src/security-layer/container/servePage.go @@ -0,0 +1,31 @@ +package main + +import ( + "github.com/gorilla/mux" + "log" + "net/http" + "time" +) + +// Serve a directory to a port. +func servePage(router *mux.Router, dest string, path string, port int) { + router.PathPrefix(path).Handler(http.FileServer(http.Dir(dest))) + + srv := &http.Server{ + Addr: "0.0.0.0: " + string(port), + // Good practice to set timeouts to avoid Slowloris attacks. + WriteTimeout: time.Second * 15, + ReadTimeout: time.Second * 15, + IdleTimeout: time.Second * 60, + Handler: router, // Pass our instance of gorilla/mux in. + } + + // Run our server in a goroutine so that it doesn't block. + go func() { + if err := srv.ListenAndServe(); err != nil && err != http.ErrServerClosed { + log.Println(err) + } + }() + + log.Println("Web server is running on port " + string(port)) +}