diff --git a/aid/crypto.go b/aid/crypto.go index 4229554..ce2440b 100644 --- a/aid/crypto.go +++ b/aid/crypto.go @@ -46,6 +46,20 @@ func (k *keyPair) DecryptAndVerify(encryptedMessage []byte, signature []byte) [] return decryptedMessage } +func (k *keyPair) DecryptAndVerifyB64(encryptedMessage string, signature string) ([]byte, bool) { + encryptedMessageBytes, err := Base64Decode(encryptedMessage) + if err { + return []byte{}, true + } + + signatureBytes, err := Base64Decode(signature) + if err { + return []byte{}, true + } + + return k.DecryptAndVerify(encryptedMessageBytes, signatureBytes), false +} + func (k *keyPair) ExportPrivateKey() []byte { privateKey := x509.MarshalPKCS1PrivateKey(&k.PrivateKey) return privateKey diff --git a/aid/token.go b/aid/token.go new file mode 100644 index 0000000..9454b60 --- /dev/null +++ b/aid/token.go @@ -0,0 +1,44 @@ +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/go.mod b/go.mod index 64f4c0d..ecac7d1 100644 --- a/go.mod +++ b/go.mod @@ -5,7 +5,7 @@ go 1.21.3 require ( github.com/goccy/go-json v0.10.2 github.com/gofiber/fiber/v2 v2.50.0 - github.com/golang-jwt/jwt/v5 v5.0.0 + github.com/golang-jwt/jwt/v5 v5.2.0 github.com/google/uuid v1.4.0 github.com/lib/pq v1.10.9 github.com/r3labs/diff/v3 v3.0.1 diff --git a/go.sum b/go.sum index dab4c45..bc7a77b 100644 --- a/go.sum +++ b/go.sum @@ -97,6 +97,8 @@ github.com/gogo/protobuf v1.1.1/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7a github.com/gogo/protobuf v1.2.1/go.mod h1:hp+jE20tsWTFYpLwKvXlhS1hjn+gTNwPg2I6zVXpSg4= github.com/golang-jwt/jwt/v5 v5.0.0 h1:1n1XNM9hk7O9mnQoNBGolZvzebBQ7p93ULHRc28XJUE= github.com/golang-jwt/jwt/v5 v5.0.0/go.mod h1:pqrtFR0X4osieyHYxtmOUWsAWrfe1Q5UVIyoH402zdk= +github.com/golang-jwt/jwt/v5 v5.2.0 h1:d/ix8ftRUorsN+5eMIlF4T6J8CAt9rch3My2winC1Jw= +github.com/golang-jwt/jwt/v5 v5.2.0/go.mod h1:pqrtFR0X4osieyHYxtmOUWsAWrfe1Q5UVIyoH402zdk= github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q= github.com/golang/groupcache v0.0.0-20190129154638-5b532d6fd5ef/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= github.com/golang/groupcache v0.0.0-20190702054246-869f871628b6/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= diff --git a/handlers/auth.go b/handlers/auth.go index e7e2906..2cf2d11 100644 --- a/handlers/auth.go +++ b/handlers/auth.go @@ -6,7 +6,6 @@ import ( "github.com/ectrc/snow/aid" p "github.com/ectrc/snow/person" - "github.com/ectrc/snow/storage" "github.com/gofiber/fiber/v2" ) @@ -14,13 +13,16 @@ var ( oauthTokenGrantTypes = map[string]func(c *fiber.Ctx, body *FortniteTokenBody) error{ "client_credentials": PostTokenClientCredentials, "password": PostTokenPassword, + "exchange_code": PostTokenExchangeCode, } ) type FortniteTokenBody struct { GrantType string `form:"grant_type" binding:"required"` + ExchangeCode string `form:"exchange_code"` Username string `form:"username"` Password string `form:"password"` + TokenType string `form:"token_type"` } func PostFortniteToken(c *fiber.Ctx) error { @@ -42,18 +44,87 @@ func PostTokenClientCredentials(c *fiber.Ctx, body *FortniteTokenBody) error { hash := aid.Hash([]byte(client + "." + sig)) return c.Status(fiber.StatusOK).JSON(aid.JSON{ - "access_token": hash, + "access_token": "eg1~" + 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", }) } +func PostTokenExchangeCode(c *fiber.Ctx, body *FortniteTokenBody) error { + if body.ExchangeCode == "" { + return c.Status(fiber.StatusBadRequest).JSON(aid.ErrorBadRequest("Exchange Code is empty")) + } + + codeParts := strings.Split(body.ExchangeCode, ".") + if len(codeParts) != 2 { + return c.Status(fiber.StatusBadRequest).JSON(aid.ErrorBadRequest("Invalid Exchange Code")) + } + + code, failed := aid.KeyPair.DecryptAndVerifyB64(codeParts[0], codeParts[1]) + if failed { + return c.Status(fiber.StatusBadRequest).JSON(aid.ErrorBadRequest("Invalid Exchange Code")) + } + + personParts := strings.Split(string(code), "=") + if len(personParts) != 2 { + return c.Status(fiber.StatusBadRequest).JSON(aid.ErrorBadRequest("Invalid Exchange Code")) + } + + personId := personParts[0] + expire, err := time.Parse("2006-01-02T15:04:05.999Z", personParts[1]) + if err != nil { + return c.Status(fiber.StatusBadRequest).JSON(aid.ErrorBadRequest("Invalid Exchange Code")) + } + + if expire.Add(time.Hour).Before(time.Now()) { + return c.Status(fiber.StatusBadRequest).JSON(aid.ErrorBadRequest("Invalid Exchange Code")) + } + + person := p.Find(personId) + if person == nil { + return c.Status(fiber.StatusBadRequest).JSON(aid.ErrorBadRequest("Invalid Exchange Code")) + } + + 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) + } + + 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) + } + + return c.Status(fiber.StatusOK).JSON(aid.JSON{ + "access_token": "eg1~" + access, + "account_id": person.ID, + "client_id": c.IP(), + "client_service": "fortnite", + "app": "fortnite", + "device_id": "default", + "display_name": person.DisplayName, + "expires_at": time.Now().Add(time.Hour * 24).Format("2006-01-02T15:04:05.999Z"), + "expires_in": 86400, + "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, + "token_type": "bearer", + "product_id": "prod-fn", + "sandbox_id": "fn", + }) +} + func PostTokenPassword(c *fiber.Ctx, body *FortniteTokenBody) error { if body.Username == "" || body.Password == "" { return c.Status(fiber.StatusBadRequest).JSON(aid.ErrorBadRequest("Username/Password is empty")) @@ -64,31 +135,24 @@ func PostTokenPassword(c *fiber.Ctx, body *FortniteTokenBody) error { return c.Status(fiber.StatusBadRequest).JSON(aid.ErrorBadRequest("No Account Found")) } - access, ac_sig := aid.KeyPair.EncryptAndSignB64([]byte(person.ID)) - ac_hash := aid.Hash([]byte(access + "." + ac_sig)) - - ac_token := &storage.DB_GameToken{ - ID: ac_hash, - PersonID: person.ID, - AccessToken: access + "." + ac_sig, - Type: "access", + 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) } - 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", + 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) } - storage.Repo.SaveToken(re_token) return c.Status(fiber.StatusOK).JSON(aid.JSON{ - // "access_token": access + "." + ac_sig, - "access_token": ac_hash, + "access_token": "eg1~" + access, "account_id": person.ID, "client_id": c.IP(), "client_service": "fortnite", @@ -100,8 +164,7 @@ 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": refresh + "." + re_sig, - "refresh_token": re_hash, + "refresh_token": "eg1~" + refresh, "token_type": "bearer", "product_id": "prod-fn", "sandbox_id": "fn", @@ -113,13 +176,21 @@ 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 ", "") + real := strings.ReplaceAll(auth, "bearer eg1~", "") - found := storage.Repo.GetToken(real) - if found == nil { + 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 { return c.Status(fiber.StatusForbidden).JSON(aid.ErrorBadRequest("Invalid Access Token")) } - snowId := found.PersonID person := p.Find(snowId) if person == nil { @@ -150,13 +221,21 @@ func MiddlewareFortnite(c *fiber.Ctx) error { if auth == "" { return c.Status(fiber.StatusForbidden).JSON(aid.ErrorBadRequest("Authorization Header is empty")) } - real := strings.ReplaceAll(auth, "bearer ", "") + real := strings.ReplaceAll(auth, "bearer eg1~", "") - found := storage.Repo.GetToken(real) - if found == nil { + 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 { return c.Status(fiber.StatusForbidden).JSON(aid.ErrorBadRequest("Invalid Access Token")) } - snowId := found.PersonID person := p.Find(snowId) if person == nil { @@ -173,11 +252,23 @@ func MiddlewareWeb(c *fiber.Ctx) error { return c.Status(fiber.StatusForbidden).JSON(aid.ErrorBadRequest("Authorization Header is empty")) } - found := storage.Repo.GetToken(auth) - if found == nil { + 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 { 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/discovery.go b/handlers/discovery.go index 6c19b67..acc8561 100644 --- a/handlers/discovery.go +++ b/handlers/discovery.go @@ -154,6 +154,7 @@ func PostAssets(c *fiber.Ctx) error { func GetContentPages(c *fiber.Ctx) error { seasonString := strconv.Itoa(aid.Config.Fortnite.Season) + playlists := []aid.JSON{} for playlist := range fortnite.PlaylistImages { playlists = append(playlists, aid.JSON{ @@ -162,7 +163,21 @@ func GetContentPages(c *fiber.Ctx) error { "hidden": false, }) } - + + backgrounds := []aid.JSON{} + switch aid.Config.Fortnite.Season { + case 11: + backgrounds = append(backgrounds, aid.JSON{ + "key": "lobby", + "stage": "Winter19", + }) + default: + backgrounds = append(backgrounds, aid.JSON{ + "key": "lobby", + "stage": "season" + seasonString, + }) + } + return c.Status(fiber.StatusOK).JSON(aid.JSON{ "subgameselectdata": aid.JSON{ "saveTheWorldUnowned": aid.JSON{ @@ -195,16 +210,7 @@ func GetContentPages(c *fiber.Ctx) error { "lastModified": "0000-00-00T00:00:00.000Z", }, "dynamicbackgrounds": aid.JSON{ - "backgrounds": aid.JSON{"backgrounds": []aid.JSON{ - { - "key": "lobby", - "stage": "season" + seasonString, - }, - { - "key": "vault", - "stage": "season" + seasonString, - }, - }}, + "backgrounds": aid.JSON{"backgrounds": backgrounds}, "lastModified": "0000-00-00T00:00:00.000Z", }, "shopSections": aid.JSON{ diff --git a/handlers/lightswitch.go b/handlers/lightswitch.go index 5433c34..08d4255 100644 --- a/handlers/lightswitch.go +++ b/handlers/lightswitch.go @@ -66,6 +66,20 @@ func GetFortniteTimeline(c *fiber.Ctx) error { "activeSince": "0001-01-01T00:00:00Z", "eventType": "EventFlag.LobbySeason6Halloween", }) + case 11: + events = append(events, aid.JSON{ + "activeUntil": "9999-01-01T00:00:00.000Z", + "activeSince": "0001-01-01T00:00:00Z", + "eventType": "EventFlag.LTE_WinterFest2019", + }, aid.JSON{ + "activeUntil": "9999-01-01T00:00:00.000Z", + "activeSince": "0001-01-01T00:00:00Z", + "eventType": "EventFlag.LTE_WinterFest", + }, aid.JSON{ + "activeUntil": "9999-01-01T00:00:00.000Z", + "activeSince": "0001-01-01T00:00:00Z", + "eventType": "EventFlag.Winterfest.Tree", + }) default: events = append(events, aid.JSON{ "activeUntil": "9999-12-31T23:59:59.999Z", diff --git a/storage/postgres.go b/storage/postgres.go index dd9a060..a124dee 100644 --- a/storage/postgres.go +++ b/storage/postgres.go @@ -46,7 +46,6 @@ func (s *PostgresStorage) MigrateAll() { s.Migrate(&DB_TemporaryCode{}, "ExchangeCodes") s.Migrate(&DB_DiscordPerson{}, "Discords") s.Migrate(&DB_SeasonStat{}, "Stats") - s.Migrate(&DB_GameToken{}, "GameTokens") } func (s *PostgresStorage) DropTables() { @@ -242,23 +241,4 @@ 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 cd9909a..52af0e2 100644 --- a/storage/storage.go +++ b/storage/storage.go @@ -48,10 +48,6 @@ 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 { @@ -193,16 +189,4 @@ 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 a57ceba..fa2ad7a 100644 --- a/storage/tables.go +++ b/storage/tables.go @@ -177,16 +177,4 @@ 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