2020-01-09 03:05:42 +00:00
/ *
2021-01-01 15:10:36 +00:00
* Copyright ( c ) 2019 - 2021 GeyserMC . http : //geysermc.org
2020-01-09 03:05:42 +00:00
*
* Permission is hereby granted , free of charge , to any person obtaining a copy
* of this software and associated documentation files ( the " Software " ) , to deal
* in the Software without restriction , including without limitation the rights
* to use , copy , modify , merge , publish , distribute , sublicense , and / or sell
* copies of the Software , and to permit persons to whom the Software is
* furnished to do so , subject to the following conditions :
*
* The above copyright notice and this permission notice shall be included in
* all copies or substantial portions of the Software .
*
* THE SOFTWARE IS PROVIDED " AS IS " , WITHOUT WARRANTY OF ANY KIND , EXPRESS OR
* IMPLIED , INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY ,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT . IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM , DAMAGES OR OTHER
* LIABILITY , WHETHER IN AN ACTION OF CONTRACT , TORT OR OTHERWISE , ARISING FROM ,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
* THE SOFTWARE .
*
* @author GeyserMC
* @link https : //github.com/GeyserMC/Geyser
* /
2019-07-24 22:53:15 +00:00
package org.geysermc.connector.utils ;
import com.fasterxml.jackson.databind.DeserializationFeature ;
import com.fasterxml.jackson.databind.JsonNode ;
import com.fasterxml.jackson.databind.ObjectMapper ;
import com.fasterxml.jackson.databind.node.JsonNodeType ;
2021-01-11 20:52:02 +00:00
import com.github.steveice10.mc.auth.service.MsaAuthenticationService ;
2019-07-24 22:53:15 +00:00
import com.nimbusds.jose.JWSObject ;
import com.nukkitx.network.util.Preconditions ;
2019-08-02 03:02:17 +00:00
import com.nukkitx.protocol.bedrock.packet.LoginPacket ;
import com.nukkitx.protocol.bedrock.packet.ServerToClientHandshakePacket ;
2019-07-24 22:53:15 +00:00
import com.nukkitx.protocol.bedrock.util.EncryptionUtils ;
2021-01-11 20:52:02 +00:00
import org.geysermc.common.window.* ;
2020-04-17 12:54:04 +00:00
import org.geysermc.common.window.button.FormButton ;
2019-12-21 17:35:48 +00:00
import org.geysermc.common.window.component.InputComponent ;
import org.geysermc.common.window.component.LabelComponent ;
import org.geysermc.common.window.response.CustomFormResponse ;
2021-01-11 20:52:02 +00:00
import org.geysermc.common.window.response.ModalFormResponse ;
2020-04-17 12:54:04 +00:00
import org.geysermc.common.window.response.SimpleFormResponse ;
2019-08-02 03:02:17 +00:00
import org.geysermc.connector.GeyserConnector ;
import org.geysermc.connector.network.session.GeyserSession ;
2019-12-21 17:35:48 +00:00
import org.geysermc.connector.network.session.auth.AuthData ;
2019-11-30 12:34:45 +00:00
import org.geysermc.connector.network.session.auth.BedrockClientData ;
2019-08-02 03:02:17 +00:00
import org.geysermc.connector.network.session.cache.WindowCache ;
2019-07-24 22:53:15 +00:00
2019-08-02 03:02:17 +00:00
import javax.crypto.SecretKey ;
import java.io.IOException ;
import java.security.KeyPair ;
import java.security.KeyPairGenerator ;
import java.security.PublicKey ;
2019-07-24 22:53:15 +00:00
import java.security.interfaces.ECPublicKey ;
2019-08-02 03:02:17 +00:00
import java.security.spec.ECGenParameterSpec ;
import java.util.UUID ;
2019-07-24 22:53:15 +00:00
public class LoginEncryptionUtils {
2019-08-02 03:02:17 +00:00
private static final ObjectMapper JSON_MAPPER = new ObjectMapper ( ) . disable ( DeserializationFeature . FAIL_ON_UNKNOWN_PROPERTIES ) ;
2019-07-24 22:53:15 +00:00
2019-08-02 03:02:17 +00:00
private static boolean validateChainData ( JsonNode data ) throws Exception {
2019-07-24 22:53:15 +00:00
ECPublicKey lastKey = null ;
boolean validChain = false ;
for ( JsonNode node : data ) {
JWSObject jwt = JWSObject . parse ( node . asText ( ) ) ;
if ( ! validChain ) {
validChain = EncryptionUtils . verifyJwt ( jwt , EncryptionUtils . getMojangPublicKey ( ) ) ;
}
if ( lastKey ! = null ) {
2020-08-17 16:04:09 +00:00
if ( ! EncryptionUtils . verifyJwt ( jwt , lastKey ) ) return false ;
2019-07-24 22:53:15 +00:00
}
JsonNode payloadNode = JSON_MAPPER . readTree ( jwt . getPayload ( ) . toString ( ) ) ;
JsonNode ipkNode = payloadNode . get ( " identityPublicKey " ) ;
Preconditions . checkState ( ipkNode ! = null & & ipkNode . getNodeType ( ) = = JsonNodeType . STRING , " identityPublicKey node is missing in chain " ) ;
lastKey = EncryptionUtils . generateKey ( ipkNode . asText ( ) ) ;
}
return validChain ;
}
2019-08-02 03:02:17 +00:00
public static void encryptPlayerConnection ( GeyserConnector connector , GeyserSession session , LoginPacket loginPacket ) {
JsonNode certData ;
try {
certData = JSON_MAPPER . readTree ( loginPacket . getChainData ( ) . toByteArray ( ) ) ;
} catch ( IOException ex ) {
throw new RuntimeException ( " Certificate JSON can not be read. " ) ;
}
JsonNode certChainData = certData . get ( " chain " ) ;
if ( certChainData . getNodeType ( ) ! = JsonNodeType . ARRAY ) {
throw new RuntimeException ( " Certificate data is not valid " ) ;
}
2019-08-02 20:55:06 +00:00
encryptConnectionWithCert ( connector , session , loginPacket . getSkinData ( ) . toString ( ) , certChainData ) ;
2019-08-02 03:02:17 +00:00
}
2019-11-30 12:34:45 +00:00
private static void encryptConnectionWithCert ( GeyserConnector connector , GeyserSession session , String clientData , JsonNode certChainData ) {
2019-08-02 03:02:17 +00:00
try {
boolean validChain = validateChainData ( certChainData ) ;
connector . getLogger ( ) . debug ( String . format ( " Is player data valid? %s " , validChain ) ) ;
2020-08-17 16:04:09 +00:00
if ( ! validChain & & ! session . getConnector ( ) . getConfig ( ) . isEnableProxyConnections ( ) ) {
2020-08-12 18:48:40 +00:00
session . disconnect ( LanguageUtils . getLocaleStringLog ( " geyser.network.remote.invalid_xbox_account " ) ) ;
2020-08-12 15:42:02 +00:00
return ;
}
2019-08-02 03:02:17 +00:00
JWSObject jwt = JWSObject . parse ( certChainData . get ( certChainData . size ( ) - 1 ) . asText ( ) ) ;
JsonNode payload = JSON_MAPPER . readTree ( jwt . getPayload ( ) . toBytes ( ) ) ;
if ( payload . get ( " extraData " ) . getNodeType ( ) ! = JsonNodeType . OBJECT ) {
throw new RuntimeException ( " AuthData was not found! " ) ;
}
2019-11-30 12:34:45 +00:00
JsonNode extraData = payload . get ( " extraData " ) ;
2020-01-04 05:58:58 +00:00
session . setAuthenticationData ( new AuthData (
2019-11-30 12:34:45 +00:00
extraData . get ( " displayName " ) . asText ( ) ,
UUID . fromString ( extraData . get ( " identity " ) . asText ( ) ) ,
extraData . get ( " XUID " ) . asText ( )
) ) ;
2019-08-02 03:02:17 +00:00
if ( payload . get ( " identityPublicKey " ) . getNodeType ( ) ! = JsonNodeType . STRING ) {
throw new RuntimeException ( " Identity Public Key was not found! " ) ;
}
ECPublicKey identityPublicKey = EncryptionUtils . generateKey ( payload . get ( " identityPublicKey " ) . textValue ( ) ) ;
2019-11-30 12:34:45 +00:00
JWSObject clientJwt = JWSObject . parse ( clientData ) ;
2019-08-02 03:02:17 +00:00
EncryptionUtils . verifyJwt ( clientJwt , identityPublicKey ) ;
2019-11-30 12:34:45 +00:00
session . setClientData ( JSON_MAPPER . convertValue ( JSON_MAPPER . readTree ( clientJwt . getPayload ( ) . toBytes ( ) ) , BedrockClientData . class ) ) ;
2019-08-02 03:02:17 +00:00
if ( EncryptionUtils . canUseEncryption ( ) ) {
LoginEncryptionUtils . startEncryptionHandshake ( session , identityPublicKey ) ;
}
} catch ( Exception ex ) {
session . disconnect ( " disconnectionScreen.internalError.cantConnect " ) ;
throw new RuntimeException ( " Unable to complete login " , ex ) ;
}
}
private static void startEncryptionHandshake ( GeyserSession session , PublicKey key ) throws Exception {
KeyPairGenerator generator = KeyPairGenerator . getInstance ( " EC " ) ;
generator . initialize ( new ECGenParameterSpec ( " secp384r1 " ) ) ;
KeyPair serverKeyPair = generator . generateKeyPair ( ) ;
byte [ ] token = EncryptionUtils . generateRandomToken ( ) ;
SecretKey encryptionKey = EncryptionUtils . getSecretKey ( serverKeyPair . getPrivate ( ) , key , token ) ;
2019-10-02 20:45:29 +00:00
session . getUpstream ( ) . getSession ( ) . enableEncryption ( encryptionKey ) ;
2019-08-02 03:02:17 +00:00
ServerToClientHandshakePacket packet = new ServerToClientHandshakePacket ( ) ;
packet . setJwt ( EncryptionUtils . createHandshakeJwt ( serverKeyPair , token ) . serialize ( ) ) ;
2020-05-05 15:51:43 +00:00
session . sendUpstreamPacketImmediately ( packet ) ;
2019-08-02 03:02:17 +00:00
}
2021-01-11 20:52:02 +00:00
private static final int AUTH_MSA_DETAILS_FORM_ID = 1334 ;
private static final int AUTH_MSA_CODE_FORM_ID = 1335 ;
private static final int AUTH_FORM_ID = 1336 ;
private static final int AUTH_DETAILS_FORM_ID = 1337 ;
2019-08-02 03:02:17 +00:00
public static void showLoginWindow ( GeyserSession session ) {
2021-01-11 20:52:02 +00:00
// Set DoDaylightCycle to false so the time doesn't accelerate while we're here
session . setDaylightCycle ( false ) ;
2020-10-29 22:30:52 +00:00
String userLanguage = session . getLocale ( ) ;
2020-07-05 23:35:51 +00:00
SimpleFormWindow window = new SimpleFormWindow ( LanguageUtils . getPlayerLocaleString ( " geyser.auth.login.form.notice.title " , userLanguage ) , LanguageUtils . getPlayerLocaleString ( " geyser.auth.login.form.notice.desc " , userLanguage ) ) ;
2021-01-11 20:52:02 +00:00
if ( session . getConnector ( ) . getConfig ( ) . getRemote ( ) . isPasswordAuthentication ( ) ) {
window . getButtons ( ) . add ( new FormButton ( LanguageUtils . getPlayerLocaleString ( " geyser.auth.login.form.notice.btn_login.mojang " , userLanguage ) ) ) ;
}
window . getButtons ( ) . add ( new FormButton ( LanguageUtils . getPlayerLocaleString ( " geyser.auth.login.form.notice.btn_login.microsoft " , userLanguage ) ) ) ;
2020-07-05 23:35:51 +00:00
window . getButtons ( ) . add ( new FormButton ( LanguageUtils . getPlayerLocaleString ( " geyser.auth.login.form.notice.btn_disconnect " , userLanguage ) ) ) ;
2020-04-17 12:54:04 +00:00
session . sendForm ( window , AUTH_FORM_ID ) ;
}
public static void showLoginDetailsWindow ( GeyserSession session ) {
2020-10-29 22:30:52 +00:00
String userLanguage = session . getLocale ( ) ;
2020-07-05 23:35:51 +00:00
CustomFormWindow window = new CustomFormBuilder ( LanguageUtils . getPlayerLocaleString ( " geyser.auth.login.form.details.title " , userLanguage ) )
. addComponent ( new LabelComponent ( LanguageUtils . getPlayerLocaleString ( " geyser.auth.login.form.details.desc " , userLanguage ) ) )
. addComponent ( new InputComponent ( LanguageUtils . getPlayerLocaleString ( " geyser.auth.login.form.details.email " , userLanguage ) , " account@geysermc.org " , " " ) )
. addComponent ( new InputComponent ( LanguageUtils . getPlayerLocaleString ( " geyser.auth.login.form.details.pass " , userLanguage ) , " 123456 " , " " ) )
2019-08-02 03:02:17 +00:00
. build ( ) ;
2020-04-17 12:54:04 +00:00
session . sendForm ( window , AUTH_DETAILS_FORM_ID ) ;
2019-08-02 03:02:17 +00:00
}
2021-01-11 20:52:02 +00:00
/ * *
* Prompts the user between either OAuth code login or manual password authentication
* /
public static void showMicrosoftAuthenticationWindow ( GeyserSession session ) {
String userLanguage = session . getLocale ( ) ;
SimpleFormWindow window = new SimpleFormWindow ( LanguageUtils . getPlayerLocaleString ( " geyser.auth.login.form.notice.btn_login.microsoft " , userLanguage ) , " " ) ;
window . getButtons ( ) . add ( new FormButton ( LanguageUtils . getPlayerLocaleString ( " geyser.auth.login.method.browser " , userLanguage ) ) ) ;
window . getButtons ( ) . add ( new FormButton ( LanguageUtils . getPlayerLocaleString ( " geyser.auth.login.method.password " , userLanguage ) ) ) ; // This form won't show if password authentication is disabled
window . getButtons ( ) . add ( new FormButton ( LanguageUtils . getPlayerLocaleString ( " geyser.auth.login.form.notice.btn_disconnect " , userLanguage ) ) ) ;
session . sendForm ( window , AUTH_MSA_DETAILS_FORM_ID ) ;
}
/ * *
* Shows the code that a user must input into their browser
* /
public static void showMicrosoftCodeWindow ( GeyserSession session , MsaAuthenticationService . MsCodeResponse response ) {
ModalFormWindow msaCodeWindow = new ModalFormWindow ( " %xbox.signin " , " %xbox.signin.website \ n%xbox.signin.url \ n%xbox.signin.enterCode \ n " +
response . user_code , " Done " , " %menu.disconnect " ) ;
session . sendForm ( msaCodeWindow , LoginEncryptionUtils . AUTH_MSA_CODE_FORM_ID ) ;
}
2020-04-17 12:54:04 +00:00
public static boolean authenticateFromForm ( GeyserSession session , GeyserConnector connector , int formId , String formData ) {
2019-08-02 03:02:17 +00:00
WindowCache windowCache = session . getWindowCache ( ) ;
2020-04-17 12:54:04 +00:00
if ( ! windowCache . getWindows ( ) . containsKey ( formId ) )
2019-08-02 03:02:17 +00:00
return false ;
2021-01-11 20:52:02 +00:00
if ( formId = = AUTH_MSA_DETAILS_FORM_ID | | formId = = AUTH_FORM_ID | | formId = = AUTH_DETAILS_FORM_ID | | formId = = AUTH_MSA_CODE_FORM_ID ) {
2020-04-17 12:54:04 +00:00
FormWindow window = windowCache . getWindows ( ) . remove ( formId ) ;
window . setResponse ( formData . trim ( ) ) ;
if ( ! session . isLoggedIn ( ) ) {
if ( formId = = AUTH_DETAILS_FORM_ID & & window instanceof CustomFormWindow ) {
CustomFormWindow customFormWindow = ( CustomFormWindow ) window ;
CustomFormResponse response = ( CustomFormResponse ) customFormWindow . getResponse ( ) ;
if ( response ! = null ) {
String email = response . getInputResponses ( ) . get ( 1 ) ;
String password = response . getInputResponses ( ) . get ( 2 ) ;
session . authenticate ( email , password ) ;
2021-01-05 18:45:01 +00:00
// Clear windows so authentication data isn't accidentally cached
windowCache . getWindows ( ) . clear ( ) ;
2020-05-20 15:12:03 +00:00
} else {
showLoginDetailsWindow ( session ) ;
2020-04-17 12:54:04 +00:00
}
} else if ( formId = = AUTH_FORM_ID & & window instanceof SimpleFormWindow ) {
2021-01-11 20:52:02 +00:00
boolean isPasswordAuthentication = session . getConnector ( ) . getConfig ( ) . getRemote ( ) . isPasswordAuthentication ( ) ;
int microsoftButton = isPasswordAuthentication ? 1 : 0 ;
int disconnectButton = isPasswordAuthentication ? 2 : 1 ;
SimpleFormResponse response = ( SimpleFormResponse ) window . getResponse ( ) ;
if ( response ! = null ) {
if ( isPasswordAuthentication & & response . getClickedButtonId ( ) = = 0 ) {
session . setMicrosoftAccount ( false ) ;
showLoginDetailsWindow ( session ) ;
} else if ( response . getClickedButtonId ( ) = = microsoftButton ) {
session . setMicrosoftAccount ( true ) ;
if ( isPasswordAuthentication ) {
showMicrosoftAuthenticationWindow ( session ) ;
} else {
// Just show the OAuth code
session . authenticateWithMicrosoftCode ( ) ;
}
} else if ( response . getClickedButtonId ( ) = = disconnectButton ) {
session . disconnect ( LanguageUtils . getPlayerLocaleString ( " geyser.auth.login.form.disconnect " , session . getLocale ( ) ) ) ;
}
} else {
showLoginWindow ( session ) ;
}
} else if ( formId = = AUTH_MSA_DETAILS_FORM_ID & & window instanceof SimpleFormWindow ) {
2020-04-17 12:54:04 +00:00
SimpleFormResponse response = ( SimpleFormResponse ) window . getResponse ( ) ;
2020-05-20 15:12:03 +00:00
if ( response ! = null ) {
if ( response . getClickedButtonId ( ) = = 0 ) {
2021-01-11 20:52:02 +00:00
session . authenticateWithMicrosoftCode ( ) ;
} else if ( response . getClickedButtonId ( ) = = 1 ) {
2020-04-17 12:54:04 +00:00
showLoginDetailsWindow ( session ) ;
2021-01-11 20:52:02 +00:00
} else if ( response . getClickedButtonId ( ) = = 2 ) {
2020-10-29 22:30:52 +00:00
session . disconnect ( LanguageUtils . getPlayerLocaleString ( " geyser.auth.login.form.disconnect " , session . getLocale ( ) ) ) ;
2020-04-17 12:54:04 +00:00
}
2020-05-20 15:12:03 +00:00
} else {
showLoginWindow ( session ) ;
2020-04-17 12:54:04 +00:00
}
2021-01-11 20:52:02 +00:00
} else if ( formId = = AUTH_MSA_CODE_FORM_ID & & window instanceof ModalFormWindow ) {
ModalFormResponse response = ( ModalFormResponse ) window . getResponse ( ) ;
if ( response ! = null ) {
if ( response . getClickedButtonId ( ) = = 1 ) {
session . disconnect ( LanguageUtils . getPlayerLocaleString ( " geyser.auth.login.form.disconnect " , session . getLocale ( ) ) ) ;
}
} else {
showMicrosoftAuthenticationWindow ( session ) ;
}
2019-08-02 20:54:40 +00:00
}
2019-08-02 03:02:17 +00:00
}
}
return true ;
}
2019-07-24 22:53:15 +00:00
}