Minecraft 1.19 support

This commit is contained in:
md_5
2022-06-08 02:00:00 +10:00
parent 862bb2ac72
commit eccdf87f22
49 changed files with 851 additions and 227 deletions

View File

@@ -1,5 +1,9 @@
package net.md_5.bungee;
import com.google.common.io.ByteStreams;
import com.google.common.primitives.Longs;
import java.io.IOException;
import java.nio.charset.StandardCharsets;
import java.security.GeneralSecurityException;
import java.security.Key;
import java.security.KeyFactory;
@@ -7,8 +11,11 @@ import java.security.KeyPair;
import java.security.KeyPairGenerator;
import java.security.NoSuchAlgorithmException;
import java.security.PublicKey;
import java.security.Signature;
import java.security.spec.InvalidKeySpecException;
import java.security.spec.X509EncodedKeySpec;
import java.util.Arrays;
import java.util.Base64;
import java.util.Random;
import javax.crypto.Cipher;
import javax.crypto.SecretKey;
@@ -18,6 +25,7 @@ import net.md_5.bungee.jni.NativeCode;
import net.md_5.bungee.jni.cipher.BungeeCipher;
import net.md_5.bungee.jni.cipher.JavaCipher;
import net.md_5.bungee.jni.cipher.NativeCipher;
import net.md_5.bungee.protocol.PlayerPublicKey;
import net.md_5.bungee.protocol.packet.EncryptionRequest;
import net.md_5.bungee.protocol.packet.EncryptionResponse;
@@ -28,10 +36,12 @@ public class EncryptionUtil
{
private static final Random random = new Random();
private static final Base64.Encoder MIME_ENCODER = Base64.getMimeEncoder( 76, "\n".getBytes( StandardCharsets.UTF_8 ) );
public static final KeyPair keys;
@Getter
private static final SecretKey secret = new SecretKeySpec( new byte[ 16 ], "AES" );
public static final NativeCode<BungeeCipher> nativeFactory = new NativeCode<>( "native-cipher", JavaCipher::new, NativeCipher::new );
private static final PublicKey MOJANG_KEY;
static
{
@@ -44,6 +54,14 @@ public class EncryptionUtil
{
throw new ExceptionInInitializerError( ex );
}
try
{
MOJANG_KEY = KeyFactory.getInstance( "RSA" ).generatePublic( new X509EncodedKeySpec( ByteStreams.toByteArray( EncryptionUtil.class.getResourceAsStream( "/yggdrasil_session_pubkey.der" ) ) ) );
} catch ( IOException | NoSuchAlgorithmException | InvalidKeySpecException ex )
{
throw new ExceptionInInitializerError( ex );
}
}
public static EncryptionRequest encryptRequest()
@@ -55,17 +73,40 @@ public class EncryptionUtil
return new EncryptionRequest( hash, pubKey, verify );
}
public static boolean check(PlayerPublicKey publicKey) throws GeneralSecurityException
{
Signature signature = Signature.getInstance( "SHA1withRSA" );
signature.initVerify( MOJANG_KEY );
signature.update( ( publicKey.getExpiry() + "-----BEGIN RSA PUBLIC KEY-----\n" + MIME_ENCODER.encodeToString( getPubkey( publicKey.getKey() ).getEncoded() ) + "\n-----END RSA PUBLIC KEY-----\n" ).getBytes( StandardCharsets.US_ASCII ) );
return signature.verify( publicKey.getSignature() );
}
public static boolean check(PlayerPublicKey publicKey, EncryptionResponse resp, EncryptionRequest request) throws GeneralSecurityException
{
if ( publicKey != null )
{
Signature signature = Signature.getInstance( "SHA256withRSA" );
signature.initVerify( getPubkey( publicKey.getKey() ) );
signature.update( request.getVerifyToken() );
signature.update( Longs.toByteArray( resp.getEncryptionData().getSalt() ) );
return signature.verify( resp.getEncryptionData().getSignature() );
} else
{
Cipher cipher = Cipher.getInstance( "RSA" );
cipher.init( Cipher.DECRYPT_MODE, keys.getPrivate() );
byte[] decrypted = cipher.doFinal( resp.getVerifyToken() );
return Arrays.equals( request.getVerifyToken(), decrypted );
}
}
public static SecretKey getSecret(EncryptionResponse resp, EncryptionRequest request) throws GeneralSecurityException
{
Cipher cipher = Cipher.getInstance( "RSA" );
cipher.init( Cipher.DECRYPT_MODE, keys.getPrivate() );
byte[] decrypted = cipher.doFinal( resp.getVerifyToken() );
if ( !Arrays.equals( request.getVerifyToken(), decrypted ) )
{
throw new IllegalStateException( "Key pairs do not match!" );
}
cipher.init( Cipher.DECRYPT_MODE, keys.getPrivate() );
return new SecretKeySpec( cipher.doFinal( resp.getSharedSecret() ), "AES" );
}
@@ -80,7 +121,12 @@ public class EncryptionUtil
public static PublicKey getPubkey(EncryptionRequest request) throws GeneralSecurityException
{
return KeyFactory.getInstance( "RSA" ).generatePublic( new X509EncodedKeySpec( request.getPublicKey() ) );
return getPubkey( request.getPublicKey() );
}
private static PublicKey getPubkey(byte[] b) throws GeneralSecurityException
{
return KeyFactory.getInstance( "RSA" ).generatePublic( new X509EncodedKeySpec( b ) );
}
public static byte[] encrypt(Key key, byte[] b) throws GeneralSecurityException

