Skip to content

Commit

Permalink
Merge pull request #17 from hexis-revival/reconnect-packet
Browse files Browse the repository at this point in the history
Implement reconnect packet & response password hashing
  • Loading branch information
Lekuruu authored Oct 11, 2024
2 parents 4a1b1ae + 130600b commit e55bf3f
Show file tree
Hide file tree
Showing 6 changed files with 263 additions and 55 deletions.
2 changes: 1 addition & 1 deletion common/go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ require (
github.com/jackc/puddle/v2 v2.2.1 // indirect
github.com/jinzhu/inflection v1.0.0 // indirect
github.com/jinzhu/now v1.1.5 // indirect
golang.org/x/crypto v0.17.0 // indirect
golang.org/x/crypto v0.17.0
golang.org/x/sync v0.8.0 // indirect
golang.org/x/text v0.14.0 // indirect
gorm.io/gorm v1.25.12
Expand Down
81 changes: 81 additions & 0 deletions common/passwords.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
package common

import (
"crypto/sha512"
"encoding/hex"

"golang.org/x/crypto/bcrypt"
)

var passwordCache = map[string]bool{}

func GetPasswordCache() map[string]bool {
return passwordCache
}

func ClearPasswordCache() {
passwordCache = map[string]bool{}
}

func CreatePasswordHash(password string) (string, error) {
hashedPassword := GetSHA512Hash(password)
hashedBytes, err := bcrypt.GenerateFromPassword(
hashedPassword,
bcrypt.DefaultCost,
)

return string(hashedBytes), err
}

func CheckPassword(input string, bcryptString string) bool {
inputHashed := GetSHA512Hash(input)

if isCorrect, ok := passwordCache[string(inputHashed)]; ok {
return isCorrect
}

err := bcrypt.CompareHashAndPassword(
[]byte(bcryptString),
inputHashed,
)

isCorrect := err == nil
passwordCache[string(inputHashed)] = isCorrect
return isCorrect
}

func CheckPasswordHashed(inputHashed []byte, bcryptString string) bool {
if len(inputHashed) != sha512.Size {
return false
}

if isCorrect, ok := passwordCache[string(inputHashed)]; ok {
return isCorrect
}

err := bcrypt.CompareHashAndPassword(
[]byte(bcryptString),
inputHashed,
)

isCorrect := err == nil
passwordCache[string(inputHashed)] = isCorrect
return isCorrect
}

func CheckPasswordHashedHex(inputHex string, bcryptString string) bool {
inputHashed, err := hex.DecodeString(inputHex)

if err != nil {
return false
}

return CheckPasswordHashed(inputHashed, bcryptString)
}

func GetSHA512Hash(input string) []byte {
hash := sha512.New()
hash.Write([]byte(input))
hashedBytes := hash.Sum(nil)
return hashedBytes
}
32 changes: 32 additions & 0 deletions common/passwords_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
package common

import (
"testing"
)

func TestPasswords(t *testing.T) {
password := "password"
hash, err := CreatePasswordHash(password)

if err != nil {
t.Error(err)
return
}

if !CheckPassword(password, hash) {
t.Error("password check failed")
return
}

passwordHashed := GetSHA512Hash(password)

if !CheckPasswordHashed(passwordHashed, hash) {
t.Error("hashed password check failed")
return
}

if len(passwordCache) != 1 {
t.Error("password cache should have 1 entry")
return
}
}
102 changes: 48 additions & 54 deletions hnet/handler.go
Original file line number Diff line number Diff line change
@@ -1,10 +1,10 @@
package hnet

import (
"encoding/hex"
"fmt"

"github.com/hexis-revival/hexagon/common"
"golang.org/x/crypto/bcrypt"
)

var Handlers = map[uint32]func(*common.IOStream, *Player) error{}
Expand All @@ -21,8 +21,7 @@ func handleLogin(stream *common.IOStream, player *Player) error {
player.Client = request.Client

if !player.Client.IsValid() {
player.CloseConnection()
player.Logger.Warning("Login attempt failed: Invalid client info")
player.OnLoginFailed("Invalid client info")
return nil
}

Expand All @@ -32,89 +31,83 @@ func handleLogin(stream *common.IOStream, player *Player) error {
)

if err != nil {
player.CloseConnection()
player.Logger.Warning("Login attempt failed: User not found")
player.OnLoginFailed("User not found")
return nil
}

err = bcrypt.CompareHashAndPassword(
[]byte(userObject.Password),
[]byte(request.Password),
isCorrect := common.CheckPassword(
request.Password,
userObject.Password,
)

if err != nil {
player.CloseConnection()
player.Logger.Warning("Login attempt failed: Incorrect password")
if !isCorrect {
player.OnLoginFailed("Incorrect password")
return nil
}

if !userObject.Activated {
player.CloseConnection()
player.Logger.Warning("Login attempt failed: Account not activated")
player.OnLoginFailed("Account not activated")
return nil
}

if userObject.Restricted {
player.CloseConnection()
player.Logger.Warning("Login attempt failed: Account restricted")
player.OnLoginFailed("Account restricted")
return nil
}

otherUser := player.Server.Players.ByID(uint32(userObject.Id))
responsePasswordRaw := common.GetSHA512Hash(request.Password)
responsePassword := hex.EncodeToString(responsePasswordRaw)

return player.OnLoginSuccess(responsePassword, userObject)
}

func handleReconnect(stream *common.IOStream, player *Player) error {
request := ReadLoginRequestReconnect(stream)

if otherUser != nil {
otherUser.CloseConnection()
if request == nil {
player.RevokeLogin()
return fmt.Errorf("failed to read login request")
}

// Ensure that the stats object exists
userObject.EnsureStats(player.Server.State)
player.LogIncomingPacket(CLIENT_LOGIN_RECONNECT, request)
player.Client = request.Client

// Populate player info & stats
player.ApplyUserData(userObject)
player.Server.Players.Add(player)
if !player.Client.IsValid() {
player.OnLoginFailed("Invalid client info")
return nil
}

player.Logger.Infof(
"Login attempt as '%s' with version %s",
player.Info.Name,
player.Client.Version.String(),
userObject, err := common.FetchUserByNameCaseInsensitive(
request.Username,
player.Server.State,
)

player.Logger.SetName(fmt.Sprintf(
"Player \"%s\"",
player.Info.Name,
))

for _, other := range player.Server.Players.All() {
other.SendPacket(SERVER_USER_INFO, player.Info)
player.SendPacket(SERVER_USER_INFO, other.Info)
if err != nil {
player.OnLoginFailed("User not found")
return nil
}

response := LoginResponse{
UserId: player.Info.Id,
Username: player.Info.Name,
Password: request.Password,
}
isCorrect := common.CheckPasswordHashedHex(
request.Password,
userObject.Password,
)

// Send login response
err = player.SendPacket(SERVER_LOGIN_RESPONSE, response)
if err != nil {
player.CloseConnection()
return err
if !isCorrect {
player.OnLoginFailed("Incorrect password")
return nil
}

friendIds, err := player.GetFriendIds()
if err != nil {
return err
if !userObject.Activated {
player.OnLoginFailed("Account not activated")
return nil
}

// Send friends list
friendsList := FriendsList{FriendIds: friendIds}
err = player.SendPacket(SERVER_FRIENDS_LIST, friendsList)
if err != nil {
return err
if userObject.Restricted {
player.OnLoginFailed("Account restricted")
return nil
}

return nil
return player.OnLoginSuccess(request.Password, userObject)
}

func handleStatusChange(stream *common.IOStream, player *Player) error {
Expand Down Expand Up @@ -201,6 +194,7 @@ func handleUserRelationshipRemove(stream *common.IOStream, player *Player) error

func init() {
Handlers[CLIENT_LOGIN] = handleLogin
Handlers[CLIENT_LOGIN_RECONNECT] = handleReconnect
Handlers[CLIENT_CHANGE_STATUS] = handleStatusChange
Handlers[CLIENT_REQUEST_STATS] = handleRequestStats
Handlers[CLIENT_RELATIONSHIP_ADD] = handleUserRelationshipAdd
Expand Down
36 changes: 36 additions & 0 deletions hnet/parsers.go
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,42 @@ func ReadLoginRequest(stream *common.IOStream) *LoginRequest {
minorVersion := stream.ReadU32()
patchVersion := stream.ReadU32()
clientInfo := stream.ReadString()
_ = stream.ReadU8() // TODO

version := &VersionInfo{
Major: majorVersion,
Minor: minorVersion,
Patch: patchVersion,
}

client := ParseClientInfo(clientInfo)
client.Version = version

return &LoginRequest{
Username: username,
Password: password,
Client: client,
}
}

func ReadLoginRequestReconnect(stream *common.IOStream) *LoginRequest {
defer handlePanic()

username := stream.ReadString()
password := stream.ReadString()

// Reconnect packet contains extra 4 bytes of null bytes
_ = stream.ReadU32()

majorVersion := stream.ReadU32()
minorVersion := stream.ReadU32()
patchVersion := stream.ReadU32()
clientInfo := stream.ReadString()
_ = stream.ReadU8() // TODO

// At the end there are an extra 4 bytes
// {0xFF, 0xFF, 0xFF, 0xFF}
_ = stream.ReadU32()

version := &VersionInfo{
Major: majorVersion,
Expand Down
Loading

0 comments on commit e55bf3f

Please sign in to comment.