Skip to content
GitLab
Menu
Projects
Groups
Snippets
Loading...
Help
Help
Support
Community forum
Keyboard shortcuts
?
Submit feedback
Sign in / Register
Toggle navigation
Menu
Open sidebar
ziqian zhang
Grasscutter
Commits
bc2c5deb
Commit
bc2c5deb
authored
Jul 13, 2022
by
AnimeGitB
Committed by
Luke H-W
Jul 17, 2022
Browse files
Add Dispatch Password authentication
parent
42e3af4e
Changes
12
Show whitespace changes
Inline
Side-by-side
src/main/java/emu/grasscutter/auth/DefaultAuthentication.java
View file @
bc2c5deb
...
...
@@ -6,6 +6,7 @@ import emu.grasscutter.game.Account;
import
emu.grasscutter.server.http.objects.ComboTokenResJson
;
import
emu.grasscutter.server.http.objects.LoginResultJson
;
import
static
emu
.
grasscutter
.
Configuration
.
ACCOUNT
;
import
static
emu
.
grasscutter
.
utils
.
Language
.
translate
;
/**
...
...
@@ -13,11 +14,19 @@ import static emu.grasscutter.utils.Language.translate;
* Allows all users to access any account.
*/
public
final
class
DefaultAuthentication
implements
AuthenticationSystem
{
private
final
Authenticator
<
LoginResultJson
>
passwordAuthenticator
=
new
PasswordAuthenticator
();
private
final
Authenticator
<
LoginResultJson
>
tokenAuthenticator
=
new
TokenAuthenticator
();
private
final
Authenticator
<
ComboTokenResJson
>
sessionKeyAuthenticator
=
new
SessionKeyAuthenticator
();
private
final
ExternalAuthenticator
externalAuthenticator
=
new
ExternalAuthentication
();
private
final
OAuthAuthenticator
oAuthAuthenticator
=
new
OAuthAuthentication
();
private
Authenticator
<
LoginResultJson
>
passwordAuthenticator
;
private
Authenticator
<
LoginResultJson
>
tokenAuthenticator
=
new
TokenAuthenticator
();
private
Authenticator
<
ComboTokenResJson
>
sessionKeyAuthenticator
=
new
SessionKeyAuthenticator
();
private
ExternalAuthenticator
externalAuthenticator
=
new
ExternalAuthentication
();
private
OAuthAuthenticator
oAuthAuthenticator
=
new
OAuthAuthentication
();
public
DefaultAuthentication
()
{
if
(
ACCOUNT
.
EXPERIMENTAL_RealPassword
)
{
passwordAuthenticator
=
new
ExperimentalPasswordAuthenticator
();
}
else
{
passwordAuthenticator
=
new
PasswordAuthenticator
();
}
}
@Override
public
void
createAccount
(
String
username
,
String
password
)
{
...
...
src/main/java/emu/grasscutter/auth/DefaultAuthenticators.java
View file @
bc2c5deb
package
emu.grasscutter.auth
;
import
at.favre.lib.crypto.bcrypt.BCrypt
;
import
emu.grasscutter.Grasscutter
;
import
emu.grasscutter.auth.AuthenticationSystem.AuthenticationRequest
;
import
emu.grasscutter.database.DatabaseHelper
;
import
emu.grasscutter.game.Account
;
import
emu.grasscutter.server.http.objects.*
;
import
emu.grasscutter.utils.FileUtils
;
import
emu.grasscutter.utils.Utils
;
import
javax.crypto.Cipher
;
import
java.nio.charset.StandardCharsets
;
import
java.security.KeyFactory
;
import
java.security.interfaces.RSAPrivateKey
;
import
java.security.spec.PKCS8EncodedKeySpec
;
import
static
emu
.
grasscutter
.
Configuration
.*;
import
static
emu
.
grasscutter
.
utils
.
Language
.
translate
;
...
...
@@ -18,7 +27,8 @@ public final class DefaultAuthenticators {
* Handles the authentication request from the username and password form.
*/
public
static
class
PasswordAuthenticator
implements
Authenticator
<
LoginResultJson
>
{
@Override
public
LoginResultJson
authenticate
(
AuthenticationRequest
request
)
{
@Override
public
LoginResultJson
authenticate
(
AuthenticationRequest
request
)
{
var
response
=
new
LoginResultJson
();
var
requestData
=
request
.
getPasswordRequest
();
...
...
@@ -34,12 +44,12 @@ public final class DefaultAuthenticators {
Account
account
=
DatabaseHelper
.
getAccountByName
(
requestData
.
account
);
if
(
ACCOUNT
.
maxPlayer
<=
-
1
||
playerCount
<
ACCOUNT
.
maxPlayer
)
{
// Check if account exists.
if
(
account
==
null
&&
ACCOUNT
.
autoCreate
)
{
if
(
account
==
null
&&
ACCOUNT
.
autoCreate
)
{
// This account has been created AUTOMATICALLY. There will be no permissions added.
account
=
DatabaseHelper
.
createAccountWithUid
(
requestData
.
account
,
0
);
// Check if the account was created successfully.
if
(
account
==
null
)
{
if
(
account
==
null
)
{
responseMessage
=
translate
(
"messages.dispatch.account.username_create_error"
);
Grasscutter
.
getLogger
().
info
(
translate
(
"messages.dispatch.account.account_login_create_error"
,
address
));
}
else
{
...
...
@@ -49,7 +59,7 @@ public final class DefaultAuthenticators {
// Log the creation.
Grasscutter
.
getLogger
().
info
(
translate
(
"messages.dispatch.account.account_login_create_success"
,
address
,
response
.
data
.
account
.
uid
));
}
}
else
if
(
account
!=
null
)
}
else
if
(
account
!=
null
)
successfulLogin
=
true
;
else
loggerMessage
=
translate
(
"messages.dispatch.account.account_login_exist_error"
,
address
);
...
...
@@ -61,7 +71,118 @@ public final class DefaultAuthenticators {
// Set response data.
if
(
successfulLogin
)
{
if
(
successfulLogin
)
{
response
.
message
=
"OK"
;
response
.
data
.
account
.
uid
=
account
.
getId
();
response
.
data
.
account
.
token
=
account
.
generateSessionKey
();
response
.
data
.
account
.
email
=
account
.
getEmail
();
loggerMessage
=
translate
(
"messages.dispatch.account.login_success"
,
address
,
account
.
getId
());
}
else
{
response
.
retcode
=
-
201
;
response
.
message
=
responseMessage
;
}
Grasscutter
.
getLogger
().
info
(
loggerMessage
);
return
response
;
}
}
public
static
class
ExperimentalPasswordAuthenticator
implements
Authenticator
<
LoginResultJson
>
{
@Override
public
LoginResultJson
authenticate
(
AuthenticationRequest
request
)
{
var
response
=
new
LoginResultJson
();
var
requestData
=
request
.
getPasswordRequest
();
assert
requestData
!=
null
;
// This should never be null.
int
playerCount
=
Grasscutter
.
getGameServer
().
getPlayers
().
size
();
boolean
successfulLogin
=
false
;
String
address
=
request
.
getRequest
().
ip
();
String
responseMessage
=
translate
(
"messages.dispatch.account.username_error"
);
String
loggerMessage
=
""
;
String
decryptedPassword
=
""
;
// Get Password
if
(
GAME_OPTIONS
.
uaPatchCompatible
)
{
// Make sure your patch can send passwords in plain text
decryptedPassword
=
request
.
getPasswordRequest
().
password
;
}
else
{
try
{
byte
[]
key
=
FileUtils
.
readResource
(
"/keys/auth_private-key.der"
);
PKCS8EncodedKeySpec
keySpec
=
new
PKCS8EncodedKeySpec
(
key
);
KeyFactory
keyFactory
=
KeyFactory
.
getInstance
(
"RSA"
);
RSAPrivateKey
private_key
=
(
RSAPrivateKey
)
keyFactory
.
generatePrivate
(
keySpec
);
Cipher
cipher
=
Cipher
.
getInstance
(
"RSA/ECB/PKCS1Padding"
);
cipher
.
init
(
Cipher
.
DECRYPT_MODE
,
private_key
);
decryptedPassword
=
new
String
(
cipher
.
doFinal
(
Utils
.
base64Decode
(
request
.
getPasswordRequest
().
password
)),
StandardCharsets
.
UTF_8
);
}
catch
(
Exception
ignored
)
{
ignored
.
printStackTrace
();
}
}
if
(
decryptedPassword
==
null
)
{
successfulLogin
=
false
;
loggerMessage
=
translate
(
"messages.dispatch.account.login_password_error"
,
address
);
responseMessage
=
translate
(
"messages.dispatch.account.password_error"
);
}
// Get account from database.
Account
account
=
DatabaseHelper
.
getAccountByName
(
requestData
.
account
);
if
(
ACCOUNT
.
maxPlayer
<=
-
1
||
playerCount
<
ACCOUNT
.
maxPlayer
)
{
// Check if account exists.
if
(
account
==
null
&&
ACCOUNT
.
autoCreate
)
{
// This account has been created AUTOMATICALLY. There will be no permissions added.
if
(
decryptedPassword
.
length
()
>=
8
)
{
account
=
DatabaseHelper
.
createAccountWithUid
(
requestData
.
account
,
0
);
account
.
setPassword
(
BCrypt
.
withDefaults
().
hashToString
(
12
,
decryptedPassword
.
toCharArray
()));
account
.
save
();
// Check if the account was created successfully.
if
(
account
==
null
)
{
responseMessage
=
translate
(
"messages.dispatch.account.username_create_error"
);
loggerMessage
=
translate
(
"messages.dispatch.account.account_login_create_error"
,
address
);
}
else
{
// Continue with login.
successfulLogin
=
true
;
// Log the creation.
Grasscutter
.
getLogger
().
info
(
translate
(
"messages.dispatch.account.account_login_create_success"
,
address
,
response
.
data
.
account
.
uid
));
}
}
else
{
successfulLogin
=
false
;
loggerMessage
=
translate
(
"messages.dispatch.account.login_password_error"
,
address
);
responseMessage
=
translate
(
"messages.dispatch.account.password_length_error"
);
}
}
else
if
(
account
!=
null
)
{
if
(
account
.
getPassword
()
!=
null
&&
!
account
.
getPassword
().
isEmpty
())
{
if
(
BCrypt
.
verifyer
().
verify
(
decryptedPassword
.
toCharArray
(),
account
.
getPassword
()).
verified
)
{
successfulLogin
=
true
;
}
else
{
successfulLogin
=
false
;
loggerMessage
=
translate
(
"messages.dispatch.account.login_password_error"
,
address
);
responseMessage
=
translate
(
"messages.dispatch.account.password_error"
);
}
}
else
{
successfulLogin
=
false
;
loggerMessage
=
translate
(
"messages.dispatch.account.login_password_storage_error"
,
address
);
responseMessage
=
translate
(
"password_storage_error"
);
}
}
else
{
loggerMessage
=
translate
(
"messages.dispatch.account.account_login_exist_error"
,
address
);
}
}
else
{
responseMessage
=
translate
(
"messages.dispatch.account.server_max_player_limit"
);
loggerMessage
=
translate
(
"messages.dispatch.account.login_max_player_limit"
,
address
);
}
// Set response data.
if
(
successfulLogin
)
{
response
.
message
=
"OK"
;
response
.
data
.
account
.
uid
=
account
.
getId
();
response
.
data
.
account
.
token
=
account
.
generateSessionKey
();
...
...
@@ -83,7 +204,8 @@ public final class DefaultAuthenticators {
* Handles the authentication request from the game when using a registry token.
*/
public
static
class
TokenAuthenticator
implements
Authenticator
<
LoginResultJson
>
{
@Override
public
LoginResultJson
authenticate
(
AuthenticationRequest
request
)
{
@Override
public
LoginResultJson
authenticate
(
AuthenticationRequest
request
)
{
var
response
=
new
LoginResultJson
();
var
requestData
=
request
.
getTokenRequest
();
...
...
@@ -106,7 +228,7 @@ public final class DefaultAuthenticators {
successfulLogin
=
account
!=
null
&&
account
.
getSessionKey
().
equals
(
requestData
.
token
);
// Set response data.
if
(
successfulLogin
)
{
if
(
successfulLogin
)
{
response
.
message
=
"OK"
;
response
.
data
.
account
.
uid
=
account
.
getId
();
response
.
data
.
account
.
token
=
account
.
getSessionKey
();
...
...
@@ -138,12 +260,14 @@ public final class DefaultAuthenticators {
* Handles the authentication request from the game when using a combo token/session key.
*/
public
static
class
SessionKeyAuthenticator
implements
Authenticator
<
ComboTokenResJson
>
{
@Override
public
ComboTokenResJson
authenticate
(
AuthenticationRequest
request
)
{
@Override
public
ComboTokenResJson
authenticate
(
AuthenticationRequest
request
)
{
var
response
=
new
ComboTokenResJson
();
var
requestData
=
request
.
getSessionKeyRequest
();
var
loginData
=
request
.
getSessionKeyData
();
assert
requestData
!=
null
;
assert
loginData
!=
null
;
assert
requestData
!=
null
;
assert
loginData
!=
null
;
boolean
successfulLogin
;
String
address
=
request
.
getRequest
().
ip
();
...
...
@@ -158,7 +282,7 @@ public final class DefaultAuthenticators {
successfulLogin
=
account
!=
null
&&
account
.
getSessionKey
().
equals
(
loginData
.
token
);
// Set response data.
if
(
successfulLogin
)
{
if
(
successfulLogin
)
{
response
.
message
=
"OK"
;
response
.
data
.
open_id
=
account
.
getId
();
response
.
data
.
combo_id
=
"157795300"
;
...
...
@@ -190,17 +314,20 @@ public final class DefaultAuthenticators {
* Handles authentication requests from external sources.
*/
public
static
class
ExternalAuthentication
implements
ExternalAuthenticator
{
@Override
public
void
handleLogin
(
AuthenticationRequest
request
)
{
@Override
public
void
handleLogin
(
AuthenticationRequest
request
)
{
assert
request
.
getResponse
()
!=
null
;
request
.
getResponse
().
send
(
"Authentication is not available with the default authentication method."
);
}
@Override
public
void
handleAccountCreation
(
AuthenticationRequest
request
)
{
@Override
public
void
handleAccountCreation
(
AuthenticationRequest
request
)
{
assert
request
.
getResponse
()
!=
null
;
request
.
getResponse
().
send
(
"Authentication is not available with the default authentication method."
);
}
@Override
public
void
handlePasswordReset
(
AuthenticationRequest
request
)
{
@Override
public
void
handlePasswordReset
(
AuthenticationRequest
request
)
{
assert
request
.
getResponse
()
!=
null
;
request
.
getResponse
().
send
(
"Authentication is not available with the default authentication method."
);
}
...
...
@@ -210,17 +337,20 @@ public final class DefaultAuthenticators {
* Handles authentication requests from OAuth sources.
*/
public
static
class
OAuthAuthentication
implements
OAuthAuthenticator
{
@Override
public
void
handleLogin
(
AuthenticationRequest
request
)
{
@Override
public
void
handleLogin
(
AuthenticationRequest
request
)
{
assert
request
.
getResponse
()
!=
null
;
request
.
getResponse
().
send
(
"Authentication is not available with the default authentication method."
);
}
@Override
public
void
handleRedirection
(
AuthenticationRequest
request
,
ClientType
type
)
{
@Override
public
void
handleRedirection
(
AuthenticationRequest
request
,
ClientType
type
)
{
assert
request
.
getResponse
()
!=
null
;
request
.
getResponse
().
send
(
"Authentication is not available with the default authentication method."
);
}
@Override
public
void
handleTokenProcess
(
AuthenticationRequest
request
)
{
@Override
public
void
handleTokenProcess
(
AuthenticationRequest
request
)
{
assert
request
.
getResponse
()
!=
null
;
request
.
getResponse
().
send
(
"Authentication is not available with the default authentication method."
);
}
...
...
src/main/java/emu/grasscutter/command/commands/AccountCommand.java
View file @
bc2c5deb
package
emu.grasscutter.command.commands
;
import
at.favre.lib.crypto.bcrypt.BCrypt
;
import
emu.grasscutter.Configuration
;
import
emu.grasscutter.Grasscutter
;
import
emu.grasscutter.command.Command
;
import
emu.grasscutter.command.CommandHandler
;
...
...
@@ -11,7 +13,7 @@ import java.util.List;
import
static
emu
.
grasscutter
.
utils
.
Language
.
translate
;
@Command
(
label
=
"account"
,
usage
=
"account <create|delete> <username> [uid
]
"
,
description
=
"commands.account.description"
,
targetRequirement
=
Command
.
TargetRequirement
.
NONE
)
@Command
(
label
=
"account"
,
usage
=
"account <create|delete
|resetpass
> <username> [uid
|password] [uid]
"
,
description
=
"commands.account.description"
,
targetRequirement
=
Command
.
TargetRequirement
.
NONE
)
public
final
class
AccountCommand
implements
CommandHandler
{
@Override
...
...
@@ -35,6 +37,30 @@ public final class AccountCommand implements CommandHandler {
return
;
case
"create"
:
int
uid
=
0
;
String
password
=
""
;
if
(
Configuration
.
ACCOUNT
.
EXPERIMENTAL_RealPassword
==
true
)
{
if
(
args
.
size
()
<
3
)
{
CommandHandler
.
sendMessage
(
null
,
"EXPERIMENTAL_RealPassword requires a password argument"
);
CommandHandler
.
sendMessage
(
null
,
"Usage: account create <username> <password> [uid]"
);
return
;
}
password
=
args
.
get
(
2
);
if
(
args
.
size
()
==
4
)
{
try
{
uid
=
Integer
.
parseInt
(
args
.
get
(
3
));
}
catch
(
NumberFormatException
ignored
)
{
CommandHandler
.
sendMessage
(
null
,
translate
(
sender
,
"commands.account.invalid"
));
if
(
Configuration
.
ACCOUNT
.
EXPERIMENTAL_RealPassword
==
true
)
{
CommandHandler
.
sendMessage
(
null
,
"EXPERIMENTAL_RealPassword requires argument 2 to be a password, not a uid"
);
CommandHandler
.
sendMessage
(
null
,
"Usage: account create <username> <password> [uid]"
);
}
return
;
}
}
}
else
{
if
(
args
.
size
()
>
2
)
{
try
{
uid
=
Integer
.
parseInt
(
args
.
get
(
2
));
...
...
@@ -43,12 +69,16 @@ public final class AccountCommand implements CommandHandler {
return
;
}
}
}
emu
.
grasscutter
.
game
.
Account
account
=
DatabaseHelper
.
createAccountWithUid
(
username
,
uid
);
if
(
account
==
null
)
{
CommandHandler
.
sendMessage
(
null
,
translate
(
sender
,
"commands.account.exists"
));
return
;
}
else
{
if
(
Configuration
.
ACCOUNT
.
EXPERIMENTAL_RealPassword
==
true
)
{
account
.
setPassword
(
BCrypt
.
withDefaults
().
hashToString
(
12
,
password
.
toCharArray
()));
}
account
.
addPermission
(
"*"
);
account
.
save
();
// Save account to database.
...
...
@@ -74,6 +104,37 @@ public final class AccountCommand implements CommandHandler {
// Finally, we do the actual deletion.
DatabaseHelper
.
deleteAccount
(
toDelete
);
CommandHandler
.
sendMessage
(
null
,
translate
(
sender
,
"commands.account.delete"
));
return
;
case
"resetpass"
:
if
(
Configuration
.
ACCOUNT
.
EXPERIMENTAL_RealPassword
!=
true
)
{
CommandHandler
.
sendMessage
(
null
,
"resetpass requires EXPERIMENTAL_RealPassword to be true."
);
return
;
}
if
(
args
.
size
()
!=
3
)
{
CommandHandler
.
sendMessage
(
null
,
"Invalid Args"
);
CommandHandler
.
sendMessage
(
null
,
"Usage: account resetpass <username> <password>"
);
return
;
}
Account
toUpdate
=
DatabaseHelper
.
getAccountByName
(
username
);
if
(
toUpdate
==
null
)
{
CommandHandler
.
sendMessage
(
null
,
translate
(
sender
,
"commands.account.no_account"
));
return
;
}
// Get the player for the account.
// If that player is currently online, we kick them before proceeding with the deletion.
Player
uPlayer
=
Grasscutter
.
getGameServer
().
getPlayerByAccountId
(
toUpdate
.
getId
());
if
(
uPlayer
!=
null
)
{
uPlayer
.
getSession
().
close
();
}
toUpdate
.
setPassword
(
BCrypt
.
withDefaults
().
hashToString
(
12
,
args
.
get
(
2
).
toCharArray
()));
toUpdate
.
save
();
CommandHandler
.
sendMessage
(
null
,
"Password Updated."
);
return
;
}
}
}
src/main/java/emu/grasscutter/server/http/HttpServer.java
View file @
bc2c5deb
...
...
@@ -11,7 +11,7 @@ import org.eclipse.jetty.server.ServerConnector;
import
org.eclipse.jetty.util.ssl.SslContextFactory
;
import
java.io.File
;
import
java.io.
IO
Exception
;
import
java.io.
UnsupportedEncoding
Exception
;
import
static
emu
.
grasscutter
.
Configuration
.*;
import
static
emu
.
grasscutter
.
utils
.
Language
.
translate
;
...
...
@@ -126,8 +126,9 @@ public final class HttpServer {
/**
* Starts listening on the HTTP server.
* @throws UnsupportedEncodingException
*/
public
void
start
()
{
public
void
start
()
throws
UnsupportedEncodingException
{
// Attempt to start the HTTP server.
if
(
HTTP_INFO
.
bindAddress
.
equals
(
""
)){
this
.
express
.
listen
(
HTTP_INFO
.
bindPort
);
...
...
src/main/java/emu/grasscutter/server/http/dispatch/RegionHandler.java
View file @
bc2c5deb
package
emu.grasscutter.server.http.dispatch
;
import
com.google.protobuf.ByteString
;
import
com.google.protobuf.InvalidProtocolBufferException
;
import
emu.grasscutter.Grasscutter
;
import
emu.grasscutter.Grasscutter.ServerRunMode
;
import
emu.grasscutter.net.proto.QueryCurrRegionHttpRspOuterClass.*
;
import
emu.grasscutter.net.proto.RegionInfoOuterClass
;
import
emu.grasscutter.net.proto.QueryCurrRegionHttpRspOuterClass.QueryCurrRegionHttpRsp
;
import
emu.grasscutter.net.proto.RegionInfoOuterClass.RegionInfo
;
import
emu.grasscutter.net.proto.RegionSimpleInfoOuterClass.RegionSimpleInfo
;
import
emu.grasscutter.server.event.dispatch.QueryAllRegionsEvent
;
import
emu.grasscutter.server.event.dispatch.QueryCurrentRegionEvent
;
import
emu.grasscutter.server.http.Router
;
import
emu.grasscutter.server.http.objects.QueryCurRegionRspJson
;
import
emu.grasscutter.utils.Crypto
;
import
emu.grasscutter.utils.FileUtils
;
import
emu.grasscutter.utils.Utils
;
import
express.Express
;
import
express.http.Request
;
import
express.http.Response
;
import
io.javalin.Javalin
;
import
java.io.File
;
import
java.util.ArrayList
;
import
java.util.Base64
;
import
java.util.List
;
import
java.util.Map
;
import
javax.crypto.Cipher
;
import
java.io.ByteArrayOutputStream
;
import
java.util.*
;
import
java.util.concurrent.ConcurrentHashMap
;
import
java.security.Signature
;
import
static
emu
.
grasscutter
.
Configuration
.*;
import
static
emu
.
grasscutter
.
net
.
proto
.
QueryRegionListHttpRspOuterClass
.
*
;
import
static
emu
.
grasscutter
.
net
.
proto
.
QueryRegionListHttpRspOuterClass
.
QueryRegionListHttpRsp
;
/**
* Handles requests related to region queries.
...
...
@@ -127,19 +125,72 @@ public final class RegionHandler implements Router {
private
static
void
queryCurrentRegion
(
Request
request
,
Response
response
)
{
// Get region to query.
String
regionName
=
request
.
params
(
"region"
);
String
versionName
=
request
.
query
(
"version"
);
var
region
=
regions
.
get
(
regionName
);
// Get region data.
String
regionData
=
"CAESGE5vdCBGb3VuZCB2ZXJzaW9uIGNvbmZpZw=="
;
if
(
request
.
query
().
values
().
size
()
>
0
)
{
var
region
=
regions
.
get
(
regionName
);
if
(
region
!=
null
)
regionData
=
region
.
getBase64
();
if
(
region
!=
null
)
regionData
=
region
.
getBase64
();
}
if
(
versionName
.
contains
(
"2.7.5"
)
||
versionName
.
contains
(
"2.8."
))
{
try
{
QueryCurrentRegionEvent
event
=
new
QueryCurrentRegionEvent
(
regionData
);
event
.
call
();
if
(
GAME_OPTIONS
.
uaPatchCompatible
)
{
// More love for UA Patch players
var
rsp
=
new
QueryCurRegionRspJson
();
rsp
.
content
=
event
.
getRegionInfo
();
rsp
.
sign
=
"TW9yZSBsb3ZlIGZvciBVQSBQYXRjaCBwbGF5ZXJz"
;
response
.
send
(
rsp
);
return
;
}
String
key_id
=
request
.
query
(
"key_id"
);
Cipher
cipher
=
Cipher
.
getInstance
(
"RSA/ECB/PKCS1Padding"
);
cipher
.
init
(
Cipher
.
ENCRYPT_MODE
,
key_id
.
equals
(
"3"
)
?
Crypto
.
CUR_OS_ENCRYPT_KEY
:
Crypto
.
CUR_CN_ENCRYPT_KEY
);
var
regionInfo
=
Utils
.
base64Decode
(
event
.
getRegionInfo
());
//Encrypt regionInfo in chunks
ByteArrayOutputStream
encryptedRegionInfoStream
=
new
ByteArrayOutputStream
();
//Thank you so much GH Copilot
int
chunkSize
=
256
-
11
;
int
regionInfoLength
=
regionInfo
.
length
;
int
numChunks
=
(
int
)
Math
.
ceil
(
regionInfoLength
/
(
double
)
chunkSize
);
for
(
int
i
=
0
;
i
<
numChunks
;
i
++)
{
byte
[]
chunk
=
Arrays
.
copyOfRange
(
regionInfo
,
i
*
chunkSize
,
Math
.
min
((
i
+
1
)
*
chunkSize
,
regionInfoLength
));
byte
[]
encryptedChunk
=
cipher
.
doFinal
(
chunk
);
encryptedRegionInfoStream
.
write
(
encryptedChunk
);
}
Signature
privateSignature
=
Signature
.
getInstance
(
"SHA256withRSA"
);
privateSignature
.
initSign
(
Crypto
.
CUR_SIGNING_KEY
);
privateSignature
.
update
(
regionInfo
);
var
rsp
=
new
QueryCurRegionRspJson
();
rsp
.
content
=
Utils
.
base64Encode
(
encryptedRegionInfoStream
.
toByteArray
());
rsp
.
sign
=
Utils
.
base64Encode
(
privateSignature
.
sign
());
response
.
send
(
rsp
);
}
catch
(
Exception
e
)
{
Grasscutter
.
getLogger
().
error
(
"An error occurred while handling query_cur_region."
,
e
);
}
}
else
{
// Invoke event.
QueryCurrentRegionEvent
event
=
new
QueryCurrentRegionEvent
(
regionData
);
event
.
call
();
// Respond with event result.
response
.
send
(
event
.
getRegionInfo
());
}
// Log to console.
Grasscutter
.
getLogger
().
info
(
String
.
format
(
"Client %s request: query_cur_region/%s"
,
request
.
ip
(),
regionName
));
}
...
...
src/main/java/emu/grasscutter/server/http/objects/QueryCurRegionRspJson.java
0 → 100644
View file @
bc2c5deb
package
emu.grasscutter.server.http.objects
;
public
class
QueryCurRegionRspJson
{
public
String
content
;
public
String
sign
;
}
src/main/java/emu/grasscutter/server/packet/recv/HandlerGetPlayerTokenReq.java
View file @
bc2c5deb
package
emu.grasscutter.server.packet.recv
;
import
static
emu
.
grasscutter
.
Configuration
.
ACCOUNT
;
import
static
emu
.
grasscutter
.
Configuration
.
GAME_OPTIONS
;
import
emu.grasscutter.Grasscutter
;
import
emu.grasscutter.database.DatabaseHelper
;
...
...
@@ -14,6 +15,14 @@ import emu.grasscutter.server.event.game.PlayerCreationEvent;
import
emu.grasscutter.server.game.GameSession
;
import
emu.grasscutter.server.game.GameSession.SessionState
;
import
emu.grasscutter.server.packet.send.PacketGetPlayerTokenRsp
;
import
emu.grasscutter.utils.ByteHelper
;
import
emu.grasscutter.utils.Crypto
;
import
emu.grasscutter.utils.Utils
;
import
javax.crypto.Cipher
;
import
java.nio.ByteBuffer
;
import
java.security.Signature
;
@Opcodes
(
PacketOpcodes
.
GetPlayerTokenReq
)
public
class
HandlerGetPlayerTokenReq
extends
PacketHandler
{
...
...
@@ -90,8 +99,45 @@ public class HandlerGetPlayerTokenReq extends PacketHandler {
session
.
setUseSecretKey
(
true
);
session
.
setState
(
SessionState
.
WAITING_FOR_LOGIN
);
// Only >= 2.7.50 has this
if
(
req
.
getKeyId
()
>
0
)
{
if
(
GAME_OPTIONS
.
uaPatchCompatible
)
{
// More love for ua patch plz 😭
byte
[]
clientBytes
=
Utils
.
base64Decode
(
req
.
getClientSeed
());
byte
[]
seed
=
ByteHelper
.
longToBytes
(
Crypto
.
ENCRYPT_SEED
);
Crypto
.
xor
(
clientBytes
,
seed
);
String
base64str
=
Utils
.
base64Encode
(
clientBytes
);
session
.
send
(
new
PacketGetPlayerTokenRsp
(
session
,
base64str
,
"bm90aGluZyBoZXJl"
));
return
;
}
Cipher
cipher
=
Cipher
.
getInstance
(
"RSA/ECB/PKCS1Padding"
);
cipher
.
init
(
Cipher
.
DECRYPT_MODE
,
Crypto
.
CUR_SIGNING_KEY
);
var
client_seed_encrypted
=
Utils
.
base64Decode
(
req
.
getClientSeed
());
var
client_seed
=
ByteBuffer
.
wrap
(
cipher
.
doFinal
(
client_seed_encrypted
))
.
getLong
();
byte
[]
seed_bytes
=
ByteBuffer
.
wrap
(
new
byte
[
8
])
.
putLong
(
Crypto
.
ENCRYPT_SEED
^
client_seed
)
.
array
();
//Kind of a hack, but whatever
cipher
.
init
(
Cipher
.
ENCRYPT_MODE
,
req
.
getKeyId
()
==
3
?
Crypto
.
CUR_OS_ENCRYPT_KEY
:
Crypto
.
CUR_CN_ENCRYPT_KEY
);
var
seed_encrypted
=
cipher
.
doFinal
(
seed_bytes
);
Signature
privateSignature
=
Signature
.
getInstance
(
"SHA256withRSA"
);
privateSignature
.
initSign
(
Crypto
.
CUR_SIGNING_KEY
);
privateSignature
.
update
(
seed_bytes
);
session
.
send
(
new
PacketGetPlayerTokenRsp
(
session
,
Utils
.
base64Encode
(
seed_encrypted
),
Utils
.
base64Encode
(
privateSignature
.
sign
())));
}
else
{
// Send packet
session
.
send
(
new
PacketGetPlayerTokenRsp
(
session
));
}
}
}
src/main/java/emu/grasscutter/server/packet/send/PacketGetPlayerTokenRsp.java
View file @
bc2c5deb
...
...
@@ -51,4 +51,29 @@ public class PacketGetPlayerTokenRsp extends BasePacket {
this
.
setData
(
p
.
toByteArray
());
}
public
PacketGetPlayerTokenRsp
(
GameSession
session
,
String
encryptedSeed
,
String
encryptedSeedSign
)
{
super
(
PacketOpcodes
.
GetPlayerTokenRsp
,
true
);
this
.
setUseDispatchKey
(
true
);
GetPlayerTokenRsp
p
=
GetPlayerTokenRsp
.
newBuilder
()
.
setUid
(
session
.
getPlayer
().
getUid
())
.
setToken
(
session
.
getAccount
().
getToken
())
.
setAccountType
(
1
)
.
setIsProficientPlayer
(
session
.
getPlayer
().
getAvatars
().
getAvatarCount
()
>
0
)
// Not sure where this goes
.
setSecretKeySeed
(
Crypto
.
ENCRYPT_SEED
)
.
setSecurityCmdBuffer
(
ByteString
.
copyFrom
(
Crypto
.
ENCRYPT_SEED_BUFFER
))
.
setPlatformType
(
3
)
.
setChannelId
(
1
)
.
setCountryCode
(
"US"
)
.
setClientVersionRandomKey
(
"c25-314dd05b0b5f"
)
.
setRegPlatform
(
3
)
.
setClientIpStr
(
session
.
getAddress
().
getAddress
().
getHostAddress
())
.
setEncryptedSeed
(
encryptedSeed
)
.
setSeedSignature
(
encryptedSeedSign
)
.
build
();
this
.
setData
(
p
.
toByteArray
());
}
}
src/main/java/emu/grasscutter/utils/ByteHelper.java
0 → 100644
View file @
bc2c5deb
package
emu.grasscutter.utils
;
public
class
ByteHelper
{
public
static
byte
[]
changeBytes
(
byte
[]
a
)
{
byte
[]
b
=
new
byte
[
a
.
length
];
for
(
int
i
=
0
;
i
<
a
.
length
;
i
++)
{
b
[
i
]
=
a
[
a
.
length
-
i
-
1
];
}
return
b
;
}
public
static
byte
[]
longToBytes
(
long
x
)
{
byte
[]
bytes
=
new
byte
[
8
];
bytes
[
0
]
=
(
byte
)
(
x
>>
56
);
bytes
[
1
]
=
(
byte
)
(
x
>>
48
);
bytes
[
2
]
=
(
byte
)
(
x
>>
40
);
bytes
[
3
]
=
(
byte
)
(
x
>>
32
);
bytes
[
4
]
=
(
byte
)
(
x
>>
24
);
bytes
[
5
]
=
(
byte
)
(
x
>>
16
);
bytes
[
6
]
=
(
byte
)
(
x
>>
8
);
bytes
[
7
]
=
(
byte
)
(
x
);
return
bytes
;
}
}
\ No newline at end of file
src/main/java/emu/grasscutter/utils/ConfigContainer.java
View file @
bc2c5deb
...
...
@@ -112,6 +112,7 @@ public class ConfigContainer {
public
static
class
Account
{
public
boolean
autoCreate
=
false
;
public
boolean
EXPERIMENTAL_RealPassword
=
false
;
public
String
[]
defaultPermissions
=
{};
public
int
maxPlayer
=
-
1
;
}
...
...
@@ -210,6 +211,7 @@ public class ConfigContainer {
public
int
cap
=
160
;
public
int
rechargeTime
=
480
;
}
public
boolean
uaPatchCompatible
=
false
;
}
public
static
class
JoinOptions
{
...
...
src/main/java/emu/grasscutter/utils/Crypto.java
View file @
bc2c5deb
package
emu.grasscutter.utils
;
import
java.security.KeyFactory
;
import
java.security.PrivateKey
;
import
java.security.PublicKey
;
import
java.security.SecureRandom
;
import
java.util.Base64
;
import
java.security.spec.PKCS8EncodedKeySpec
;
import
java.security.spec.X509EncodedKeySpec
;
import
emu.grasscutter.Grasscutter
;
import
emu.grasscutter.net.proto.GetPlayerTokenRspOuterClass.GetPlayerTokenRsp
;
import
emu.grasscutter.net.proto.QueryCurrRegionHttpRspOuterClass.QueryCurrRegionHttpRsp
;
import
static
emu
.
grasscutter
.
Configuration
.*;
public
final
class
Crypto
{
private
static
final
SecureRandom
secureRandom
=
new
SecureRandom
();
...
...
@@ -19,12 +19,31 @@ public final class Crypto {
public
static
long
ENCRYPT_SEED
=
Long
.
parseUnsignedLong
(
"11468049314633205968"
);
public
static
byte
[]
ENCRYPT_SEED_BUFFER
=
new
byte
[
0
];
public
static
PublicKey
CUR_OS_ENCRYPT_KEY
;
public
static
PublicKey
CUR_CN_ENCRYPT_KEY
;
public
static
PrivateKey
CUR_SIGNING_KEY
;
public
static
void
loadKeys
()
{
DISPATCH_KEY
=
FileUtils
.
readResource
(
"/keys/dispatchKey.bin"
);
DISPATCH_SEED
=
FileUtils
.
readResource
(
"/keys/dispatchSeed.bin"
);
ENCRYPT_KEY
=
FileUtils
.
readResource
(
"/keys/secretKey.bin"
);
ENCRYPT_SEED_BUFFER
=
FileUtils
.
readResource
(
"/keys/secretKeyBuffer.bin"
);
try
{
//These should be loaded from ChannelConfig_whatever.json
CUR_SIGNING_KEY
=
KeyFactory
.
getInstance
(
"RSA"
)
.
generatePrivate
(
new
PKCS8EncodedKeySpec
(
FileUtils
.
readResource
(
"/keys/SigningKey.der"
)));
CUR_OS_ENCRYPT_KEY
=
KeyFactory
.
getInstance
(
"RSA"
)
.
generatePublic
(
new
X509EncodedKeySpec
(
FileUtils
.
readResource
(
"/keys/OSCB_Pub.der"
)));
CUR_CN_ENCRYPT_KEY
=
KeyFactory
.
getInstance
(
"RSA"
)
.
generatePublic
(
new
X509EncodedKeySpec
(
FileUtils
.
readResource
(
"/keys/OSCN_Pub.der"
)));
}
catch
(
Exception
e
)
{
Grasscutter
.
getLogger
().
error
(
"An error occurred while loading keys."
,
e
);
}
}
public
static
void
xor
(
byte
[]
packet
,
byte
[]
key
)
{
...
...
src/main/resources/languages/en-US.json
View file @
bc2c5deb
...
...
@@ -28,6 +28,8 @@
"login_token_attempt"
:
"[Dispatch] Client %s is trying to log in via token."
,
"login_token_error"
:
"[Dispatch] Client %s failed to log in via token."
,
"login_token_success"
:
"[Dispatch] Client %s logged in via token as %s."
,
"login_password_error"
:
"[Dispatch] Client %s failed to log in via password."
,
"login_password_storage_error"
:
"[Dispatch] Client %s failed to log in via password because there is no password in the database."
,
"combo_token_success"
:
"[Dispatch] Client %s succeed to exchange combo token."
,
"combo_token_error"
:
"[Dispatch] Client %s failed to exchange combo token."
,
"account_login_create_success"
:
"[Dispatch] Client %s failed to log in: Account %s created."
,
...
...
@@ -37,6 +39,9 @@
"session_key_error"
:
"Wrong session key."
,
"username_error"
:
"Username not found."
,
"username_create_error"
:
"Username not found, create failed."
,
"password_error"
:
"Invalid Password"
,
"password_length_error"
:
"Password length must be greater then or equal to 8"
,
"password_storage_error"
:
"You don't have a password for your account. Please contact an administrator."
,
"server_max_player_limit"
:
"The number of online players has reached the limit"
},
"router_error"
:
"[Dispatch] Unable to attach router."
...
...
Write
Preview
Markdown
is supported
0%
Try again
or
attach a new file
.
Attach a file
Cancel
You are about to add
0
people
to the discussion. Proceed with caution.
Finish editing this message first!
Cancel
Please
register
or
sign in
to comment