diff --git a/go.mod b/go.mod index 542cf33..d20a4eb 100644 --- a/go.mod +++ b/go.mod @@ -3,9 +3,9 @@ module git.ailur.dev/ailur/fulgens go 1.23.0 require ( - git.ailur.dev/ailur/pow-argon2 v0.0.0-20240922143345-8f9af6dce3a5 + git.ailur.dev/ailur/pow v0.0.0-20240929101731-4d0b2593b7dd github.com/cespare/xxhash/v2 v2.3.0 - github.com/go-chi/chi v1.5.5 + github.com/go-chi/chi/v5 v5.1.0 github.com/go-playground/validator/v10 v10.22.1 github.com/golang-jwt/jwt v3.2.2+incompatible github.com/google/uuid v1.6.0 diff --git a/go.sum b/go.sum index 2f6e606..9ffe6ef 100644 --- a/go.sum +++ b/go.sum @@ -1,5 +1,5 @@ -git.ailur.dev/ailur/pow-argon2 v0.0.0-20240922143345-8f9af6dce3a5 h1:QGICG5QsXtGVDV0YjR4bXiEV2kWQ96riPR9qFWuFpr4= -git.ailur.dev/ailur/pow-argon2 v0.0.0-20240922143345-8f9af6dce3a5/go.mod h1:dDulL+Bfr47BPmvSPRkRD3uOVNsfM6yOTwhdAkctZU4= +git.ailur.dev/ailur/pow v0.0.0-20240929101731-4d0b2593b7dd h1:yJRi9yGRICOb6NSIE9dBRbHsWU+jSUEeAFohVW59n38= +git.ailur.dev/ailur/pow v0.0.0-20240929101731-4d0b2593b7dd/go.mod h1:BHl7H6B6uN+q2cCCUlno6JMhqLa2A52wkbAdJbq2izA= github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs= github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= @@ -10,6 +10,8 @@ github.com/gabriel-vasile/mimetype v1.4.3 h1:in2uUcidCuFcDKtdcBxlR0rJ1+fsokWf+uq github.com/gabriel-vasile/mimetype v1.4.3/go.mod h1:d8uq/6HKRL6CGdk+aubisF/M5GcPfT7nKyLpA0lbSSk= github.com/go-chi/chi v1.5.5 h1:vOB/HbEMt9QqBqErz07QehcOKHaWFtuj87tTDVz2qXE= github.com/go-chi/chi v1.5.5/go.mod h1:C9JqLr3tIYjDOZpzn+BCuxY8z8vmca43EeMgyZt7irw= +github.com/go-chi/chi/v5 v5.1.0 h1:acVI1TYaD+hhedDJ3r54HyA6sExp3HfXq7QWEEY/xMw= +github.com/go-chi/chi/v5 v5.1.0/go.mod h1:DslCQbL2OYiznFReuXYUmQ2hGd1aDpCnlMNITLSKoi8= github.com/go-playground/assert/v2 v2.2.0 h1:JvknZsQTYeFEAhQwI4qEt9cyV5ONwRHC+lYKSsYSR8s= github.com/go-playground/assert/v2 v2.2.0/go.mod h1:VDjEfimB/XKnb+ZQfWdccd7VUvScMdVu0Titje2rxJ4= github.com/go-playground/locales v0.14.1 h1:EWaQ/wswjilfKLTECiXz7Rh+3BjFhfDFKv/oXslEjJA= diff --git a/library/main.go b/library/main.go index 8610c00..b11a44e 100644 --- a/library/main.go +++ b/library/main.go @@ -1,7 +1,7 @@ package library import ( - "github.com/go-chi/chi" + "github.com/go-chi/chi/v5" "github.com/google/uuid" "io/fs" "time" diff --git a/main.go b/main.go index 519b6b0..ea582d6 100644 --- a/main.go +++ b/main.go @@ -18,7 +18,7 @@ import ( "net/http" "path/filepath" - "github.com/go-chi/chi" + "github.com/go-chi/chi/v5" "github.com/go-playground/validator/v10" "github.com/google/uuid" diff --git a/services-src/auth/main.go b/services-src/auth/main.go index 9b6b176..3e27b21 100644 --- a/services-src/auth/main.go +++ b/services-src/auth/main.go @@ -4,9 +4,7 @@ import ( // Fulgens libraries "git.ailur.dev/ailur/fulgens/library" authLibrary "git.ailur.dev/ailur/fulgens/services-src/auth/library" - - // First-party libraries - pow "git.ailur.dev/ailur/pow-argon2/library" + "git.ailur.dev/ailur/pow" // Standard libraries "bytes" @@ -417,9 +415,58 @@ func Main(information library.ServiceInitializationInformation) { logFunc(err.Error(), 1, information) } }(r.Body) - renderTemplate(200, w, map[string]interface{}{ - "identifier": identifier, - }, "clientKeyShare.html", information) + // Parse the JWT from the query string + if r.URL.Query().Get("token") == "" { + renderString(400, w, "No token provided", information) + return + } + + // Verify the JWT + _, claims, ok := verifyJwt(strings.TrimPrefix(r.Header.Get("Authorization"), "Bearer "), publicKey, mem) + if !ok { + renderString(401, w, "Invalid token", information) + return + } + + // Check if they have the clientKeyShare scope + var scopes string + err = conn.QueryRow("SELECT scopes FROM oauth WHERE appId = ?", claims["aud"]).Scan(&scopes) + if err != nil { + renderString(500, w, "Sorry, something went wrong on our end. Error code: 20. Please report to the administrator.", information) + logFunc(err.Error(), 2, information) + return + } + + // Unmarshal the scopes + var scopesArray []string + err = json.Unmarshal([]byte(scopes), &scopesArray) + if err != nil { + renderString(500, w, "Sorry, something went wrong on our end. Error code: 21. Please report to the administrator.", information) + logFunc(err.Error(), 2, information) + return + } + + // Check if the clientKeyShare scope is present + var hasClientKeyShare bool + for _, scope := range scopesArray { + if scope == "clientKeyShare" { + hasClientKeyShare = true + break + } + } + if !hasClientKeyShare { + renderString(403, w, "Missing scope", information) + return + } + + // Check it's not an openid token + if claims["isOpenID"] == true { + renderString(400, w, "Invalid token", information) + } else { + renderTemplate(200, w, map[string]interface{}{ + "identifier": identifier, + }, "clientKeyShare.html", information) + } }) router.Get("/.well-known/openid-configuration", func(w http.ResponseWriter, r *http.Request) { @@ -520,24 +567,9 @@ func Main(information library.ServiceInitializationInformation) { return } - // Check if the PoW is spent - hash := make([]byte, 8) - binary.LittleEndian.PutUint64(hash, xxhash.Sum64String(data.ProofOfWork)) - _, err = mem.Exec("INSERT INTO spent (hash, expires) VALUES (?, ?)", hash, time.Now().Unix()+60) - if err != nil { - if strings.Contains(err.Error(), "UNIQUE constraint failed") { - renderJSON(409, w, map[string]interface{}{"error": "Proof of work already spent"}, information) - return - } else { - renderJSON(500, w, map[string]interface{}{"error": "Internal server error", "code": "07"}, information) - logFunc(err.Error(), 2, information) - return - } - } - // Check if the difficulty, timestamp and resource are correct powSlice := strings.Split(data.ProofOfWork, ":") - if powSlice[0] != "3" || powSlice[3] != "fg-auth-signup" { + if powSlice[0] != "2" || powSlice[3] != "fg-auth-signup" { renderJSON(400, w, map[string]interface{}{"error": "Invalid PoW"}, information) return } @@ -554,11 +586,26 @@ func Main(information library.ServiceInitializationInformation) { } // Verify the PoW - if !pow.VerifyPoW(data.ProofOfWork) { + if !ailur_pow.VerifyPoW(data.ProofOfWork) { renderJSON(400, w, map[string]interface{}{"error": "Invalid PoW"}, information) return } + // Check if the PoW is spent + hash := make([]byte, 8) + binary.LittleEndian.PutUint64(hash, xxhash.Sum64String(data.ProofOfWork)) + _, err = mem.Exec("INSERT INTO spent (hash, expires) VALUES (?, ?)", hash, time.Now().Unix()+60) + if err != nil { + if strings.Contains(err.Error(), "UNIQUE constraint failed") { + renderJSON(400, w, map[string]interface{}{"error": "Proof of work already spent"}, information) + return + } else { + renderJSON(500, w, map[string]interface{}{"error": "Internal server error", "code": "07"}, information) + logFunc(err.Error(), 2, information) + return + } + } + // Decode the password password, err := base64.StdEncoding.DecodeString(data.Password) if err != nil { @@ -858,65 +905,6 @@ func Main(information library.ServiceInitializationInformation) { renderJSON(200, w, map[string]interface{}{"username": username, "sub": uuid.Must(uuid.FromBytes(userId)).String()}, information) }) - router.Get("/api/oauth/clientKeyShare", func(w http.ResponseWriter, r *http.Request) { - defer func(Body io.ReadCloser) { - err := Body.Close() - if err != nil { - logFunc(err.Error(), 1, information) - } - }(r.Body) - // Parse the JWT - if r.Header.Get("Authorization") == "" { - renderJSON(401, w, map[string]interface{}{"error": "No token provided"}, information) - return - } - - // Verify the JWT - _, claims, ok := verifyJwt(strings.TrimPrefix(r.Header.Get("Authorization"), "Bearer "), publicKey, mem) - if !ok { - renderJSON(401, w, map[string]interface{}{"error": "Invalid token"}, information) - return - } - - // Check if they have the clientKeyShare scope - var scopes string - err = conn.QueryRow("SELECT scopes FROM oauth WHERE appId = ?", claims["aud"]).Scan(&scopes) - if err != nil { - renderJSON(500, w, map[string]interface{}{"error": "Internal server error", "code": "20"}, information) - logFunc(err.Error(), 2, information) - return - } - - // Unmarshal the scopes - var scopesArray []string - err = json.Unmarshal([]byte(scopes), &scopesArray) - if err != nil { - renderJSON(500, w, map[string]interface{}{"error": "Internal server error", "code": "21"}, information) - logFunc(err.Error(), 2, information) - return - } - - // Check if the clientKeyShare scope is present - var hasClientKeyShare bool - for _, scope := range scopesArray { - if scope == "clientKeyShare" { - hasClientKeyShare = true - break - } - } - if !hasClientKeyShare { - renderJSON(403, w, map[string]interface{}{"error": "Missing scope"}, information) - return - } - - // Check it's not an openid token - if claims["isOpenID"] == true { - renderJSON(401, w, map[string]interface{}{"error": "Invalid token"}, information) - } else { - renderJSON(200, w, map[string]interface{}{"key": publicKey}, information) - } - }) - router.Post("/api/authorize", func(w http.ResponseWriter, r *http.Request) { defer func(Body io.ReadCloser) { err := Body.Close() diff --git a/services-src/auth/resources/go.mod b/services-src/auth/resources/go.mod index 0f68e65..151e7d5 100644 --- a/services-src/auth/resources/go.mod +++ b/services-src/auth/resources/go.mod @@ -3,7 +3,6 @@ module git.ailur.dev/fulgens/services-src/auth/resources-src go 1.23.0 require ( - git.ailur.dev/ailur/pow-argon2 v0.0.0-20240922143345-8f9af6dce3a5 github.com/cespare/xxhash/v2 v2.3.0 golang.org/x/crypto v0.27.0 ) diff --git a/services-src/auth/resources/go.sum b/services-src/auth/resources/go.sum index e90db0e..ff348bf 100644 --- a/services-src/auth/resources/go.sum +++ b/services-src/auth/resources/go.sum @@ -1,5 +1,5 @@ -git.ailur.dev/ailur/pow-argon2 v0.0.0-20240922143345-8f9af6dce3a5 h1:QGICG5QsXtGVDV0YjR4bXiEV2kWQ96riPR9qFWuFpr4= -git.ailur.dev/ailur/pow-argon2 v0.0.0-20240922143345-8f9af6dce3a5/go.mod h1:dDulL+Bfr47BPmvSPRkRD3uOVNsfM6yOTwhdAkctZU4= +git.ailur.dev/ailur/pow v0.0.0-20240929101731-4d0b2593b7dd h1:yJRi9yGRICOb6NSIE9dBRbHsWU+jSUEeAFohVW59n38= +git.ailur.dev/ailur/pow v0.0.0-20240929101731-4d0b2593b7dd/go.mod h1:BHl7H6B6uN+q2cCCUlno6JMhqLa2A52wkbAdJbq2izA= github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs= github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= golang.org/x/crypto v0.27.0 h1:GXm2NjJrPaiv/h1tb2UH8QfgC/hOf/+z0p6PT8o1w7A= diff --git a/services-src/auth/resources/static/css/style.css b/services-src/auth/resources/static/css/style.css index fd6ee05..32ef8ec 100644 --- a/services-src/auth/resources/static/css/style.css +++ b/services-src/auth/resources/static/css/style.css @@ -11,6 +11,9 @@ --hover-nonimportant-theme-color: #dbdbdb; --nonimportant-text-color: #000; --inOutDiv: #fafafa; + --disabled: lightgray; + --disabled-hover: #a2a0a0; + --disabled-text-color: gray; } /* dark mode */ @@ -21,8 +24,11 @@ --inOutDiv: #2d2f31; --text-color: #ffffff; --editor: #1E1E1E; + --nonimporant-theme-color: #8E8E8E; --nonimportant-text-color: #fff; --border-color: #393b3d; + --disabled: #606060; + --disabled-hover: #737373; } .inOutDiv p { @@ -46,6 +52,8 @@ h2, h3, h4, h5, +span, +text, h6 { color: var(--text-color); white-space: break-spaces; @@ -90,11 +98,43 @@ input { min-width: 20px; } +.inputBox .captchaDiv { + background-color: var(--editor); + height: 32px; + width: calc(100% - 15px); + margin: 0 5px 0 5px; + border-radius: 8px; + border: 1px var(--border-color) solid; + display: flex; +} + +.inputBox .captchaDiv button { + background-color: var(--editor); + color: var(--text-color); + border-right: 1px solid var(--border-color); + border-radius: 0; + padding: 0 10px 0 0; + margin: 0 0 0 10px; +} + +.inputBox .captchaDiv .vAlign { + margin-left: 5px; +} + +.inputBox .captchaDiv .vAlign span { + font-size: 14px; +} + .inputBox input { margin-left: 5px; margin-right: 0; } +.inputBox input:disabled { + background-color: var(--disabled); + color: var(--disabled-text-color); +} + @media only screen and (max-width: 600px) { body { background-color: var(--inOutDiv); @@ -210,6 +250,15 @@ button { transition: 0.125s; } +button:disabled { + background-color: var(--disabled); + color: var(--disabled-text-color); +} + +button:disabled:hover { + background-color: var(--disabled-hover); +} + button:hover { background-color: var(--hover-theme-color); transition: all 0.3s ease 0s; diff --git a/services-src/auth/resources/static/svg/gopher.svg b/services-src/auth/resources/static/svg/gopher.svg deleted file mode 100644 index d6451bf..0000000 --- a/services-src/auth/resources/static/svg/gopher.svg +++ /dev/null @@ -1,174 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/services-src/auth/resources/templates/signup.html b/services-src/auth/resources/templates/signup.html index b37f57e..ee08e71 100644 --- a/services-src/auth/resources/templates/signup.html +++ b/services-src/auth/resources/templates/signup.html @@ -18,16 +18,27 @@

- - + + - - + + + + + +
Username:CAPTCHA: +
+ +
+ Loading... +
+
+
Password:
Username:
Password:

- +

Privacy & Terms diff --git a/services-src/auth/resources/wasm/clientKeyShare/main.go b/services-src/auth/resources/wasm/clientKeyShare/main.go index 64e72a4..49f0165 100644 --- a/services-src/auth/resources/wasm/clientKeyShare/main.go +++ b/services-src/auth/resources/wasm/clientKeyShare/main.go @@ -1,15 +1,11 @@ package main import ( - "bytes" "crypto/aes" "crypto/cipher" "crypto/ecdh" "crypto/rand" "encoding/base64" - "encoding/json" - "fmt" - "net/http" "net/url" "strings" "syscall/js" @@ -24,161 +20,71 @@ func main() { statusBox := js.Global().Get("document").Call("getElementById", "statusBox") - // Check if the token is valid - requestUri, err := url.JoinPath(js.Global().Get("window").Get("location").Get("origin").String(), "/api/loggedIn") - if err != nil { - statusBox.Set("innerText", "Error joining URL: "+err.Error()) - return - } - - loggedInBody := map[string]interface{}{ - "token": localStorage.Call("getItem", "DONOTSHARE-secretKey").String(), - } - - // Marshal the body - body, err := json.Marshal(loggedInBody) - if err != nil { - statusBox.Set("innerText", "Error marshaling signup body: "+err.Error()) - return - } - - response, err := http.Post(requestUri, "application/json", bytes.NewReader(body)) - if err != nil { - statusBox.Set("innerText", "Error contacting server: "+err.Error()) - return - } - - // Check if the response is 200 - if response.StatusCode == 401 { - // Close the response body - err = response.Body.Close() - if err != nil { - fmt.Println("Could not close response body: " + err.Error() + ", memory leaks may occur") - } - - // Redirect to log-out if not signed in - js.Global().Get("window").Get("location").Call("replace", "/logout"+js.Global().Get("window").Get("location").Get("search").String()) - return - } else if response.StatusCode == 500 { - // Read the response - var responseMap map[string]interface{} - decoder := json.NewDecoder(response.Body) - err = decoder.Decode(&responseMap) - if err != nil { - js.Global().Call("alert", "Error decoding server response: "+err.Error()) - return - } - - // Close the response body - err = response.Body.Close() - if err != nil { - fmt.Println("Could not close response body: " + err.Error() + ", memory leaks may occur") - } - - // Alert the user if the server is down - js.Global().Call("alert", "Something went wrong! (error code: "+responseMap["code"].(string)+")") - return - } - - // Close the response body - err = response.Body.Close() - if err != nil { - fmt.Println("Could not close response body: " + err.Error() + ", memory leaks may occur") - } - + // Parse the query string query, err := url.ParseQuery(strings.TrimPrefix(js.Global().Get("window").Get("location").Get("search").String(), "?")) if err != nil { statusBox.Set("innerText", "Error parsing query: "+err.Error()) return } - // Check if the access token we were given is valid and that the scope is correct - requestUri, err = url.JoinPath(js.Global().Get("window").Get("location").Get("origin").String(), "/api/oauth/clientKeyShare") + // Get the ECDH public key from the query string + clientKeyBytes, err := base64.URLEncoding.DecodeString(query.Get("ecdhPublicKey")) if err != nil { - statusBox.Set("innerText", "Error joining URL: "+err.Error()) + statusBox.Set("innerText", "Error decoding ECDH public key: "+err.Error()) return } - request, err := http.NewRequest("GET", requestUri, nil) + // Encode the ECDH public key + key, err := ecdh.X25519().NewPublicKey(clientKeyBytes) if err != nil { - statusBox.Set("innerText", "Error creating request: "+err.Error()) + statusBox.Set("innerText", "Error encoding ECDH public key: "+err.Error()) return } - request.Header.Set("Authorization", "Bearer "+query.Get("accessToken")) - - response, err = http.DefaultClient.Do(request) + // Generate a new ECDH key pair + privateKey, err := ecdh.X25519().GenerateKey(rand.Reader) if err != nil { - statusBox.Set("innerText", "Error contacting server: "+err.Error()) + statusBox.Set("innerText", "Error generating ECDH key pair: "+err.Error()) return } - // Close the response body - err = response.Body.Close() + // Generate the shared secret + sharedSecret, err := privateKey.ECDH(key) if err != nil { - fmt.Println("Could not close response body: " + err.Error() + ", memory leaks may occur") + statusBox.Set("innerText", "Error generating shared secret: "+err.Error()) + return } - if response.StatusCode == 200 { - // Get the ECDH public key from the query string - clientKeyBytes, err := base64.URLEncoding.DecodeString(query.Get("ecdhPublicKey")) - if err != nil { - statusBox.Set("innerText", "Error decoding ECDH public key: "+err.Error()) - return - } - - // Encode the ECDH public key - key, err := ecdh.X25519().NewPublicKey(clientKeyBytes) - if err != nil { - statusBox.Set("innerText", "Error encoding ECDH public key: "+err.Error()) - return - } - - // Generate a new ECDH key pair - privateKey, err := ecdh.X25519().GenerateKey(rand.Reader) - if err != nil { - statusBox.Set("innerText", "Error generating ECDH key pair: "+err.Error()) - return - } - - // Generate the shared secret - sharedSecret, err := privateKey.ECDH(key) - if err != nil { - statusBox.Set("innerText", "Error generating shared secret: "+err.Error()) - return - } - - // AES-GCM encrypt the DONOTSHARE-clientKey - block, err := aes.NewCipher(sharedSecret) - if err != nil { - statusBox.Set("innerText", "Error creating AES cipher: "+err.Error()) - return - } - - gcm, err := cipher.NewGCM(block) - if err != nil { - statusBox.Set("innerText", "Error creating GCM cipher: "+err.Error()) - return - } - - nonce := make([]byte, gcm.NonceSize()) - _, err = rand.Read(nonce) - if err != nil { - statusBox.Set("innerText", "Error generating nonce: "+err.Error()) - return - } - - // Un-base64 the client key - decodedClientKey, err := base64.StdEncoding.DecodeString(localStorage.Call("getItem", "DONOTSHARE-clientKey").String()) - if err != nil { - statusBox.Set("innerText", "Error decoding client key: "+err.Error()) - return - } - - encryptedClientKey := gcm.Seal(nil, nonce, decodedClientKey, nil) - - // Redirect back to the referrer with the encrypted client key - redirectUri := strings.Split(js.Global().Get("document").Get("referrer").String(), "?")[0] - js.Global().Get("window").Get("location").Call("replace", redirectUri+"?ecdhPublicKey="+base64.URLEncoding.EncodeToString(privateKey.PublicKey().Bytes())+"&nonce="+base64.URLEncoding.EncodeToString(nonce)+"&cipherText="+base64.URLEncoding.EncodeToString(encryptedClientKey)) + // AES-GCM encrypt the DONOTSHARE-clientKey + block, err := aes.NewCipher(sharedSecret) + if err != nil { + statusBox.Set("innerText", "Error creating AES cipher: "+err.Error()) + return } + + gcm, err := cipher.NewGCM(block) + if err != nil { + statusBox.Set("innerText", "Error creating GCM cipher: "+err.Error()) + return + } + + nonce := make([]byte, gcm.NonceSize()) + _, err = rand.Read(nonce) + if err != nil { + statusBox.Set("innerText", "Error generating nonce: "+err.Error()) + return + } + + // Un-base64 the client key + decodedClientKey, err := base64.StdEncoding.DecodeString(localStorage.Call("getItem", "DONOTSHARE-clientKey").String()) + if err != nil { + statusBox.Set("innerText", "Error decoding client key: "+err.Error()) + return + } + + encryptedClientKey := gcm.Seal(nil, nonce, decodedClientKey, nil) + + // Redirect back to the referrer with the encrypted client key + redirectUri := strings.Split(js.Global().Get("document").Get("referrer").String(), "?")[0] + js.Global().Get("window").Get("location").Call("replace", redirectUri+"?ecdhPublicKey="+base64.URLEncoding.EncodeToString(privateKey.PublicKey().Bytes())+"&nonce="+base64.URLEncoding.EncodeToString(nonce)+"&cipherText="+base64.URLEncoding.EncodeToString(encryptedClientKey)) } diff --git a/services-src/auth/resources/wasm/login/main.go b/services-src/auth/resources/wasm/login/main.go index 27c4dab..dba31c6 100644 --- a/services-src/auth/resources/wasm/login/main.go +++ b/services-src/auth/resources/wasm/login/main.go @@ -155,81 +155,89 @@ func main() { fmt.Println("Could not close response body: " + err.Error() + ", memory leaks may occur") } - // Decode the salt - salt, err := base64.StdEncoding.DecodeString(responseMap["salt"].(string)) - if err != nil { - showInput(1, inputContainer, usernameBox, signupButton, passwordBox, backButton, inputNameBox, statusBox, nextButton) - statusBox.Set("innerText", "Error decoding salt: "+err.Error()) - return - } - - hashedPassword := hashPassword(password, salt) - - // Hashed password computed, contact server - statusBox.Set("innerText", "Contacting server...") - signupBody := map[string]interface{}{ - "username": username, - "password": hashedPassword, - } - - // Marshal the body - body, err = json.Marshal(signupBody) - if err != nil { - showInput(1, inputContainer, usernameBox, signupButton, passwordBox, backButton, inputNameBox, statusBox, nextButton) - statusBox.Set("innerText", "Error marshaling signup body: "+err.Error()) - return - } - - // Send the password to the server - requestUri, err = url.JoinPath(js.Global().Get("window").Get("location").Get("origin").String(), "/api/login") - if err != nil { - showInput(1, inputContainer, usernameBox, signupButton, passwordBox, backButton, inputNameBox, statusBox, nextButton) - statusBox.Set("innerText", "Error joining URL: "+err.Error()) - return - } - - // Send the request - fmt.Println("Sending request to", requestUri) - response, err = http.Post(requestUri, "application/json", bytes.NewReader(body)) - if err != nil { - showInput(1, inputContainer, usernameBox, signupButton, passwordBox, backButton, inputNameBox, statusBox, nextButton) - statusBox.Set("innerText", "Error contacting server: "+err.Error()) - return - } - - // Read the response - fmt.Println("Reading response...") - decoder = json.NewDecoder(response.Body) - err = decoder.Decode(&responseMap) - if err != nil { - showInput(1, inputContainer, usernameBox, signupButton, passwordBox, backButton, inputNameBox, statusBox, nextButton) - statusBox.Set("innerText", "Error decoding server response: "+err.Error()) - return - } - - // Close the response body - err = response.Body.Close() - if err != nil { - fmt.Println("Could not close response body: " + err.Error() + ", memory leaks may occur") - } - if response.StatusCode == 200 { - // Logged in - fmt.Println("Logged in!") - statusBox.Set("innerText", "Setting up encryption keys...") - localStorage.Call("setItem", "DONOTSHARE-secretKey", responseMap["key"].(string)) - localStorage.Call("setItem", "DONOTSHARE-clientKey", hashPassword(password, []byte("fg-auth-client"))) + // Decode the salt + salt, err := base64.StdEncoding.DecodeString(responseMap["salt"].(string)) + if err != nil { + showInput(1, inputContainer, usernameBox, signupButton, passwordBox, backButton, inputNameBox, statusBox, nextButton) + statusBox.Set("innerText", "Error decoding salt: "+err.Error()) + return + } - // Redirect to app - statusBox.Set("innerText", "Welcome!") - time.Sleep(time.Second) - js.Global().Get("window").Get("location").Call("replace", "/authorize"+js.Global().Get("window").Get("location").Get("search").String()) - } else if response.StatusCode == 401 { - // Login failed + hashedPassword := hashPassword(password, salt) + + // Hashed password computed, contact server + statusBox.Set("innerText", "Contacting server...") + signupBody := map[string]interface{}{ + "username": username, + "password": hashedPassword, + } + + // Marshal the body + body, err = json.Marshal(signupBody) + if err != nil { + showInput(1, inputContainer, usernameBox, signupButton, passwordBox, backButton, inputNameBox, statusBox, nextButton) + statusBox.Set("innerText", "Error marshaling signup body: "+err.Error()) + return + } + + // Send the password to the server + requestUri, err = url.JoinPath(js.Global().Get("window").Get("location").Get("origin").String(), "/api/login") + if err != nil { + showInput(1, inputContainer, usernameBox, signupButton, passwordBox, backButton, inputNameBox, statusBox, nextButton) + statusBox.Set("innerText", "Error joining URL: "+err.Error()) + return + } + + // Send the request + fmt.Println("Sending request to", requestUri) + response, err = http.Post(requestUri, "application/json", bytes.NewReader(body)) + if err != nil { + showInput(1, inputContainer, usernameBox, signupButton, passwordBox, backButton, inputNameBox, statusBox, nextButton) + statusBox.Set("innerText", "Error contacting server: "+err.Error()) + return + } + + // Read the response + fmt.Println("Reading response...") + decoder = json.NewDecoder(response.Body) + err = decoder.Decode(&responseMap) + if err != nil { + showInput(1, inputContainer, usernameBox, signupButton, passwordBox, backButton, inputNameBox, statusBox, nextButton) + statusBox.Set("innerText", "Error decoding server response: "+err.Error()) + return + } + + // Close the response body + err = response.Body.Close() + if err != nil { + fmt.Println("Could not close response body: " + err.Error() + ", memory leaks may occur") + } + + if response.StatusCode == 200 { + // Logged in + fmt.Println("Logged in!") + statusBox.Set("innerText", "Setting up encryption keys...") + localStorage.Call("setItem", "DONOTSHARE-secretKey", responseMap["key"].(string)) + localStorage.Call("setItem", "DONOTSHARE-clientKey", hashPassword(password, []byte("fg-auth-client"))) + + // Redirect to app + statusBox.Set("innerText", "Welcome!") + time.Sleep(time.Second) + js.Global().Get("window").Get("location").Call("replace", "/authorize"+js.Global().Get("window").Get("location").Get("search").String()) + } else if response.StatusCode == 401 { + // Login failed + showInput(1, inputContainer, usernameBox, signupButton, passwordBox, backButton, inputNameBox, statusBox, nextButton) + statusBox.Set("innerText", "Username or password incorrect!") + } else { + // Unknown error + showInput(1, inputContainer, usernameBox, signupButton, passwordBox, backButton, inputNameBox, statusBox, nextButton) + statusBox.Set("innerText", "Something went wrong! (error code: "+responseMap["code"].(string)+")") + } + } else if response.StatusCode != 500 { showInput(1, inputContainer, usernameBox, signupButton, passwordBox, backButton, inputNameBox, statusBox, nextButton) - statusBox.Set("innerText", "Username or password incorrect!") + statusBox.Set("innerText", responseMap["error"].(string)) } else { - // Unknown error showInput(1, inputContainer, usernameBox, signupButton, passwordBox, backButton, inputNameBox, statusBox, nextButton) statusBox.Set("innerText", "Something went wrong! (error code: "+responseMap["code"].(string)+")") } diff --git a/services-src/auth/resources/wasm/signup/main.go b/services-src/auth/resources/wasm/signup/main.go index 04cf7ec..7d9ef47 100644 --- a/services-src/auth/resources/wasm/signup/main.go +++ b/services-src/auth/resources/wasm/signup/main.go @@ -4,14 +4,16 @@ import ( "bytes" "crypto/rand" "encoding/base64" + "encoding/binary" + "encoding/hex" "fmt" + "strconv" "time" "encoding/json" "net/http" "net/url" - "git.ailur.dev/ailur/pow-argon2/library" "golang.org/x/crypto/argon2" "syscall/js" @@ -40,6 +42,21 @@ func hashPassword(password string, salt []byte) string { ) } +// This is my code: I can re-license it I all like, despite it being MIT licensed +func pow(resource string) (string, string, error) { + initialTime := time.Now().Unix() + var timestamp [8]byte + binary.LittleEndian.PutUint64(timestamp[:], uint64(initialTime)) + + var nonce [16]byte + _, err := rand.Read(nonce[:]) + if err != nil { + return "", "", err + } + + return strconv.FormatInt(initialTime, 10) + ":" + hex.EncodeToString(nonce[:]) + ":" + resource + ":", hex.EncodeToString(argon2.IDKey(nonce[:], bytes.Join([][]byte{timestamp[:], []byte(resource)}, []byte{}), 1, 64*1024, 4, 32)), nil +} + func main() { // Redirect to app if already signed in localStorage := js.Global().Get("localStorage") @@ -53,6 +70,20 @@ func main() { var signupButton = js.Global().Get("document").Call("getElementById", "signupButton") var loginButton = js.Global().Get("document").Call("getElementById", "loginButton") var inputContainer = js.Global().Get("document").Call("getElementById", "inputContainer") + var captchaButton = js.Global().Get("document").Call("getElementById", "captchaButton") + var captchaStatus = js.Global().Get("document").Call("getElementById", "captchaStatus") + + captchaButton.Set("disabled", false) + usernameBox.Set("disabled", true) + passwordBox.Set("disabled", true) + signupButton.Set("disabled", true) + if localStorage.Call("getItem", "CONFIG-captchaStarted").IsNull() { + captchaStatus.Set("innerText", "CAPTCHA not started - start CAPTCHA to signup.") + } else { + captchaStatus.Set("innerText", "Captcha calculation paused.") + } + + var captcha string signupButton.Call("addEventListener", "click", js.FuncOf(func(this js.Value, args []js.Value) interface{} { go func() { @@ -76,20 +107,8 @@ func main() { // Start the signup process fmt.Println("Starting signup process for user: " + username) showElements(false, inputContainer, signupButton, loginButton) - // Wait about 10ms to allow the UI to update - var pow string - if localStorage.Call("getItem", "DEBUG-customPoW").IsNull() { - var err error - statusBox.Set("innerText", "Computing PoW Challenge...\nThe UI may be unresponsive during this time, as we are performing a lot of work! Please wait a few minutes for the process to complete.") - time.Sleep(time.Millisecond * 10) - pow, err = library.PoW(3, "fg-auth-signup") - if err != nil { - showElements(true, inputContainer, signupButton, loginButton) - statusBox.Set("innerText", "Error computing PoW challenge: "+err.Error()) - return - } - } else { - pow = localStorage.Call("getItem", "DEBUG-customPoW").String() + if captcha == "" { + statusBox.Set("innerText", "You must have a valid captcha! Press the \"Start\" button to start calculating a captcha.") } // PoW challenge computed, hash password @@ -113,7 +132,7 @@ func main() { "username": username, "password": hashedPassword, "salt": base64.StdEncoding.EncodeToString(salt), - "proofOfWork": pow, + "proofOfWork": captcha, } // Marshal the body @@ -170,7 +189,7 @@ func main() { } else if response.StatusCode == 409 { // Username taken showElements(true, inputContainer, signupButton, loginButton) - statusBox.Set("innerText", "Username or password taken!") + statusBox.Set("innerText", "Username already taken!") } else if response.StatusCode != 500 { // Other error showElements(true, inputContainer, signupButton, loginButton) @@ -190,6 +209,55 @@ func main() { return nil })) + captchaInProgress := false + captchaButton.Call("addEventListener", "click", js.FuncOf(func(this js.Value, args []js.Value) interface{} { + if !captchaInProgress { + captchaInProgress = true + captchaButton.Set("innerText", "Pause") + localStorage.Call("setItem", "CONFIG-captchaStarted", "true") + go func() { + go func() { + time.Sleep(time.Minute * 5) + if captchaInProgress { + captchaStatus.Set("innerText", "Taking a long time? Try the desktop version.") + } + }() + for { + if !captchaInProgress { + captchaStatus.Set("innerText", "Captcha calculation paused.") + captchaButton.Set("innerText", "Start") + break + } else { + captchaStatus.Set("innerText", "Calculating captcha... Stopping or refreshing will not lose progress.") + powParams, powResult, err := pow("fg-auth-signup") + if err != nil { + captchaStatus.Set("innerText", "Error calculating captcha: "+err.Error()) + captchaInProgress = false + break + } + if powResult[:2] == "00" { + localStorage.Call("removeItem", "CONFIG-captchaStarted") + captcha = "2:" + powParams + captchaStatus.Set("innerText", "Captcha calculated!") + captchaButton.Set("disabled", true) + captchaButton.Set("innerText", "Start") + usernameBox.Set("disabled", false) + passwordBox.Set("disabled", false) + signupButton.Set("disabled", false) + captchaInProgress = false + break + } + time.Sleep(time.Millisecond) + } + } + }() + } else { + captchaInProgress = false + } + + return nil + })) + // Wait for events select {} }