diff --git a/aid/aid.go b/aid/aid.go index fb96140..03ce41e 100644 --- a/aid/aid.go +++ b/aid/aid.go @@ -1,7 +1,7 @@ package aid import ( - "math/rand" + m "math/rand" "os" "os/signal" "strconv" @@ -32,7 +32,7 @@ func RandomString(n int) string { s := make([]rune, n) for i := range s { - s[i] = letters[rand.Intn(len(letters))] + s[i] = letters[m.Intn(len(letters))] } return string(s) } diff --git a/aid/crypto.go b/aid/crypto.go new file mode 100644 index 0000000..4229554 --- /dev/null +++ b/aid/crypto.go @@ -0,0 +1,74 @@ +package aid + +import ( + "crypto/rand" + "crypto/rsa" + "crypto/sha256" + "crypto/x509" + "encoding/base64" + "encoding/hex" +) + +type keyPair struct { + PrivateKey rsa.PrivateKey + PublicKey rsa.PublicKey +} + +var KeyPair = GeneratePublicPrivateKeyPair() + +func GeneratePublicPrivateKeyPair() keyPair { + privateKey, _ := rsa.GenerateKey(rand.Reader, 2048) + publicKey := privateKey.PublicKey + + return keyPair{ + PrivateKey: *privateKey, + PublicKey: publicKey, + } +} + +func (k *keyPair) EncryptAndSign(message []byte) ([]byte, []byte) { + encryptedMessage, _ := rsa.EncryptPKCS1v15(rand.Reader, &k.PublicKey, message) + signature, _ := rsa.SignPKCS1v15(rand.Reader, &k.PrivateKey, 0, encryptedMessage) + + return encryptedMessage, signature +} + +func (k *keyPair) EncryptAndSignB64(message []byte) (string, string) { + encryptedMessage, signature := k.EncryptAndSign(message) + + return Base64Encode(encryptedMessage), Base64Encode(signature) +} + +func (k *keyPair) DecryptAndVerify(encryptedMessage []byte, signature []byte) []byte { + decryptedMessage, _ := rsa.DecryptPKCS1v15(rand.Reader, &k.PrivateKey, encryptedMessage) + _ = rsa.VerifyPKCS1v15(&k.PublicKey, 0, encryptedMessage, signature) + + return decryptedMessage +} + +func (k *keyPair) ExportPrivateKey() []byte { + privateKey := x509.MarshalPKCS1PrivateKey(&k.PrivateKey) + return privateKey +} + +func (k *keyPair) ExportPublicKey() []byte { + publicKey := x509.MarshalPKCS1PublicKey(&k.PublicKey) + return publicKey +} + +func Base64Encode(input []byte) string { + return base64.StdEncoding.EncodeToString(input) +} + +func Base64Decode(input string) ([]byte, bool) { + data, err := base64.StdEncoding.DecodeString(input) + if err != nil { + return []byte{}, true + } + return data, false +} + +func Hash(input []byte) string { + shaBytes := sha256.Sum256(input) + return hex.EncodeToString(shaBytes[:]) +} \ No newline at end of file diff --git a/aid/token.go b/aid/token.go deleted file mode 100644 index 9454b60..0000000 --- a/aid/token.go +++ /dev/null @@ -1,44 +0,0 @@ -package aid - -import ( - "fmt" - - "github.com/golang-jwt/jwt/v5" -) - -func JWTSign(m JSON) (string, error) { - claims := jwt.MapClaims{} - - for k, v := range m { - claims[k] = v - } - - token := jwt.NewWithClaims(jwt.SigningMethodHS256, claims) - return token.SignedString([]byte(Config.JWT.Secret)) -} - -func JWTVerify(tokenString string) (JSON, error) { - token, err := jwt.Parse(tokenString, func(token *jwt.Token) (interface{}, error) { - return []byte(Config.JWT.Secret), nil - }) - - if err != nil { - return nil, err - } - - if !token.Valid { - return nil, fmt.Errorf("invalid token") - } - - claims, ok := token.Claims.(jwt.MapClaims) - if !ok { - return nil, fmt.Errorf("invalid claims") - } - - json := JSON{} - for k, v := range claims { - json[k] = v - } - - return json, nil -} \ No newline at end of file diff --git a/aid/type.go b/aid/type.go index 8b87c23..60f24b3 100644 --- a/aid/type.go +++ b/aid/type.go @@ -1,3 +1,16 @@ package aid -type JSON map[string]interface{} \ No newline at end of file +import "github.com/goccy/go-json" + +type JSON map[string]interface{} + +func JSONFromBytes(input []byte) JSON { + var output JSON + json.Unmarshal(input, &output) + return output +} + +func (j *JSON) ToBytes() []byte { + json, _ := json.Marshal(j) + return json +} \ No newline at end of file diff --git a/fortnite/person.go b/fortnite/person.go index cad9720..ec1e962 100644 --- a/fortnite/person.go +++ b/fortnite/person.go @@ -145,4 +145,6 @@ func GiveEverything(person *p.Person) { } storage.Repo.BulkCreateItems(&items) + aid.Print("Gave everything to " + person.DisplayName) + person.Save() } \ No newline at end of file diff --git a/fortnite/shop.go b/fortnite/shop.go index e844d0f..fc125ad 100644 --- a/fortnite/shop.go +++ b/fortnite/shop.go @@ -366,7 +366,7 @@ func GenerateRandomStorefront() { } minimumItems := 8 - if aid.Config.Fortnite.Season < 14 { + if aid.Config.Fortnite.Season < 11 { minimumItems = 3 } diff --git a/handlers/auth.go b/handlers/auth.go index b704f30..1148f2d 100644 --- a/handlers/auth.go +++ b/handlers/auth.go @@ -6,6 +6,7 @@ import ( "github.com/ectrc/snow/aid" p "github.com/ectrc/snow/person" + "github.com/ectrc/snow/storage" "github.com/gofiber/fiber/v2" ) @@ -37,22 +38,19 @@ func PostFortniteToken(c *fiber.Ctx) error { } func PostTokenClientCredentials(c *fiber.Ctx, body *FortniteTokenBody) error { - credentials, err := aid.JWTSign(aid.JSON{ - "snow_id": 0, // custom - "creation_date": time.Now().Format("2006-01-02T15:04:05.999Z"), - }) - if err != nil { - return c.Status(fiber.StatusInternalServerError).JSON(aid.ErrorInternalServer) - } + client, sig := aid.KeyPair.EncryptAndSignB64([]byte(c.IP())) + hash := aid.Hash([]byte(client + "." + sig)) return c.Status(fiber.StatusOK).JSON(aid.JSON{ - "access_token": "eg1~"+credentials, + "access_token": hash, "token_type": "bearer", "client_id": c.IP(), "client_service": "fortnite", "internal_client": true, "expires_in": 3600, "expires_at": time.Now().Add(time.Hour).Format("2006-01-02T15:04:05.999Z"), + "product_id": "prod-fn", + "sandbox_id": "fn", }) } @@ -66,24 +64,31 @@ func PostTokenPassword(c *fiber.Ctx, body *FortniteTokenBody) error { return c.Status(fiber.StatusBadRequest).JSON(aid.ErrorBadRequest("No Account Found")) } - access, err := aid.JWTSign(aid.JSON{ - "snow_id": person.ID, // custom - "creation_date": time.Now().Format("2006-01-02T15:04:05.999Z"), - }) - if err != nil { - return c.Status(fiber.StatusInternalServerError).JSON(aid.ErrorInternalServer) - } + access, ac_sig := aid.KeyPair.EncryptAndSignB64([]byte(person.ID)) + ac_hash := aid.Hash([]byte(access + "." + ac_sig)) - refresh, err := aid.JWTSign(aid.JSON{ - "snow_id": person.ID, - "creation_date": time.Now().Format("2006-01-02T15:04:05.999Z"), - }) - if err != nil { - return c.Status(fiber.StatusInternalServerError).JSON(aid.ErrorInternalServer) + ac_token := &storage.DB_GameToken{ + ID: ac_hash, + PersonID: person.ID, + AccessToken: access + "." + ac_sig, + Type: "access", } + storage.Repo.SaveToken(ac_token) + + refresh, re_sig := aid.KeyPair.EncryptAndSignB64([]byte(person.ID)) + re_hash := aid.Hash([]byte(refresh + "." + re_sig)) + + re_token := &storage.DB_GameToken{ + ID: re_hash, + PersonID: person.ID, + AccessToken: refresh + "." + re_sig, + Type: "refresh", + } + storage.Repo.SaveToken(re_token) return c.Status(fiber.StatusOK).JSON(aid.JSON{ - "access_token": "eg1~"+access, + // "access_token": access + "." + ac_sig, + "access_token": ac_hash, "account_id": person.ID, "client_id": c.IP(), "client_service": "fortnite", @@ -95,8 +100,11 @@ func PostTokenPassword(c *fiber.Ctx, body *FortniteTokenBody) error { "internal_client": true, "refresh_expires": 86400, "refresh_expires_at": time.Now().Add(time.Hour * 24).Format("2006-01-02T15:04:05.999Z"), - "refresh_token": "eg1~"+refresh, + // "refresh_token": refresh + "." + re_sig, + "refresh_token": re_hash, "token_type": "bearer", + "product_id": "prod-fn", + "sandbox_id": "fn", }) } @@ -105,21 +113,13 @@ func GetOAuthVerify(c *fiber.Ctx) error { if auth == "" { return c.Status(fiber.StatusForbidden).JSON(aid.ErrorBadRequest("Authorization Header is empty")) } - real := strings.ReplaceAll(auth, "bearer eg1~", "") + real := strings.ReplaceAll(auth, "bearer ", "") - claims, err := aid.JWTVerify(real) - if err != nil { - return c.Status(fiber.StatusForbidden).JSON(aid.ErrorBadRequest("Invalid Access Token")) - } - - if claims["snow_id"] == nil { - return c.Status(fiber.StatusForbidden).JSON(aid.ErrorBadRequest("Invalid Access Token")) - } - - snowId, ok := claims["snow_id"].(string) - if !ok { + found := storage.Repo.GetToken(real) + if found == nil { return c.Status(fiber.StatusForbidden).JSON(aid.ErrorBadRequest("Invalid Access Token")) } + snowId := found.PersonID person := p.Find(snowId) if person == nil { @@ -128,7 +128,7 @@ func GetOAuthVerify(c *fiber.Ctx) error { return c.Status(fiber.StatusOK).JSON(aid.JSON{ "app": "fortnite", - "token": "eg1~"+real, + "token": real, "token_type": "bearer", "expires_at": time.Now().Add(time.Hour * 24).Format("2006-01-02T15:04:05.999Z"), "expires_in": 86400, @@ -140,6 +140,8 @@ func GetOAuthVerify(c *fiber.Ctx) error { "in_app_id": person.ID, "account_id": person.ID, "displayName": person.DisplayName, + "product_id": "prod-fn", + "sandbox_id": "fn", }) } @@ -148,21 +150,13 @@ func FortniteMiddleware(c *fiber.Ctx) error { if auth == "" { return c.Status(fiber.StatusForbidden).JSON(aid.ErrorBadRequest("Authorization Header is empty")) } - real := strings.ReplaceAll(auth, "bearer eg1~", "") + real := strings.ReplaceAll(auth, "bearer ", "") - claims, err := aid.JWTVerify(real) - if err != nil { - return c.Status(fiber.StatusForbidden).JSON(aid.ErrorBadRequest("Invalid Access Token")) - } - - if claims["snow_id"] == nil { - return c.Status(fiber.StatusForbidden).JSON(aid.ErrorBadRequest("Invalid Access Token")) - } - - snowId, ok := claims["snow_id"].(string) - if !ok { + found := storage.Repo.GetToken(real) + if found == nil { return c.Status(fiber.StatusForbidden).JSON(aid.ErrorBadRequest("Invalid Access Token")) } + snowId := found.PersonID person := p.Find(snowId) if person == nil { @@ -179,23 +173,11 @@ func FrontendMiddleware(c *fiber.Ctx) error { return c.Status(fiber.StatusForbidden).JSON(aid.ErrorBadRequest("Authorization Header is empty")) } - claims, err := aid.JWTVerify(auth) - if err != nil { - return c.Status(fiber.StatusForbidden).JSON(aid.ErrorBadRequest("Invalid Access Token")) - } - - if claims["snow_id"] == nil { - return c.Status(fiber.StatusForbidden).JSON(aid.ErrorBadRequest("Invalid Access Token")) - } - - if claims["frontend"] == nil { - return c.Status(fiber.StatusForbidden).JSON(aid.ErrorBadRequest("Invalid Claims")) - } - - snowId, ok := claims["snow_id"].(string) - if !ok { + found := storage.Repo.GetToken(auth) + if found == nil { return c.Status(fiber.StatusForbidden).JSON(aid.ErrorBadRequest("Invalid Access Token")) } + snowId := found.PersonID person := p.Find(snowId) if person == nil { diff --git a/handlers/discord.go b/handlers/discord.go index 2d1ce1e..a2e8ec9 100644 --- a/handlers/discord.go +++ b/handlers/discord.go @@ -3,7 +3,6 @@ package handlers import ( "net/http" "net/url" - "time" "github.com/ectrc/snow/aid" p "github.com/ectrc/snow/person" @@ -75,18 +74,14 @@ func GetDiscordOAuthURL(c *fiber.Ctx) error { person.Discord.Banner = user.Banner storage.Repo.SaveDiscordPerson(person.Discord) - access, err := aid.JWTSign(aid.JSON{ - "snow_id": person.ID, // custom - "frontend": true, - "creation_date": time.Now().Format("2006-01-02T15:04:05.999Z"), - }) + access, sig := aid.KeyPair.EncryptAndSignB64([]byte(person.ID + ".frontend")) if err != nil { return c.Status(fiber.StatusInternalServerError).JSON(aid.ErrorInternalServer) } c.Cookie(&fiber.Cookie{ Name: "access_token", - Value: access, + Value: access + "." + sig, }) return c.Redirect(aid.Config.API.Host + aid.Config.API.FrontendPort + "/attempt") } \ No newline at end of file diff --git a/main.go b/main.go index a83b070..5695aea 100644 --- a/main.go +++ b/main.go @@ -8,6 +8,7 @@ import ( "github.com/ectrc/snow/discord" "github.com/ectrc/snow/fortnite" "github.com/ectrc/snow/handlers" + "github.com/ectrc/snow/person" "github.com/ectrc/snow/storage" "github.com/goccy/go-json" @@ -42,8 +43,11 @@ func init() { fortnite.PreloadCosmetics(aid.Config.Fortnite.Season) fortnite.GenerateRandomStorefront() fortnite.GeneratePlaylistImages() -} + if found := person.FindByDisplay("god"); found == nil { + fortnite.NewFortnitePerson("god", true) + } +} func main() { r := fiber.New(fiber.Config{ DisableStartupMessage: true, diff --git a/readme.md b/readme.md index 46c4439..c209ffb 100644 --- a/readme.md +++ b/readme.md @@ -21,15 +21,12 @@ Performance first, universal Fortnite private server backend written in Go. ### Supported - **_Chapter 1 Season 2_** `Fortnite+Release-2.5-CL-3889387-Windows` I started with this build of the game as it requires more work to get working, this means snow can support _most_ versions of the game. +- **_Chapter 1 Season 4_** `Fortnite+Release-4.5-CL-4159770-Windows` Fixed the access token issue causing errors logging in! - **_Chapter 1 Season 5_** `Fortnite+Release-5.41-CL-4363240-Windows` This build was used to make sure challenges, variants and lobby backgrounds work. - **_Chapter 1 Season 8_** `Fortnite+Release-8.51-CL-6165369-Windows` Fixed the invisible player bug caused by invalid account responses. Also fixed the issue with the item shop spamming the api. - **_Chapter 2 Season 2_** `Fortnite+Release-12.41-CL-12905909-Windows` Item Shop length is correct, also Creative profile stopping login has also been fixed. - **_Chapter 3 Season 1_** `Fortnite+Release-19.10-CL-Unknown-Windows` This is a very new build of fortnite that introfuces alot of different methods e.g. locker data is now stored as an item. Every MCP action is now fully working and tested. You need to start using easy anticheat otherwise this will not work. -### Broken - -- **_Chapter 1 Season 4_** `Fortnite+Release-4.5-CL-4159770-Windows` Does not accept the Access Token for user authentication. I have some ideas why however not planned for a fix. - ## How do I use this? - **[Discord OAuth Setup Guide](oauth.md)** How to setup Discord OAuth for your backend. This enabled the ability to login to the web app with Discord. diff --git a/storage/postgres.go b/storage/postgres.go index 8a4efe9..dd9a060 100644 --- a/storage/postgres.go +++ b/storage/postgres.go @@ -43,9 +43,10 @@ func (s *PostgresStorage) MigrateAll() { s.Migrate(&DB_Loot{}, "Loot") s.Migrate(&DB_VariantChannel{}, "Variants") s.Migrate(&DB_PAttribute{}, "Attributes") - s.Migrate(&DB_TemporaryCode{}, "Exchanges") + s.Migrate(&DB_TemporaryCode{}, "ExchangeCodes") s.Migrate(&DB_DiscordPerson{}, "Discords") s.Migrate(&DB_SeasonStat{}, "Stats") + s.Migrate(&DB_GameToken{}, "GameTokens") } func (s *PostgresStorage) DropTables() { @@ -241,4 +242,23 @@ func (s *PostgresStorage) SaveDiscordPerson(discordPerson *DB_DiscordPerson) { func (s *PostgresStorage) DeleteDiscordPerson(discordPersonId string) { s.Postgres.Delete(&DB_DiscordPerson{}, "id = ?", discordPersonId) +} + +func (s *PostgresStorage) SaveToken(token *DB_GameToken) { + s.Postgres.Save(token) +} + +func (s *PostgresStorage) DeleteToken(tokenId string) { + s.Postgres.Delete(&DB_GameToken{}, "id = ?", tokenId) +} + +func (s *PostgresStorage) GetToken(tokenId string) *DB_GameToken { + var token DB_GameToken + s.Postgres.Model(&DB_GameToken{}).Where("id = ?", tokenId).Find(&token) + + if token.ID == "" { + return nil + } + + return &token } \ No newline at end of file diff --git a/storage/storage.go b/storage/storage.go index 52af0e2..cd9909a 100644 --- a/storage/storage.go +++ b/storage/storage.go @@ -48,6 +48,10 @@ type Storage interface { SaveDiscordPerson(person *DB_DiscordPerson) DeleteDiscordPerson(personId string) + + SaveToken(token *DB_GameToken) + GetToken(tokenId string) *DB_GameToken + DeleteToken(tokenId string) } type Repository struct { @@ -189,4 +193,16 @@ func (r *Repository) SaveDiscordPerson(person *DB_DiscordPerson) { func (r *Repository) DeleteDiscordPerson(personId string) { r.Storage.DeleteDiscordPerson(personId) +} + +func (r *Repository) SaveToken(token *DB_GameToken) { + r.Storage.SaveToken(token) +} + +func (r *Repository) GetToken(tokenId string) *DB_GameToken { + return r.Storage.GetToken(tokenId) +} + +func (r *Repository) DeleteToken(tokenId string) { + r.Storage.DeleteToken(tokenId) } \ No newline at end of file diff --git a/storage/tables.go b/storage/tables.go index a7447a3..a57ceba 100644 --- a/storage/tables.go +++ b/storage/tables.go @@ -146,7 +146,7 @@ type DB_TemporaryCode struct { } func (DB_TemporaryCode) TableName() string { - return "Exchanges" + return "ExchangeCodes" } type DB_DiscordPerson struct { @@ -177,4 +177,16 @@ type DB_SeasonStat struct { func (DB_SeasonStat) TableName() string { return "Stats" +} + +type DB_GameToken struct { + ID string `gorm:"primary_key"` + PersonID string + AccessToken string + Type string + ExpiresAt int64 +} + +func (DB_GameToken) TableName() string { + return "GameTokens" } \ No newline at end of file