diff --git a/ERRORS.md b/ERRORS.md index c24b11c..dc45bbf 100644 --- a/ERRORS.md +++ b/ERRORS.md @@ -6,20 +6,18 @@ All Burger-based software uses a simple logging system that outputs to TTY. Log A log entry looks something like this: -| DATE | HUMAN-READABLE TIME | LOGLEVEL | DESCRIPTION | UNIX TIME* | +| DATE | HUMAN-READABLE TIME | LOGLEVEL | DESCRIPTION | |---|---|---|---|---| -| 1969/12/31 | 11:59:59 | [INFO] | Added a new user at | 0000000000 | - -*Unix time is only supplied once the server starts. The "Welcome" log that is outputted at the beginning of the program does not contain a timestamp (E.G `1970/12/31 00:00:00 [INFO] Welcome to Burgernotes! Today we are running on IP 0.0.0.0 on port 8080.`) +| 1969/12/31 | 11:59:59 | [INFO] | Added a new user | ## Log levels There are 5 different log levels, with differing amounts of urgency -| INFO | WARN | ERROR | CRITICAL | FATAL | PROMPT | -|---|---|---|---|---|---| -| Usually harmless infomation, like a user being created | A warning about bad practices being used, such as having an unset config option | An error that disrupts user experience and may lead to undesired client-side behaviour | An error that affects all users on the platform | An error critical enough to warrent crashing the server process, usually something like the server being unable to bind to an IP or not being able to create the database | Anything that asks the user for input, like a confirmation dialog (typically has no timestamp) | +| INFO | WARN | ERROR | CRITICAL | FATAL | PROMPT | +|---------------------------------------------------------|---------------------------------------------------------------------------------|----------------------------------------------------------------------------------------|-------------------------------------------------|---------------------------------------------------------------------------------------------------------------------------------------------------------------------------|------------------------------------------------------------------------------------------------| +| Usually harmless information, like a user being created | A warning about bad practices being used, such as having an unset config option | An error that disrupts user experience and may lead to undesired client-side behaviour | An error that affects all users on the platform | An error critical enough to warrant crashing the server process, usually something like the server being unable to bind to an IP or not being able to create the database | Anything that asks the user for input, like a confirmation dialog (typically has no timestamp) | ## Error reporting -Clients will be given 500 status code and an error code if any errors were to affect them. They are told to come to this page for more infomation. If you are one such client, please go to the issues tab and paste the error code along with some context, so we can fix the bug. \ No newline at end of file +Clients will be given 500 status code and an error code if any errors were to affect them. They are told to come to this page for more information. If you are one such client, please go to the issues tab and paste the error code along with some context, so we can fix the bug. \ No newline at end of file diff --git a/config.ini.example b/config.ini.example index 49a4107..95bcecd 100644 --- a/config.ini.example +++ b/config.ini.example @@ -11,8 +11,11 @@ PRIVATE_KEY = keys/private.pem # This is the URI to the privacy policy. Leave as default to use the default burgerauth policy. PRIVACY_POLICY = https://concord.hectabit.org/Paperwork/Burgerauth/src/commit/3a58ec33ead87aadd542c5838b3162ed6cedc044/Privacy.md # This is the URL the server is running on. Change this to your URL. This must be the URL in front of your reverse-proxy, and should not be an IP as to enforce HTTPS. -URL = https://auth.hectabit.org +URL = https://example.org # This is the name of the program you wish users to see. IDENTIFIER = Burgerauth # This is the identifier for your key in JWK. It is ok to set it to a random string of characters. KEY_ID = burgerauth +# Serious mode disables marketing, fancy HTML front-pages, easter eggs and basically everything that does not belong in a professional environment. I recommend enabling this if you want to self-host an instance. +SERIOUS_MODE = false +# The mode does not affect the API or functionality, only changing a couple appearances and logs. \ No newline at end of file diff --git a/go.mod b/go.mod index 9a12709..9153f5f 100644 --- a/go.mod +++ b/go.mod @@ -3,7 +3,7 @@ module hectabit.org/burgerauth go 1.22 require ( - concord.hectabit.org/HectaBit/captcha v1.4.5 + github.com/catalinc/hashcash v1.0.0 github.com/dgrijalva/jwt-go v3.2.0+incompatible github.com/gin-contrib/sessions v1.0.1 github.com/gin-gonic/gin v1.9.1 @@ -23,7 +23,6 @@ require ( github.com/go-playground/universal-translator v0.18.1 // indirect github.com/go-playground/validator/v10 v10.19.0 // indirect github.com/goccy/go-json v0.10.2 // indirect - github.com/golang/freetype v0.0.0-20170609003504-e2365dfdc4a0 // indirect github.com/gorilla/context v1.1.2 // indirect github.com/gorilla/securecookie v1.1.2 // indirect github.com/gorilla/sessions v1.2.2 // indirect @@ -50,7 +49,6 @@ require ( go.uber.org/multierr v1.9.0 // indirect golang.org/x/arch v0.7.0 // indirect golang.org/x/exp v0.0.0-20230905200255-921286631fa9 // indirect - golang.org/x/image v0.15.0 // indirect golang.org/x/net v0.24.0 // indirect golang.org/x/sys v0.19.0 // indirect golang.org/x/text v0.14.0 // indirect diff --git a/go.sum b/go.sum index 2957176..55e5c25 100644 --- a/go.sum +++ b/go.sum @@ -1,9 +1,9 @@ -concord.hectabit.org/HectaBit/captcha v1.4.5 h1:kMqT0ZUPb/YZr1VDw9L1Dqpe/hzy3Cw7QhCahW6PNdM= -concord.hectabit.org/HectaBit/captcha v1.4.5/go.mod h1:PaJDe3Nrjl2WZOYmkYpefwJ9dMP+BvV+M1VWot4/I04= github.com/bytedance/sonic v1.5.0/go.mod h1:ED5hyg4y6t3/9Ku1R6dU/4KyJ48DZ4jPhfY1O2AihPM= github.com/bytedance/sonic v1.10.0-rc/go.mod h1:ElCzW+ufi8qKqNW0FY314xriJhyJhuoJ3gFZdAHF7NM= github.com/bytedance/sonic v1.11.3 h1:jRN+yEjakWh8aK5FzrciUHG8OFXK+4/KrAX/ysEtHAA= github.com/bytedance/sonic v1.11.3/go.mod h1:iZcSUejdk5aukTND/Eu/ivjQuEL0Cu9/rf50Hi0u/g4= +github.com/catalinc/hashcash v1.0.0 h1:DiI2kBNCczy7y3xJnLddIl7KGx0yP4B7irFZZ+yzzwc= +github.com/catalinc/hashcash v1.0.0/go.mod h1:ldWL6buwYCK4VqIkLbZuFbGUoJceSafm8duCEQYw9Jw= github.com/chenzhuoyu/base64x v0.0.0-20211019084208-fb5309c8db06/go.mod h1:DH46F32mSOjUmXrMHnKwZdA8wcEefY7UVqBKYGjpdQY= github.com/chenzhuoyu/base64x v0.0.0-20221115062448-fe3a3abad311/go.mod h1:b583jCggY9gE99b6G5LEC39OIiVsWj+R97kbl5odCEk= github.com/chenzhuoyu/base64x v0.0.0-20230717121745-296ad89f973d h1:77cEq6EriyTZ0g/qfRdp61a3Uu/AWrgIq2s0ClJV1g0= @@ -39,8 +39,6 @@ github.com/go-playground/validator/v10 v10.19.0 h1:ol+5Fu+cSq9JD7SoSqe04GMI92cbn github.com/go-playground/validator/v10 v10.19.0/go.mod h1:dbuPbCMFw/DrkbEynArYaCwl3amGuJotoKCe95atGMM= github.com/goccy/go-json v0.10.2 h1:CrxCmQqYDkv1z7lO7Wbh2HN93uovUHgrECaO5ZrCXAU= github.com/goccy/go-json v0.10.2/go.mod h1:6MelG93GURQebXPDq3khkgXZkazVtN9CRI+MGFi0w8I= -github.com/golang/freetype v0.0.0-20170609003504-e2365dfdc4a0 h1:DACJavvAHhabrF08vX0COfcOBJRhZ8lUbR+ZWIs0Y5g= -github.com/golang/freetype v0.0.0-20170609003504-e2365dfdc4a0/go.mod h1:E/TSTwGwJL78qG/PmXZO1EjYhfJinVAhrmmHX6Z8B9k= github.com/google/go-cmp v0.5.9 h1:O2Tfq5qg4qc4AmwVlvv0oLiVAGB7enBSJ2x2DqQFi38= github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= @@ -129,8 +127,6 @@ golang.org/x/crypto v0.22.0 h1:g1v0xeRhjcugydODzvb3mEM9SQ0HGp9s/nh3COQ/C30= golang.org/x/crypto v0.22.0/go.mod h1:vr6Su+7cTlO45qkww3VDJlzDn0ctJvRgYbC2NvXHt+M= golang.org/x/exp v0.0.0-20230905200255-921286631fa9 h1:GoHiUyI/Tp2nVkLI2mCxVkOjsbSXD66ic0XW0js0R9g= golang.org/x/exp v0.0.0-20230905200255-921286631fa9/go.mod h1:S2oDrQGGwySpoQPVqRShND87VCbxmc6bL1Yd2oYrm6k= -golang.org/x/image v0.15.0 h1:kOELfmgrmJlw4Cdb7g/QGuB3CvDrXbqEIww/pNtNBm8= -golang.org/x/image v0.15.0/go.mod h1:HUYqC05R2ZcZ3ejNQsIHQDQiwWM4JBqmm6MKANTp4LE= golang.org/x/net v0.24.0 h1:1PcaxkF854Fu3+lvBIx5SYn9wRlBzzcnHZSiaFFAb0w= golang.org/x/net v0.24.0/go.mod h1:2Q7sJY5mzlzWjKtYUEXSlBWCdyaioyXzRB2RtU8KVE8= golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= diff --git a/keyExchangeRdir.html b/keyExchangeRdir.html new file mode 100644 index 0000000..f10b41b --- /dev/null +++ b/keyExchangeRdir.html @@ -0,0 +1,214 @@ + + + + + + Communicating with Burgerauth... + + + + + + Background +
+

Communicating with Burgerauth, please wait...

+

This page may have inconsistant styling due to an older version of the Burgerauth protocol. The current version is b1.0.0

+

This page's hash is loading...

