From b598a090bcc2f3bae64a4d9882517578b2146745 Mon Sep 17 00:00:00 2001 From: Jesse Malotaux Date: Fri, 4 Apr 2025 11:27:19 +0200 Subject: [PATCH] Major Update, registration and authentication of devices works now. This is the first iteration of the registration of devices and authentication for a couple of endpoints. This needs to be refactored, the code is a bit of a mess. Also because of testing some endpoints are available for remotes that shouldn't be. --- be/app/api.go | 29 +++++++-- be/app/device.go | 112 ++++++++++++++++++++++++-------- be/app/helper/api-helper.go | 42 ++++++++++-- be/app/helper/device-helper.go | 23 ++++++- be/app/helper/encrypt-helper.go | 105 ++++++++++++++++++++++++++++++ be/app/helper/env-helper.go | 16 +++++ be/app/macro.go | 6 +- be/app/structs/api-struct.go | 3 + be/app/structs/device-struct.go | 6 ++ be/go.mod | 7 +- be/go.sum | 6 +- be/main.go | 2 +- 12 files changed, 308 insertions(+), 49 deletions(-) create mode 100644 be/app/helper/encrypt-helper.go create mode 100644 be/app/helper/env-helper.go diff --git a/be/app/api.go b/be/app/api.go index ff65ed2..41535e3 100644 --- a/be/app/api.go +++ b/be/app/api.go @@ -48,7 +48,15 @@ func ApiGet(w http.ResponseWriter, r *http.Request) { func ApiPost(w http.ResponseWriter, r *http.Request) { - if !helper.EndpointAccess(w, r) { + access, data := helper.EndpointAccess(w, r) + + if !access { + return + } + + log.Println("api post", data == "") + if data != "" { + ApiAuth(data, w, r) return } @@ -60,7 +68,7 @@ func ApiPost(w http.ResponseWriter, r *http.Request) { case "/macro/delete": DeleteMacro(w, r) case "/macro/play": - PlayMacro(w, r) + PlayMacro("", w, r) case "/device/list": DeviceList(w, r) case "/device/access/check": @@ -71,10 +79,21 @@ func ApiPost(w http.ResponseWriter, r *http.Request) { PingLink(w, r) case "/device/link/start": StartLink(w, r) + case "/device/link/poll": + PollLink(w, r) + case "/device/link/remove": + RemoveLink("", w, r) case "/device/handshake": Handshake(w, r) - case "/poll/remote": - PollRemote(w, r) - + } +} + +func ApiAuth(data string, w http.ResponseWriter, r *http.Request) { + log.Println("apiauth", data != "") + switch r.URL.Path { + case "/macro/play": + PlayMacro(data, w, r) + case "/device/link/remove": + RemoveLink(data, w, r) } } diff --git a/be/app/device.go b/be/app/device.go index 3f7f2df..90165db 100644 --- a/be/app/device.go +++ b/be/app/device.go @@ -11,7 +11,7 @@ import ( "os" "path/filepath" "strings" - "sync" + "time" ) func DeviceList(w http.ResponseWriter, r *http.Request) { @@ -38,7 +38,7 @@ func DeviceList(w http.ResponseWriter, r *http.Request) { if ext == ".json" { devices[device]["settings"] = readDeviceSettings(filePath) } - if ext == ".pem" { + if ext == ".key" { devices[device]["key"] = true } } @@ -64,15 +64,6 @@ func readDeviceSettings(filepath string) (settings structs.Settings) { return settings } -var ( - mu sync.Mutex - queue = make(map[string][]structs.RemoteWebhook) -) - -func PollRemote(w http.ResponseWriter, r *http.Request) { - -} - func DeviceAccessCheck(w http.ResponseWriter, r *http.Request) { log.Println("device access check") var req structs.Check @@ -85,19 +76,17 @@ func DeviceAccessCheck(w http.ResponseWriter, r *http.Request) { } _, errSett := os.Stat("devices/" + req.Uuid + ".json") - _, errKey := os.Stat("devices/" + req.Uuid + ".pem") - - log.Println(errSett, errKey) + _, errKey := os.Stat("devices/" + req.Uuid + ".key") if (errSett == nil) && (errKey == nil) { log.Println("authorized") json.NewEncoder(w).Encode("authorized") } else if (errSett == nil) && (errKey != nil) { - log.Println("requested") - json.NewEncoder(w).Encode("requested") - } else { log.Println("unauthorized") json.NewEncoder(w).Encode("unauthorized") + } else { + log.Println("unauthorized") + json.NewEncoder(w).Encode("unlinked") } return @@ -128,7 +117,7 @@ func DeviceAccessRequest(w http.ResponseWriter, r *http.Request) { return } - json.NewEncoder(w).Encode(true) + json.NewEncoder(w).Encode("unauthorized") } func PingLink(w http.ResponseWriter, r *http.Request) { @@ -141,20 +130,24 @@ func PingLink(w http.ResponseWriter, r *http.Request) { return } - var filename = "devices/" + req.Uuid + ".tmp" + key, keyErr := os.ReadFile("devices/" + req.Uuid + ".key") + pin, pinErr := os.ReadFile("devices/" + req.Uuid + ".tmp") - _, err = os.ReadFile(filename) + encryptedKey, encErr := helper.EncryptAES(string(pin), string(key)) - if err == nil { - json.NewEncoder(w).Encode(true) + log.Println(encryptedKey, string(pin), string(key)) + + if keyErr == nil && pinErr == nil && encErr == nil { + w.Header().Set("Content-Type", "application/json") + w.Write([]byte(encryptedKey)) return } json.NewEncoder(w).Encode(false) - return } func StartLink(w http.ResponseWriter, r *http.Request) { + log.Println("start link") var req structs.Check err := json.NewDecoder(r.Body).Decode(&req) @@ -166,9 +159,52 @@ func StartLink(w http.ResponseWriter, r *http.Request) { pin := fmt.Sprintf("%04d", rand.Intn(10000)) - if helper.TempPinFile(req.Uuid, pin) { + deviceKey := helper.GenerateKey() + + err = helper.SaveDeviceKey(req.Uuid, deviceKey) + + if err == nil && helper.TempPinFile(req.Uuid, pin) { json.NewEncoder(w).Encode(pin) } + + return +} + +func PollLink(w http.ResponseWriter, r *http.Request) { + var req structs.Check + + err := json.NewDecoder(r.Body).Decode(&req) + + if err != nil { + json.NewEncoder(w).Encode(false) + return + } + + if helper.CheckPinFile(req.Uuid) { + json.NewEncoder(w).Encode(true) + return + } + + json.NewEncoder(w).Encode(false) +} + +func RemoveLink(data string, w http.ResponseWriter, r *http.Request) { + req := &structs.Check{} + _, err := helper.ParseRequest(req, data, r) + + if err != nil { + json.NewEncoder(w).Encode(false) + return + } + + err = os.Remove("devices/" + req.Uuid + ".key") + + if err != nil { + json.NewEncoder(w).Encode(false) + return + } + + json.NewEncoder(w).Encode(true) } func Handshake(w http.ResponseWriter, r *http.Request) { @@ -177,13 +213,33 @@ func Handshake(w http.ResponseWriter, r *http.Request) { err := json.NewDecoder(r.Body).Decode(&req) if err != nil { - http.Error(w, err.Error(), http.StatusBadRequest) return } - log.Println(req.Shake) + deviceKey, err := helper.GetKeyByUuid(req.Uuid) + + if err != nil { + json.NewEncoder(w).Encode(false) + return + } + + decryptShake, _ := helper.DecryptAES(deviceKey, req.Shake) + + if decryptShake == getDateStr() { + os.Remove("devices/" + req.Uuid + ".tmp") + + json.NewEncoder(w).Encode(true) + return + } else { + os.Remove("devices/" + req.Uuid + ".key") + } + + json.NewEncoder(w).Encode(false) } -func DeviceAuth(w http.ResponseWriter, r *http.Request) bool { - return true +func getDateStr() string { + date := time.Now() + year, month, day := date.Date() + formattedDate := fmt.Sprintf("%04d%02d%02d", year, month, day) + return formattedDate } diff --git a/be/app/helper/api-helper.go b/be/app/helper/api-helper.go index ab3387c..8adacde 100644 --- a/be/app/helper/api-helper.go +++ b/be/app/helper/api-helper.go @@ -1,15 +1,17 @@ package helper import ( + "encoding/json" "log" "net" "net/http" "strings" + "be/app/structs" . "be/app/structs" ) -func EndpointAccess(w http.ResponseWriter, r *http.Request) bool { +func EndpointAccess(w http.ResponseWriter, r *http.Request) (bool, string) { ip, _, err := net.SplitHostPort(r.RemoteAddr) if err != nil { log.Fatal(err) @@ -18,14 +20,18 @@ func EndpointAccess(w http.ResponseWriter, r *http.Request) bool { if (isLocal(ip) && isEndpointAllowed("Local", r.URL.Path)) || (isLanRemote(ip) && isEndpointAllowed("Remote", r.URL.Path)) { log.Println(r.URL.Path, "endpoint access: accessible") - return true - } else if isLanRemote(ip) && isEndpointAllowed("auth", r.URL.Path) && isDeviceAuthorized() { + return true, "" + } else if isLanRemote(ip) && isEndpointAllowed("Auth", r.URL.Path) { log.Println(r.URL.Path, "endpoint access: authorized") + + data := decryptAuth(r) + + return data != "", data } log.Println(r.URL.Path, "endpoint access: not authorized or accessible") - return false + return false, "" } func isLocal(ip string) bool { @@ -67,6 +73,30 @@ func getAllowedEndpoints(source string) (endpoints []string, err string) { return []string{}, "No allowed endpoints" } -func isDeviceAuthorized() bool { - return false +func decryptAuth(r *http.Request) string { + var req structs.Authcall + + err := json.NewDecoder(r.Body).Decode(&req) + + if err != nil || req.Uuid == "" || req.Data == "" { + return "" + } + + deviceKey, errKey := GetKeyByUuid(req.Uuid) + decryptData, errDec := DecryptAES(deviceKey, req.Data) + + if errKey != nil && errDec != nil || decryptData == "" { + return "" + } + + return decryptData +} + +func ParseRequest(req interface{}, data string, r *http.Request) (d interface{}, err error) { + if data != "" { + dataBytes := []byte(data) + return req, json.Unmarshal(dataBytes, &req) + } else { + return req, json.NewDecoder(r.Body).Decode(&req) + } } diff --git a/be/app/helper/device-helper.go b/be/app/helper/device-helper.go index a6b3eaa..38db5ba 100644 --- a/be/app/helper/device-helper.go +++ b/be/app/helper/device-helper.go @@ -22,6 +22,25 @@ func TempPinFile(Uuid string, pin string) bool { return true } -func CheckPinFile(encryptedData []byte) bool { - return false +func CheckPinFile(Uuid string) bool { + _, err := os.Stat("devices/" + Uuid + ".tmp") + return err == nil +} + +func SaveDeviceKey(Uuid string, key string) error { + err := os.WriteFile("devices/"+Uuid+".key", []byte(key), 0644) + + if err != nil { + return err + } + + return nil +} + +func GetKeyByUuid(Uuid string) (string, error) { + data, err := os.ReadFile("devices/" + Uuid + ".key") + if err != nil { + return "", err + } + return string(data), nil } diff --git a/be/app/helper/encrypt-helper.go b/be/app/helper/encrypt-helper.go new file mode 100644 index 0000000..f5a6f91 --- /dev/null +++ b/be/app/helper/encrypt-helper.go @@ -0,0 +1,105 @@ +package helper + +import ( + "bytes" + "crypto/aes" + "crypto/cipher" + "crypto/rand" + "encoding/base64" + "errors" + "strings" +) + +func EncryptAES(key string, plaintext string) (string, error) { + origData := []byte(plaintext) + + // Create AES cipher + block, err := aes.NewCipher(keyToBytes(key)) + if err != nil { + return "", err + } + blockSize := block.BlockSize() + + origData = PKCS5Padding(origData, blockSize) + + iv := []byte(EnvGet("MCRM__IV")) + blockMode := cipher.NewCBCEncrypter(block, iv) + + crypted := make([]byte, len(origData)) + blockMode.CryptBlocks(crypted, origData) + + cryptedString := base64.StdEncoding.EncodeToString(crypted) + + return cryptedString, nil +} + +func DecryptAES(key string, cryptedText string) (string, error) { + crypted, err := base64.StdEncoding.DecodeString(cryptedText) + + if err != nil { + return "", err + } + + block, err := aes.NewCipher(keyToBytes(key)) + if err != nil { + return "", err + } + + iv := []byte(EnvGet("MCRM__IV")) + blockMode := cipher.NewCBCDecrypter(block, iv) + + origData := make([]byte, len(crypted)) + + blockMode.CryptBlocks(origData, crypted) + origData, err = PKCS5UnPadding(origData) + + if err != nil || len(origData) <= 3 { + return "", errors.New("invalid key") + } + + origDataString := string(origData) + + return origDataString, nil +} + +func generateRandomString(length int) string { + b := make([]byte, length) + _, err := rand.Read(b) + if err != nil { + panic(err) + } + return base64.StdEncoding.EncodeToString(b) +} + +func GenerateKey() string { + return strings.Replace(generateRandomString(24), "=", "", -1) +} + +func keyToBytes(key string) []byte { + // Convert key to bytes + keyBytes := []byte(key) + + // If key is 4 characters, append salt + if len(key) == 4 { + keyBytes = []byte(key + EnvGet("MCRM__SALT")) + } + + return keyBytes +} + +func PKCS5Padding(ciphertext []byte, blockSize int) []byte { + padding := blockSize - len(ciphertext)%blockSize + padtext := bytes.Repeat([]byte{byte(padding)}, padding) + return append(ciphertext, padtext...) +} + +func PKCS5UnPadding(origData []byte) ([]byte, error) { + length := len(origData) + unpadding := int(origData[length-1]) + + if (unpadding >= length) || (unpadding == 0) { + return nil, errors.New("unpadding error") + } + + return origData[:(length - unpadding)], nil +} diff --git a/be/app/helper/env-helper.go b/be/app/helper/env-helper.go new file mode 100644 index 0000000..0c58023 --- /dev/null +++ b/be/app/helper/env-helper.go @@ -0,0 +1,16 @@ +package helper + +import ( + "log" + "os" + + "github.com/joho/godotenv" +) + +func EnvGet(key string) string { + err := godotenv.Load("../.env") + if err != nil { + log.Fatal("Error loading .env file") + } + return os.Getenv("VITE_" + key) +} diff --git a/be/app/macro.go b/be/app/macro.go index 58504b0..4b82e5a 100644 --- a/be/app/macro.go +++ b/be/app/macro.go @@ -63,10 +63,10 @@ func ListMacros(w http.ResponseWriter, r *http.Request) { func DeleteMacro(w http.ResponseWriter, r *http.Request) {} -func PlayMacro(w http.ResponseWriter, r *http.Request) { - var req structs.MacroRequest +func PlayMacro(data string, w http.ResponseWriter, r *http.Request) { + req := &structs.MacroRequest{} + _, err := helper.ParseRequest(req, data, r) - err := json.NewDecoder(r.Body).Decode(&req) if err != nil { http.Error(w, err.Error(), http.StatusBadRequest) return diff --git a/be/app/structs/api-struct.go b/be/app/structs/api-struct.go index 61966c5..774bbad 100644 --- a/be/app/structs/api-struct.go +++ b/be/app/structs/api-struct.go @@ -17,6 +17,8 @@ var Endpoints = Allowed{ "/device/access/request", "/device/link/ping", "/device/link/start", + "/device/link/poll", + "/device/link/remove", "/device/handshake", }, Remote: []string{ @@ -30,5 +32,6 @@ var Endpoints = Allowed{ }, Auth: []string{ "/macro/play", + "/device/link/remove", }, } diff --git a/be/app/structs/device-struct.go b/be/app/structs/device-struct.go index ce396f2..39586cd 100644 --- a/be/app/structs/device-struct.go +++ b/be/app/structs/device-struct.go @@ -21,5 +21,11 @@ type Request struct { } type Handshake struct { + Uuid string `json:"uuid"` Shake string `json:"shake"` } + +type Authcall struct { + Uuid string `json:"uuid"` + Data string `json:"d"` +} diff --git a/be/go.mod b/be/go.mod index d4c6e93..abe1604 100644 --- a/be/go.mod +++ b/be/go.mod @@ -2,7 +2,10 @@ module be go 1.24.0 -require github.com/go-vgo/robotgo v0.110.6 +require ( + github.com/go-vgo/robotgo v0.110.6 + github.com/joho/godotenv v1.5.1 +) require ( github.com/dblohm7/wingoes v0.0.0-20240820181039-f2b84150679e // indirect @@ -30,6 +33,6 @@ require ( github.com/yusufpapurcu/wmi v1.2.4 // indirect golang.org/x/exp v0.0.0-20250215185904-eff6e970281f // indirect golang.org/x/image v0.24.0 // indirect - golang.org/x/net v0.37.0 // indirect + golang.org/x/net v0.38.0 // indirect golang.org/x/sys v0.31.0 // indirect ) diff --git a/be/go.sum b/be/go.sum index a95f44a..27c1608 100644 --- a/be/go.sum +++ b/be/go.sum @@ -19,6 +19,8 @@ github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI= github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= github.com/jezek/xgb v1.1.1 h1:bE/r8ZZtSv7l9gk6nU0mYx51aXrvnyb44892TwSaqS4= github.com/jezek/xgb v1.1.1/go.mod h1:nrhwO0FX/enq75I7Y7G8iN1ubpSGZEiA3v9e9GyRFlk= +github.com/joho/godotenv v1.5.1 h1:7eLL/+HRGLY0ldzfGMeQkb7vMd0as4CfYvUVzLqw0N0= +github.com/joho/godotenv v1.5.1/go.mod h1:f4LDr5Voq0i2e/R5DDNOoa2zzDfwtkZa6DnEwAbqwq4= github.com/kbinani/screenshot v0.0.0-20250118074034-a3924b7bbc8c h1:1IlzDla/ZATV/FsRn1ETf7ir91PHS2mrd4VMunEtd9k= github.com/kbinani/screenshot v0.0.0-20250118074034-a3924b7bbc8c/go.mod h1:Pmpz2BLf55auQZ67u3rvyI2vAQvNetkK/4zYUmpauZQ= github.com/lufia/plan9stats v0.0.0-20240909124753-873cd0166683 h1:7UMa6KCCMjZEMDtTVdcGu0B1GmmC7QJKiCCjyTAWQy0= @@ -66,8 +68,8 @@ golang.org/x/exp v0.0.0-20250215185904-eff6e970281f h1:oFMYAjX0867ZD2jcNiLBrI9Bd golang.org/x/exp v0.0.0-20250215185904-eff6e970281f/go.mod h1:BHOTPb3L19zxehTsLoJXVaTktb06DFgmdW6Wb9s8jqk= golang.org/x/image v0.24.0 h1:AN7zRgVsbvmTfNyqIbbOraYL8mSwcKncEj8ofjgzcMQ= golang.org/x/image v0.24.0/go.mod h1:4b/ITuLfqYq1hqZcjofwctIhi7sZh2WaCjvsBNjjya8= -golang.org/x/net v0.37.0 h1:1zLorHbz+LYj7MQlSf1+2tPIIgibq2eL5xkrGk6f+2c= -golang.org/x/net v0.37.0/go.mod h1:ivrbrMbzFq5J41QOQh0siUuly180yBYtLp+CKbEaFx8= +golang.org/x/net v0.38.0 h1:vRMAPTMaeGqVhG5QyLJHqNDwecKTomGeqbnfZyKlBI8= +golang.org/x/net v0.38.0/go.mod h1:ivrbrMbzFq5J41QOQh0siUuly180yBYtLp+CKbEaFx8= golang.org/x/sys v0.0.0-20190916202348-b4ddaad3f8a3/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20201018230417-eeed37f84f13/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20201204225414-ed752295db88/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= diff --git a/be/main.go b/be/main.go index 8542958..6c02643 100644 --- a/be/main.go +++ b/be/main.go @@ -12,7 +12,7 @@ func main() { apiInit(w, r) }) - log.Fatal(http.ListenAndServe(":6970", nil)) + log.Println(http.ListenAndServe(":6970", nil)) } func apiInit(w http.ResponseWriter, r *http.Request) {