View File

@@ -123,7 +123,7 @@ public class ServerConnector extends PacketHandler
channel.write( copiedHandshake );
channel.setProtocol( Protocol.LOGIN );
channel.write( new LoginRequest( user.getName() ) );
channel.write( new LoginRequest( user.getName(), null ) );
}
@Override
@@ -225,7 +225,7 @@ public class ServerConnector extends PacketHandler
// Set tab list size, TODO: what shall we do about packet mutability
Login modLogin = new Login( login.getEntityId(), login.isHardcore(), login.getGameMode(), login.getPreviousGameMode(), login.getWorldNames(), login.getDimensions(), login.getDimension(), login.getWorldName(), login.getSeed(), login.getDifficulty(),
(byte) user.getPendingConnection().getListener().getTabListSize(), login.getLevelType(), login.getViewDistance(), login.getSimulationDistance(), login.isReducedDebugInfo(), login.isNormalRespawn(), login.isDebug(), login.isFlat() );
(byte) user.getPendingConnection().getListener().getTabListSize(), login.getLevelType(), login.getViewDistance(), login.getSimulationDistance(), login.isReducedDebugInfo(), login.isNormalRespawn(), login.isDebug(), login.isFlat(), login.getDeathLocation() );
user.unsafe().sendPacket( modLogin );
@@ -243,7 +243,7 @@ public class ServerConnector extends PacketHandler
}
user.getSentBossBars().clear();
user.unsafe().sendPacket( new Respawn( login.getDimension(), login.getWorldName(), login.getSeed(), login.getDifficulty(), login.getGameMode(), login.getPreviousGameMode(), login.getLevelType(), login.isDebug(), login.isFlat(), false ) );
user.unsafe().sendPacket( new Respawn( login.getDimension(), login.getWorldName(), login.getSeed(), login.getDifficulty(), login.getGameMode(), login.getPreviousGameMode(), login.getLevelType(), login.isDebug(), login.isFlat(), false, login.getDeathLocation() ) );
user.getServer().disconnect( "Quitting" );
} else
{
@@ -292,11 +292,11 @@ public class ServerConnector extends PacketHandler
user.setDimensionChange( true );
if ( login.getDimension() == user.getDimension() )
{
user.unsafe().sendPacket( new Respawn( (Integer) login.getDimension() >= 0 ? -1 : 0, login.getWorldName(), login.getSeed(), login.getDifficulty(), login.getGameMode(), login.getPreviousGameMode(), login.getLevelType(), login.isDebug(), login.isFlat(), false ) );
user.unsafe().sendPacket( new Respawn( (Integer) login.getDimension() >= 0 ? -1 : 0, login.getWorldName(), login.getSeed(), login.getDifficulty(), login.getGameMode(), login.getPreviousGameMode(), login.getLevelType(), login.isDebug(), login.isFlat(), false, login.getDeathLocation() ) );
}
user.setServerEntityId( login.getEntityId() );
user.unsafe().sendPacket( new Respawn( login.getDimension(), login.getWorldName(), login.getSeed(), login.getDifficulty(), login.getGameMode(), login.getPreviousGameMode(), login.getLevelType(), login.isDebug(), login.isFlat(), false ) );
user.unsafe().sendPacket( new Respawn( login.getDimension(), login.getWorldName(), login.getSeed(), login.getDifficulty(), login.getGameMode(), login.getPreviousGameMode(), login.getLevelType(), login.isDebug(), login.isFlat(), false, login.getDeathLocation() ) );
if ( user.getPendingConnection().getVersion() >= ProtocolConstants.MINECRAFT_1_14 )
{
user.unsafe().sendPacket( new ViewDistance( login.getViewDistance() ) );

View File

@@ -59,6 +59,7 @@ import net.md_5.bungee.protocol.packet.Kick;
import net.md_5.bungee.protocol.packet.PlayerListHeaderFooter;
import net.md_5.bungee.protocol.packet.PluginMessage;
import net.md_5.bungee.protocol.packet.SetCompression;
import net.md_5.bungee.protocol.packet.SystemChat;
import net.md_5.bungee.tab.ServerUnique;
import net.md_5.bungee.tab.TabList;
import net.md_5.bungee.util.CaseInsensitiveSet;
@@ -416,6 +417,10 @@ public final class UserConnection implements ProxiedPlayer
public void chat(String message)
{
Preconditions.checkState( server != null, "Not connected to server" );
if ( getPendingConnection().getVersion() >= ProtocolConstants.MINECRAFT_1_19 )
{
throw new UnsupportedOperationException( "Cannot spoof chat on this client version!" );
}
server.getCh().write( new Chat( message ) );
}
@@ -472,7 +477,13 @@ public final class UserConnection implements ProxiedPlayer
private void sendMessage(ChatMessageType position, UUID sender, String message)
{
unsafe().sendPacket( new Chat( message, (byte) position.ordinal(), sender ) );
if ( getPendingConnection().getVersion() >= ProtocolConstants.MINECRAFT_1_19 )
{
unsafe().sendPacket( new SystemChat( message, position.ordinal() ) );
} else
{
unsafe().sendPacket( new Chat( message, (byte) position.ordinal(), sender ) );
}
}
private void sendMessage(ChatMessageType position, UUID sender, BaseComponent... message)

View File

@@ -47,6 +47,10 @@ public class Configuration implements ProxyConfig
* Should we check minecraft.net auth.
*/
private boolean onlineMode = true;
/**
* Whether to check the authentication server public key.
*/
private boolean enforceSecureProfile;
/**
* Whether we log proxy commands to the proxy log
*/
@@ -86,6 +90,7 @@ public class Configuration implements ProxyConfig
timeout = adapter.getInt( "timeout", timeout );
uuid = adapter.getString( "stats", uuid );
onlineMode = adapter.getBoolean( "online_mode", onlineMode );
enforceSecureProfile = adapter.getBoolean( "enforce_secure_profile", enforceSecureProfile );
logCommands = adapter.getBoolean( "log_commands", logCommands );
logPings = adapter.getBoolean( "log_pings", logPings );
remotePingCache = adapter.getInt( "remote_ping_cache", remotePingCache );

View File

@@ -9,6 +9,7 @@ import java.net.SocketAddress;
import java.net.URLEncoder;
import java.nio.charset.StandardCharsets;
import java.security.MessageDigest;
import java.time.Instant;
import java.util.HashSet;
import java.util.Set;
import java.util.UUID;
@@ -49,6 +50,7 @@ import net.md_5.bungee.netty.cipher.CipherDecoder;
import net.md_5.bungee.netty.cipher.CipherEncoder;
import net.md_5.bungee.protocol.DefinedPacket;
import net.md_5.bungee.protocol.PacketWrapper;
import net.md_5.bungee.protocol.PlayerPublicKey;
import net.md_5.bungee.protocol.Protocol;
import net.md_5.bungee.protocol.ProtocolConstants;
import net.md_5.bungee.protocol.packet.EncryptionRequest;
@@ -358,6 +360,29 @@ public class InitialHandler extends PacketHandler implements PendingConnection
disconnect( bungee.getTranslation( "name_invalid" ) );
return;
}
if ( BungeeCord.getInstance().config.isEnforceSecureProfile() )
{
PlayerPublicKey publicKey = loginRequest.getPublicKey();
if ( publicKey == null )
{
disconnect( bungee.getTranslation( "secure_profile_required" ) );
return;
}
if ( Instant.ofEpochMilli( publicKey.getExpiry() ).isBefore( Instant.now() ) )
{
disconnect( bungee.getTranslation( "secure_profile_expired" ) );
return;
}
if ( !EncryptionUtil.check( publicKey ) )
{
disconnect( bungee.getTranslation( "secure_profile_invalid" ) );
return;
}
}
this.loginRequest = loginRequest;
int limit = BungeeCord.getInstance().config.getPlayerLimit();
@@ -410,6 +435,7 @@ public class InitialHandler extends PacketHandler implements PendingConnection
public void handle(final EncryptionResponse encryptResponse) throws Exception
{
Preconditions.checkState( thisState == State.ENCRYPT, "Not expecting ENCRYPT" );
Preconditions.checkState( EncryptionUtil.check( loginRequest.getPublicKey(), encryptResponse, request ), "Invalid verification" );
SecretKey sharedKey = EncryptionUtil.getSecret( encryptResponse, request );
BungeeCipher decrypt = EncryptionUtil.getCipher( false, sharedKey );
@@ -524,7 +550,7 @@ public class InitialHandler extends PacketHandler implements PendingConnection
userCon.setCompressionThreshold( BungeeCord.getInstance().config.getCompressionThreshold() );
userCon.init();
unsafe.sendPacket( new LoginSuccess( getUniqueId(), getName() ) );
unsafe.sendPacket( new LoginSuccess( getUniqueId(), getName(), loginProfile.getProperties() ) );
ch.setProtocol( Protocol.GAME );
ch.getHandle().pipeline().get( HandlerBoss.class ).setHandler( new UpstreamBridge( bungee, userCon ) );

View File

@@ -2,6 +2,7 @@ package net.md_5.bungee.connection;
import lombok.AllArgsConstructor;
import lombok.Data;
import net.md_5.bungee.protocol.Property;
@Data
@AllArgsConstructor
@@ -11,14 +12,4 @@ public class LoginResult
private String id;
private String name;
private Property[] properties;
@Data
@AllArgsConstructor
public static class Property
{
private String name;
private String value;
private String signature;
}
}

View File

@@ -26,6 +26,8 @@ import net.md_5.bungee.netty.PacketHandler;
import net.md_5.bungee.protocol.PacketWrapper;
import net.md_5.bungee.protocol.ProtocolConstants;
import net.md_5.bungee.protocol.packet.Chat;
import net.md_5.bungee.protocol.packet.ClientChat;
import net.md_5.bungee.protocol.packet.ClientCommand;
import net.md_5.bungee.protocol.packet.ClientSettings;
import net.md_5.bungee.protocol.packet.KeepAlive;
import net.md_5.bungee.protocol.packet.PlayerListItem;
@@ -145,9 +147,33 @@ public class UpstreamBridge extends PacketHandler
@Override
public void handle(Chat chat) throws Exception
{
for ( int index = 0, length = chat.getMessage().length(); index < length; index++ )
String message = handleChat( chat.getMessage() );
if ( message != null )
{
char c = chat.getMessage().charAt( index );
chat.setMessage( message );
con.getServer().unsafe().sendPacket( chat );
}
throw CancelSendSignal.INSTANCE;
}
@Override
public void handle(ClientChat chat) throws Exception
{
handleChat( chat.getMessage() );
}
@Override
public void handle(ClientCommand command) throws Exception
{
handleChat( "/" + command.getCommand() );
}
private String handleChat(String message)
{
for ( int index = 0, length = message.length(); index < length; index++ )
{
char c = message.charAt( index );
if ( !AllowedCharacters.isChatAllowedCharacter( c ) )
{
con.disconnect( bungee.getTranslation( "illegal_chat_characters", Util.unicode( c ) ) );
@@ -155,16 +181,16 @@ public class UpstreamBridge extends PacketHandler
}
}
ChatEvent chatEvent = new ChatEvent( con, con.getServer(), chat.getMessage() );
ChatEvent chatEvent = new ChatEvent( con, con.getServer(), message );
if ( !bungee.getPluginManager().callEvent( chatEvent ).isCancelled() )
{
chat.setMessage( chatEvent.getMessage() );
if ( !chatEvent.isCommand() || !bungee.getPluginManager().dispatchCommand( con, chat.getMessage().substring( 1 ) ) )
message = chatEvent.getMessage();
if ( !chatEvent.isCommand() || !bungee.getPluginManager().dispatchCommand( con, message.substring( 1 ) ) )
{
con.getServer().unsafe().sendPacket( chat );
return message;
}
}
throw CancelSendSignal.INSTANCE;
return null;
}
@Override

View File

@@ -74,6 +74,8 @@ public abstract class EntityMap
case ProtocolConstants.MINECRAFT_1_18:
case ProtocolConstants.MINECRAFT_1_18_2:
return EntityMap_1_16_2.INSTANCE_1_18;
case ProtocolConstants.MINECRAFT_1_19:
return EntityMap_1_16_2.INSTANCE_1_19;
}
throw new RuntimeException( "Version " + version + " has no entity map" );
}