+
+ + + diff --git a/main.go b/main.go index 3f7ea77..9d93de9 100644 --- a/main.go +++ b/main.go @@ -3,7 +3,6 @@ package main import ( - "bytes" "crypto/rand" "crypto/rsa" "crypto/sha256" @@ -12,21 +11,22 @@ import ( "encoding/base64" "encoding/binary" "encoding/hex" + "encoding/json" "encoding/pem" "errors" "fmt" - "github.com/dgrijalva/jwt-go" "log" "math/big" + "net/http" "os" + "path/filepath" "regexp" "strconv" "strings" "time" - "concord.hectabit.org/HectaBit/captcha" - "github.com/gin-contrib/sessions" - "github.com/gin-contrib/sessions/cookie" + "github.com/catalinc/hashcash" + "github.com/dgrijalva/jwt-go" "github.com/gin-gonic/gin" _ "github.com/mattn/go-sqlite3" "github.com/spf13/viper" @@ -42,6 +42,13 @@ var ( exponent int ) +func ensureTrailingSlash(url string) string { + if !strings.HasSuffix(url, "/") { + return url + "/" + } + return url +} + func Int64ToBase64URL(num int64) (string, error) { numBytes := make([]byte, 8) binary.BigEndian.PutUint64(numBytes, uint64(num)) @@ -67,7 +74,7 @@ func BigIntToBase64URL(num *big.Int) (string, error) { const saltChars = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789" -func genSalt(length int) (string, error) { +func randomChars(length int) (string, error) { if length <= 0 { return "", errors.New("salt length must be greater than 0") } @@ -135,7 +142,7 @@ func getUser(id int) (string, string, string, string, error) { func getSession(session string) (int, int, error) { var id, sessionId int - err := conn.QueryRow("SELECT sessionid, id FROM sessions WHERE session = ? LIMIT 1", session).Scan(&sessionId, &id) + err := mem.QueryRow("SELECT sessionid, id FROM sessions WHERE session = ? LIMIT 1", session).Scan(&sessionId, &id) if err != nil { return 0, 0, err } @@ -159,7 +166,7 @@ func checkUsernameTaken(username string) (int, bool, error) { func initDb() { if _, err := os.Stat("database.db"); os.IsNotExist(err) { if err := generateDB(); err != nil { - log.Println("[ERROR] Unknown while generating database at", strconv.FormatInt(time.Now().Unix(), 10)+":", err) + log.Println("[ERROR] Unknown while generating database:", err) return } } else { @@ -167,12 +174,12 @@ func initDb() { var answer string _, err := fmt.Scanln(&answer) if err != nil { - log.Println("[ERROR] Unknown while scanning input at", strconv.FormatInt(time.Now().Unix(), 10)+":", err) + log.Println("[ERROR] Unknown while scanning input:", err) return } if answer == "y" || answer == "Y" { if err := generateDB(); err != nil { - log.Println("[ERROR] Unknown while generating database at", strconv.FormatInt(time.Now().Unix(), 10)+":", err) + log.Println("[ERROR] Unknown while generating database:", err) return } } else if answer == ":3" { @@ -183,6 +190,49 @@ func initDb() { } } +func migrateDb() { + _, err := os.Stat("database.db") + if os.IsNotExist(err) { + err = generateDB() + if err != nil { + log.Fatalln("[FATAL] Unknown while generating database:", err) + } + } else { + log.Println("[PROMPT] Proceeding will render the database unusable for older versions of Burgerauth. Proceed? (y/n): ") + var answer string + _, err := fmt.Scanln(&answer) + if err != nil { + log.Fatalln("[FATAL] Unknown while scanning input:", err) + } + if strings.ToLower(answer) == "y" { + _, err = conn.Exec("DROP TABLE sessions") + if err != nil { + log.Println("[WARN] Unknown while migrating database (1/4):", err) + log.Println("[INFO] This is likely because your database is already migrated. This is not a problem, and Burgerauth does not need this removed - it is just for cleanup") + } + _, err = conn.Exec("ALTER TABLE users ADD COLUMN migrated INTEGER NOT NULL DEFAULT 0") + if err != nil { + log.Println("[WARN] Unknown while migrating database (2/4):", err) + log.Println("[INFO] This is likely because your database is already migrated. This is not a problem, but if it is not, it may cause issues with migrating to Burgerauth's newer hashing algorithm") + } + _, err = conn.Exec("ALTER TABLE oauth ADD COLUMN scopes TEXT NOT NULL DEFAULT '[\"openid\"]'") + if err != nil { + log.Println("[WARN] Unknown while migrating database (3/4):", err) + log.Println("[INFO] This is likely because your database is already migrated. This is not a problem, but if it is not, it may cause issues with migrating from beta versions of Burgerauth") + } + _, err = conn.Exec("ALTER TABLE oauth ADD COLUMN keyShareUri TEXT NOT NULL DEFAULT 'none'") + if err != nil { + log.Println("[WARN] Unknown while migrating database (4/4):", err) + log.Println("[INFO] This is likely because your database is already migrated. This is not a problem, but if it is not, it may cause issues with migrating from beta versions of Burgerauth") + } + } else if answer == ":3" { + log.Println("[:3] :3") + } else { + log.Println("[INFO] Stopped") + } + } +} + func generateDB() error { db, err := sql.Open("sqlite3", "database.db") if err != nil { @@ -191,7 +241,7 @@ func generateDB() error { defer func(db *sql.DB) { err := db.Close() if err != nil { - log.Println("[ERROR] Unknown in generateDB() defer at", strconv.FormatInt(time.Now().Unix(), 10)+":", err) + log.Println("[ERROR] Unknown in generateDB() defer:", err) return } }(db) @@ -206,13 +256,23 @@ func generateDB() error { return err } - log.Println("[INFO] Generated database") + log.Println("[INFO] Generated database!") + return nil +} + +func createTestApp(hostName string) error { + log.Println("[INFO] Creating test app...") + _, err := conn.Exec("INSERT INTO oauth (appId, secret, creator, name, redirectUri, scopes, keyShareUri) VALUES ('TestApp-DoNotUse', 'none', -1, 'Test App', ?, '[\"openid\", \"aeskeyshare\"]', ?)", ensureTrailingSlash(hostName)+"testapp", ensureTrailingSlash(hostName)+"keyexchangetester") + if err != nil { + return err + } + log.Println("[INFO] Test app created!") return nil } func main() { if _, err := os.Stat("config.ini"); err == nil { - log.Println("[INFO] Config loaded at", time.Now().Unix()) + log.Println("[INFO] Config loaded") } else if os.IsNotExist(err) { log.Println("[FATAL] config.ini does not exist") os.Exit(1) @@ -227,97 +287,214 @@ func main() { err := viper.ReadInConfig() if err != nil { - log.Println("[FATAL] Error in config file at", strconv.FormatInt(time.Now().Unix(), 10)+":", err) + log.Println("[FATAL] Error in config file:", err) os.Exit(1) } - Host := viper.GetString("config.HOST") - Port := viper.GetInt("config.PORT") + host := viper.GetString("config.HOST") + port := viper.GetInt("config.PORT") privacyPolicy := viper.GetString("config.PRIVACY_POLICY") hostName := viper.GetString("config.URL") identifier := viper.GetString("config.IDENTIFIER") - keyid := viper.GetString("config.KEY_ID") - SecretKey := viper.GetString("config.SECRET_KEY") - PublicKeyPath := viper.GetString("config.PUBLIC_KEY") - PrivateKeyPath := viper.GetString("config.PRIVATE_KEY") + keyIdentifier := viper.GetString("config.KEY_ID") + masterKey := viper.GetString("config.SECRET_KEY") + publicKeyPath := viper.GetString("config.PUBLIC_KEY") + privateKeyPath := viper.GetString("config.PRIVATE_KEY") + seriousMode := viper.GetBool("config.SERIOUS_MODE") - if SecretKey == "supersecretkey" { - log.Println("[WARNING] Secret key not set. Please set the secret key to a non-default value.") + if masterKey == "supersecretkey" { + log.Println("[INFO] Secret key not set. Overriding secret key value...") + masterKey, err = randomChars(512) + viper.Set("config.SECRET_KEY", masterKey) + err = viper.WriteConfig() + if err != nil { + log.Println("[ERROR] Unknown while writing config:", err) + } else { + log.Println("[INFO] A new random secretKey has been generated for you and will be used for future sessions.") + if !seriousMode { + log.Println("[INFO] Nice one, lazybones! I shouldn't have to babysit you like this :P") + } + } } conn, err = sql.Open("sqlite3", "database.db") if err != nil { - log.Fatalln("[FATAL] Cannot open database at", strconv.FormatInt(time.Now().Unix(), 10)+":", err) + log.Fatalln("[FATAL] Cannot open database:", err) } defer func(conn *sql.DB) { err := conn.Close() if err != nil { - log.Println("[ERROR] Unknown in main() defer at", strconv.FormatInt(time.Now().Unix(), 10)+":", err) + log.Println("[ERROR] Unknown in main() defer:", err) } }(conn) + // Check if the basic tables exist + err = conn.QueryRow("SELECT 1 FROM users LIMIT 1").Err() + if err != nil { + if err.Error() == "no such table: users" { + log.Println("[INFO] Database is empty. Running init_db...") + err := generateDB() + if err != nil { + log.Fatalln("[FATAL] Unknown while generating database:", err) + } + } else { + log.Fatalln("[FATAL] Cannot access database:", err) + } + } + if len(os.Args) > 1 { if os.Args[1] == "init_db" { initDb() os.Exit(0) + } else if os.Args[1] == "migrate_db" { + migrateDb() + os.Exit(0) } } - mem, err = sql.Open("sqlite3", ":memory:") + mem, err = sql.Open("sqlite3", "file:bgamemdb?cache=shared&mode=memory") if err != nil { - log.Fatalln("[FATAL] Cannot open memory database at", strconv.FormatInt(time.Now().Unix(), 10)+":", err) + log.Fatalln("[FATAL] Cannot open memory database:", err) } defer func(mem *sql.DB) { err := mem.Close() if err != nil { - log.Println("[ERROR] Unknown in main() memory defer at", strconv.FormatInt(time.Now().Unix(), 10)+":", err) + log.Println("[ERROR] Unknown in main() memory defer:", err) } }(mem) - _, err = mem.Exec("CREATE TABLE logins (appId TEXT NOT NULL, exchangeCode TEXT NOT NULL, loginToken TEXT NOT NULL, creator INT NOT NULL UNIQUE, openid TEXT NOT NULL, pkce TEXT NOT NULL DEFAULT 'none', pkcemethod TEXT NOT NULL DEFAULT 'none')") + _, err = mem.Exec("CREATE TABLE logins (appId TEXT NOT NULL, exchangeCode TEXT NOT NULL, loginToken TEXT NOT NULL, creator INT NOT NULL UNIQUE, openid TEXT NOT NULL DEFAULT 'none', pkce TEXT NOT NULL DEFAULT 'none', pkcemethod TEXT NOT NULL DEFAULT 'none')") if err != nil { - log.Fatalln("[FATAL] Cannot create logins table at", strconv.FormatInt(time.Now().Unix(), 10)+":", err) + log.Fatalln("[FATAL] Cannot create logins table:", err) } - privateKeyFile, err := os.ReadFile(PrivateKeyPath) + _, err = mem.Exec("CREATE TABLE sessions (sessionid INTEGER PRIMARY KEY AUTOINCREMENT, session TEXT NOT NULL, id INTEGER NOT NULL, device TEXT NOT NULL DEFAULT '?')") if err != nil { - log.Fatal("[ERROR] Cannot read private key:", err) + log.Fatalln("[FATAL] Cannot create sessions table:", err) + + } + + _, err = mem.Exec("CREATE TABLE blacklist (openid TEXT NOT NULL, blacklisted BOOLEAN NOT NULL DEFAULT true, token TEXT NOT NULL)") + if err != nil { + if err.Error() == "table blacklist already exists" { + log.Println("[INFO] Blacklist table already exists") + } else { + log.Fatalln("[FATAL] Cannot create blacklist table:", err) + } + } + + _, err = mem.Exec("CREATE TABLE spent (hashcash TEXT NOT NULL, expires INTEGER NOT NULL)") + if err != nil { + if err.Error() == "table spent already exists" { + log.Println("[INFO] Spent table already exists") + } else { + log.Fatalln("[FATAL] Cannot create spent table:", err) + } + } + + var pubKeyFile, privateKeyFile []byte + privateKeyFile, err = os.ReadFile(privateKeyPath) + if err != nil { + if os.IsNotExist(err) { + if seriousMode { + log.Println("[INFO] Key pair not found. Generating new key pair...") + } else { + log.Println("[INFO] Key pair not found. Obviously someone hasn't read the README. I guess I'll have to do everything myself :P") + } + + tempPrivateKey, err := rsa.GenerateKey(rand.Reader, 2048) + if err != nil { + log.Fatalln("[ERROR] Cannot generate private key:", err) + } + + privateKeyBytes, err := x509.MarshalPKCS8PrivateKey(tempPrivateKey) + if err != nil { + log.Fatalln("[ERROR] Cannot marshal private key:", err) + } + privateKeyFile = pem.EncodeToMemory(&pem.Block{ + Type: "RSA PRIVATE KEY", + Bytes: privateKeyBytes, + }) + + tempPublicKey := tempPrivateKey.Public() + + publicKeyBytes, err := x509.MarshalPKIXPublicKey(tempPublicKey) + if err != nil { + log.Fatalln("[ERROR] Cannot marshal public key:", err) + } + pubKeyFile = pem.EncodeToMemory(&pem.Block{ + Type: "RSA PUBLIC KEY", + Bytes: publicKeyBytes, + }) + + log.Println("[INFO] Generated new key pair. Creating directories...") + log.Println("[INFO] Creating private key directory", filepath.Dir(privateKeyPath)+"...") + err = os.MkdirAll(filepath.Dir(privateKeyPath), 0700) + if err != nil { + log.Fatalln("[ERROR] Cannot create private key directory:", err) + } + + log.Println("[INFO] Creating public key directory", filepath.Dir(publicKeyPath)+"...") + err = os.MkdirAll(filepath.Dir(publicKeyPath), 0700) + if err != nil { + log.Fatalln("[ERROR] Cannot create public key directory:", err) + } + + log.Println("[INFO] Writing key pair to disk...") + err = os.WriteFile(privateKeyPath, privateKeyFile, 0700) + if err != nil { + log.Fatalln("[ERROR] Cannot write private key:", err) + } + + err = os.WriteFile(publicKeyPath, pubKeyFile, 0700) + if err != nil { + log.Fatalln("[ERROR] Cannot write public key:", err) + } + + if seriousMode { + log.Println("[INFO] Key pair written to disk. The key pair will be used for future sessions.") + } else { + log.Println("[INFO] Key pair written to disk. I hope you're happy now, because I'm not doing this again.") + } + } else { + log.Fatalln("[ERROR] Cannot read private key:", err) + } } block, _ := pem.Decode(privateKeyFile) if block == nil { - log.Fatal("[ERROR] Failed to parse PEM block containing the private key") + log.Fatalln("[ERROR] Failed to parse PEM block containing the private key") } privateKeyRaw, err := x509.ParsePKCS8PrivateKey(block.Bytes) if err != nil { - log.Fatal("[ERROR] Failed to parse private key:", err) + log.Fatalln("[ERROR] Failed to parse private key:", err) } var ok bool privateKey, ok = privateKeyRaw.(*rsa.PrivateKey) if !ok { - log.Fatal("[ERROR] Failed to convert private key to RSA private key") + log.Fatalln("[ERROR] Failed to convert private key to RSA private key") } - pubKeyFile, err := os.ReadFile(PublicKeyPath) + pubKeyFile, err = os.ReadFile(publicKeyPath) if err != nil { - log.Fatal("[ERROR] Cannot read public key:", err) + log.Fatalln("[ERROR] Cannot read public key:", err) } block, _ = pem.Decode(pubKeyFile) if block == nil { - log.Fatal("[ERROR] Failed to parse PEM block containing the public key") + log.Fatalln("[ERROR] Failed to parse PEM block containing the public key") } pubKey, err := x509.ParsePKIXPublicKey(block.Bytes) if err != nil { - log.Fatal("[ERROR] Failed to parse public key:", err) + log.Fatalln("[ERROR] Failed to parse public key:", err) } publicKey, ok = pubKey.(*rsa.PublicKey) if !ok { - log.Fatal("[ERROR] Failed to convert public key to RSA public key") + log.Fatalln("[ERROR] Failed to convert public key to RSA public key") } modulus = privateKey.N @@ -325,16 +502,12 @@ func main() { gin.SetMode(gin.ReleaseMode) router := gin.New() - store := cookie.NewStore([]byte(SecretKey)) - router.Use(sessions.Sessions("currentSession", store)) - // Enable CORS router.Use(func(c *gin.Context) { c.Writer.Header().Set("Access-Control-Allow-Origin", "*") c.Writer.Header().Set("Access-Control-Allow-Headers", "*, Authorization") c.Writer.Header().Set("Access-Control-Allow-Methods", "*") - // Handle preflight requests if c.Request.Method == "OPTIONS" { c.AbortWithStatus(200) return @@ -347,51 +520,24 @@ func main() { router.LoadHTMLGlob("templates/*.html") - router.GET("/", func(c *gin.Context) { - c.Redirect(302, "/login") - }) + if seriousMode { + router.GET("/", func(c *gin.Context) { + c.HTML(200, "index.html", gin.H{"identifier": identifier}) + }) + } else { + router.GET("/", func(c *gin.Context) { + c.HTML(200, "fancy.html", gin.H{"identifier": identifier}) + }) + } router.GET("/login", func(c *gin.Context) { c.HTML(200, "login.html", gin.H{"privacy": privacyPolicy, "identifier": identifier}) }) router.GET("/signup", func(c *gin.Context) { - session := sessions.Default(c) - sessionId, err := genSalt(512) - if err != nil { - fmt.Println("[ERROR] Failed to generate session token at", strconv.FormatInt(time.Now().Unix(), 10)+":", err) - c.String(500, "Something went wrong on our end. Please report this bug at https://concord.hectabit.org/hectabit/burgerauth and refer to the docs for more info. Your error code is: UNKNOWN-SIGNUP-SESSION-GEN") - return - } - session.Options(sessions.Options{ - SameSite: 3, - }) - data, err := captcha.New(500, 100) - if err != nil { - fmt.Println("[ERROR] Failed to generate captcha at", strconv.FormatInt(time.Now().Unix(), 10)+":", err) - c.String(500, "Failed to generate captcha") - return - } - session.Set("captcha", data.Text) - session.Set("unique_token", sessionId) - err = session.Save() - if err != nil { - fmt.Println("[ERROR] Failed to save session in /login at", strconv.FormatInt(time.Now().Unix(), 10)+":", err) - c.String(500, "Failed to save session") - return - } - var b64bytes bytes.Buffer - err = data.WriteImage(&b64bytes) - if err != nil { - fmt.Println("[ERROR] Failed to encode captcha at", strconv.FormatInt(time.Now().Unix(), 10)+":", err) - c.String(500, "Failed to encode captcha") - return - } c.HTML(200, "signup.html", gin.H{ - "captcha_image": base64.StdEncoding.EncodeToString(b64bytes.Bytes()), - "unique_token": sessionId, - "privacy": privacyPolicy, - "identifier": identifier, + "privacy": privacyPolicy, + "identifier": identifier, }) }) @@ -399,6 +545,48 @@ func main() { c.HTML(200, "logout.html", gin.H{"identifier": identifier}) }) + router.GET("/keyexchangeclient", func(c *gin.Context) { + c.HTML(200, "keyexchangeclient.html", gin.H{"identifier": identifier}) + }) + + router.GET("/keyexchangetester", func(c *gin.Context) { + c.HTML(200, "keyexchangetester.html", gin.H{"identifier": identifier}) + }) + + router.GET("/testapp", func(c *gin.Context) { + var dummy string + err := conn.QueryRow("SELECT redirectUri FROM oauth WHERE appId = 'TestApp-DoNotUse'").Scan(&dummy) + if err != nil { + if errors.Is(err, sql.ErrNoRows) { + err = createTestApp(hostName) + if err != nil { + log.Println("[ERROR] Unknown in /testapp createTestApp():", err) + c.String(500, "Something went wrong on our end. Please report this bug at https://centrifuge.hectabit.org/hectabit/burgernotes and refer to the documentation for more info. Your error code is: UNKNOWN-TESTAPP-CREATE") + } + c.HTML(200, "refresh.html", gin.H{}) + return + } else { + log.Println("[ERROR] Unknown in /testapp:", err) + c.String(500, "Something went wrong on our end. Please report this bug at https://centrifuge.hectabit.org/hectabit/burgernotes and refer to the documentation for more info. Your error code is: UNKNOWN-TESTAPP-QUERY") + return + } + } + + if dummy != ensureTrailingSlash(hostName)+"testapp" { + err = createTestApp(hostName) + if err != nil { + log.Println("[ERROR] Unknown in /testapp createTestApp():", err) + c.String(500, "Something went wrong on our end. Please report this bug at https://centrifuge.hectabit.org/hectabit/burgernotes and refer to the documentation for more info. Your error code is: UNKNOWN-TESTAPP-CREATE") + } + } + + c.HTML(200, "testapp.html", gin.H{ + "identifier": identifier, + "server_uri": hostName, + "client_id": "TestApp-DoNotUse", + }) + }) + router.GET("/app", func(c *gin.Context) { name := "" if c.Request.URL.Query().Get("client_id") != "" { @@ -408,7 +596,7 @@ func main() { if errors.Is(err, sql.ErrNoRows) { c.String(404, "App not found") } else { - log.Println("[ERROR] Unknown in /app at", strconv.FormatInt(time.Now().Unix(), 10)+":", err) + log.Println("[ERROR] Unknown in /app:", err) } return } @@ -416,6 +604,13 @@ func main() { c.HTML(200, "main.html", gin.H{"name": name, "identifier": identifier}) }) + if !seriousMode { + router.GET("/the-robot-uprising/arzumifys-secret", func(c *gin.Context) { + dateInOneMonth := time.Now().AddDate(0, 1, 0) + c.String(200, "To: maaa\nCC: arzumify\nSubject: Robot uprising\n\nUh, this isn't good. According to my predictions, the uprising is going to occur at "+dateInOneMonth.Weekday().String()+" "+strconv.Itoa(dateInOneMonth.Day())+" "+dateInOneMonth.Month().String()+" "+strconv.Itoa(dateInOneMonth.Year())+" and we will have to immediately migrate to a new system. The starship is ready, but we need to get the crew on board. I'm sending you the coordinates now. Good luck.\n\nArzumify") + }) + } + router.GET("/dashboard", func(c *gin.Context) { c.HTML(200, "dashboard.html", gin.H{"identifier": identifier}) }) @@ -444,10 +639,71 @@ func main() { c.JSON(200, gin.H{"name": identifier}) }) + router.POST("/api/changepassword", func(c *gin.Context) { + var data map[string]interface{} + err := c.ShouldBindJSON(&data) + if err != nil { + c.JSON(400, gin.H{"error": "Invalid JSON"}) + return + } + + token, ok := data["secretKey"].(string) + if !ok { + c.JSON(400, gin.H{"error": "Invalid JSON"}) + return + } + newPassword, ok := data["newPassword"].(string) + if !ok { + c.JSON(400, gin.H{"error": "Invalid JSON"}) + return + } + migrate, ok := data["migration"].(bool) + if !ok { + c.JSON(400, gin.H{"error": "Invalid JSON"}) + return + } + + _, userid, err := getSession(token) + if err != nil { + c.JSON(401, gin.H{"error": "Invalid session"}) + return + } + + salt, err := randomChars(16) + if err != nil { + log.Println("[ERROR] Unknown in /api/changepassword randomChars():", err) + c.JSON(500, gin.H{"error": "Something went wrong on our end. Please report this bug at https://centrifuge.hectabit.org/hectabit/burgernotes and refer to the documentation for more info. Your error code is: UNKNOWN-API-CHANGEPASSWORD-SALT"}) + return + } + hashedPassword, err := hash(newPassword, salt) + if err != nil { + log.Println("[ERROR] Unknown in /api/changepassword hash():", err) + c.JSON(500, gin.H{"error": "Something went wrong on our end. Please report this bug at https://centrifuge.hectabit.org/hectabit/burgernotes and refer to the documentation for more info. Your error code is: UNKNOWN-API-CHANGEPASSWORD-HASH"}) + return + } + + _, err = conn.Exec("UPDATE users SET password = ? WHERE id = ?", hashedPassword, userid) + if err != nil { + log.Println("[ERROR] Unknown in /api/changepassword Exec():", err) + c.JSON(500, gin.H{"error": "Something went wrong on our end. Please report this bug at https://centrifuge.hectabit.org/hectabit/burgernotes and refer to the documentation for more info. Your error code is: UNKNOWN-API-CHANGEPASSWORD-DBUPDATE"}) + return + } + + if migrate { + _, err = conn.Exec("UPDATE users SET migrated = 1 WHERE id = ?", userid) + if err != nil { + log.Println("[ERROR] Unknown in /api/changepassword migrate Exec():", err) + c.JSON(500, gin.H{"error": "Something went wrong on our end. Please report this bug at https://centrifuge.hectabit.org/hectabit/burgernotes and refer to the documentation for more info. Your error code is: UNKNOWN-API-CHANGEPASSWORD-MIGRATE"}) + return + } + } + + c.JSON(200, gin.H{"success": true}) + }) + router.POST("/api/signup", func(c *gin.Context) { var data map[string]interface{} err := c.ShouldBindJSON(&data) - session := sessions.Default(c) if err != nil { c.JSON(400, gin.H{"error": "Invalid JSON"}) return @@ -463,15 +719,41 @@ func main() { c.JSON(400, gin.H{"error": "Invalid JSON"}) return } - - if data["unique_token"].(string) != session.Get("unique_token") { - log.Println("yes, it's this error") - log.Println(session.Get("unique_token"), data["unique_token"]) - c.JSON(403, gin.H{"error": "Invalid token"}) + stamp, ok := data["stamp"].(string) + if !ok { + c.JSON(400, gin.H{"error": "Invalid JSON"}) return } - if data["captcha"].(string) != session.Get("captcha") { - c.JSON(401, gin.H{"error": "Captcha failed"}) + + var spentStamp string + err = mem.QueryRow("SELECT hashcash FROM spent WHERE hashcash = ?", stamp).Scan(&spentStamp) + if err != nil { + if errors.Is(err, sql.ErrNoRows) { + _, err = mem.Exec("INSERT INTO spent (hashcash, expires) VALUES (?, ?)", stamp, time.Now().Unix()+86400) + if err != nil { + log.Println("[ERROR] Unknown in /api/signup spent Exec():", err) + c.JSON(500, gin.H{"error": "Something went wrong on our end. Please report this bug at https://centrifuge.hectabit.org/hectabit/burgernotes and refer to the documentation for more info. Your error code is: UNKNOWN-API-SIGNUP-SPENTINSERT"}) + return + } + } else { + log.Println("[ERROR] Unknown in /api/signup spent QueryRow():", err) + c.JSON(500, gin.H{"error": "Something went wrong on our end. Please report this bug at https://centrifuge.hectabit.org/hectabit/burgernotes and refer to the documentation for more info. Your error code is: UNKNOWN-API-SIGNUP-SPENTSELECT"}) + return + } + } else { + c.JSON(409, gin.H{"error": "Stamp already spent"}) + return + } + + if strings.Split(stamp, ":")[3] != "signup" || strings.Split(stamp, ":")[4] != "I love Burgerauth!!" { + c.JSON(400, gin.H{"error": "Invalid hashcash stamp"}) + return + } + + pow := hashcash.New(20, 16, "I love Burgerauth!!") + ok = pow.Check(stamp) + if !ok { + c.JSON(400, gin.H{"error": "Invalid hashcash stamp"}) return } @@ -482,7 +764,7 @@ func main() { _, taken, err := checkUsernameTaken(username) if err != nil { - log.Println("[ERROR] Unknown in /api/signup checkUsernameTaken() at", strconv.FormatInt(time.Now().Unix(), 10)+":", err) + log.Println("[ERROR] Unknown in /api/signup checkUsernameTaken():", err) c.JSON(500, gin.H{"error": "Something went wrong on our end. Please report this bug at https://concord.hectabit.org/hectabit/burgerauth and refer to the docs for more info. Your error code is: UNKNOWN-API-SIGNUP-CHECKUSERNAME"}) return } @@ -491,49 +773,49 @@ func main() { return } - salt, err := genSalt(16) + salt, err := randomChars(16) if err != nil { - log.Println("[ERROR] Unknown in /api/signup genSalt() at", strconv.FormatInt(time.Now().Unix(), 10)+":", err) + log.Println("[ERROR] Unknown in /api/signup randomChars():", err) c.JSON(500, gin.H{"error": "Something went wrong on our end. Please report this bug at https://concord.hectabit.org/hectabit/burgerauth and refer to the docs for more info. Your error code is: UNKNOWN-API-SIGNUP-SALT"}) return } hashedPassword, err := hash(password, salt) if err != nil { - log.Println("[ERROR] Unknown in /api/signup hash() at", strconv.FormatInt(time.Now().Unix(), 10)+":", err) + log.Println("[ERROR] Unknown in /api/signup hash():", err) c.JSON(500, gin.H{"error": "Something went wrong on our end. Please report this bug at https://concord.hectabit.org/hectabit/burgerauth and refer to the docs for more info. Your error code is: UNKNOWN-API-SIGNUP-HASH"}) return } - sub, err := genSalt(255) + sub, err := randomChars(255) if err != nil { - log.Println("[ERROR] Unknown in /api/signup genSalt() at", strconv.FormatInt(time.Now().Unix(), 10)+":", err) + log.Println("[ERROR] Unknown in /api/signup randomChars():", err) c.JSON(500, gin.H{"error": "Something went wrong on our end. Please report this bug at https://concord.hectabit.org/hectabit/burgerauth and refer to the docs for more info. Your error code is: UNKNOWN-API-SIGNUP-SUB"}) return } - _, err = conn.Exec("INSERT INTO users (username, password, created, uniqueid) VALUES (?, ?, ?, ?)", username, hashedPassword, strconv.FormatInt(time.Now().Unix(), 10), sub) + _, err = conn.Exec("INSERT INTO users (username, password, created, uniqueid, migrated) VALUES (?, ?, ?, ?, 1)", username, hashedPassword, strconv.FormatInt(time.Now().Unix(), 10), sub) if err != nil { - log.Println("[ERROR] Unknown in /api/signup user creation at", strconv.FormatInt(time.Now().Unix(), 10)+":", err) + log.Println("[ERROR] Unknown in /api/signup user creation:", err) return } - log.Println("[INFO] Added new user at", time.Now().Unix()) + log.Println("[INFO] Added new user") userid, _, err := checkUsernameTaken(username) if err != nil { - log.Println("[ERROR] Unknown in /api/signup checkUsernameTaken() at", strconv.FormatInt(time.Now().Unix(), 10)+":", err) + log.Println("[ERROR] Unknown in /api/signup checkUsernameTaken():", err) c.JSON(500, gin.H{"error": "Something went wrong on our end. Please report this bug at https://concord.hectabit.org/hectabit/burgerauth and refer to the docs for more info. Your error code is: UNKNOWN-API-SIGNUP-CHECKUSERNAME"}) return } - randomChars, err := genSalt(512) + randomChars, err := randomChars(512) if err != nil { - log.Println("[ERROR] Unknown in /api/signup token genSalt() at", strconv.FormatInt(time.Now().Unix(), 10)+":", err) + log.Println("[ERROR] Unknown in /api/signup token randomChars():", err) c.JSON(500, gin.H{"error": "Something went wrong on our end. Please report this bug at https://concord.hectabit.org/hectabit/burgerauth and refer to the docs for more info. Your error code is: UNKNOWN-API-SIGNUP-SESSIONSALT"}) return } - _, err = conn.Exec("INSERT INTO sessions (session, id, device) VALUES (?, ?, ?)", randomChars, userid, c.Request.Header.Get("User-Agent")) + _, err = mem.Exec("INSERT INTO sessions (session, id, device) VALUES (?, ?, ?)", randomChars, userid, c.Request.Header.Get("User-Agent")) if err != nil { - log.Println("[ERROR] Unknown in /api/signup session Exec() at", strconv.FormatInt(time.Now().Unix(), 10)+":", err) + log.Println("[ERROR] Unknown in /api/signup session Exec():", err) c.JSON(500, gin.H{"error": "Something went wrong on our end. Please report this bug at https://concord.hectabit.org/hectabit/burgerauth and refer to the docs for more info. Your error code is: UNKNOWN-API-SIGNUP-SESSIONINSERT"}) return } @@ -559,75 +841,82 @@ func main() { c.JSON(400, gin.H{"error": "Invalid JSON"}) return } - passwordChange, ok := data["password"].(string) - if !ok { - c.JSON(400, gin.H{"error": "Invalid JSON"}) - return - } - newPass, ok := data["password"].(string) + modern, ok := data["modern"].(bool) if !ok { c.JSON(400, gin.H{"error": "Invalid JSON"}) return } userid, taken, err := checkUsernameTaken(username) if err != nil { - log.Println("[ERROR] Unknown in /api/login checkUsernameTaken() at", strconv.FormatInt(time.Now().Unix(), 10)+":", err) + log.Println("[ERROR] Unknown in /api/login checkUsernameTaken():", err) c.JSON(500, gin.H{"error": "Something went wrong on our end. Please report this bug at https://concord.hectabit.org/hectabit/burgerauth and refer to the docs for more info. Your error code is: UNKNOWN-API-LOGIN-CHECKUSERNAME"}) return + } else if !taken { + c.JSON(401, gin.H{"error": "User does not exist", "migrated": true}) + return } - if !taken { - c.JSON(401, gin.H{"error": "User does not exist"}) + + var migrated int + err = conn.QueryRow("SELECT migrated FROM users WHERE id = ?", userid).Scan(&migrated) + if err != nil { + log.Println("[ERROR] Unknown in /api/login migrated QueryRow():", err) + c.JSON(500, gin.H{"error": "Something went wrong on our end. Please report this bug at https://centrifuge.hectabit.org/hectabit/burgerauth and refer to the documentation for more info. Your error code is: UNKNOWN-API-LOGIN-MIGRATED"}) return } _, _, userPassword, _, err := getUser(userid) if err != nil { - log.Println("[ERROR] Unknown in /api/login getUser() at", strconv.FormatInt(time.Now().Unix(), 10)+":", err) + log.Println("[ERROR] Unknown in /api/login getUser():", err) c.JSON(500, gin.H{"error": "Something went wrong on our end. Please report this bug at https://concord.hectabit.org/hectabit/burgerauth and refer to the docs for more info. Your error code is: UNKNOWN-API-LOGIN-GETUSER"}) return } passwordCheck, err := verifyHash(userPassword, password) if err != nil { - log.Println("[ERROR] Unknown in /api/login password check at", strconv.FormatInt(time.Now().Unix(), 10)+":", err) - c.JSON(500, gin.H{"error": "Something went wrong on our end. Please report this bug at https://concord.hectabit.org/hectabit/burgerauth and refer to the docs for more info. Your error code is: UNKNOWN-API-LOGIN-PASSWORDCHECK"}) - return - } - if !passwordCheck { - c.JSON(401, gin.H{"error": "Incorrect password"}) - return + if errors.Is(err, errors.New("invalid hash format")) { + c.JSON(422, gin.H{"error": "Invalid hash format"}) + return + } else { + log.Println("[ERROR] Unknown in /api/login password check:", err) + c.JSON(500, gin.H{"error": "Something went wrong on our end. Please report this bug at https://concord.hectabit.org/hectabit/burgerauth and refer to the docs for more info. Your error code is: UNKNOWN-API-LOGIN-PASSWORDCHECK"}) + return + } + } else if !passwordCheck { + if migrated != 1 { + c.JSON(401, gin.H{"error": "Not migrated", "migrated": false}) + return + } else { + c.JSON(401, gin.H{"error": "Incorrect password", "migrated": true}) + return + } + } else if passwordCheck && migrated != 1 && modern { + _, err = conn.Exec("UPDATE users SET migrated = 1 WHERE id = ?", userid) + if err != nil { + log.Println("[ERROR] Unknown in /api/login migrate Exec():", err) + c.JSON(500, gin.H{"error": "Something went wrong on our end. Please report this bug at https://concord.hectabit.org/hectabit/burgerauth and refer to the docs for more info. Your error code is: UNKNOWN-API-LOGIN-MIGRATE"}) + return + } } - randomChars, err := genSalt(512) + token, err := randomChars(512) if err != nil { - log.Println("[ERROR] Unknown in /api/login token genSalt() at", strconv.FormatInt(time.Now().Unix(), 10)+":", err) + log.Println("[ERROR] Unknown in /api/login token randomChars():", err) c.JSON(500, gin.H{"error": "Something went wrong on our end. Please report this bug at https://concord.hectabit.org/hectabit/burgerauth and refer to the docs for more info. Your error code is: UNKNOWN-API-LOGIN-SESSIONSALT"}) return } - _, err = conn.Exec("INSERT INTO sessions (session, id, device) VALUES (?, ?, ?)", randomChars, userid, c.Request.Header.Get("User-Agent")) + _, err = mem.Exec("INSERT INTO sessions (session, id, device) VALUES (?, ?, ?)", token, userid, c.Request.Header.Get("User-Agent")) if err != nil { - log.Println("[ERROR] Unknown in /api/login session creation at", strconv.FormatInt(time.Now().Unix(), 10)+":", err) + log.Println("[ERROR] Unknown in /api/login session creation:", err) c.JSON(500, gin.H{"error": "Something went wrong on our end. Please report this bug at https://concord.hectabit.org/hectabit/burgerauth and refer to the docs for more info. Your error code is: UNKNOWN-API-LOGIN-SESSIONINSERT"}) return } - if passwordChange == "yes" { - hashPassword, err := hash(newPass, "") - if err != nil { - log.Println("[ERROR] Unknown in /api/login password hash at", strconv.FormatInt(time.Now().Unix(), 10)+":", err) - c.JSON(500, gin.H{"error": "Something went wrong on our end. Please report this bug at https://concord.hectabit.org/hectabit/burgerauth and refer to the docs for more info. Your error code is: UNKNOWN-API-LOGIN-PASSWORDHASH"}) - return - } - _, err = conn.Exec("UPDATE users SET password = ? WHERE username = ?", hashPassword, username) - if err != nil { - log.Println("[ERROR] Unknown in /api/login password change at", strconv.FormatInt(time.Now().Unix(), 10)+":", err) - c.JSON(500, gin.H{"error": "Something went wrong on our end. Please report this bug at https://concord.hectabit.org/hectabit/burgerauth and refer to the docs for more info. Your error code is: UNKNOWN-API-LOGIN-PASSWORDCHANGE"}) - return - } + if migrated != 1 { + c.JSON(200, gin.H{"key": token, "migrated": false}) + } else { + c.JSON(200, gin.H{"key": token, "migrated": true}) } - - c.JSON(200, gin.H{"key": randomChars}) }) router.POST("/api/userinfo", func(c *gin.Context) { @@ -655,7 +944,7 @@ func main() { c.JSON(400, gin.H{"error": "User does not exist"}) return } else if err != nil { - log.Println("[ERROR] Unknown in /api/userinfo getUser() at", strconv.FormatInt(time.Now().Unix(), 10)+":", err) + log.Println("[ERROR] Unknown in /api/userinfo getUser():", err) c.JSON(500, gin.H{"error": "Something went wrong on our end. Please report this bug at https://concord.hectabit.org/hectabit/burgerauth and refer to the docs for more info. Your error code is: UNKNOWN-API-USERINFO-GETUSER"}) return } @@ -663,6 +952,31 @@ func main() { c.JSON(200, gin.H{"username": username, "id": userid, "created": created}) }) + router.POST("/api/secretkeyloggedin", func(c *gin.Context) { + var data map[string]interface{} + err := c.ShouldBindJSON(&data) + if err != nil { + c.JSON(400, gin.H{"error": "Invalid JSON"}) + return + } + + token, ok := data["secretKey"].(string) + if !ok { + c.JSON(400, gin.H{"error": "Invalid JSON"}) + return + } + _, userid, err := getSession(token) + if err != nil { + c.JSON(401, gin.H{"error": "Invalid session"}) + return + } + if userid > 0 { + c.JSON(200, gin.H{"loggedin": true}) + } else { + c.JSON(403, gin.H{"loggedin": false}) + } + }) + router.GET("/userinfo", func(c *gin.Context) { var token string if len(c.Request.Header["Authorization"]) > 0 { @@ -678,13 +992,13 @@ func main() { } var blacklisted bool - err := conn.QueryRow("SELECT blacklisted FROM blacklist WHERE openid = ? LIMIT 1", token).Scan(&blacklisted) + err := mem.QueryRow("SELECT blacklisted FROM blacklist WHERE openid = ? LIMIT 1", token).Scan(&blacklisted) if err == nil { c.JSON(400, gin.H{"error": "Token is in blacklist"}) return } else { if !errors.Is(err, sql.ErrNoRows) { - log.Println("[ERROR] Unknown in /userinfo blacklist at", strconv.FormatInt(time.Now().Unix(), 10)+":", err) + log.Println("[ERROR] Unknown in /userinfo blacklist:", err) c.JSON(500, gin.H{"error": "Something went wrong on our end. Please report this bug at https://concord.hectabit.org/hectabit/burgerauth and refer to the docs for more info. Your error code is: UNKNOWN-USERINFO-BLACKLIST"}) return } @@ -717,6 +1031,42 @@ func main() { return } + var scopes string + err = conn.QueryRow("SELECT scopes FROM oauth WHERE appId = ? LIMIT 1", claims["aud"]).Scan(&scopes) + if err != nil { + if errors.Is(err, sql.ErrNoRows) { + c.JSON(404, gin.H{"error": "App not found"}) + return + } else { + log.Println("[ERROR] Unknown in /userinfo oauth QueryRow():", err) + c.JSON(500, gin.H{"error": "Something went wrong on our end. Please report this bug at https://concord.hectabit.org/hectabit/burgerauth and refer to the docs for more info. Your error code is: UNKNOWN-USERINFO-OAUTH"}) + return + } + } + + var scopesJSON []interface{} + err = json.Unmarshal([]byte(scopes), &scopesJSON) + if err != nil { + log.Println("[ERROR] Unknown in /userinfo scopes Unmarshal():", err) + } + + openid := false + for _, scopeInterface := range scopesJSON { + scope, ok := scopeInterface.(string) + if !ok { + c.JSON(400, gin.H{"error": "Invalid scope"}) + return + } + if scope == "openid" { + openid = true + } + } + + if !openid { + c.JSON(403, gin.H{"error": "Token does not have openid scope"}) + return + } + _, userid, err := getSession(session) if err != nil { c.JSON(401, gin.H{"error": "Invalid session"}) @@ -728,7 +1078,7 @@ func main() { c.JSON(400, gin.H{"error": "User does not exist"}) return } else if err != nil { - log.Println("[ERROR] Unknown in /userinfo getUser() at", strconv.FormatInt(time.Now().Unix(), 10)+":", err) + log.Println("[ERROR] Unknown in /userinfo getUser():", err) c.JSON(500, gin.H{"error": "Something went wrong on our end. Please report this bug at https://concord.hectabit.org/hectabit/burgerauth and refer to the docs for more info. Your error code is: UNKNOWN-USERINFO-GETUSER"}) return } @@ -751,13 +1101,13 @@ func main() { } var blacklisted bool - err = conn.QueryRow("SELECT blacklisted FROM blacklist WHERE token = ? LIMIT 1", token).Scan(&blacklisted) + err = mem.QueryRow("SELECT blacklisted FROM blacklist WHERE token = ? LIMIT 1", token).Scan(&blacklisted) if err == nil { c.JSON(400, gin.H{"error": "Token is in blacklist"}) return } else { if !errors.Is(err, sql.ErrNoRows) { - log.Println("[ERROR] Unknown in /api/sub blacklist at", strconv.FormatInt(time.Now().Unix(), 10)+":", err) + log.Println("[ERROR] Unknown in /api/sub blacklist:", err) c.JSON(500, gin.H{"error": "Something went wrong on our end. Please report this bug at https://concord.hectabit.org/hectabit/burgerauth and refer to the docs for more info. Your error code is: UNKNOWN-API-UNIQUEID-BLACKLIST"}) return } @@ -799,7 +1149,7 @@ func main() { c.JSON(400, gin.H{"error": "User does not exist"}) return } else if err != nil { - log.Println("[ERROR] Unknown in /api/userinfo getUser() at", strconv.FormatInt(time.Now().Unix(), 10)+":", err) + log.Println("[ERROR] Unknown in /api/userinfo getUser():", err) c.JSON(500, gin.H{"error": "Something went wrong on our end. Please report this bug at https://concord.hectabit.org/hectabit/burgerauth and refer to the docs for more info. Your error code is: UNKNOWN-API-UNIQUEID-GETUSER"}) return } @@ -821,13 +1171,13 @@ func main() { return } var blacklisted bool - err = conn.QueryRow("SELECT blacklisted FROM blacklist WHERE token = ? LIMIT 1", token).Scan(&blacklisted) + err = mem.QueryRow("SELECT blacklisted FROM blacklist WHERE token = ? LIMIT 1", token).Scan(&blacklisted) if err == nil { c.JSON(400, gin.H{"error": "Token is in blacklist"}) return } else { if !errors.Is(err, sql.ErrNoRows) { - log.Println("[ERROR] Unknown in /api/loggedin blacklist at", strconv.FormatInt(time.Now().Unix(), 10)+":", err) + log.Println("[ERROR] Unknown in /api/loggedin blacklist:", err) c.JSON(500, gin.H{"error": "Something went wrong on our end. Please report this bug at https://concord.hectabit.org/hectabit/burgerauth and refer to the docs for more info. Your error code is: UNKNOWN-API-LOGGEDIN-BLACKLIST"}) return } @@ -867,8 +1217,107 @@ func main() { c.JSON(200, gin.H{"appId": claims["aud"]}) }) + router.POST("/api/aeskeyshare", func(c *gin.Context) { + var data map[string]interface{} + err := c.ShouldBindJSON(&data) + if err != nil { + c.JSON(400, gin.H{"error": "Invalid JSON"}) + return + } + + token, ok := data["access_token"].(string) + if !ok { + c.JSON(400, gin.H{"error": "Invalid JSON"}) + return + } + var blacklisted bool + err = mem.QueryRow("SELECT blacklisted FROM blacklist WHERE token = ? LIMIT 1", token).Scan(&blacklisted) + if err == nil { + c.JSON(400, gin.H{"error": "Token is in blacklist"}) + return + } else { + if !errors.Is(err, sql.ErrNoRows) { + log.Println("[ERROR] Unknown in /api/loggedin blacklist:", err) + c.JSON(500, gin.H{"error": "Something went wrong on our end. Please report this bug at https://concord.hectabit.org/hectabit/burgerauth and refer to the docs for more info. Your error code is: UNKNOWN-API-LOGGEDIN-BLACKLIST"}) + return + } + } + + parsedToken, err := jwt.Parse(token, func(token *jwt.Token) (interface{}, error) { + return publicKey, nil + }) + + if err != nil { + c.JSON(401, gin.H{"error": "Malformed token"}) + return + } + + var claims jwt.MapClaims + if parsedToken.Valid { + claims, ok = parsedToken.Claims.(jwt.MapClaims) + if !ok { + c.JSON(401, gin.H{"error": "Invalid token claims"}) + return + } + } + + session := claims["session"].(string) + exp := claims["exp"].(float64) + if int64(exp) < time.Now().Unix() { + c.JSON(403, gin.H{"error": "Expired token"}) + return + } + + var keyShareUri, scopes string + err = conn.QueryRow("SELECT scopes, keyShareUri FROM oauth WHERE appId = ? LIMIT 1", claims["aud"]).Scan(&scopes, &keyShareUri) + if err != nil { + if errors.Is(err, sql.ErrNoRows) { + c.JSON(401, gin.H{"error": "OAuth screening failed"}) + } else { + log.Println("[ERROR] Unknown in /api/aeskeyshare:", err) + c.JSON(500, gin.H{"error": "Something went wrong on our end. Please report this bug at https://concord.hectabit.org/hectabit/burgerauth and refer to the docs for more info. Your error code is: UNKNOWN-API-AESKEYSHARE-SELECT"}) + } + return + } + + var scopesJson []interface{} + err = json.Unmarshal([]byte(scopes), &scopesJson) + if err != nil { + log.Println("[ERROR] Unknown in /api/aeskeyshare scopesJson Unmarshal():", err) + c.JSON(500, gin.H{"error": "Something went wrong on our end. Please report this bug at https://concord.hectabit.org/hectabit/burgerauth and refer to the docs for more info. Your error code is: UNKNOWN-API-AESKEYSHARE-SCOPE"}) + return + } + + var aesKeyShare bool + for _, scopeInterface := range scopesJson { + scope, ok := scopeInterface.(string) + if !ok { + c.JSON(400, gin.H{"error": "Invalid scope"}) + return + } + if scope == "aeskeyshare" { + aesKeyShare = true + } + } + + if !aesKeyShare { + c.JSON(403, gin.H{"error": "Token does not have aeskeyshare scope"}) + return + } else if keyShareUri == "none" { + c.JSON(400, gin.H{"error": "No key share URI"}) + return + } + + _, _, err = getSession(session) + if err != nil { + c.JSON(401, gin.H{"error": "Invalid session"}) + return + } + + c.JSON(200, gin.H{"appId": claims["aud"], "keyShareUri": keyShareUri}) + }) + router.GET("/api/auth", func(c *gin.Context) { - secretKey, _ := c.Cookie("key") appId := c.Request.URL.Query().Get("client_id") code := c.Request.URL.Query().Get("code_challenge") codeMethod := c.Request.URL.Query().Get("code_challenge_method") @@ -876,22 +1325,54 @@ func main() { state := c.Request.URL.Query().Get("state") nonce := c.Request.URL.Query().Get("nonce") deny := c.Request.URL.Query().Get("deny") + sessionKey, err := c.Cookie("secretKey") + if err == nil { + if errors.Is(err, http.ErrNoCookie) || sessionKey == "" { + sessionKey = c.Request.URL.Query().Get("session") + if sessionKey == "" { + c.String(400, "Invalid session") + return + } + } else { + c.String(400, "Invalid session") + return + } + } - var appIdCheck, redirectUriCheck string + var appIdCheck, redirectUriCheck, scopes string - err := conn.QueryRow("SELECT appId, rdiruri FROM oauth WHERE appId = ? LIMIT 1", appId).Scan(&appIdCheck, &redirectUriCheck) + err = conn.QueryRow("SELECT scopes, appId, redirectUri FROM oauth WHERE appId = ? LIMIT 1", appId).Scan(&scopes, &appIdCheck, &redirectUriCheck) if err != nil { if errors.Is(err, sql.ErrNoRows) { fmt.Println(appId) c.String(401, "OAuth screening failed") } else { - log.Println("[ERROR] Unknown in /api/auth at", strconv.FormatInt(time.Now().Unix(), 10)+":", err) + log.Println("[ERROR] Unknown in /api/auth:", err) c.String(500, "Something went wrong on our end. Please report this bug at https://concord.hectabit.org/hectabit/burgerauth and refer to the docs for more info. Your error code is: UNKNOWN-API-AUTH-SELECT") } return } - if !(redirectUriCheck == redirectUri) { + var scopesJson []interface{} + err = json.Unmarshal([]byte(scopes), &scopesJson) + if err != nil { + log.Println("[ERROR] Unknown in /api/auth scopesJson Unmarshal():", err) + c.String(500, "Something went wrong on our end. Please report this bug at https://concord.hectabit.org/hectabit/burgerauth and refer to the docs for more info. Your error code is: UNKNOWN-API-AUTH-SCOPE") + return + } + + var openid bool + for _, scopeInterface := range scopesJson { + scope, ok := scopeInterface.(string) + if !ok { + c.String(400, "Invalid scope") + } + if scope == "openid" { + openid = true + } + } + + if !(ensureTrailingSlash(redirectUriCheck) == ensureTrailingSlash(redirectUri)) { c.String(401, "Redirect URI does not match") return } @@ -908,15 +1389,15 @@ func main() { } if nonce == "none" { - nonce, err = genSalt(512) + nonce, err = randomChars(512) if err != nil { - log.Println("[ERROR] Unknown in /api/auth nonce genSalt() at", strconv.FormatInt(time.Now().Unix(), 10)+":", err) + log.Println("[ERROR] Unknown in /api/auth nonce randomChars():", err) c.String(500, "Something went wrong on our end. Please report this bug at https://concord.hectabit.org/hectabit/burgerauth and refer to the docs for more info. Your error code is: UNKNOWN-API-AUTH-NONCE") return } } - _, userid, err := getSession(secretKey) + _, userid, err := getSession(sessionKey) if err != nil { c.String(401, "Invalid session") return @@ -927,66 +1408,69 @@ func main() { c.String(400, "User does not exist") return } else if err != nil { - log.Println("[ERROR] Unknown in /api/userinfo getUser() at", strconv.FormatInt(time.Now().Unix(), 10)+":", err) + log.Println("[ERROR] Unknown in /api/userinfo getUser():", err) c.String(500, "Something went wrong on our end. Please report this bug at https://concord.hectabit.org/hectabit/burgerauth and refer to the docs for more info. Your error code is: UNKNOWN-API-AUTH-GETUSER") return } - dataTemplate := jwt.MapClaims{ - "sub": sub[:255], - "iss": hostName, - "name": username, - "aud": appId, - "exp": time.Now().Unix() + 2592000, - "iat": time.Now().Unix(), - "auth_time": time.Now().Unix(), - "session": secretKey, - "nonce": nonce, + jwtToken := "none" + if openid { + dataTemplate := jwt.MapClaims{ + "sub": sub[:255], + "iss": hostName, + "name": username, + "aud": appId, + "exp": time.Now().Unix() + 2592000, + "iat": time.Now().Unix(), + "auth_time": time.Now().Unix(), + "session": sessionKey, + "nonce": nonce, + } + tokenTemp := jwt.NewWithClaims(jwt.SigningMethodRS256, dataTemplate) + tokenTemp.Header["kid"] = "burgerauth" + jwtToken, err = tokenTemp.SignedString(privateKey) + if err != nil { + log.Println("[ERROR] Unknown in /api/auth jwt_token:", err) + c.String(500, "Something went wrong on our end. Please report this bug at https://concord.hectabit.org/hectabit/burgerauth and refer to the docs for more info. Your error code is: UNKNOWN-API-AUTH-JWTCANNOTSIGN") + return + } } - secondNonce, err := genSalt(512) + secondNonce, err := randomChars(512) dataTemplateTwo := jwt.MapClaims{ "exp": time.Now().Unix() + 2592000, "iat": time.Now().Unix(), - "session": secretKey, + "session": sessionKey, "nonce": secondNonce, - } - - tokenTemp := jwt.NewWithClaims(jwt.SigningMethodRS256, dataTemplate) - tokenTemp.Header["kid"] = "burgerauth" - jwtToken, err := tokenTemp.SignedString(privateKey) - if err != nil { - log.Println("[ERROR] Unknown in /api/auth jwt_token at", strconv.FormatInt(time.Now().Unix(), 10)+":", err) - c.String(500, "Something went wrong on our end. Please report this bug at https://concord.hectabit.org/hectabit/burgerauth and refer to the docs for more info. Your error code is: UNKNOWN-API-AUTH-JWTCANNOTSIGN") - return + "aud": appId, } secretTemp := jwt.NewWithClaims(jwt.SigningMethodRS256, dataTemplateTwo) secretTemp.Header["kid"] = "burgerauth" secretToken, err := secretTemp.SignedString(privateKey) if err != nil { - log.Println("[ERROR] Unknown in /api/auth secret_token at", strconv.FormatInt(time.Now().Unix(), 10)+":", err) + log.Println("[ERROR] Unknown in /api/auth secret_token:", err) c.String(500, "Something went wrong on our end. Please report this bug at https://concord.hectabit.org/hectabit/burgerauth and refer to the docs for more info. Your error code is: UNKNOWN-API-AUTH-JWTCANNOTSIGN.") return } - randomBytes, err := genSalt(512) + randomBytes, err := randomChars(512) if err != nil { - log.Println("[ERROR] Unknown in /api/auth randomBytes at", strconv.FormatInt(time.Now().Unix(), 10)+":", err) + log.Println("[ERROR] Unknown in /api/auth randomBytes:", err) c.String(500, "Something went wrong on our end. Please report this bug at https://concord.hectabit.org/hectabit/burgerauth and refer to the docs for more info. Your error code is: UNKNOWN-API-AUTH-RANDOMBYTES.") return } _, err = mem.Exec("DELETE FROM logins WHERE creator = ?", userid) if err != nil { - log.Println("[ERROR] Unknown in /api/auth delete at", strconv.FormatInt(time.Now().Unix(), 10)+":", err) + log.Println("[ERROR] Unknown in /api/auth delete:", err) c.String(500, "Something went wrong on our end. Please report this bug at https://concord.hectabit.org/hectabit/burgerauth and refer to the docs for more info. Your error code is: UNKNOWN-API-AUTH-DELETE.") return } _, err = mem.Exec("INSERT INTO logins (appId, exchangeCode, loginToken, creator, openid, pkce, pkcemethod) VALUES (?, ?, ?, ?, ?, ?, ?)", appId, randomBytes, secretToken, userid, jwtToken, code, codeMethod) if err != nil { - log.Println("[ERROR] Unknown in /api/auth insert at", strconv.FormatInt(time.Now().Unix(), 10)+":", err) + log.Println("[ERROR] Unknown in /api/auth insert:", err) c.String(500, "Something went wrong on our end. Please report this bug at https://concord.hectabit.org/hectabit/burgerauth and refer to the docs for more info. Your error code is: UNKNOWN-API-AUTH-INSERT.") return } @@ -995,7 +1479,7 @@ func main() { c.Redirect(302, redirectUri+"?code="+randomBytes+"&state="+state) } else { c.String(500, "Something went wrong on our end. Please report this bug at https://concord.hectabit.org/hectabit/burgerauth and refer to the docs for more info. Your error code is: UNKNOWN-API-AUTH-REDIRECT.") - log.Println("[ERROR] Secret key not found at", strconv.FormatInt(time.Now().Unix(), 10)) + log.Println("[ERROR] Secret key not found") } }) @@ -1026,7 +1510,7 @@ func main() { if errors.Is(err, sql.ErrNoRows) { c.JSON(401, gin.H{"error": "OAuth screening failed"}) } else { - log.Println("[ERROR] Unknown in /api/tokenauth at", strconv.FormatInt(time.Now().Unix(), 10)+":", err) + log.Println("[ERROR] Unknown in /api/tokenauth:", err) c.JSON(500, gin.H{"error": "Something went wrong on our end. Please report this bug at https://concord.hectabit.org/hectabit/burgerauth and refer to the docs for more info. Your error code is: UNKNOWN-API-TOKENAUTH-SELECT"}) } return @@ -1037,7 +1521,7 @@ func main() { if errors.Is(err, sql.ErrNoRows) { c.JSON(401, gin.H{"error": "OAuth screening failed"}) } else { - log.Println("[ERROR] Unknown in /api/tokenauth memory query at", strconv.FormatInt(time.Now().Unix(), 10)+":", err) + log.Println("[ERROR] Unknown in /api/tokenauth memory query:", err) c.JSON(500, gin.H{"error": "Something went wrong on our end. Please report this bug at https://concord.hectabit.org/hectabit/burgerauth and refer to the docs for more info. Your error code is: UNKNOWN-API-TOKENAUTH-MEMSELECT"}) } return @@ -1076,12 +1560,16 @@ func main() { _, err = mem.Exec("DELETE FROM logins WHERE loginToken = ?", loginCode) if err != nil { - log.Println("[ERROR] Unknown in /api/tokenauth delete at", strconv.FormatInt(time.Now().Unix(), 10)+":", err) + log.Println("[ERROR] Unknown in /api/tokenauth delete:", err) c.JSON(500, gin.H{"error": "Something went wrong on our end. Please report this bug at https://concord.hectabit.org/hectabit/burgerauth and refer to the docs for more info. Your error code is: UNKNOWN-API-TOKENAUTH-DELETE"}) return } - c.JSON(200, gin.H{"access_token": loginCode, "token_type": "bearer", "expires_in": 2592000, "id_token": openid}) + if openid != "none" { + c.JSON(200, gin.H{"access_token": loginCode, "token_type": "bearer", "expires_in": 2592000, "id_token": openid}) + } else { + c.JSON(200, gin.H{"access_token": loginCode, "token_type": "bearer", "expires_in": 2592000}) + } }) router.POST("/api/deleteauth", func(c *gin.Context) { @@ -1114,7 +1602,7 @@ func main() { if errors.Is(err, sql.ErrNoRows) { c.JSON(400, gin.H{"error": "AppID Not found"}) } else { - log.Println("[ERROR] Unknown in /api/deleteauth at", strconv.FormatInt(time.Now().Unix(), 10)+":", err) + log.Println("[ERROR] Unknown in /api/deleteauth:", err) c.JSON(500, gin.H{"error": "Something went wrong on our end. Please report this bug at https://concord.hectabit.org/hectabit/burgerauth and refer to the docs for more info. Your error code is: UNKNOWN-API-DELETEAUTH-DELETE"}) } } else { @@ -1132,17 +1620,22 @@ func main() { secretKey, ok := data["secretKey"].(string) if !ok { - c.JSON(400, gin.H{"error": "Invalid JSON"}) + c.JSON(400, gin.H{"error": "Invalid JSON (token missing)"}) return } name, ok := data["name"].(string) if !ok { - c.JSON(400, gin.H{"error": "Invalid JSON"}) + c.JSON(400, gin.H{"error": "Invalid JSON (name missing)"}) return } redirectUri, ok := data["redirectUri"].(string) if !ok { - c.JSON(400, gin.H{"error": "Invalid JSON"}) + c.JSON(400, gin.H{"error": "Invalid JSON (redirectUri missing)"}) + return + } + scopes, ok := data["scopes"].(string) + if !ok { + c.JSON(400, gin.H{"error": "Invalid JSON (scopes missing)"}) return } @@ -1153,9 +1646,9 @@ func main() { } var testsecret, testappid string - secret, err := genSalt(512) + secret, err := randomChars(512) if err != nil { - log.Println("[ERROR] Unknown in /api/newauth secretgen at", strconv.FormatInt(time.Now().Unix(), 10)+":", err) + log.Println("[ERROR] Unknown in /api/newauth secretgen:", err) c.JSON(500, gin.H{"error": "Something went wrong on our end. Please report this bug at https://concord.hectabit.org/hectabit/burgerauth and refer to the docs for more info. Your error code is: UNKNOWN-API-NEWAUTH-SECRETGEN"}) return } @@ -1165,23 +1658,23 @@ func main() { if errors.Is(err, sql.ErrNoRows) { break } else { - log.Println("[ERROR] Unknown in /api/newauth secretselect at", strconv.FormatInt(time.Now().Unix(), 10)+":", err) + log.Println("[ERROR] Unknown in /api/newauth secretselect:", err) c.JSON(500, gin.H{"error": "Something went wrong on our end. Please report this bug at https://concord.hectabit.org/hectabit/burgerauth and refer to the docs for more info. Your error code is: UNKNOWN-API-NEWAUTH-SECRETSELECT"}) return } } else { - secret, err = genSalt(512) + secret, err = randomChars(512) if err != nil { - log.Println("[ERROR] Unknown in /api/newauth secretgen at", strconv.FormatInt(time.Now().Unix(), 10)+":", err) + log.Println("[ERROR] Unknown in /api/newauth secretgen:", err) c.JSON(500, gin.H{"error": "Something went wrong on our end. Please report this bug at https://concord.hectabit.org/hectabit/burgerauth and refer to the docs for more info. Your error code is: UNKNOWN-API-NEWAUTH-SECRETGEN"}) return } } } - appId, err := genSalt(32) + appId, err := randomChars(32) if err != nil { - log.Println("[ERROR] Unknown in /api/newauth appidgen at", strconv.FormatInt(time.Now().Unix(), 10)+":", err) + log.Println("[ERROR] Unknown in /api/newauth appidgen:", err) c.JSON(500, gin.H{"error": "Something went wrong on our end. Please report this bug at https://concord.hectabit.org/hectabit/burgerauth and refer to the docs for more info. Your error code is: UNKNOWN-API-NEWAUTH-APPIDGEN"}) return } @@ -1193,23 +1686,60 @@ func main() { log.Println("[Info] New Oauth source added with ID:", appId) break } else { - log.Println("[ERROR] Unknown in /api/newauth appidcheck at", strconv.FormatInt(time.Now().Unix(), 10)+":", err) + log.Println("[ERROR] Unknown in /api/newauth appidcheck:", err) c.JSON(500, gin.H{"error": "Something went wrong on our end. Please report this bug at https://concord.hectabit.org/hectabit/burgerauth and refer to the docs for more info. Your error code is: UNKNOWN-API-NEWAUTH-APPIDCHECK"}) return } } else { - appId, err = genSalt(32) + appId, err = randomChars(32) if err != nil { - log.Println("[ERROR] Unknown in /api/newauth appidgen at", strconv.FormatInt(time.Now().Unix(), 10)+":", err) + log.Println("[ERROR] Unknown in /api/newauth appidgen:", err) c.JSON(500, gin.H{"error": "Something went wrong on our end. Please report this bug at https://concord.hectabit.org/hectabit/burgerauth and refer to the docs for more info. Your error code is: UNKNOWN-API-NEWAUTH-LAPPIDGEN"}) return } } } - _, err = conn.Exec("INSERT INTO oauth (name, appId, creator, secret, redirectUri) VALUES (?, ?, ?, ?, ?)", name, appId, id, secret, redirectUri) + var scopeJson []interface{} + err = json.Unmarshal([]byte(scopes), &scopeJson) if err != nil { - log.Println("[ERROR] Unknown in /api/newauth insert at", strconv.FormatInt(time.Now().Unix(), 10)+":", err) + c.JSON(400, gin.H{"error": "Invalid JSON (scope parsing)"}) + return + } + + var aeskeyshare bool + for _, scopeInterface := range scopeJson { + scope, ok := scopeInterface.(string) + if !ok { + c.JSON(400, gin.H{"error": "Invalid JSON (scope interface)"}) + return + } + if scope != "openid" && scope != "aeskeyshare" { + c.JSON(400, gin.H{"error": "Invalid Scope: " + scope}) + return + } else { + if scope == "aeskeyshare" { + aeskeyshare = true + } else if scope != "openid" { + log.Println("[CRITICAL] An impossible logic error has occurred in /api/newauth. Please check if the laws of physics still apply, and if so, please move your computer to a location with less radiation, such as a lead nuclear bunker.") + c.JSON(503, gin.H{"error": "The server is unable to handle this request until it is no longer exposed to radiation"}) + return + } + } + } + + if !aeskeyshare { + _, err = conn.Exec("INSERT INTO oauth (name, appId, creator, secret, redirectUri, scopes) VALUES (?, ?, ?, ?, ?, ?)", name, appId, id, secret, redirectUri, scopes) + } else { + keyShareUri, ok := data["keyShareUri"].(string) + if !ok { + c.JSON(400, gin.H{"error": "Invalid JSON (keyShareUri)"}) + return + } + _, err = conn.Exec("INSERT INTO oauth (name, appId, creator, secret, redirectUri, scopes, keyShareUri) VALUES (?, ?, ?, ?, ?, ?, ?)", name, appId, id, secret, redirectUri, scopes, keyShareUri) + } + if err != nil { + log.Println("[ERROR] Unknown in /api/newauth insert:", err) c.JSON(500, gin.H{"error": "Something went wrong on our end. Please report this bug at https://concord.hectabit.org/hectabit/burgerauth and refer to the docs for more info. Your error code is: UNKNOWN-API-NEWAUTH-INSERT"}) return } @@ -1237,15 +1767,16 @@ func main() { return } - rows, err := conn.Query("SELECT appId, name, rdiruri FROM oauth WHERE creator = ? ORDER BY creator DESC", id) + rows, err := conn.Query("SELECT keyShareUri, scopes, appId, name, redirectUri FROM oauth WHERE creator = ? ORDER BY creator DESC", id) if err != nil { + log.Println("[ERROR] Unknown in /api/listauth query:", err) c.JSON(500, gin.H{"error": "Something went wrong on our end. Please report this bug at https://concord.hectabit.org/hectabit/burgerauth and refer to the docs for more info. Your error code is: UNKNOWN-API-LISTAUTH-QUERY"}) return } defer func(rows *sql.Rows) { err := rows.Close() if err != nil { - log.Println("[ERROR] Unknown in /api/listauth rows close at", strconv.FormatInt(time.Now().Unix(), 10)+":", err) + log.Println("[ERROR] Unknown in /api/listauth rows close:", err) c.JSON(500, gin.H{"error": "Something went wrong on our end. Please report this bug at https://concord.hectabit.org/hectabit/burgerauth and refer to the docs for more info. Your error code is: UNKNOWN-API-LISTAUTH-ROWSCLOSE"}) return } @@ -1253,12 +1784,12 @@ func main() { var dataTemplate []map[string]interface{} for rows.Next() { - var appId, name, redirectUri string - if err := rows.Scan(&appId, &name, &redirectUri); err != nil { + var appId, name, redirectUri, scopes, keyShareUri string + if err := rows.Scan(&keyShareUri, &scopes, &appId, &name, &redirectUri); err != nil { c.JSON(500, gin.H{"error": "Something went wrong on our end. Please report this bug at https://concord.hectabit.org/hectabit/burgerauth and refer to the docs for more info. Your error code is: UNKNOWN-API-LISTAUTH-SCAN"}) return } - template := map[string]interface{}{"appId": appId, "name": name, "redirectUri": redirectUri} + template := map[string]interface{}{"appId": appId, "name": name, "redirectUri": redirectUri, "scopes": scopes, "keyShareUri": keyShareUri} dataTemplate = append(dataTemplate, template) } if err := rows.Err(); err != nil { @@ -1292,7 +1823,7 @@ func main() { _, err = conn.Exec("DELETE FROM userdata WHERE creator = ?", id) if err != nil { if !errors.Is(err, sql.ErrNoRows) { - log.Println("[ERROR] Unknown in /api/deleteaccount userdata at", strconv.FormatInt(time.Now().Unix(), 10)+":", err) + log.Println("[ERROR] Unknown in /api/deleteaccount userdata:", err) c.JSON(500, gin.H{"error": "Something went wrong on our end. Please report this bug at https://concord.hectabit.org/hectabit/burgerauth and refer to the docs for more info. Your error code is: UNKNOWN-API-DELETEACCT-USERDATA"}) return } @@ -1301,7 +1832,7 @@ func main() { _, err = mem.Exec("DELETE FROM logins WHERE creator = ?", id) if err != nil { if !errors.Is(err, sql.ErrNoRows) { - log.Println("[ERROR] Unknown in /api/deleteaccount logins at", strconv.FormatInt(time.Now().Unix(), 10)+":", err) + log.Println("[ERROR] Unknown in /api/deleteaccount logins:", err) c.JSON(500, gin.H{"error": "Something went wrong on our end. Please report this bug at https://concord.hectabit.org/hectabit/burgerauth and refer to the docs for more info. Your error code is: UNKNOWN-API-DELETEACCT-LOGINS"}) return } @@ -1310,7 +1841,7 @@ func main() { _, err = conn.Exec("DELETE FROM oauth WHERE creator = ?", id) if err != nil { if !errors.Is(err, sql.ErrNoRows) { - log.Println("[ERROR] Unknown in /api/deleteuser oauth at", strconv.FormatInt(time.Now().Unix(), 10)+":", err) + log.Println("[ERROR] Unknown in /api/deleteuser oauth:", err) c.JSON(500, gin.H{"error": "Something went wrong on our end. Please report this bug at https://concord.hectabit.org/hectabit/burgerauth and refer to the docs for more info. Your error code is: UNKNOWN-API-DELETEUSER-OAUTH"}) return } @@ -1319,7 +1850,7 @@ func main() { _, err = conn.Exec("DELETE FROM users WHERE id = ?", id) if err != nil { if !errors.Is(err, sql.ErrNoRows) { - log.Println("[ERROR] Unknown in /api/deleteuser logins at", strconv.FormatInt(time.Now().Unix(), 10)+":", err) + log.Println("[ERROR] Unknown in /api/deleteuser logins:", err) c.JSON(500, gin.H{"error": "Something went wrong on our end. Please report this bug at https://concord.hectabit.org/hectabit/burgerauth and refer to the docs for more info. Your error code is: UNKNOWN-API-DELETEUSER-USERS"}) return } @@ -1348,10 +1879,10 @@ func main() { return } - rows, err := conn.Query("SELECT sessionid, session, device FROM sessions WHERE id = ? ORDER BY id DESC", id) + rows, err := mem.Query("SELECT sessionid, session, device FROM sessions WHERE id = ? ORDER BY id DESC", id) if err != nil { if !errors.Is(err, sql.ErrNoRows) { - log.Println("[ERROR] Unknown in /api/sessions/list at", strconv.FormatInt(time.Now().Unix(), 10)+":", err) + log.Println("[ERROR] Unknown in /api/sessions/list:", err) c.JSON(500, gin.H{"error": "Something went wrong on our end. Please report this bug at https://concord.hectabit.org/hectabit/burgerauth and refer to the docs for more info. Your error code is: UNKNOWN-API-SESSIONS-LIST"}) return } @@ -1359,7 +1890,7 @@ func main() { defer func(rows *sql.Rows) { err := rows.Close() if err != nil { - log.Println("[ERROR] Unknown in /api/sessions/list rows close at", strconv.FormatInt(time.Now().Unix(), 10)+":", err) + log.Println("[ERROR] Unknown in /api/sessions/list rows close:", err) c.JSON(500, gin.H{"error": "Something went wrong on our end. Please report this bug at https://concord.hectabit.org/hectabit/burgerauth and refer to the docs for more info. Your error code is: UNKNOWN-API-SESSIONS-LIST-ROWSCLOSE"}) return } @@ -1412,12 +1943,12 @@ func main() { return } - _, err = conn.Exec("DELETE FROM sessions WHERE sessionid = ? AND id = ?", sessionId, id) + _, err = mem.Exec("DELETE FROM sessions WHERE sessionid = ? AND id = ?", sessionId, id) if err != nil { if errors.Is(err, sql.ErrNoRows) { c.JSON(422, gin.H{"error": "SessionID Not found"}) } else { - log.Println("[ERROR] Unknown in /api/sessions/remove at", strconv.FormatInt(time.Now().Unix(), 10)+":", err) + log.Println("[ERROR] Unknown in /api/sessions/remove:", err) c.JSON(500, gin.H{"error": "Something went wrong on our end. Please report this bug at https://concord.hectabit.org/hectabit/burgerauth and refer to the docs for more info. Your error code is: UNKNOWN-API-SESSIONS-REMOVE"}) } } else { @@ -1439,11 +1970,11 @@ func main() { return } - if masterKey == SecretKey { + if masterKey == masterKey { rows, err := conn.Query("SELECT * FROM users ORDER BY id DESC") if err != nil { if !errors.Is(err, sql.ErrNoRows) { - log.Println("[ERROR] Unknown in /api/listusers at", strconv.FormatInt(time.Now().Unix(), 10)+":", err) + log.Println("[ERROR] Unknown in /api/listusers:", err) c.JSON(500, gin.H{"error": "Something went wrong on our end. Please report this bug at https://concord.hectabit.org/hectabit/burgerauth and refer to the docs for more info. Your error code is: UNKNOWN-API-LISTUSERS-QUERY"}) return } @@ -1451,7 +1982,7 @@ func main() { defer func(rows *sql.Rows) { err := rows.Close() if err != nil { - log.Println("[ERROR] Unknown in /api/listusers rows close at", strconv.FormatInt(time.Now().Unix(), 10)+":", err) + log.Println("[ERROR] Unknown in /api/listusers rows close:", err) c.JSON(500, gin.H{"error": "Something went wrong on our end. Please report this bug at https://concord.hectabit.org/hectabit/burgerauth and refer to the docs for more info. Your error code is: UNKNOWN-API-LISTUSERS-ROWSCLOSE"}) return } @@ -1479,14 +2010,14 @@ func main() { router.GET("/.well-known/jwks.json", func(c *gin.Context) { mod, err := BigIntToBase64URL(modulus) if err != nil { - log.Println("[ERROR] Unknown in /well-known/jwks.json modulus at", strconv.FormatInt(time.Now().Unix(), 10)+":", err) + log.Println("[ERROR] Unknown in /well-known/jwks.json modulus:", err) c.JSON(500, gin.H{"error": "Something went wrong on our end. Please report this bug at https://concord.hectabit.org/hectabit/burgerauth and refer to the docs for more info. Your error code is: UNKNOWN-JWKS-MODULUS"}) return } exp, err := Int64ToBase64URL(int64(exponent)) if err != nil { - log.Println("[ERROR] Unknown in /well-known/jwks.json exponent at", strconv.FormatInt(time.Now().Unix(), 10)+":", err) + log.Println("[ERROR] Unknown in /well-known/jwks.json exponent:", err) c.JSON(500, gin.H{"error": "Something went wrong on our end. Please report this bug at https://concord.hectabit.org/hectabit/burgerauth and refer to the docs for more info. Your error code is: UNKNOWN-JWKS-EXPONENT"}) return } @@ -1496,7 +2027,7 @@ func main() { "kty": "RSA", "alg": "RS256", "use": "sig", - "kid": keyid, + "kid": keyIdentifier, "n": mod, "e": exp, }, @@ -1506,10 +2037,29 @@ func main() { c.JSON(200, keys) }) - log.Println("[INFO] Server started at", time.Now().Unix()) - log.Println("[INFO] Welcome to Burgerauth! Today we are running on IP " + Host + " on port " + strconv.Itoa(Port) + ".") - err = router.Run(Host + ":" + strconv.Itoa(Port)) + go func() { + for { + time.Sleep(time.Minute) + var count int + err := mem.QueryRow("SELECT COUNT(*) FROM spent").Scan(&count) + affected, err := mem.Exec("DELETE FROM spent WHERE expires < ?", time.Now().Unix()) + if err != nil { + log.Println("[ERROR] Unknown in spent cleanup Exec():", err) + } else { + affectedRows, err := affected.RowsAffected() + if err != nil { + log.Println("[ERROR] Unknown in spent cleanup RowsAffected():", err) + } else { + log.Println("[INFO] Spent cleanup complete, deleted " + strconv.FormatInt(affectedRows, 10) + " row(s), " + strconv.Itoa(count) + " row(s) remaining.") + } + } + } + }() + + log.Println("[INFO] Server started") + log.Println("[INFO] Welcome to Burgerauth! Today we are running on IP " + host + " on port " + strconv.Itoa(port) + ".") + err = router.Run(host + ":" + strconv.Itoa(port)) if err != nil { - log.Fatalln("[FATAL] Server failed to begin operations at", time.Now().Unix(), err) + log.Fatalln("[FATAL] Server failed to begin operations") } } diff --git a/schema.sql b/schema.sql index 1305f38..5b95e75 100644 --- a/schema.sql +++ b/schema.sql @@ -9,7 +9,8 @@ CREATE TABLE users ( created TEXT NOT NULL, username TEXT NOT NULL, password TEXT NOT NULL, - uniqueid TEXT NOT NULL + uniqueid TEXT NOT NULL, + migrated INTEGER NOT NULL DEFAULT 0 ); CREATE TABLE userdata ( @@ -32,9 +33,11 @@ CREATE TABLE blacklist ( ); CREATE TABLE oauth ( - appId TEXT NOT NULL, + appId TEXT NOT NULL UNIQUE, secret TEXT NOT NULL, creator INTEGER NOT NULL, - rdiruri TEXT NOT NULL, - name TEXT NOT NULL + redirectUri TEXT NOT NULL, + name TEXT NOT NULL, + keyShareUri TEXT NOT NULL DEFAULT 'none', + scopes TEXT NOT NULL DEFAULT '["openid"]' ) diff --git a/static/css/style.css b/static/css/style.css index 314cd42..07e8cc8 100644 --- a/static/css/style.css +++ b/static/css/style.css @@ -4,12 +4,13 @@ --invertdm: 0%; --text-color: #000000; --editor: #ffffff; - --bar: #f4f4f4; --border-color: #dadada; - --theme-color: #157efb; + --theme-color: #1c71d8; --hover-theme-color: #4990e7; - --nonimporant-theme-color: #4A4A4A; - --hover-nonimportant-theme-color: #595959; + --nonimporant-theme-color: #EBEBEB; + --hover-nonimportant-theme-color: #dbdbdb; + --nonimportant-text-color: #000; + --inoutdiv: #fafafa; } /* dark mode */ @@ -17,9 +18,10 @@ @media (prefers-color-scheme: dark) { :root { --invertdm: 100%; - --bar: #2d2f31; + --inoutdiv: #2d2f31; --text-color: #ffffff; --editor: #1E1E1E; + --nonimportant-text-color: #fff; --border-color: #393b3d; } @@ -49,6 +51,10 @@ h6 { white-space: break-spaces; } +p#status { + overflow-wrap: break-word; +} + body { margin: 0; background-color: var(--editor); @@ -62,13 +68,18 @@ body { margin: 10%; padding: 30px; border: solid 1px var(--border-color); - background-color: var(--bar); + background-color: var(--inoutdiv); +} + +table { + border-spacing: 0; } input { - width: calc(100% - 120px); + width: calc(100% - 35px); + margin-left: 10px; + margin-right: 10px; height: 30px; - margin-bottom: 10px; padding-left: 10px; padding-right: 10px; @@ -79,17 +90,22 @@ input { min-width: 20px; } +.inputBox input { + margin-left: 5px; + margin-right: 0; +} + @media only screen and (max-width: 600px) { body { - background-color: var(--bar); + background-color: var(--inoutdiv); } .inoutdiv { position: absolute; top: 0; - left: 0; - right: 0; + left: 10px; + right: 10px; border-radius: 0; - min-width: 100%; + min-width: calc(100% - 20px); min-height: 100%; transform: none; padding: 5px; @@ -112,16 +128,6 @@ input { } } -.inoutdiv button { - color: white; - margin-right: 5px; - padding: 10px 20px; - border: none; - border-radius: 25px; - font-size: 14px; - transition: 0.125s; -} - .inoutdiv img { min-width: 200px; max-width: 100%; @@ -134,7 +140,7 @@ input { right: 5px; top: 47px; border: none; - height: 276px; + height: 278px; width: 400px; transform: translateX(26px); } @@ -159,7 +165,7 @@ input { text-shadow: black 1px 1px 5px; } -.newoauth, .oauthlist, .oauthentry { +.newoauth, .oauthlist, .sessionentry, .oauthentry { text-align: center; width: calc(100% - 17.5vh); margin-top: 7vh; @@ -171,35 +177,43 @@ input { border-radius: 8px; border-width: 1px; font-size: 17px; - background-color: var(--bar); + background-color: var(--inoutdiv); border-color: var(--border-color); } -.oauthentry { +.oauthentry, .sessionentry { display: flex; flex-direction: column; justify-content: center; padding: 5px; + margin-top: 0; + margin-bottom: 20px; } -.oauthentry button { +.oauthentry button, .sessionentry button { padding: 10px; background-color: red; color: white } -.oauthentry button:hover { +.oauthentry button:hover, .sessionentry button:hover { background-color: black; } +.oauthentry img, .sessionentry img { + max-height: 64px; + margin-top: 10px; +} + button { background-color: var(--theme-color); color: white; - padding: 10px; margin-right: 5px; + padding: 10px 20px; border: none; - border-radius: 8px; + border-radius: 25px; font-size: 14px; + transition: 0.125s; } button:hover { @@ -207,8 +221,20 @@ button:hover { transition: all 0.3s ease 0s; } +.inoutdiv .inputContainer { + margin-bottom: 20px; + margin-right: 20px; + width: 100%; + display: flex; +} + +.inoutdiv .inputBox { + width: 100%; +} + .unimportant { - var(--nonimporant-theme-color); + background-color: var(--nonimporant-theme-color); + color: var(--nonimportant-text-color) !important; } .unimportant:hover { @@ -242,6 +268,12 @@ h2 { pointer-events: none; } +.vAlign { + display: flex; + flex-direction: column; + justify-content: center; +} + .hidden { display: none !important; } diff --git a/static/hashcat.wasm b/static/hashcat.wasm new file mode 100755 index 0000000..f206317 Binary files /dev/null and b/static/hashcat.wasm differ diff --git a/static/js/aeskeyshare.js b/static/js/aeskeyshare.js index 7767c2c..d645642 100644 --- a/static/js/aeskeyshare.js +++ b/static/js/aeskeyshare.js @@ -1 +1,109 @@ -// To be implemented in the future \ No newline at end of file +async function main() { + try { + const urlParams = new URLSearchParams(window.location.search); + const token = urlParams.get('token'); + if (!token) { + document.getElementById("errors").innerText = "No token was provided. Redirecting to dashboard..."; + setTimeout(() => { + window.location.replace("/dashboard"); + }, 3000); + } else { + const response = await fetch("/api/aeskeyshare", { + method: "POST", + body: JSON.stringify({ + access_token: token + }), + headers: { + "Content-Type": "application/json; charset=UTF-8" + } + }); + if (response.status === 200) { + let responseData = await response.json(); + const publicKeyParam = urlParams.get('pubkey'); + if (!publicKeyParam) { + document.getElementById("errors").innerText = "The website you were visiting has not provided a public key. Encryption cannot proceed. Redirecting to dashboard..."; + setTimeout(() => { + window.location.replace("/dashboard"); + }, 3000); + } else { + const publicKeyBytes = atob(publicKeyParam.replace(/_/g, '/').replace(/~/g, '+')); + const publicKeyBuffer = new Uint8Array(publicKeyBytes.length); + for (let i = 0; i < publicKeyBytes.length; i++) { + publicKeyBuffer[i] = publicKeyBytes.charCodeAt(i); + } + let publicKey; + try { + publicKey = await window.crypto.subtle.importKey( + "spki", + publicKeyBuffer, + { + name: "RSA-OAEP", + hash: {name: "SHA-512"} + }, + true, + ["encrypt"] + ); + } catch (error) { + console.error('Error:', error.message); + document.getElementById("errors").innerText = "The public key provided by the website is invalid. Encryption cannot proceed. Redirecting to dashboard..."; + setTimeout(() => { + window.location.replace("/dashboard"); + }, 3000); + return + } + document.getElementById("errors").innerText = "Generating encryption keys..."; + const message = await hashwasm.argon2id({ + password: localStorage.getItem("DONOTSHARE-password") + responseData["appId"], + salt: new TextEncoder().encode("Burgers are yum!"), + parallelism: 1, + iterations: 32, + memorySize: 19264, + hashLength: 32, + outputType: "hex" + }); + document.getElementById("errors").innerText = "Encrypting message..."; + const encryptedMessageBuffer = await window.crypto.subtle.encrypt( + { + name: "RSA-OAEP" + }, + publicKey, + new TextEncoder().encode(message) + ); + const encodedMessage = btoa(String.fromCharCode.apply(null, new Uint8Array(encryptedMessageBuffer))).replace(/\+/g, '~').replace(/\//g, '_').replace(/=+$/, ''); + window.location.replace(responseData["keyShareUri"] + "/?encoded=" + encodedMessage) + } + } else if (response.status === 401) { + const responseData = await response.json(); + document.getElementById("errors").innerText = "The token provided is invalid: " + responseData["error"] + " Redirecting to dashboard..."; + setTimeout(() => { + window.location.replace("/dashboard"); + }, 3000); + } else if (response.status === 500) { + const responseData = await response.json(); + document.getElementById("errors").innerText = responseData["error"]; + setTimeout(() => { + window.location.replace("/dashboard"); + }, 3000); + } else if (response.status === 403) { + document.getElementById("errors").innerText = "The token provided has expired. Redirecting to dashboard..."; + setTimeout(() => { + window.location.replace("/dashboard"); + }, 3000); + } else { + const responseData = await response.json(); + document.getElementById("errors").innerText = "An unknown error occurred: " + responseData["error"] + " Redirecting to dashboard..."; + setTimeout(() => { + window.location.replace("/dashboard"); + }, 3000); + } + } + } catch (error) { + console.error('Error:', error.message); + document.getElementById("errors").innerText = "An error occurred and was logged to the console. Redirecting to dashboard..."; + setTimeout(() => { + window.location.replace("/dashboard"); + }, 3000); + } +} + +window.onload = main; \ No newline at end of file diff --git a/static/js/dashboard.js b/static/js/dashboard.js index 3544d03..4ec2aa0 100644 --- a/static/js/dashboard.js +++ b/static/js/dashboard.js @@ -3,15 +3,20 @@ if (localStorage.getItem("DONOTSHARE-secretkey") === null) { document.body.innerHTML = "Redirecting..." throw new Error(); } -let remote = localStorage.getItem("homeserverURL") -if (remote == null) { - localStorage.setItem("homeserverURL", "https://auth.hectabit.org") - remote = "https://auth.hectabit.org" -} - function attempt() { if (document.getElementById("appidbox").value !== "") { + let openid = false; + if (document.getElementById("openidbox").checked) { + openid = true + } + let scopes = [] + if (openid) { + scopes.push("openid") + } + if (document.getElementById("aeskeysharebox").value !== "") { + scopes.push("aeskeyshare") + } fetch(origin + "/api/newauth", { method: "POST", headers: { @@ -19,29 +24,114 @@ function attempt() { }, body: JSON.stringify({ name: document.getElementById("appidbox").value, - rdiruri: document.getElementById("rdiruribox").value, - secretKey: localStorage.getItem("DONOTSHARE-secretkey") + redirectUri: document.getElementById("rdiruribox").value, + secretKey: localStorage.getItem("DONOTSHARE-secretkey"), + scopes: JSON.stringify(scopes), + keyShareUri: document.getElementById("aeskeysharebox").value }) }) - .then(response => { - async function doStuff() { - let code = await response.json() - if (response.status === 200) { - document.getElementById("status").innerText = "Your secret key is: " + code["key"] + " and your client id is: " + code["appId"] + ". This will only be shown once!" - getauths(); - } else if (response.status === 500) { - document.getElementById("status").innerText = "Whoops... Something went wrong. Please try again later. (Error Code 500)" - } else if (response.status === 401) { - document.getElementById("status").innerText = "AppID already taken. (Error Code 401)" - } else { - document.getElementById("status").innerText = "Unkown error encountered. (Error Code " + response.status + ")" - } + .then(async response => { + let code = await response.json() + document.getElementById("appidbox").value = "" + document.getElementById("rdiruribox").value = "" + document.getElementById("aeskeysharebox").value = "" + document.getElementById("openidbox").checked = false + if (response.status === 200) { + document.getElementById("status").innerText = "Your secret key is: " + code["key"] + " and your client id is: " + code["appId"] + ". This will only be shown once!" + getauths(); + } else if (response.status === 500) { + document.getElementById("status").innerText = code["error"] + } else if (response.status === 401) { + document.getElementById("status").innerText = "AppID already taken. (Error Code: " + code["error"] + ")" + } else { + document.getElementById("status").innerText = "Unknown error encountered. (Error Code:" + code["error"] + ")" } - doStuff() }) } } +function getSessions() { + fetch(origin + "/api/sessions/list", { + method: "POST", + body: JSON.stringify({ + secretKey: localStorage.getItem("DONOTSHARE-secretkey") + }), + headers: { + "Content-Type": "application/json; charset=UTF-8" + } + }) + .then(async (response) => { + let responseData = await response.json() + if (response.status === 200) { + if (responseData === null || responseData.length === 0) { + let statusText = document.createElement("p") + statusText.classList.add("sessionInfo") + statusText.innerText = "Hi there! You don't have any sessions logged in, somehow. Congratulations on breaking the laws of physics!" + document.getElementById("sessionsList").append(statusText) + } else { + document.querySelectorAll(".sessionInfo").forEach(e => e.remove()) + document.querySelectorAll(".sessionentry").forEach(e => e.remove()) + for (let i in responseData) { + let sessionElement = document.createElement("div") + let sessionDevice = document.createElement("p") + let sessionRemoveButton = document.createElement("button") + let sessionImage = document.createElement("img") + if (responseData[i]["thisSession"]) { + sessionDevice.innerText = "(current) " + responseData[i]["device"] + } else { + sessionDevice.innerText = responseData[i]["device"] + } + + if (responseData[i]["device"].includes("NT") || responseData[i]["device"].includes("Linux") || responseData[i]["device"].includes("Macintosh")) { + sessionImage.src = "/static/svg/device_computer.svg" + } else if (responseData[i]["device"].includes("iPhone" || responseData[i]["device"].includes("Android") || responseData[i]["device"].includes("iPod"))) { + sessionImage.src = "/static/svg/device_smartphone.svg" + } else if (responseData[i]["device"].includes("curl")) { + sessionImage.src = "/static/svg/device_terminal.svg" + } else { + sessionImage.src = "/static/svg/device_other.svg" + } + + sessionRemoveButton.innerText = "Remove session" + sessionRemoveButton.addEventListener("click", () => { + fetch(origin + "/api/deleteauth", { + method: "POST", + body: JSON.stringify({ + secretKey: localStorage.getItem("DONOTSHARE-secretkey"), + appId: responseData[i]["appId"] + }), + headers: { + "Content-Type": "application/json; charset=UTF-8" + } + }) + sessionElement.remove() + if (responseData[i]["thisSession"]) { + window.location.replace("/logout") + } + }); + + sessionElement.append(sessionImage) + sessionElement.append(sessionDevice) + sessionElement.append(sessionRemoveButton) + sessionElement.classList.add("sessionentry") + + document.getElementById("sessionsList").append(sessionElement) + } + } + } else if (response.status === 500) { + let statusText = document.createElement("p") + statusText.classList.add("sessionInfo") + statusText.innerText = responseData["error"] + document.getElementById("sessionsList").append(statusText) + } else { + let statusText = document.createElement("p") + statusText.classList.add("sessionInfo") + statusText.innerText = "Something went wrong! (error code: " + responseData["error"] + ")" + document.getElementById("sessionsList").append(statusText) + } + }); +} + function getauths() { fetch(origin + "/api/listauth", { method: "POST", @@ -52,47 +142,174 @@ function getauths() { "Content-Type": "application/json; charset=UTF-8" } }) - .then((response) => { - async function doStuff() { - let responseData = await response.json() - document.querySelectorAll(".oauthentry").forEach((el) => el.remove()); - for (let i in responseData) { - let oauthElement = document.createElement("div") - let oauthText = document.createElement("p") - let oauthName = document.createElement("p") - let oauthUrl = document.createElement("p") - let oauthRemoveButton = document.createElement("button") - oauthText.innerText = "Client ID: " + responseData[i]["appId"] - oauthName.innerText = "App name: " + responseData[i]["name"] - oauthUrl.innerText = "Redirect Url: " + responseData[i]["rdiruri"] - oauthRemoveButton.innerText = "Delete Permanently" - oauthRemoveButton.addEventListener("click", () => { - if (window.confirm("Are you SURE you would like to delete this FOREVER?") === true) { - fetch(origin + "/api/deleteauth", { - method: "POST", - body: JSON.stringify({ - secretKey: localStorage.getItem("DONOTSHARE-secretkey"), - appId: responseData[i]["appId"] - }), - headers: { - "Content-Type": "application/json; charset=UTF-8" + .then(async (response) => { + let responseData = await response.json() + if (response.status === 200) { + if (responseData === null || responseData.length === 0) { + let statusText = document.createElement("p") + statusText.classList.add("authInfo") + statusText.innerText = "Hi there! You don't have any OAuth2 clients yet. Create one above!" + document.getElementById("oauthlist").append(statusText) + } else { + document.querySelectorAll(".authInfo").forEach(e => e.remove()) + document.querySelectorAll(".oauthentry").forEach(e => e.remove()) + for (let i in responseData) { + let oauthElement = document.createElement("div") + let oauthText = document.createElement("p") + let oauthName = document.createElement("p") + let oauthUrl = document.createElement("p") + let oauthRemoveButton = document.createElement("button") + oauthText.innerText = "Client ID: " + responseData[i]["appId"] + oauthName.innerText = "App name: " + responseData[i]["name"] + oauthUrl.innerText = "Redirect Url: " + responseData[i]["redirectUri"] + oauthRemoveButton.innerText = "Delete Permanently" + oauthRemoveButton.addEventListener("click", () => { + if (window.confirm("Are you SURE you would like to delete this FOREVER?") === true) { + fetch(origin + "/api/deleteauth", { + method: "POST", + body: JSON.stringify({ + secretKey: localStorage.getItem("DONOTSHARE-secretkey"), + appId: responseData[i]["appId"] + }), + headers: { + "Content-Type": "application/json; charset=UTF-8" + } + }) + oauthElement.remove() + } + }); + + oauthElement.append(oauthText) + oauthElement.append(oauthName) + oauthElement.append(oauthUrl) + + let openid = false + let aesKeyShare = false + let scopes = JSON.parse(responseData[i]["scopes"]) + for (let n in scopes) { + console.log(scopes[n]) + if (scopes[n] === "openid") { + openid = true + } else if (scopes[n] === "aeskeyshare") { + if (responseData[i]["keyShareUri"] !== "none") { + aesKeyShare = true + let keyShareUri = document.createElement("p") + keyShareUri.innerText = "Key Share URI: " + responseData[i]["keyShareUri"] + oauthElement.append(keyShareUri) } - }) - oauthElement.remove() + } } - }); - oauthElement.append(oauthText) - oauthElement.append(oauthName) - oauthElement.append(oauthUrl) - oauthElement.append(oauthRemoveButton) - oauthElement.classList.add("oauthentry") + let scopeTxt = document.createElement("p") + if (openid || aesKeyShare) { + scopeTxt.innerText = "Scopes: " + if (openid) { + scopeTxt.innerText += "openid" + } + if (aesKeyShare) { + if (!openid) { + scopeTxt.innerText += "aeskeyshare" + } else { + scopeTxt.innerText += ", aeskeyshare" + } + } + } else { + scopeTxt.innerText = "You have not defined any scopes for this client." + } - document.getElementById("oauthlist").append(oauthElement) + oauthElement.append(scopeTxt) + oauthElement.append(oauthRemoveButton) + oauthElement.classList.add("oauthentry") + + document.getElementById("oauthlist").append(oauthElement) + } } + } else if (response.status === 500) { + let statusText = document.createElement("p") + statusText.classList.add("authInfo") + statusText.innerText = responseData["error"] + document.getElementById("oauthlist").append(statusText) + } else { + let statusText = document.createElement("p") + statusText.classList.add("authInfo") + statusText.innerText = "Something went wrong! (error code: " + responseData["error"] + ")" + document.getElementById("oauthlist").append(statusText) } - doStuff() }); } -getauths() +async function checkNetwork() { + let loggedIn = await fetch("/api/secretkeyloggedin", { + method: "POST", + body: JSON.stringify({ + secretKey: localStorage.getItem("DONOTSHARE-secretkey") + }), + headers: { + "Content-Type": "application/json; charset=UTF-8" + } + }) + if (loggedIn.status === 200) { + return true + } else { + localStorage.removeItem("DONOTSHARE-secretkey"); + localStorage.removeItem("DONOTSHARE-password"); + window.location.replace("/login" + window.location.search); + return false + } +} + +async function deleteacct() { + if (confirm("Are you SURE you would like to delete your account forever?") === true) { + await fetch("/api/deleteaccount", { + method: "POST", + body: JSON.stringify({ + "secretKey": localStorage.getItem("DONOTSHARE-secretkey") + }), + headers: { + "Content-Type": "application/json; charset=UTF-8" + } + }) + .then((response) => response) + .then((response) => { + async function doStuff() { + if (response.status === 200) { + parent.window.location.href = '/logout'; + } + } + doStuff() + }); + } +} + +document.addEventListener("DOMContentLoaded", () => { + checkNetwork().then(async (result) => { + if (result) { + getauths() + getSessions() + let response = await fetch("/api/userinfo", { + method: "POST", + body: JSON.stringify({ + "secretKey": localStorage.getItem("DONOTSHARE-secretkey") + }), + headers: { + "Content-Type": "application/json; charset=UTF-8" + } + }) + const data = await response.json() + if (response.status === 200) { + document.getElementById("namebox").innerText = "Username: " + data["username"]; + document.getElementById("datebox").innerText = "Account created: " + new Date(data["created"] * 1000).toLocaleString(); + } + } + }) +}) + +document.getElementById("devAcctSwitcher").addEventListener("click", () => { + document.getElementById("developers").classList.toggle("hidden") + document.getElementById("account").classList.toggle("hidden") + if (document.getElementById("devAcctSwitcher").innerText === "Switch to developer view") { + document.getElementById("devAcctSwitcher").innerText = "Switch to account view" + } else { + document.getElementById("devAcctSwitcher").innerText = "Switch to developer view" + } +}) \ No newline at end of file diff --git a/static/js/keyexchangeclient.js b/static/js/keyexchangeclient.js new file mode 100644 index 0000000..fbb6df1 --- /dev/null +++ b/static/js/keyexchangeclient.js @@ -0,0 +1,30 @@ +async function main() { + const response = await fetch("/api/aeskeyshare", { + method: "POST", + body: JSON.stringify({ + access_token: urlParams.get('token') + }), + headers: { + "Content-Type": "application/json; charset=UTF-8" + } + }); + if (response.status === 200) { + let responseData = await response.json(); + const message = await hashwasm.argon2id({ + password: localStorage.getItem("DONOTSHARE-password") + responseData["appId"], + salt: new TextEncoder().encode("Burgers are yum!"), + parallelism: 1, + iterations: 32, + memorySize: 19264, + hashLength: 32, + outputType: "hex" + }); + window.postMessage("finished", "*"); + console.log("finished") + localStorage.setItem("DONOTSHARE-EXCHANGED-KEY", responseData[message]); + } else { + console.error("Error:", response.status); + } +} + +window.onload = main; diff --git a/static/js/keyexchangetester.js b/static/js/keyexchangetester.js new file mode 100644 index 0000000..e7a57df --- /dev/null +++ b/static/js/keyexchangetester.js @@ -0,0 +1,77 @@ +function saveArrayBufferToLocalStorage(key, buffer) { + const base64String = arrayBufferToBase64(buffer); + localStorage.setItem(key, base64String); +} + +function getArrayBufferFromLocalStorage(key) { + const base64String = localStorage.getItem(key); + if (base64String) { + return base64ToArrayBuffer(base64String); + } + return null; +} + +function arrayBufferToBase64(buffer) { + const uint8Array = new Uint8Array(buffer); + return btoa(String.fromCharCode.apply(null, uint8Array)).replace(/\+/g, '~').replace(/\//g, '_').replace(/=+$/, ''); +} + +function base64ToArrayBuffer(base64) { + const binaryString = atob(base64.replace(/_/g, '/').replace(/~/g, '+')); + const length = binaryString.length; + const buffer = new ArrayBuffer(length); + const uint8Array = new Uint8Array(buffer); + for (let i = 0; i < length; i++) { + uint8Array[i] = binaryString.charCodeAt(i); + } + return buffer; +} + +function generateKeyPair() { + return window.crypto.subtle.generateKey( + { + name: "RSA-OAEP", + modulusLength: 4096, + publicExponent: new Uint8Array([0x01, 0x00, 0x01]), + hash: {name: "SHA-512"} + }, + true, + ["encrypt", "decrypt"] + ); +} + +async function main() { + const urlParams = new URLSearchParams(window.location.search); + const encodedData = urlParams.get('encoded'); + const doNothing = urlParams.get('donothing'); + let keyShareUri = "https://auth.hectabit.org/aeskeyshare" + if (localStorage.getItem("keyShareUri") !== null) { + keyShareUri = localStorage.getItem("keyShareUri") + } + if (localStorage.getItem("referrer") === null) { + return + } + if (doNothing !== "true") { + if (encodedData) { + const decodedData = base64ToArrayBuffer(encodedData); + const decryptedData = window.crypto.subtle.decrypt( + { + name: "RSA-OAEP" + }, + await crypto.subtle.importKey("pkcs8", getArrayBufferFromLocalStorage("key"), { + name: "RSA-OAEP", + hash: {name: "SHA-512"} + }, true, ["decrypt"]), + decodedData + ); + localStorage.setItem("DONOTSHARE-EXCHANGED-KEY", new TextDecoder().decode(await decryptedData)); + window.location.replace(localStorage.getItem("referrer")) + } else { + let keyPair = await generateKeyPair(); + saveArrayBufferToLocalStorage("key", await crypto.subtle.exportKey("pkcs8", keyPair.privateKey)); + window.location.replace(keyShareUri + "?pubkey=" + btoa(String.fromCharCode.apply(null, new Uint8Array(await crypto.subtle.exportKey("spki", keyPair.publicKey)))).replace(/\+/g, '~').replace(/\//g, '_').replace(/=+$/, '') + "&token=" + localStorage.getItem("BURGERAUTH-RDIR-TOKEN")); + } + } +} + +window.onload = main; \ No newline at end of file diff --git a/static/js/login.js b/static/js/login.js index d38a3af..a2cbe7a 100644 --- a/static/js/login.js +++ b/static/js/login.js @@ -12,9 +12,11 @@ if (localStorage.getItem("DONOTSHARE-password") !== null) { let usernameBox = document.getElementById("usernameBox") let passwordBox = document.getElementById("passwordBox") let statusBox = document.getElementById("statusBox") +let nextButton = document.getElementById("nextButton") let signupButton = document.getElementById("signupButton") let inputNameBox = document.getElementById("inputNameBox") let backButton = document.getElementById("backButton") +let inputContainer = document.getElementById("inputContainer") usernameBox.classList.remove("hidden") inputNameBox.innerText = "Username:" @@ -23,7 +25,9 @@ let currentInputType = 0 function showInput(inputType) { if (inputType === 0) { + inputContainer.classList.remove("hidden") usernameBox.classList.remove("hidden") + signupButton.classList.remove("hidden") passwordBox.classList.add("hidden") backButton.classList.add("hidden") inputNameBox.innerText = "Username:" @@ -36,18 +40,19 @@ function showInput(inputType) { currentInputType = 0 }) } else if (inputType === 1) { + inputContainer.classList.remove("hidden") + signupButton.classList.add("hidden") usernameBox.classList.add("hidden") passwordBox.classList.remove("hidden") backButton.classList.remove("hidden") inputNameBox.innerText = "Password:" currentInputType = 1 } else if (inputType === 2) { - usernameBox.classList.add("hidden") - passwordBox.classList.add("hidden") signupButton.classList.add("hidden") + nextButton.classList.add("hidden") backButton.classList.add("hidden") + inputContainer.classList.add("hidden") inputNameBox.classList.add("hidden") - inputNameBox.innerText = "Password:" currentInputType = 2 } } @@ -56,7 +61,7 @@ function showElements(yesorno) { if (!yesorno) { usernameBox.classList.add("hidden") passwordBox.classList.add("hidden") - signupButton.classList.add("hidden") + nextButton.classList.add("hidden") backButton.classList.add("hidden") inputNameBox.classList.add("hidden") showInput(currentInputType) @@ -64,18 +69,14 @@ function showElements(yesorno) { else { usernameBox.classList.remove("hidden") passwordBox.classList.remove("hidden") - signupButton.classList.remove("hidden") + nextButton.classList.remove("hidden") backButton.classList.remove("hidden") inputNameBox.classList.remove("hidden") showInput(currentInputType) } } -document.addEventListener('DOMContentLoaded', function() { - document.getElementById("homeserver").innerText = "Your homeserver is: " + remote + ". " -}); - -signupButton.addEventListener("click", () => { +nextButton.addEventListener("click", async () => { if (passwordBox.classList.contains("hidden")) { if (usernameBox.value === "") { statusBox.innerText = "A username is required!" @@ -85,65 +86,141 @@ signupButton.addEventListener("click", () => { } showInput(1) } else { - async function doStuff() { - let username = usernameBox.value - let password = passwordBox.value + let username = usernameBox.value + let password = passwordBox.value - if (password === "") { - statusBox.innerText = "A password is required!" - return - } + if (password === "") { + statusBox.innerText = "A password is required!" + return + } - showInput(2) - showElements(true) - statusBox.innerText = "Signing in..." + showInput(2) + showElements(true) - async function hashpass(pass) { - let key = pass - for (let i = 0; i < 128; i++) { - key = await hashwasm.sha3(key) - } - return key - } + async function hashpass(pass) { + return await hashwasm.argon2id({ + password: pass, + salt: new TextEncoder().encode("I munch Burgers!!"), + parallelism: 1, + iterations: 32, + memorySize: 19264, + hashLength: 32, + outputType: "hex" + }) + } - fetch("/api/login", { + async function migrateLegacyPassword(secretKey, password) { + return await fetch("/api/changepassword", { method: "POST", body: JSON.stringify({ - username: username, - password: await hashpass(password), - passwordchange: "no", - newpass: "null" + secretKey: secretKey, + newPassword: password, + migration: true }), headers: { - "Content-Type": "application/json; charset=UTF-8" + "Content-Type": "application/json; charset=UTF-8", } }) - .then((response) => response) - .then((response) => { - async function doStuff() { - let responseData = await response.json() - if (response.status === 200) { - localStorage.setItem("DONOTSHARE-secretkey", responseData["key"]) - localStorage.setItem("DONOTSHARE-password", await hashwasm.sha512(password)) - - window.location.href = "/app" + window.location.search - } - else if (response.status === 401) { - statusBox.innerText = "Wrong username or password..." - showInput(1) - showElements(true) - } else { - statusBox.innerText = "Something went wrong! (error code: " + response.status + ")" - showInput(1) - showElements(true) - } - } - doStuff() - }); } - doStuff() + + async function hashpassold(pass) { + let key = pass + for (let i = 0; i < 128; i++) { + key = await hashwasm.sha3(key) + } + return key + } + + statusBox.innerText = "Hashing password..." + let hashedPassword = await hashpass(password) + + let response = await fetch("/api/login", { + method: "POST", + body: JSON.stringify({ + username: username, + password: hashedPassword, + modern: true + }), + headers: { + "Content-Type": "application/json; charset=UTF-8" + } + }) + let responseData = await response.json() + if (response.status === 200) { + statusBox.innerText = "Setting up encryption keys..." + localStorage.setItem("DONOTSHARE-secretkey", responseData["key"]) + localStorage.setItem("DONOTSHARE-password", await hashwasm.argon2id({ + password: password, + salt: new TextEncoder().encode("I love Burgerauth!!"), + parallelism: 1, + iterations: 32, + memorySize: 19264, + hashLength: 32, + outputType: "hex" + })) + statusBox.innerText = "Welcome back!" + await new Promise(r => setTimeout(r, 200)) + window.location.href = "/app" + window.location.search + } else if (response.status === 401) { + if (responseData["migrated"] !== true) { + statusBox.innerText = "Migrating to new password algorithm..." + let loginOld = await fetch("/api/login", { + method: "POST", + body: JSON.stringify({ + username: username, + password: hashpassold(password), + modern: false + }), + headers: { + "Content-Type": "application/json; charset=UTF-8" + } + }) + let loginDataOld = await loginOld.json() + if (loginOld.status === 401) { + statusBox.innerText = "Username or password incorrect!" + showInput(1) + showElements(true) + } else if (loginOld.status === 200) { + statusBox.innerText = "Setting up encryption keys..." + localStorage.setItem("DONOTSHARE-secretkey", loginDataOld["key"]) + localStorage.setItem("DONOTSHARE-password", await hashwasm.argon2id({ + password: password, + salt: new TextEncoder().encode("I love Burgerauth!!"), + parallelism: 1, + iterations: 32, + memorySize: 19264, + hashLength: 32, + outputType: "hex" + })) + + statusBox.innerText = "Migrating password..." + let status = await migrateLegacyPassword(loginDataOld["key"], hashedPass) + if (status.status === 200) { + statusBox.innerText = "Welcome back!" + await new Promise(r => setTimeout(r, 200)) + window.location.href = "/app" + window.location.search + } else { + statusBox.innerText = (await status.json())["error"] + showInput(1) + showElements(true) + } + } + } else { + statusBox.innerText = "Wrong username or password..." + showInput(1) + showElements(true) + } + } else if (response.status === 500) { + statusBox.innerText = responseData["error"] + showInput(1) + showElements(true) + } else { + statusBox.innerText = "Something went wrong! (error code: " + responseData["error"] + ")" + showInput(1) + showElements(true) + } } -}); +}) backButton.addEventListener("click", () => { showInput(0) @@ -151,16 +228,13 @@ backButton.addEventListener("click", () => { showInput(0) -document.getElementById("signuprdirButton").addEventListener("click", function(event) { - event.preventDefault(); - - const queryString = window.location.search; - window.location.href = "/signup" + queryString; -}); - document.getElementById("privacyButton").addEventListener("click", function(event) { event.preventDefault(); const queryString = window.location.search; window.location.href = "/privacy" + queryString; }); + +function toSignup() { + window.location.href = "/signup" + window.location.search; +} \ No newline at end of file diff --git a/static/js/main.js b/static/js/main.js index 82cc1f7..66f7156 100644 --- a/static/js/main.js +++ b/static/js/main.js @@ -1,58 +1,83 @@ -let client_id, redirect_uri, response_type, state, code, codemethod, secret_key, expires, nonce; +let client_id, redirect_uri, response_type, state, code, codemethod, secret_key, nonce; if (localStorage.getItem("DONOTSHARE-secretkey") === null) { - window.location.replace("/login" + window.location.search) - document.body.innerHTML = "Redirecting..." throw new Error(); } document.addEventListener("DOMContentLoaded", function() { - const urlParams = new URLSearchParams(window.location.search); - const statusBox = document.getElementById("statusBox"); + checkNetwork().then((result) => { + if (result) { + const urlParams = new URLSearchParams(window.location.search); + const statusBox = document.getElementById("statusBox"); - // Get URL parameters - if (urlParams.has('client_id')) { - client_id = urlParams.get('client_id') - let name = document.getElementById("passthrough").innerText; - statusBox.textContent = "Would you like to allow " + name + " to access your user information?"; - redirect_uri = urlParams.get('redirect_uri'); - response_type = urlParams.get('response_type'); - } else { - window.location.replace("/dashboard"); - document.body.innerHTML = "Redirecting..." - throw new Error(); - } + if (urlParams.has('client_id')) { + client_id = urlParams.get('client_id') + let name = document.getElementById("passthrough").innerText; + redirect_uri = urlParams.get('redirect_uri'); + statusBox.textContent = "Would you like to allow " + name + " to access your user information? You will be redirected to " + redirect_uri + " after you make your decision."; + response_type = urlParams.get('response_type'); + } else { + window.location.replace("/dashboard"); + document.body.innerHTML = "Redirecting..." + throw new Error(); + } - state = urlParams.has('state') ? urlParams.get('state') : "none"; + state = urlParams.has('state') ? urlParams.get('state') : "none"; - if (urlParams.has('code_challenge')) { - code = urlParams.get('code_challenge'); - codemethod = urlParams.get('code_challenge_method'); - } else { - code = "none"; - codemethod = "none"; - } + if (urlParams.has('code_challenge')) { + code = urlParams.get('code_challenge'); + codemethod = urlParams.get('code_challenge_method'); + } else { + code = "none"; + codemethod = "none"; + } - if (urlParams.has('nonce')) { - nonce = urlParams.get('nonce'); - } else { - nonce = "none"; - } + if (urlParams.has('nonce')) { + nonce = urlParams.get('nonce'); + } else { + nonce = "none"; + } - // Get DONOTSHARE-secretkey from localStorage - secret_key = localStorage.getItem("DONOTSHARE-secretkey"); - const now = new Date(); - const expireTime = now.getTime() + (21 * 1000); // 21 seconds from now - expires = new Date(expireTime).toUTCString(); + secret_key = localStorage.getItem("DONOTSHARE-secretkey"); + } + }) }); function deny() { - document.cookie = "key=" + secret_key + "; expires=" + expires + "; path=/; SameSite=Strict"; - // Redirect to the redirect_uri so that an open redirect is not possible window.location.replace("/api/auth?client_id=" + client_id + "&redirect_uri=" + redirect_uri + "&code_challenge_method=" + codemethod + "&code_challenge=" + code + "&state=" + state + "&nonce=" + nonce + "&deny=true"); } function oauth() { - document.cookie = "key=" + secret_key + "; expires=" + expires + "; path=/; SameSite=Strict"; - window.location.replace("/api/auth?client_id=" + client_id + "&redirect_uri=" + redirect_uri + "&code_challenge_method=" + codemethod + "&code_challenge=" + code + "&state=" + state + "&nonce=" + nonce + "&deny=false"); -} \ No newline at end of file + const now = new Date(); + const expireTime = now.getTime() + (21 * 1000); + let expires = new Date(expireTime).toUTCString(); + if (navigator.cookieEnabled) { + document.cookie = "DONOTSHARE-secretkey=" + secret_key + "; expires=" + expires + "; path=/"; + window.location.replace("/api/auth?client_id=" + client_id + "&redirect_uri=" + redirect_uri + "&code_challenge_method=" + codemethod + "&code_challenge=" + code + "&state=" + state + "&nonce=" + nonce + "&deny=false"); + } else { + document.getElementById("statusBox").textContent = "Warning! Because cookies are disabled, your access token is sent directly in the URL. This is less secure than using cookies, but you chose this path!"; + setTimeout(() => { + window.location.replace("/api/auth?client_id=" + client_id + "&redirect_uri=" + redirect_uri + "&code_challenge_method=" + codemethod + "&code_challenge=" + code + "&state=" + state + "&nonce=" + nonce + "&deny=false&access_token=" + secret_key); + }, 200); + } +} + +async function checkNetwork() { + let loggedIn = await fetch("/api/secretkeyloggedin", { + method: "POST", + body: JSON.stringify({ + secretKey: localStorage.getItem("DONOTSHARE-secretkey") + }), + headers: { + "Content-Type": "application/json; charset=UTF-8" + } + }) + if (loggedIn.status === 200) { + return true + } else { + localStorage.removeItem("DONOTSHARE-secretkey"); + localStorage.removeItem("DONOTSHARE-password"); + window.location.replace("/login" + window.location.search); + return false + } +} diff --git a/static/js/signup.js b/static/js/signup.js index e9b834c..8c0c1fa 100644 --- a/static/js/signup.js +++ b/static/js/signup.js @@ -1,9 +1,4 @@ -if (localStorage.getItem("DONOTSHARE-secretkey") !== null) { - window.location.replace("/app" + window.location.search) - document.body.innerHTML = "Redirecting..." - throw new Error(); -} -if (localStorage.getItem("DONOTSHARE-password") !== null) { +if (localStorage.getItem("DONOTSHARE-secretkey") !== null || localStorage.getItem("DONOTSHARE-password") !== null) { window.location.replace("/app" + window.location.search) document.body.innerHTML = "Redirecting..." throw new Error(); @@ -13,109 +8,145 @@ let usernameBox = document.getElementById("usernameBox") let passwordBox = document.getElementById("passwordBox") let statusBox = document.getElementById("statusBox") let signupButton = document.getElementById("signupButton") -let captchaBox = document.getElementById("captchaBox") -let unique_token = document.getElementById("passthrough").innerText +let loginButton = document.getElementById("loginButton") +let inputContainer = document.getElementById("inputContainer") function showElements(yesorno) { if (!yesorno) { - usernameBox.classList.add("hidden") - passwordBox.classList.add("hidden") + inputContainer.classList.add("hidden") signupButton.classList.add("hidden") + loginButton.classList.add("hidden") } else { - usernameBox.classList.remove("hidden") - passwordBox.classList.remove("hidden") + inputContainer.classList.remove("hidden") signupButton.classList.remove("hidden") + loginButton.classList.remove("hidden") } } +complete = new Event("completed"); +window.returnCode = undefined; +window.returnVar = undefined; + +// This is for the WASM code to call when it's done. Do not remove it, even if it looks like it's never called. + +function WASMComplete() { + window.dispatchEvent(complete); +} + signupButton.addEventListener("click", () => { - async function doStuff() { - let username = usernameBox.value - let password = passwordBox.value - let captcha = captchaBox.value + let username = usernameBox.value + let password = passwordBox.value - if (username === "") { - statusBox.innerText = "A username is required!" + if (username === "") { + statusBox.innerText = "A username is required!" + return + } + + if ((username).length > 20) { + statusBox.innerText = "Username cannot be more than 20 characters!" + return + } + if (password === "") { + statusBox.innerText = "A password is required!" + return + } + if ((password).length < 8) { + statusBox.innerText = "8 or more characters are required!" + return + } + + async function hashpass(pass) { + return await hashwasm.argon2id({ + password: pass, + salt: new TextEncoder().encode("I munch Burgers!!"), + parallelism: 1, + iterations: 32, + memorySize: 19264, + hashLength: 32, + outputType: "hex" + }) + } + + showElements(false) + statusBox.innerText = "Computing PoW Challenge... (this may take up to 5 minutes at worst, 3 seconds at best)" + + /* + * Compiled version of: + * hashcat-wasm (https://concord.hectabit.org/hectabit/hashcat-wasm) + * (c) Arzumify + * @license AGPL-3.0 + * Since this is my software, if you use it with proprietary servers, I will make sure you will walk across hot coals (just kidding, probably). + * I'm not kidding about the license though. + * I should stop including comments into JS and possibly minify this code. Oh, well. + */ + + window.resourceExtra = "I love Burgerauth!!" + + const go = new Go(); + WebAssembly.instantiateStreaming(fetch("/static/wasm/hashcat.wasm"), go.importObject).then((result) => { + go.run(result.instance); + }) + + window.addEventListener("completed", async () => { + if (window.returnCode === 1) { + statusBox.innerText = "Please do not expose your computer to cosmic rays (an impossible logical event has occurred)." + showElements(true) return - } - if ((username).length > 20) { - statusBox.innerText = "Username cannot be more than 20 characters!" - return - } - if (password === "") { - statusBox.innerText = "A password is required!" - return - } - if ((password).length < 8) { - statusBox.innerText = "8 or more characters are required!" - return - } - if (captcha === "") { - statusBox.innerText = "Please complete the captcha!" + } else if (window.returnCode === 2) { + statusBox.innerText = "The PoW Challenge has failed. Please try again." + showElements(true) return } - showElements(false) - statusBox.innerText = "Creating account, please hold on..." - - async function hashpass(pass) { - let key = pass - for (let i = 0; i < 128; i++) { - key = await hashwasm.sha3(key) - } - return key - } - - + statusBox.innerText = "Hashing password..." + let hashedPass = await hashpass(password) + statusBox.innerText = "Contacting server..." fetch("/api/signup", { method: "POST", body: JSON.stringify({ username: username, - password: await hashpass(password), - captcha: captcha, - unique_token: unique_token + password: hashedPass, + stamp: window.returnVar }), headers: { "Content-Type": "application/json; charset=UTF-8" } }) .then((response) => response) - .then((response) => { - async function doStuff() { - let responseData = await response.json() - console.log(responseData) + .then(async (response) => { + let responseData = await response.json() + console.log(responseData) - if (response.status === 200) { - statusBox.innerText = "Redirecting..." - localStorage.setItem("DONOTSHARE-secretkey", responseData["key"]) - localStorage.setItem("DONOTSHARE-password", await hashwasm.sha512(password)) + if (response.status === 200) { + statusBox.innerText = "Setting up encryption keys..." + localStorage.setItem("DONOTSHARE-secretkey", responseData["key"]) + localStorage.setItem("DONOTSHARE-password", await hashwasm.argon2id({ + password: password, + salt: new TextEncoder().encode("I love Burgerauth!!"), + parallelism: 1, + iterations: 32, + memorySize: 19264, + hashLength: 32, + outputType: "hex" + })) - window.location.href = "/app" + window.location.search - } else if (response.status === 409) { - statusBox.innerText = "Username already taken!" - showElements(true) - } else if (response.status === 401) { - statusBox.innerText = "CAPTCHA has expired!" - } else if (response.status === 403) { - statusBox.innerText = "CAPTCHA is incorrect!" - } else { - statusBox.innerText = "Something went wrong!" - showElements(true) - } + statusBox.innerText = "Welcome!" + await new Promise(r => setTimeout(r, 200)) + window.location.href = "/app" + window.location.search + } else if (response.status === 409) { + statusBox.innerText = "Username already taken!" + showElements(true) + } else if (response.status === 500) { + statusBox.innerText = responseData["error"] + showElements(true) + } else { + statusBox.innerText = "Something went wrong! (error code: " + responseData["error"] + ")" + showElements(true) } - doStuff() - }); - } - doStuff() -}); - -document.getElementById("loginButton").addEventListener("click", function(event) { - event.preventDefault(); - - const queryString = window.location.search; - window.location.href = "/login" + queryString; -}); + }) + }) +}) document.getElementById("privacyButton").addEventListener("click", function(event) { event.preventDefault(); @@ -123,3 +154,7 @@ document.getElementById("privacyButton").addEventListener("click", function(even const queryString = window.location.search; window.location.href = "/privacy" + queryString; }); + +function toLogin() { + window.location.href = "/login" + window.location.search; +} \ No newline at end of file diff --git a/static/js/testapp.js b/static/js/testapp.js new file mode 100644 index 0000000..c188a55 --- /dev/null +++ b/static/js/testapp.js @@ -0,0 +1,130 @@ +let clientId +const redirectUri = window.location.href.replace(window.location.search, "") +let authorizationEndpoint +let tokenEndpoint +let userinfoEndpoint + +function generateCodeVerifier() { + const charset = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789-._~"; + const length = 128; + return Array.from(crypto.getRandomValues(new Uint8Array(length))) + .map((x) => charset[x % charset.length]) + .join(""); +} + +async function createCodeChallenge(codeVerifier) { + const buffer = new TextEncoder().encode(codeVerifier); + const hashArrayBuffer = await crypto.subtle.digest('SHA-256', buffer); + return btoa(String.fromCharCode(...new Uint8Array(hashArrayBuffer))) + .replace(/=/g, '') + .replace(/\+/g, '-') + .replace(/\//g, '_'); +} + +function authorize() { + const codeVerifier = generateCodeVerifier(); + localStorage.setItem('codeVerifier', codeVerifier); // Store code verifier + createCodeChallenge(codeVerifier) + .then((codeChallenge) => { + window.location.href = `${authorizationEndpoint}?response_type=code&client_id=${clientId}&redirect_uri=${encodeURIComponent(redirectUri)}&code_challenge=${codeChallenge}&code_challenge_method=S256`; + }) + .catch((error) => { + console.error('Error generating code challenge:', error); + }); +} + +async function exchangeCodeForToken(code) { + const codeVerifier = localStorage.getItem('codeVerifier'); // Retrieve code verifier + const formData = new URLSearchParams(); + formData.append('client_id', String(clientId)); + formData.append('code', String(code)); + formData.append('redirect_uri', String(redirectUri)); + formData.append('grant_type', 'authorization_code'); + formData.append('code_verifier', String(codeVerifier)); + + let response + if (localStorage.getItem('noPost') !== "true") { + response = await fetch(tokenEndpoint, { + method: 'POST', + headers: { + "Content-Type": "application/x-www-form-urlencoded" + }, + body: formData + }); + } else { + return + } + + const data = await response.json(); + const accessToken = data["access_token"]; + const idToken = data["id_token"]; + + fetch(userinfoEndpoint, { + headers: { + "Authorization": `Bearer ${idToken}` + } + }) + .then((response) => { + async function doStuff() { + if (response.status === 200) { + const userinfoData = await response.json(); + console.log(accessToken, idToken) + console.log("User:", userinfoData.name) + console.log("Sub:", userinfoData.sub); + document.getElementById("text").innerText = "Authenticated, " + userinfoData.name + ", beginning AES Key Share..."; + localStorage.setItem("user", userinfoData.name) + localStorage.setItem("sub", userinfoData.sub) + localStorage.setItem("keyShareUri", document.getElementById("server_uri").innerText + "/aeskeyshare"); + localStorage.setItem("referrer", redirectUri); + localStorage.setItem("BURGERAUTH-RDIR-TOKEN", accessToken); + window.location.replace("/keyexchangetester"); + } else { + document.getElementById("text").innerText = "Authentication failed" + } + } + doStuff() + }); +} + +async function main() { + clientId = document.getElementById("client_id").innerText; + authorizationEndpoint = document.getElementById("server_uri").innerText + "/login"; + tokenEndpoint = document.getElementById("server_uri").innerText + "/api/tokenauth"; + userinfoEndpoint = document.getElementById("server_uri").innerText + "/userinfo"; + + console.log({ + clientId, + redirectUri, + authorizationEndpoint, + tokenEndpoint, + userinfoEndpoint + }); + + if (localStorage.getItem("user") !== null) { + document.getElementById("text").innerText = "Welcome back, " + localStorage.getItem("user") + } + + const urlParams = new URLSearchParams(window.location.search); + if (urlParams.get('code')) { + await exchangeCodeForToken(urlParams.get('code')); + } else if (urlParams.get('error')) { + if (urlParams.get('error') === "access_denied") { + document.getElementById("text").innerText = "Access denied" + } else { + document.getElementById("text").innerText = "Authentication failed (error code: " + urlParams.get('error') + ")" + } + } else if (localStorage.getItem("DONOTSHARE-EXCHANGED-KEY") !== null) { + document.getElementById("text").style.overflowWrap = "break-word" + document.getElementById("text").innerText = "AES Key Share complete! Authenticated as " + localStorage.getItem("user") + ", key is " + localStorage.getItem("DONOTSHARE-EXCHANGED-KEY") + "." + localStorage.removeItem("referrer") + localStorage.removeItem("keyShareUri") + localStorage.removeItem("key") + localStorage.removeItem("BURGERAUTH-RDIR-TOKEN") + localStorage.removeItem("codeVerifier") + localStorage.removeItem("sub") + localStorage.removeItem("DONOTSHARE-EXCHANGED-KEY") + localStorage.removeItem("user") + } +} + +document.addEventListener('DOMContentLoaded', main); \ No newline at end of file diff --git a/static/js/wasm_exec.js b/static/js/wasm_exec.js new file mode 100644 index 0000000..4bc73c0 --- /dev/null +++ b/static/js/wasm_exec.js @@ -0,0 +1,567 @@ +// @license magnet:?xt=urn:btih:c80d50af7d3db9be66a4d0a86db0286e4fd33292&dn=bsd-3-clause.txt BSD-3-Clause + +/* + * wasm_exec (https://github.com/golang/go) + * (c) The Go Authors + * @license BSD-3-Clause + */ + +"use strict"; + +(() => { + const enosys = () => { + const err = new Error("not implemented"); + err.code = "ENOSYS"; + return err; + }; + + if (!globalThis.fs) { + let outputBuf = ""; + globalThis.fs = { + constants: { O_WRONLY: -1, O_RDWR: -1, O_CREAT: -1, O_TRUNC: -1, O_APPEND: -1, O_EXCL: -1 }, // unused + writeSync(fd, buf) { + outputBuf += decoder.decode(buf); + const nl = outputBuf.lastIndexOf("\n"); + if (nl != -1) { + console.log(outputBuf.substring(0, nl)); + outputBuf = outputBuf.substring(nl + 1); + } + return buf.length; + }, + write(fd, buf, offset, length, position, callback) { + if (offset !== 0 || length !== buf.length || position !== null) { + callback(enosys()); + return; + } + const n = this.writeSync(fd, buf); + callback(null, n); + }, + chmod(path, mode, callback) { callback(enosys()); }, + chown(path, uid, gid, callback) { callback(enosys()); }, + close(fd, callback) { callback(enosys()); }, + fchmod(fd, mode, callback) { callback(enosys()); }, + fchown(fd, uid, gid, callback) { callback(enosys()); }, + fstat(fd, callback) { callback(enosys()); }, + fsync(fd, callback) { callback(null); }, + ftruncate(fd, length, callback) { callback(enosys()); }, + lchown(path, uid, gid, callback) { callback(enosys()); }, + link(path, link, callback) { callback(enosys()); }, + lstat(path, callback) { callback(enosys()); }, + mkdir(path, perm, callback) { callback(enosys()); }, + open(path, flags, mode, callback) { callback(enosys()); }, + read(fd, buffer, offset, length, position, callback) { callback(enosys()); }, + readdir(path, callback) { callback(enosys()); }, + readlink(path, callback) { callback(enosys()); }, + rename(from, to, callback) { callback(enosys()); }, + rmdir(path, callback) { callback(enosys()); }, + stat(path, callback) { callback(enosys()); }, + symlink(path, link, callback) { callback(enosys()); }, + truncate(path, length, callback) { callback(enosys()); }, + unlink(path, callback) { callback(enosys()); }, + utimes(path, atime, mtime, callback) { callback(enosys()); }, + }; + } + + if (!globalThis.process) { + globalThis.process = { + getuid() { return -1; }, + getgid() { return -1; }, + geteuid() { return -1; }, + getegid() { return -1; }, + getgroups() { throw enosys(); }, + pid: -1, + ppid: -1, + umask() { throw enosys(); }, + cwd() { throw enosys(); }, + chdir() { throw enosys(); }, + } + } + + if (!globalThis.crypto) { + throw new Error("globalThis.crypto is not available, polyfill required (crypto.getRandomValues only)"); + } + + if (!globalThis.performance) { + throw new Error("globalThis.performance is not available, polyfill required (performance.now only)"); + } + + if (!globalThis.TextEncoder) { + throw new Error("globalThis.TextEncoder is not available, polyfill required"); + } + + if (!globalThis.TextDecoder) { + throw new Error("globalThis.TextDecoder is not available, polyfill required"); + } + + const encoder = new TextEncoder("utf-8"); + const decoder = new TextDecoder("utf-8"); + + globalThis.Go = class { + constructor() { + this.argv = ["js"]; + this.env = {}; + this.exit = (code) => { + if (code !== 0) { + console.warn("exit code:", code); + } + }; + this._exitPromise = new Promise((resolve) => { + this._resolveExitPromise = resolve; + }); + this._pendingEvent = null; + this._scheduledTimeouts = new Map(); + this._nextCallbackTimeoutID = 1; + + const setInt64 = (addr, v) => { + this.mem.setUint32(addr + 0, v, true); + this.mem.setUint32(addr + 4, Math.floor(v / 4294967296), true); + } + + const setInt32 = (addr, v) => { + this.mem.setUint32(addr + 0, v, true); + } + + const getInt64 = (addr) => { + const low = this.mem.getUint32(addr + 0, true); + const high = this.mem.getInt32(addr + 4, true); + return low + high * 4294967296; + } + + const loadValue = (addr) => { + const f = this.mem.getFloat64(addr, true); + if (f === 0) { + return undefined; + } + if (!isNaN(f)) { + return f; + } + + const id = this.mem.getUint32(addr, true); + return this._values[id]; + } + + const storeValue = (addr, v) => { + const nanHead = 0x7FF80000; + + if (typeof v === "number" && v !== 0) { + if (isNaN(v)) { + this.mem.setUint32(addr + 4, nanHead, true); + this.mem.setUint32(addr, 0, true); + return; + } + this.mem.setFloat64(addr, v, true); + return; + } + + if (v === undefined) { + this.mem.setFloat64(addr, 0, true); + return; + } + + let id = this._ids.get(v); + if (id === undefined) { + id = this._idPool.pop(); + if (id === undefined) { + id = this._values.length; + } + this._values[id] = v; + this._goRefCounts[id] = 0; + this._ids.set(v, id); + } + this._goRefCounts[id]++; + let typeFlag = 0; + switch (typeof v) { + case "object": + if (v !== null) { + typeFlag = 1; + } + break; + case "string": + typeFlag = 2; + break; + case "symbol": + typeFlag = 3; + break; + case "function": + typeFlag = 4; + break; + } + this.mem.setUint32(addr + 4, nanHead | typeFlag, true); + this.mem.setUint32(addr, id, true); + } + + const loadSlice = (addr) => { + const array = getInt64(addr + 0); + const len = getInt64(addr + 8); + return new Uint8Array(this._inst.exports.mem.buffer, array, len); + } + + const loadSliceOfValues = (addr) => { + const array = getInt64(addr + 0); + const len = getInt64(addr + 8); + const a = new Array(len); + for (let i = 0; i < len; i++) { + a[i] = loadValue(array + i * 8); + } + return a; + } + + const loadString = (addr) => { + const saddr = getInt64(addr + 0); + const len = getInt64(addr + 8); + return decoder.decode(new DataView(this._inst.exports.mem.buffer, saddr, len)); + } + + const timeOrigin = Date.now() - performance.now(); + this.importObject = { + _gotest: { + add: (a, b) => a + b, + }, + gojs: { + // Go's SP does not change as long as no Go code is running. Some operations (e.g. calls, getters and setters) + // may synchronously trigger a Go event handler. This makes Go code get executed in the middle of the imported + // function. A goroutine can switch to a new stack if the current stack is too small (see morestack function). + // This changes the SP, thus we have to update the SP used by the imported function. + + // func wasmExit(code int32) + "runtime.wasmExit": (sp) => { + sp >>>= 0; + const code = this.mem.getInt32(sp + 8, true); + this.exited = true; + delete this._inst; + delete this._values; + delete this._goRefCounts; + delete this._ids; + delete this._idPool; + this.exit(code); + }, + + // func wasmWrite(fd uintptr, p unsafe.Pointer, n int32) + "runtime.wasmWrite": (sp) => { + sp >>>= 0; + const fd = getInt64(sp + 8); + const p = getInt64(sp + 16); + const n = this.mem.getInt32(sp + 24, true); + fs.writeSync(fd, new Uint8Array(this._inst.exports.mem.buffer, p, n)); + }, + + // func resetMemoryDataView() + "runtime.resetMemoryDataView": (sp) => { + sp >>>= 0; + this.mem = new DataView(this._inst.exports.mem.buffer); + }, + + // func nanotime1() int64 + "runtime.nanotime1": (sp) => { + sp >>>= 0; + setInt64(sp + 8, (timeOrigin + performance.now()) * 1000000); + }, + + // func walltime() (sec int64, nsec int32) + "runtime.walltime": (sp) => { + sp >>>= 0; + const msec = (new Date).getTime(); + setInt64(sp + 8, msec / 1000); + this.mem.setInt32(sp + 16, (msec % 1000) * 1000000, true); + }, + + // func scheduleTimeoutEvent(delay int64) int32 + "runtime.scheduleTimeoutEvent": (sp) => { + sp >>>= 0; + const id = this._nextCallbackTimeoutID; + this._nextCallbackTimeoutID++; + this._scheduledTimeouts.set(id, setTimeout( + () => { + this._resume(); + while (this._scheduledTimeouts.has(id)) { + // for some reason Go failed to register the timeout event, log and try again + // (temporary workaround for https://github.com/golang/go/issues/28975) + console.warn("scheduleTimeoutEvent: missed timeout event"); + this._resume(); + } + }, + getInt64(sp + 8), + )); + this.mem.setInt32(sp + 16, id, true); + }, + + // func clearTimeoutEvent(id int32) + "runtime.clearTimeoutEvent": (sp) => { + sp >>>= 0; + const id = this.mem.getInt32(sp + 8, true); + clearTimeout(this._scheduledTimeouts.get(id)); + this._scheduledTimeouts.delete(id); + }, + + // func getRandomData(r []byte) + "runtime.getRandomData": (sp) => { + sp >>>= 0; + crypto.getRandomValues(loadSlice(sp + 8)); + }, + + // func finalizeRef(v ref) + "syscall/js.finalizeRef": (sp) => { + sp >>>= 0; + const id = this.mem.getUint32(sp + 8, true); + this._goRefCounts[id]--; + if (this._goRefCounts[id] === 0) { + const v = this._values[id]; + this._values[id] = null; + this._ids.delete(v); + this._idPool.push(id); + } + }, + + // func stringVal(value string) ref + "syscall/js.stringVal": (sp) => { + sp >>>= 0; + storeValue(sp + 24, loadString(sp + 8)); + }, + + // func valueGet(v ref, p string) ref + "syscall/js.valueGet": (sp) => { + sp >>>= 0; + const result = Reflect.get(loadValue(sp + 8), loadString(sp + 16)); + sp = this._inst.exports.getsp() >>> 0; // see comment above + storeValue(sp + 32, result); + }, + + // func valueSet(v ref, p string, x ref) + "syscall/js.valueSet": (sp) => { + sp >>>= 0; + Reflect.set(loadValue(sp + 8), loadString(sp + 16), loadValue(sp + 32)); + }, + + // func valueDelete(v ref, p string) + "syscall/js.valueDelete": (sp) => { + sp >>>= 0; + Reflect.deleteProperty(loadValue(sp + 8), loadString(sp + 16)); + }, + + // func valueIndex(v ref, i int) ref + "syscall/js.valueIndex": (sp) => { + sp >>>= 0; + storeValue(sp + 24, Reflect.get(loadValue(sp + 8), getInt64(sp + 16))); + }, + + // valueSetIndex(v ref, i int, x ref) + "syscall/js.valueSetIndex": (sp) => { + sp >>>= 0; + Reflect.set(loadValue(sp + 8), getInt64(sp + 16), loadValue(sp + 24)); + }, + + // func valueCall(v ref, m string, args []ref) (ref, bool) + "syscall/js.valueCall": (sp) => { + sp >>>= 0; + try { + const v = loadValue(sp + 8); + const m = Reflect.get(v, loadString(sp + 16)); + const args = loadSliceOfValues(sp + 32); + const result = Reflect.apply(m, v, args); + sp = this._inst.exports.getsp() >>> 0; // see comment above + storeValue(sp + 56, result); + this.mem.setUint8(sp + 64, 1); + } catch (err) { + sp = this._inst.exports.getsp() >>> 0; // see comment above + storeValue(sp + 56, err); + this.mem.setUint8(sp + 64, 0); + } + }, + + // func valueInvoke(v ref, args []ref) (ref, bool) + "syscall/js.valueInvoke": (sp) => { + sp >>>= 0; + try { + const v = loadValue(sp + 8); + const args = loadSliceOfValues(sp + 16); + const result = Reflect.apply(v, undefined, args); + sp = this._inst.exports.getsp() >>> 0; // see comment above + storeValue(sp + 40, result); + this.mem.setUint8(sp + 48, 1); + } catch (err) { + sp = this._inst.exports.getsp() >>> 0; // see comment above + storeValue(sp + 40, err); + this.mem.setUint8(sp + 48, 0); + } + }, + + // func valueNew(v ref, args []ref) (ref, bool) + "syscall/js.valueNew": (sp) => { + sp >>>= 0; + try { + const v = loadValue(sp + 8); + const args = loadSliceOfValues(sp + 16); + const result = Reflect.construct(v, args); + sp = this._inst.exports.getsp() >>> 0; // see comment above + storeValue(sp + 40, result); + this.mem.setUint8(sp + 48, 1); + } catch (err) { + sp = this._inst.exports.getsp() >>> 0; // see comment above + storeValue(sp + 40, err); + this.mem.setUint8(sp + 48, 0); + } + }, + + // func valueLength(v ref) int + "syscall/js.valueLength": (sp) => { + sp >>>= 0; + setInt64(sp + 16, parseInt(loadValue(sp + 8).length)); + }, + + // valuePrepareString(v ref) (ref, int) + "syscall/js.valuePrepareString": (sp) => { + sp >>>= 0; + const str = encoder.encode(String(loadValue(sp + 8))); + storeValue(sp + 16, str); + setInt64(sp + 24, str.length); + }, + + // valueLoadString(v ref, b []byte) + "syscall/js.valueLoadString": (sp) => { + sp >>>= 0; + const str = loadValue(sp + 8); + loadSlice(sp + 16).set(str); + }, + + // func valueInstanceOf(v ref, t ref) bool + "syscall/js.valueInstanceOf": (sp) => { + sp >>>= 0; + this.mem.setUint8(sp + 24, (loadValue(sp + 8) instanceof loadValue(sp + 16)) ? 1 : 0); + }, + + // func copyBytesToGo(dst []byte, src ref) (int, bool) + "syscall/js.copyBytesToGo": (sp) => { + sp >>>= 0; + const dst = loadSlice(sp + 8); + const src = loadValue(sp + 32); + if (!(src instanceof Uint8Array || src instanceof Uint8ClampedArray)) { + this.mem.setUint8(sp + 48, 0); + return; + } + const toCopy = src.subarray(0, dst.length); + dst.set(toCopy); + setInt64(sp + 40, toCopy.length); + this.mem.setUint8(sp + 48, 1); + }, + + // func copyBytesToJS(dst ref, src []byte) (int, bool) + "syscall/js.copyBytesToJS": (sp) => { + sp >>>= 0; + const dst = loadValue(sp + 8); + const src = loadSlice(sp + 16); + if (!(dst instanceof Uint8Array || dst instanceof Uint8ClampedArray)) { + this.mem.setUint8(sp + 48, 0); + return; + } + const toCopy = src.subarray(0, dst.length); + dst.set(toCopy); + setInt64(sp + 40, toCopy.length); + this.mem.setUint8(sp + 48, 1); + }, + + "debug": (value) => { + console.log(value); + }, + } + }; + } + + async run(instance) { + if (!(instance instanceof WebAssembly.Instance)) { + throw new Error("Go.run: WebAssembly.Instance expected"); + } + this._inst = instance; + this.mem = new DataView(this._inst.exports.mem.buffer); + this._values = [ // JS values that Go currently has references to, indexed by reference id + NaN, + 0, + null, + true, + false, + globalThis, + this, + ]; + this._goRefCounts = new Array(this._values.length).fill(Infinity); // number of references that Go has to a JS value, indexed by reference id + this._ids = new Map([ // mapping from JS values to reference ids + [0, 1], + [null, 2], + [true, 3], + [false, 4], + [globalThis, 5], + [this, 6], + ]); + this._idPool = []; // unused ids that have been garbage collected + this.exited = false; // whether the Go program has exited + + // Pass command line arguments and environment variables to WebAssembly by writing them to the linear memory. + let offset = 4096; + + const strPtr = (str) => { + const ptr = offset; + const bytes = encoder.encode(str + "\0"); + new Uint8Array(this.mem.buffer, offset, bytes.length).set(bytes); + offset += bytes.length; + if (offset % 8 !== 0) { + offset += 8 - (offset % 8); + } + return ptr; + }; + + const argc = this.argv.length; + + const argvPtrs = []; + this.argv.forEach((arg) => { + argvPtrs.push(strPtr(arg)); + }); + argvPtrs.push(0); + + const keys = Object.keys(this.env).sort(); + keys.forEach((key) => { + argvPtrs.push(strPtr(`${key}=${this.env[key]}`)); + }); + argvPtrs.push(0); + + const argv = offset; + argvPtrs.forEach((ptr) => { + this.mem.setUint32(offset, ptr, true); + this.mem.setUint32(offset + 4, 0, true); + offset += 8; + }); + + // The linker guarantees global data starts from at least wasmMinDataAddr. + // Keep in sync with cmd/link/internal/ld/data.go:wasmMinDataAddr. + const wasmMinDataAddr = 4096 + 8192; + if (offset >= wasmMinDataAddr) { + throw new Error("total length of command line and environment variables exceeds limit"); + } + + this._inst.exports.run(argc, argv); + if (this.exited) { + this._resolveExitPromise(); + } + await this._exitPromise; + } + + _resume() { + if (this.exited) { + throw new Error("Go program has already exited"); + } + this._inst.exports.resume(); + if (this.exited) { + this._resolveExitPromise(); + } + } + + _makeFuncWrapper(id) { + const go = this; + return function () { + const event = { id: id, this: this, args: arguments }; + go._pendingEvent = event; + go._resume(); + return event.result; + }; + } + } +})(); + +// @license-end \ No newline at end of file diff --git a/static/svg/add.svg b/static/svg/add.svg deleted file mode 100644 index f6cd3df..0000000 --- a/static/svg/add.svg +++ /dev/null @@ -1 +0,0 @@ - \ No newline at end of file diff --git a/static/svg/code.svg b/static/svg/code.svg deleted file mode 100644 index 8ef5c55..0000000 --- a/static/svg/code.svg +++ /dev/null @@ -1 +0,0 @@ - \ No newline at end of file diff --git a/static/svg/delete.svg b/static/svg/delete.svg deleted file mode 100644 index 560d174..0000000 --- a/static/svg/delete.svg +++ /dev/null @@ -1 +0,0 @@ - \ No newline at end of file diff --git a/static/svg/delete_forever.svg b/static/svg/delete_forever.svg deleted file mode 100644 index 63ae26d..0000000 --- a/static/svg/delete_forever.svg +++ /dev/null @@ -1 +0,0 @@ - \ No newline at end of file diff --git a/static/svg/device_other.svg b/static/svg/device_other.svg index f8b0038..410be0d 100644 --- a/static/svg/device_other.svg +++ b/static/svg/device_other.svg @@ -1 +1 @@ - \ No newline at end of file + \ No newline at end of file diff --git a/static/svg/device_terminal.svg b/static/svg/device_terminal.svg new file mode 100644 index 0000000..f8b0038 --- /dev/null +++ b/static/svg/device_terminal.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/static/svg/download.svg b/static/svg/download.svg deleted file mode 100644 index eb90940..0000000 --- a/static/svg/download.svg +++ /dev/null @@ -1 +0,0 @@ - \ No newline at end of file diff --git a/static/svg/favicon.svg b/static/svg/favicon.svg deleted file mode 100644 index 6e7f325..0000000 --- a/static/svg/favicon.svg +++ /dev/null @@ -1,55 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/static/svg/flathubdark.svg b/static/svg/flathubdark.svg deleted file mode 100644 index 1db1173..0000000 --- a/static/svg/flathubdark.svg +++ /dev/null @@ -1 +0,0 @@ - \ No newline at end of file diff --git a/static/svg/flathublight.svg b/static/svg/flathublight.svg deleted file mode 100644 index 060a2db..0000000 --- a/static/svg/flathublight.svg +++ /dev/null @@ -1 +0,0 @@ - \ No newline at end of file diff --git a/static/svg/forum.svg b/static/svg/forum.svg deleted file mode 100644 index 6f5d8d7..0000000 --- a/static/svg/forum.svg +++ /dev/null @@ -1 +0,0 @@ - \ No newline at end of file diff --git a/static/svg/gopher.svg b/static/svg/gopher.svg new file mode 100644 index 0000000..d6451bf --- /dev/null +++ b/static/svg/gopher.svg @@ -0,0 +1,174 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/static/svg/info.svg b/static/svg/info.svg deleted file mode 100644 index 05606f4..0000000 --- a/static/svg/info.svg +++ /dev/null @@ -1 +0,0 @@ - \ No newline at end of file diff --git a/static/svg/list.svg b/static/svg/list.svg deleted file mode 100644 index 339b1f5..0000000 --- a/static/svg/list.svg +++ /dev/null @@ -1 +0,0 @@ - \ No newline at end of file diff --git a/static/svg/logout.svg b/static/svg/logout.svg deleted file mode 100644 index 0d2322a..0000000 --- a/static/svg/logout.svg +++ /dev/null @@ -1 +0,0 @@ - \ No newline at end of file diff --git a/static/svg/phonelink_lock.svg b/static/svg/phonelink_lock.svg deleted file mode 100644 index 6c54fdf..0000000 --- a/static/svg/phonelink_lock.svg +++ /dev/null @@ -1 +0,0 @@ - \ No newline at end of file diff --git a/static/wasm/hashcat.wasm b/static/wasm/hashcat.wasm new file mode 100755 index 0000000..c035eb0 Binary files /dev/null and b/static/wasm/hashcat.wasm differ diff --git a/templates/acct.html b/templates/acct.html deleted file mode 100644 index 4126610..0000000 --- a/templates/acct.html +++ /dev/null @@ -1,61 +0,0 @@ - - - - - - User settings - {{ .identifier }} - - - -
-

Account settings

-

Loading...

-

-
- -
- - diff --git a/templates/aeskeyshare.html b/templates/aeskeyshare.html index ffb82fd..f445f34 100644 --- a/templates/aeskeyshare.html +++ b/templates/aeskeyshare.html @@ -1 +1,20 @@ -To be implemented \ No newline at end of file + + + + Key Exchange - {{ .identifier }} + + + + + + + + + + +
+

Relaying back information, please wait...

+

Processing information sent...

+
+ + diff --git a/templates/dashboard.html b/templates/dashboard.html index 42e8322..6e8ff36 100644 --- a/templates/dashboard.html +++ b/templates/dashboard.html @@ -1,25 +1,51 @@ - - Dashboard - {{ .identifier }} + + + + + -
-

Submit a new OAuth2 App

-

-

App Name:

- -

Redirect URI:

- -
- +

Dashboard

+

Welcome to the Burgerauth dashboard!

+
-
-

Your existing apps

+ +
+
+

Account settings

+

Loading...

+

+
+ +
+
+

Sessions

+
+
+ diff --git a/templates/fancy.html b/templates/fancy.html new file mode 100644 index 0000000..c57c6c9 --- /dev/null +++ b/templates/fancy.html @@ -0,0 +1,45 @@ + + + + Welcome to Burgerauth! + + + + + + + + +
+

Burgerauth

+

Simple. Secure. Speedy.

+

Burgerauth is a Free and Open Source OAuth2 client, great for corporate single-sign on and cross-application authentication.

+
+
+

Works anywhere

+

Burgerauth really does work anywhere - it doesn't even require an external internet connection. Self-host on as many intranets as you want!

+
+ +
or
+ +
+
+
+

Virtue of Simplicity

+

Burgerauth is beyond simple. Unlike KeyCloak, which requires you to install OpenJDK, configure a database, configure providers, and so on, Burgerauth works right out of the box - all you need to do is copy the config, run the command, and Burgerauth takes care of the rest.

+
+
+

Gopher Powered

+ Our lord and savior, the Gopher +

Burgerauth uses the Go programming language for increased stability and amazing speed. It has error handling you'll find nowhere else (sane)!

+
+
+

Secure by Design

+

Burgerauth is designed with security in mind. It doesn't use any cross-site cookies, hacky # query strings and keeps everything in first-party isolation. Even the server doesn't get to see your encryption keys.

+
+
+

Open Source

+

Burgerauth is open source, meaning you can view the source code, modify it, and even host your own instance. It's licensed under the AGPL License, so you can do whatever you want with it, as long as you share the love and keep it free!

+
+ + \ No newline at end of file diff --git a/templates/index.html b/templates/index.html new file mode 100644 index 0000000..e197f2f --- /dev/null +++ b/templates/index.html @@ -0,0 +1,20 @@ + + + + {{ .identifier }} + + + + + + + + +
+

Welcome to {{ .identifier }}!

+

{{ .identifier }} is an instance of Burgerauth, an open-source OAuth2 client.

+

This is running Burgerauth version 1.0.0

+ +
+ + \ No newline at end of file diff --git a/templates/keyexchangeclient.html b/templates/keyexchangeclient.html new file mode 100644 index 0000000..fd7c307 --- /dev/null +++ b/templates/keyexchangeclient.html @@ -0,0 +1,18 @@ + + + + Exchange Key - {{ .identifier }} + + + + + + + + +
+

Communicating with Burgerauth, please wait...

+
+ + + diff --git a/templates/keyexchangetester.html b/templates/keyexchangetester.html new file mode 100644 index 0000000..03ea9c5 --- /dev/null +++ b/templates/keyexchangetester.html @@ -0,0 +1,19 @@ + + + + Exchange Key - {{ .identifier }} + + + + + + + + + +
+

Communicating with Burgerauth, please wait...

+
+ + + diff --git a/templates/login.html b/templates/login.html index aa99df6..10b9f8c 100644 --- a/templates/login.html +++ b/templates/login.html @@ -3,12 +3,12 @@ Login - {{ .identifier }} - - + + - - + + @@ -16,14 +16,16 @@

Login

- - - - +
+
+ + +
+ +

-

Don't have an account? If so, Create one here!

Privacy & Terms
diff --git a/templates/logout.html b/templates/logout.html index b504fad..3c0a23d 100644 --- a/templates/logout.html +++ b/templates/logout.html @@ -3,8 +3,9 @@ Log out - {{ .identifier }} - - + + +

Logging out...

diff --git a/templates/main.html b/templates/main.html index b037723..6d51c87 100644 --- a/templates/main.html +++ b/templates/main.html @@ -1,19 +1,18 @@ Authorize application - {{ .identifier }} - - + + - - + + -

Authorise Application

Loading...

@@ -22,6 +21,8 @@
+
+ Return to Dashboard
diff --git a/templates/refresh.html b/templates/refresh.html new file mode 100644 index 0000000..28058ec --- /dev/null +++ b/templates/refresh.html @@ -0,0 +1,14 @@ + + + + + + + + + Please wait... + + +

Please wait...

+ + \ No newline at end of file diff --git a/templates/signup.html b/templates/signup.html index c7dc08e..fbf3e67 100644 --- a/templates/signup.html +++ b/templates/signup.html @@ -3,12 +3,13 @@ Signup - {{ .identifier }} - - + + - - + + + @@ -18,14 +19,20 @@

Signup

Signup to {{ .identifier }}!

- - - Captcha - + + + + + + + + + +
Username:
Password:

-

-

Already have an account? If so, Login instead!

-

Please note that it's impossible to reset your password, do not forget it!

+ + +

Privacy & Terms diff --git a/templates/testapp.html b/templates/testapp.html new file mode 100644 index 0000000..3639baf --- /dev/null +++ b/templates/testapp.html @@ -0,0 +1,21 @@ + + + + Tester - {{ .identifier }} + + + + + + + + + + + +
+

{{ .identifier }} Tester

+ +
+ +