1. Вы находитесь в сообществе Rubukkit. Мы - администраторы серверов Minecraft, разрабатываем собственные плагины и переводим на различные языки плагины наших коллег из других стран.
    Скрыть объявление
Скрыть объявление
В преддверии глобального обновления, мы проводим исследования, которые помогут нам сделать опыт пользования форумом ещё удобнее. Помогите нам, примите участие!

Помогите Как я могу определить, использует ли игрок, пиратский или лиценцзионный клиент?

Тема в разделе "Разработка плагинов для новичков", создана пользователем Ryazha_, 11 янв 2025.

  1. Автор темы
    Ryazha_

    Ryazha_ Активный участник Пользователь

    Баллы:
    76
    Имя в Minecraft:
    Ryazha_
    Как я могу определить, использует ли игрок, подключающийся к серверу, пиратский (взломанный) или лицензионный клиент Minecraft? Мне известно, что для этого можно использовать Mojang API, но он работает только когда online-mode=true.
     
  2. BetterLex

    BetterLex Активный участник Пользователь

    Баллы:
    76
    Почитай этот пост.
     
  3. Автор темы
    Ryazha_

    Ryazha_ Активный участник Пользователь

    Баллы:
    76
    Имя в Minecraft:
    Ryazha_
    Код:
    import java.io.IOException;
    import java.net.HttpURLConnection;
    import java.net.URL;
    
    public static boolean isPremiumByAPI(String playerName) {
        try {
            URL url = new URL("https://api.ashcon.app/mojang/v2/user/" + playerName);
            HttpURLConnection connection = (HttpURLConnection) url.openConnection();
            return (connection.getResponseCode() == HttpURLConnection.HTTP_OK);
        } catch (IOException e) {
            e.printStackTrace();
            return false;
        }
    }
    Это должно работать по сути в офлайн моде?
     
  4. Dymeth

    Dymeth Активный участник Пользователь

    Баллы:
    98
    Имя в Minecraft:
    Dymeth
    Так ты лишь проверишь, существует ли такой лицензионный ник в Mojang. Это вовсе не означает, что у игрока есть доступ к этому аккаунту, он вполне может входить и с пиратки с чужим ником.

    Я относительно недавно описывал, как организовать авторизацию на сервере по лицензии:
    https://rubukkit.org/threads/proverka-licenzii-na-piratke.193204/#post-1744881

    Правда с тех пор закрылся wiki.vg, вот актуальные ссылки:

    https://minecraft.wiki/w/Minecraft_Wiki:Projects/wiki.vg_merge/Protocol#Login
    https://minecraft.wiki/w/Microsoft_authentication
    https://minecraft.wiki/w/Mojang_API
     
    Последнее редактирование: 11 янв 2025
  5. Автор темы
    Ryazha_

    Ryazha_ Активный участник Пользователь

    Баллы:
    76
    Имя в Minecraft:
    Ryazha_
    Можешь ещё раз пояснить в чём была идея?
     
  6. Dymeth

    Dymeth Активный участник Пользователь

    Баллы:
    98
    Имя в Minecraft:
    Dymeth
    Ты по своей инициативе никак не узнаешь, есть ли у игрока лицензия. Инициатива должна исходить от игрока, он должен хотя бы раз переподключиться к серверу, чтобы ты смог включить ему онлайн мод и тем самым убедиться в наличии или отсутствии у него лицензии
     
  7. Автор темы
    Ryazha_

    Ryazha_ Активный участник Пользователь

    Баллы:
    76
    Имя в Minecraft:
    Ryazha_
    А нельзя ли сразу по пакетам проверять при первом входе, там вроде выше версии 1.20 какой-то новый способ есть?
     
  8. Автор темы
    Ryazha_

    Ryazha_ Активный участник Пользователь

    Баллы:
    76
    Имя в Minecraft:
    Ryazha_
    А при помощи NMS есть способ какой?
     
  9. Автор темы
    Ryazha_

    Ryazha_ Активный участник Пользователь

    Баллы:
    76
    Имя в Minecraft:
    Ryazha_
    https://github.com/yann1n/LizardAuth

    Я накидал плагин, но к сожалению не удалось правильно реализовать проверку на лицензию.
     
  10. Dymeth

    Dymeth Активный участник Пользователь

    Баллы:
    98
    Имя в Minecraft:
    Dymeth
    Какой способ? С transfer пакетом?
    Это не способ, а лишь моё личное предположение. Ты можешь попробовать. Но получится или нет - неизвестно. На текущий момент я не слышал, чтобы кому-то действительно удавалось использовать это. Хотя не исключаю, что никто и не пытался.

    НМС - это серверная сторона, а ограничения на стороне клиента.
    Поэтому нет, твою задачу это не решит.
    Ты можешь необходимые пакеты через нмс посылать, но смысла в этом мало, практически для всех действий сейчас уже есть апи.

    Честно скажу - с твоим текущим уровнем понимания работы программирования и игры тебе придётся очень несладко делать авторизацию по лицензии, даже если ты прибегнешь к гарантированно рабочему способу, реализованному на других серверах.
    Тебе предстоит ещё многое изучить и понять, чтобы лезть в эту тему.
    Поэтому советую либо взять задачку попроще, либо начать серьёзно погружаться в принцип работы игры: и в нмс, и в протокол, и в логику работы клиента.
    Благо основная документация по этому уже есть, ссылки кидал выше.

    Если есть конкретные вопросы - спрашивай. Но пока всё выглядит так, будто эта задача для тебя сложновата, если не сам не начнёшь досконально вникать в каждую деталь
     
    Последнее редактирование: 14 янв 2025
  11. Автор темы
    Ryazha_

    Ryazha_ Активный участник Пользователь

    Баллы:
    76
    Имя в Minecraft:
    Ryazha_

    Из статье о протоколе, я понял что это можно реализовать за счет Энкриптинг пакетов и запросов. Но не понимаю как именно это реализовать, опыта не было.
     
  12. Dymeth

    Dymeth Активный участник Пользователь

    Баллы:
    98
    Имя в Minecraft:
    Dymeth
    На банже можно использовать метод .setOnlineMode(true)
    На пейпере, по-моему, подобного нет. Соответственно тебе нужно как-то приостанавливать подключение на этапе login, а после отправлять пакет вручную.
    Ну, либо рефлексией как-то подменять оффлайн мод на онлайн, чтобы ядро отправило пакет самостоятельно. Но не уверен, что это возможно для отдельных игроков.
    Короче говоря, нужно проводить ресерч. И в этом и состоит основная сложность для человека без опыта
     
  13. Автор темы
    Ryazha_

    Ryazha_ Активный участник Пользователь

    Баллы:
    76
    Имя в Minecraft:
    Ryazha_
    А почему НМС тут не поможет?, там же присутствует работа с пакетами.
     
  14. Dymeth

    Dymeth Активный участник Пользователь

    Баллы:
    98
    Имя в Minecraft:
    Dymeth
    Ты можешь и через нмс пакеты отправлять, но зачем?
     
  15. Автор темы
    Ryazha_

    Ryazha_ Активный участник Пользователь

    Баллы:
    76
    Имя в Minecraft:
    Ryazha_
    Как я буду получать или создавать Server ID и ключи шифрования дла отправки пакетов, какие у них свойства, какое шифрование применять?

    Вот код, который я реализовал на данный момент:


    Код:
    import com.comphenix.protocol.PacketType;
    import com.comphenix.protocol.ProtocolLibrary;
    import com.comphenix.protocol.ProtocolManager;
    import com.comphenix.protocol.events.ListenerPriority;
    import com.comphenix.protocol.events.PacketAdapter;
    import com.comphenix.protocol.events.PacketEvent;
    import org.bukkit.entity.Player;
    import org.bukkit.event.EventHandler;
    import org.bukkit.event.Listener;
    import org.bukkit.event.player.AsyncPlayerPreLoginEvent;
    import org.bukkit.event.player.PlayerJoinEvent;
    import org.bukkit.plugin.java.JavaPlugin;
    
    import java.io.File;
    import java.io.FileOutputStream;
    import java.io.IOException;
    import java.nio.file.Files;
    import java.nio.file.Paths;
    import java.security.*;
    import java.security.spec.PKCS8EncodedKeySpec;
    import java.security.spec.X509EncodedKeySpec;
    import java.util.Arrays;
    import java.util.Random;
    import java.util.UUID;
    import java.util.concurrent.CompletableFuture;
    import javax.crypto.Cipher;
    import javax.crypto.spec.SecretKeySpec;
    
    public class LizardAuth extends JavaPlugin implements Listener {
    
        private ProtocolManager protocolManager;
        private byte[] sharedSecret;
        private byte[] verifyToken;
        private final Random random = new SecureRandom();
        private PublicKey publicKey;
        private PrivateKey privateKey;
        private File privateKeyFile;
        private final String keyPass = "password";
    
    
        @Override
        public void onEnable() {
            getLogger().info("LizardAuth is enabling...");
            protocolManager = ProtocolLibrary.getProtocolManager();
    
            
            getServer().getPluginManager().registerEvents(this, this);
            getLogger().info("LizardAuth enabled successfully");
    
            try {
                loadOrCreateKeys();
            } catch (NoSuchAlgorithmException e) {
                getLogger().severe("Failed to generate RSA keys: " + e.getMessage());
                this.getServer().getPluginManager().disablePlugin(this);
                return;
            } catch (Exception e) {
                 getLogger().severe("Failed to load or create RSA keys: " + e.getMessage());
                this.getServer().getPluginManager().disablePlugin(this);
                return;
            }
            registerPacketListeners();
        }
    
        @Override
        public void onDisable() {
            getLogger().info("LizardAuth is disabling...");
            protocolManager.removePacketListeners(this);
            getLogger().info("LizardAuth disabled successfully");
        }
    
        private void loadOrCreateKeys() throws Exception {
             privateKeyFile = new File(getDataFolder(), "private.key");
    
             if (privateKeyFile.exists()) {
                 try {
                    loadKeys();
                 } catch (Exception e){
                     getLogger().severe("Failed to load keys, creating new one! Error: " + e.getMessage());
                     generateKeys();
                     saveKeys();
                 }
             } else {
                generateKeys();
                saveKeys();
             }
        }
    
        private void generateKeys() throws NoSuchAlgorithmException {
            KeyPairGenerator keyGen = KeyPairGenerator.getInstance("RSA");
            keyGen.initialize(2048);
            KeyPair pair = keyGen.generateKeyPair();
            this.publicKey = pair.getPublic();
            this.privateKey = pair.getPrivate();
        }
    
        private void saveKeys() throws Exception {
            if (!getDataFolder().exists()) {
                getDataFolder().mkdirs();
            }
    
            PKCS8EncodedKeySpec privateKeySpec = new PKCS8EncodedKeySpec(privateKey.getEncoded());
            byte[] encryptedPrivateKey = encryptPrivateKey(privateKeySpec.getEncoded(), keyPass);
    
    
            FileOutputStream fos = new FileOutputStream(privateKeyFile);
            fos.write(encryptedPrivateKey);
            fos.close();
    
        }
    
        private void loadKeys() throws Exception{
          byte[] encryptedPrivateKey = Files.readAllBytes(Paths.get(privateKeyFile.getPath()));
          byte[] decryptedPrivateKey = decryptPrivateKey(encryptedPrivateKey, keyPass);
          PKCS8EncodedKeySpec privateKeySpec = new PKCS8EncodedKeySpec(decryptedPrivateKey);
          KeyFactory keyFactory = KeyFactory.getInstance("RSA");
          privateKey = keyFactory.generatePrivate(privateKeySpec);
    
            X509EncodedKeySpec publicKeySpec = new X509EncodedKeySpec(publicKey.getEncoded());
            publicKey = keyFactory.generatePublic(publicKeySpec);
        }
    
    
        private void registerPacketListeners() {
            protocolManager.addPacketListener(new PacketAdapter(
                    this,
                    ListenerPriority.NORMAL,
                    Arrays.asList(PacketType.Handshake.Client.SET_PROTOCOL, PacketType.Login.Client.ENCRYPTION_BEGIN)
            ) {
    
                @Override
                public void onPacketReceiving(PacketEvent event) {
                    if (event.getPacketType() == PacketType.Handshake.Client.SET_PROTOCOL) {
                        getLogger().info("C -> S: Handshake - Packet caught!");
    
                        // Create and send Encryption Begin
                        Player player = event.getPlayer();
                        sendEncryptionBeginPacket(player);
    
                        // Cancel handshake processing
                        event.setCancelled(true);
                    } else if (event.getPacketType() == PacketType.Login.Client.ENCRYPTION_BEGIN) {
                        getLogger().info("C -> S: Encryption Response - Packet caught!");
                        // Получение данных из пакета Encryption Response (от клиента)
                        sharedSecret = event.getPacket().getByteArrays().read(0);
                        byte[] encryptedVerifyToken = event.getPacket().getByteArrays().read(1);
                        try {
                            byte[] decryptedVerifyToken = decryptData(encryptedVerifyToken);
    
                            if (Arrays.equals(decryptedVerifyToken, verifyToken)) {
                                getLogger().info("Decrypted Verify Token matches");
                                byte[] decryptedSharedSecret = decryptData(sharedSecret);
                               getLogger().info("Shared Secret decrypted: " + Arrays.toString(decryptedSharedSecret));
                               sharedSecret = decryptedSharedSecret;
                            } else {
                                getLogger().severe("Decrypted Verify Token doesnt match");
                                event.setCancelled(true);
                                return;
                            }
                        } catch (Exception e) {
                            getLogger().severe("Failed to decrypt verify token: " + e.getMessage());
                            event.setCancelled(true);
                            return;
                        }
                        getLogger().info("Shared Secret: " + Arrays.toString(sharedSecret));
                       //  event.getPlayer().kickPlayer("Debug!");
                    }
    
                }
            });
        }
    
        public void sendEncryptionBeginPacket(Player player) {
            verifyToken = generateVerifyToken();
            com.comphenix.protocol.wrappers.PacketContainer encryptionBeginPacket = new com.comphenix.protocol.wrappers.PacketContainer(PacketType.Login.Server.ENCRYPTION_BEGIN);
            encryptionBeginPacket.getSpecificModifier(String.class).write(0, ""); // Server ID
            encryptionBeginPacket.getByteArrays().write(0, publicKey.getEncoded()); // Public Key
            encryptionBeginPacket.getByteArrays().write(1, verifyToken); // Verify Token
            try {
                protocolManager.sendServerPacket(player, encryptionBeginPacket);
            } catch (Exception e) {
                getLogger().severe("Failed to send Encryption Begin packet: " + e.getMessage());
            }
            getLogger().info("S -> C: Encryption Request - Packet sent!");
            getLogger().info("Public key: " + Arrays.toString(publicKey.getEncoded()));
            getLogger().info("Verify token: " + Arrays.toString(verifyToken));
        }
    
        @EventHandler
        public void onPlayerJoin(PlayerJoinEvent event) {
            getLogger().info("Player " + event.getPlayer().getName() + " has joined (PlayerJoinEvent)");
        }
    
        @EventHandler
        public void onPlayerPreLogin(AsyncPlayerPreLoginEvent event) {
            getLogger().info("Player " + event.getName() + " is attempting to log in (Async)");
            getLogger().info("UUID: " + event.getUniqueId());
            getLogger().info("IP Address: " + event.getAddress().getHostAddress());
            getLogger().info("Hostname: " + event.getHostname());
    
            CompletableFuture.runAsync(() -> {
                try {
                    if (sharedSecret == null || verifyToken == null) {
                        event.disallow(AsyncPlayerPreLoginEvent.Result.KICK_OTHER, "Failed to get shared secret and verify token.");
                        return;
                    }
                    getLogger().info("Starting authentication process...");
                    String accessToken = authenticateWithMinecraft(event.getUniqueId(), event.getName(), sharedSecret, verifyToken);
    
                    if (accessToken != null) {
                        boolean hasLicense = checkGameOwnership(accessToken);
                        if (!hasLicense) {
                            event.disallow(AsyncPlayerPreLoginEvent.Result.KICK_OTHER, "You do not own Minecraft.");
                        } else {
                            getLogger().info("Player " + event.getName() + " has a valid license.");
                        }
    
                    } else {
                        event.disallow(AsyncPlayerPreLoginEvent.Result.KICK_OTHER, "Failed to authenticate with Minecraft.");
                    }
                } catch (Exception e) {
                    getLogger().severe("An error occurred during authentication: " + e.getMessage());
                    event.disallow(AsyncPlayerPreLoginEvent.Result.KICK_OTHER, "Authentication error.");
                }
            });
    
        }
    
        private String authenticateWithMinecraft(UUID playerUUID, String playerName, byte[] sharedSecret, byte[] verifyToken) throws IOException, InterruptedException {
            // Заглушка для будущей проверки.
            return "access_token";
        }
    
        private byte[] generateVerifyToken() {
            byte[] token = new byte[4];
            random.nextBytes(token);
            return token;
        }
    
        private boolean checkGameOwnership(String accessToken) throws IOException, InterruptedException {
            // Заглушка для будущей проверки.
            return true;
        }
    
        private byte[] decryptData(byte[] encryptedData) throws Exception {
            Cipher cipher = Cipher.getInstance("RSA");
            cipher.init(Cipher.DECRYPT_MODE, privateKey);
            return cipher.doFinal(encryptedData);
        }
    
        private byte[] encryptPrivateKey(byte[] privateKeyBytes, String keyPass) throws Exception {
            SecretKeySpec key = new SecretKeySpec(keyPass.getBytes(), "AES");
            Cipher cipher = Cipher.getInstance("AES");
            cipher.init(Cipher.ENCRYPT_MODE, key);
            return cipher.doFinal(privateKeyBytes);
        }
    
       private byte[] decryptPrivateKey(byte[] encryptedPrivateKey, String keyPass) throws Exception {
            SecretKeySpec key = new SecretKeySpec(keyPass.getBytes(), "AES");
            Cipher cipher = Cipher.getInstance("AES");
            cipher.init(Cipher.DECRYPT_MODE, key);
            return cipher.doFinal(encryptedPrivateKey);
        }
    }
     
    Последнее редактирование: 15 янв 2025
  16. alexandrage

    alexandrage Старожил Пользователь

    Баллы:
    173
    Заставить клиент войти с врубленным онлайн модом, иначе игра не переключиться в режим енкрипшен.
     
  17. Автор темы
    Ryazha_

    Ryazha_ Активный участник Пользователь

    Баллы:
    76
    Имя в Minecraft:
    Ryazha_
    Я вот не понимаю как это сделать? Саморучно энкрипт пакет кидать?
     
  18. Автор темы
    Ryazha_

    Ryazha_ Активный участник Пользователь

    Баллы:
    76
    Имя в Minecraft:
    Ryazha_
    И насчёт ServerID как его генерировать?
     
  19. Dymeth

    Dymeth Активный участник Пользователь

    Баллы:
    98
    Имя в Minecraft:
    Dymeth
    Ответы на эти вопросы есть по ссылкам, которые уже кидал выше. Нужно было лишь чуть копнуть вглубь: https://minecraft.wiki/w/Minecraft_Wiki:Projects/wiki.vg_merge/Protocol_Encryption

    Либо саморучно, либо заставить ядро отправить. Но для этого придётся лезть в НМС/рефлексию. В апи Paper этого по-моему, нет. Только на банже и велосити присутствует.
    Теоретически отправить пакет было бы проще, т.к. можно использовать какой-нибудь ProtocolLib. Но проблема в другом.
    Даже если ты отправишь пакет, то ядро сервера знать об этом не будет. А значит будет нарушен процесс подключения к серверу. Будет рассинхро между тем, что ожидаешь ты и тем, что ожидает ядро. Скорее всего, тип протокола изменится на play раньше времени. Это всё тоже нужно учитывать.

    Забегая вперёд - на твой вопрос "а что конкретно нужно сделать?" не существует ответа, которые бы тебе понравился. Ответ звучит так: "изучай ядро и пытайся понять, как с ним взаимодействовать".
    Навык разбираться - это то, что делает программиста программистом.
    Кто-то из форумчан вряд ли станет тратить существенное время, чтобы лезть в ядро вместо тебя, чтобы предложить конкретное решение.
    Ты можешь попытаться его нагуглить, если кто-то уже делал подобное. Но учти, что на разных версиях игры и на разных ядрах всё может быть совершенно по-разному. Даже между Spigot и Paper может быть существенная разница в этом вопросе, хотя обычно плагины от Paper совместимы со Spigot на 99%. Но тебе придётся залеть в саму реализацию ядра, а тут ошибки уже не прощаются
     
    Последнее редактирование: 17 янв 2025
  20. Автор темы
    Ryazha_

    Ryazha_ Активный участник Пользователь

    Баллы:
    76
    Имя в Minecraft:
    Ryazha_
    Получается мне нужно залезть в НМС или ядро, найти там то как генерировать айди сервера, и как отправлять пакеты?
     

Поделиться этой страницей