View File

@@ -17,6 +17,8 @@ class EntityMap_1_16_2 extends EntityMap
static final EntityMap_1_16_2 INSTANCE_1_16_2 = new EntityMap_1_16_2( 0x04, 0x2D );
static final EntityMap_1_16_2 INSTANCE_1_17 = new EntityMap_1_16_2( 0x04, 0x2D );
static final EntityMap_1_16_2 INSTANCE_1_18 = new EntityMap_1_16_2( 0x04, 0x2D );
static final EntityMap_1_16_2 INSTANCE_1_19 = new EntityMap_1_16_2( 0x02, 0x2F );
//
private final int spawnPlayerId;
private final int spectateId;

View File

@@ -5,6 +5,7 @@ import net.md_5.bungee.BungeeCord;
import net.md_5.bungee.UserConnection;
import net.md_5.bungee.api.connection.ProxiedPlayer;
import net.md_5.bungee.connection.LoginResult;
import net.md_5.bungee.protocol.Property;
import net.md_5.bungee.protocol.packet.PlayerListItem;
@RequiredArgsConstructor
@@ -38,20 +39,15 @@ public abstract class TabList
LoginResult loginResult = player.getPendingConnection().getLoginProfile();
if ( loginResult != null && loginResult.getProperties() != null )
{
String[][] props = new String[ loginResult.getProperties().length ][];
Property[] props = new Property[ loginResult.getProperties().length ];
for ( int i = 0; i < props.length; i++ )
{
props[i] = new String[]
{
loginResult.getProperties()[i].getName(),
loginResult.getProperties()[i].getValue(),
loginResult.getProperties()[i].getSignature()
};
props[i] = new Property( loginResult.getProperties()[i].getName(), loginResult.getProperties()[i].getValue(), loginResult.getProperties()[i].getSignature() );
}
item.setProperties( props );
} else
{
item.setProperties( new String[ 0 ][ 0 ] );
item.setProperties( new Property[ 0 ] );
}
if ( playerListItem.getAction() == PlayerListItem.Action.ADD_PLAYER || playerListItem.getAction() == PlayerListItem.Action.UPDATE_GAMEMODE )
{

View File

@@ -22,6 +22,9 @@ total_players=Total players online: {0}
name_invalid=Username contains invalid characters.
ping_cannot_connect=\u00a7c[Bungee] Can''t connect to server.
offline_mode_player=Not authenticated with Minecraft.net
secure_profile_required=A secure profile is required to join this server.
secure_profile_expired=Secure profile expired.
secure_profile_invalid=Secure profile invalid.
message_needed=\u00a7cYou must supply a message.
error_occurred_player=\u00a7cAn error occurred while parsing your message. (Hover for details)
error_occurred_console=\u00a7cAn error occurred while parsing your message: {0}

Binary file not shown.