2024-09-28 19:41:34 +01:00
|
|
|
package main
|
|
|
|
|
|
|
|
import (
|
|
|
|
"bytes"
|
2024-10-13 19:20:19 +01:00
|
|
|
"crypto/ed25519"
|
2024-09-28 19:41:34 +01:00
|
|
|
"crypto/rand"
|
|
|
|
"encoding/base64"
|
2024-09-29 16:06:28 +01:00
|
|
|
"encoding/binary"
|
|
|
|
"encoding/hex"
|
2024-09-28 19:41:34 +01:00
|
|
|
"fmt"
|
2024-09-29 16:06:28 +01:00
|
|
|
"strconv"
|
2024-09-28 19:41:34 +01:00
|
|
|
"time"
|
|
|
|
|
|
|
|
"encoding/json"
|
|
|
|
"net/http"
|
|
|
|
"net/url"
|
|
|
|
|
|
|
|
"golang.org/x/crypto/argon2"
|
|
|
|
|
|
|
|
"syscall/js"
|
|
|
|
)
|
|
|
|
|
|
|
|
func showElements(show bool, elements ...js.Value) {
|
|
|
|
for _, element := range elements {
|
|
|
|
if show {
|
|
|
|
element.Get("classList").Call("remove", "hidden")
|
|
|
|
} else {
|
|
|
|
element.Get("classList").Call("add", "hidden")
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2024-10-13 19:20:19 +01:00
|
|
|
func hashPassword(password string, salt []byte) []byte {
|
|
|
|
return argon2.IDKey(
|
|
|
|
[]byte(password),
|
|
|
|
salt,
|
|
|
|
32,
|
|
|
|
19264,
|
|
|
|
1,
|
|
|
|
32,
|
2024-09-28 19:41:34 +01:00
|
|
|
)
|
|
|
|
}
|
|
|
|
|
2024-09-29 16:06:28 +01:00
|
|
|
// 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
|
|
|
|
}
|
|
|
|
|
2024-09-28 19:41:34 +01:00
|
|
|
func main() {
|
|
|
|
// Redirect to app if already signed in
|
|
|
|
localStorage := js.Global().Get("localStorage")
|
|
|
|
if !localStorage.Call("getItem", "DONOTSHARE-secretKey").IsNull() {
|
|
|
|
js.Global().Get("window").Get("location").Call("replace", "/authorize"+js.Global().Get("window").Get("location").Get("search").String())
|
|
|
|
}
|
|
|
|
|
|
|
|
var usernameBox = js.Global().Get("document").Call("getElementById", "usernameBox")
|
|
|
|
var passwordBox = js.Global().Get("document").Call("getElementById", "passwordBox")
|
|
|
|
var statusBox = js.Global().Get("document").Call("getElementById", "statusBox")
|
|
|
|
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")
|
2024-09-29 16:06:28 +01:00
|
|
|
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)
|
2024-10-13 19:20:19 +01:00
|
|
|
if localStorage.Call("getItem", "DEBUG-customCaptcha").IsNull() {
|
|
|
|
if localStorage.Call("getItem", "CONFIG-captchaStarted").IsNull() {
|
|
|
|
captchaStatus.Set("innerText", "CAPTCHA not started - start CAPTCHA to signup.")
|
|
|
|
} else {
|
|
|
|
captchaStatus.Set("innerText", "Captcha calculation paused.")
|
|
|
|
}
|
2024-09-29 16:06:28 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
var captcha string
|
2024-09-28 19:41:34 +01:00
|
|
|
|
|
|
|
signupButton.Call("addEventListener", "click", js.FuncOf(func(this js.Value, args []js.Value) interface{} {
|
|
|
|
go func() {
|
|
|
|
var username = usernameBox.Get("value").String()
|
|
|
|
var password = passwordBox.Get("value").String()
|
|
|
|
|
|
|
|
if username == "" {
|
|
|
|
statusBox.Set("innerText", "A username is required!")
|
|
|
|
return
|
|
|
|
} else if len(username) > 20 {
|
|
|
|
statusBox.Set("innerText", "Username cannot be more than 20 characters!")
|
|
|
|
return
|
|
|
|
} else if password == "" {
|
|
|
|
statusBox.Set("innerText", "A password is required!")
|
|
|
|
return
|
|
|
|
} else if len(password) < 8 {
|
|
|
|
statusBox.Set("innerText", "Password must be at least 8 characters!")
|
|
|
|
return
|
|
|
|
}
|
|
|
|
|
|
|
|
// Start the signup process
|
|
|
|
fmt.Println("Starting signup process for user: " + username)
|
|
|
|
showElements(false, inputContainer, signupButton, loginButton)
|
2024-09-29 16:06:28 +01:00
|
|
|
if captcha == "" {
|
|
|
|
statusBox.Set("innerText", "You must have a valid captcha! Press the \"Start\" button to start calculating a captcha.")
|
2024-09-28 19:41:34 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
// PoW challenge computed, hash password
|
|
|
|
statusBox.Set("innerText", "Hashing password...")
|
|
|
|
|
|
|
|
// Hash the password
|
2024-10-13 19:20:19 +01:00
|
|
|
hashedPassword := hashPassword(password, []byte(username))
|
|
|
|
|
|
|
|
// Create a keypair from the password
|
|
|
|
publicKey := ed25519.NewKeyFromSeed(hashedPassword).Public().(ed25519.PublicKey)
|
2024-09-28 19:41:34 +01:00
|
|
|
|
|
|
|
// Hashed password computed, contact server
|
|
|
|
statusBox.Set("innerText", "Contacting server...")
|
|
|
|
signupBody := map[string]interface{}{
|
|
|
|
"username": username,
|
2024-10-13 19:20:19 +01:00
|
|
|
"publicKey": base64.StdEncoding.EncodeToString(publicKey),
|
2024-09-29 16:06:28 +01:00
|
|
|
"proofOfWork": captcha,
|
2024-09-28 19:41:34 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
// Marshal the body
|
|
|
|
body, err := json.Marshal(signupBody)
|
|
|
|
if err != nil {
|
|
|
|
showElements(true, inputContainer, signupButton, loginButton)
|
|
|
|
statusBox.Set("innerText", "Error marshaling signup body: "+err.Error())
|
|
|
|
return
|
|
|
|
}
|
|
|
|
|
|
|
|
// Send the request
|
|
|
|
requestUri, err := url.JoinPath(js.Global().Get("window").Get("location").Get("origin").String(), "/api/signup")
|
|
|
|
if err != nil {
|
|
|
|
showElements(true, inputContainer, signupButton, loginButton)
|
|
|
|
statusBox.Set("innerText", "Error joining URL: "+err.Error())
|
|
|
|
return
|
|
|
|
}
|
|
|
|
|
|
|
|
response, err := http.Post(requestUri, "application/json", bytes.NewReader(body))
|
|
|
|
if err != nil {
|
|
|
|
showElements(true, inputContainer, signupButton, loginButton)
|
|
|
|
statusBox.Set("innerText", "Error contacting server: "+err.Error())
|
|
|
|
return
|
|
|
|
}
|
|
|
|
|
|
|
|
// Get all our ducks in a row
|
|
|
|
var responseMap map[string]interface{}
|
|
|
|
|
|
|
|
// Read the response
|
|
|
|
decoder := json.NewDecoder(response.Body)
|
|
|
|
err = decoder.Decode(&responseMap)
|
|
|
|
if err != nil {
|
|
|
|
showElements(true, inputContainer, signupButton, loginButton)
|
|
|
|
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 {
|
|
|
|
// Signup successful
|
|
|
|
statusBox.Set("innerText", "Setting up encryption keys...")
|
|
|
|
localStorage.Call("setItem", "DONOTSHARE-secretKey", responseMap["key"].(string))
|
2024-10-13 19:20:19 +01:00
|
|
|
localStorage.Call("setItem", "DONOTSHARE-clientKey", base64.StdEncoding.EncodeToString(hashPassword(password, []byte("fg-auth-client"))))
|
2024-09-28 19:41:34 +01:00
|
|
|
|
|
|
|
// 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 == 409 {
|
|
|
|
// Username taken
|
|
|
|
showElements(true, inputContainer, signupButton, loginButton)
|
2024-09-29 16:06:28 +01:00
|
|
|
statusBox.Set("innerText", "Username already taken!")
|
2024-09-28 19:41:34 +01:00
|
|
|
} else if response.StatusCode != 500 {
|
|
|
|
// Other error
|
|
|
|
showElements(true, inputContainer, signupButton, loginButton)
|
|
|
|
statusBox.Set("innerText", responseMap["error"].(string))
|
|
|
|
} else {
|
|
|
|
// Other error
|
|
|
|
showElements(true, inputContainer, signupButton, loginButton)
|
|
|
|
statusBox.Set("innerText", "Something went wrong! (error code: "+responseMap["code"].(string)+")")
|
|
|
|
}
|
|
|
|
}()
|
|
|
|
|
|
|
|
return nil
|
|
|
|
}))
|
|
|
|
|
|
|
|
loginButton.Call("addEventListener", "click", js.FuncOf(func(this js.Value, args []js.Value) interface{} {
|
|
|
|
js.Global().Get("window").Get("location").Call("replace", "/login"+js.Global().Get("window").Get("location").Get("search").String())
|
|
|
|
return nil
|
|
|
|
}))
|
|
|
|
|
2024-09-29 16:06:28 +01:00
|
|
|
captchaInProgress := false
|
|
|
|
captchaButton.Call("addEventListener", "click", js.FuncOf(func(this js.Value, args []js.Value) interface{} {
|
2024-10-13 19:20:19 +01:00
|
|
|
if localStorage.Call("getItem", "DEBUG-customCaptcha").IsNull() {
|
|
|
|
if !captchaInProgress {
|
|
|
|
captchaInProgress = true
|
|
|
|
captchaButton.Set("innerText", "Pause")
|
|
|
|
localStorage.Call("setItem", "CONFIG-captchaStarted", "true")
|
2024-09-29 16:06:28 +01:00
|
|
|
go func() {
|
2024-10-13 19:20:19 +01:00
|
|
|
go func() {
|
|
|
|
time.Sleep(time.Minute * 5)
|
|
|
|
if captchaInProgress {
|
|
|
|
captchaStatus.Set("innerText", "Taking a long time? Try the desktop version.")
|
2024-09-29 16:06:28 +01:00
|
|
|
}
|
2024-10-13 19:20:19 +01:00
|
|
|
}()
|
|
|
|
for {
|
|
|
|
if !captchaInProgress {
|
|
|
|
captchaStatus.Set("innerText", "Captcha calculation paused.")
|
2024-09-29 16:06:28 +01:00
|
|
|
captchaButton.Set("innerText", "Start")
|
|
|
|
break
|
2024-10-13 19:20:19 +01:00
|
|
|
} 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)
|
2024-09-29 16:06:28 +01:00
|
|
|
}
|
|
|
|
}
|
2024-10-13 19:20:19 +01:00
|
|
|
}()
|
|
|
|
} else {
|
|
|
|
captchaInProgress = false
|
|
|
|
}
|
2024-09-29 16:06:28 +01:00
|
|
|
} else {
|
2024-10-13 19:20:19 +01:00
|
|
|
captcha = localStorage.Call("getItem", "DEBUG-customCaptcha").String()
|
|
|
|
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)
|
2024-09-29 16:06:28 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
return nil
|
|
|
|
}))
|
|
|
|
|
2024-09-28 19:41:34 +01:00
|
|
|
// Wait for events
|
|
|
|
select {}
|
|
|
|
}
|